Files
n.nakhostin 154610e2a0 Add Ollama SDK with Core Functionality and Documentation
- Introduced the Ollama SDK, providing a comprehensive interface for interacting with Ollama AI models, including text generation, chat conversations, embeddings, and model management.
- Implemented core components such as Client, Config, and various request/response entities to facilitate API interactions.
- Added structured error handling with specific error types for improved clarity and debugging.
- Included a README and usage guide to assist developers in integrating the SDK into their applications, ensuring a smooth onboarding experience.
- Established a fluent API for chat interactions and streaming responses, enhancing usability and flexibility for developers.
2025-10-04 15:21:35 +03:30

291 lines
7.1 KiB
Go

package ollama
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"tm/pkg/logger"
)
// Client implements the HTTPClient interface for Ollama API
type Client struct {
config *Config
httpClient *http.Client
logger logger.Logger
}
// NewClient creates a new Ollama HTTP client
func NewClient(config *Config, logger logger.Logger) (*Client, error) {
if config == nil {
config = DefaultConfig()
}
if err := config.Validate(); err != nil {
return nil, err
}
httpClient := &http.Client{
Timeout: config.Timeout,
}
return &Client{
config: config,
httpClient: httpClient,
logger: logger,
}, nil
}
// NewClientWithHTTPClient creates a new client with a custom HTTP client
func NewClientWithHTTPClient(config *Config, httpClient *http.Client, logger logger.Logger) (*Client, error) {
if config == nil {
config = DefaultConfig()
}
if err := config.Validate(); err != nil {
return nil, err
}
return &Client{
config: config,
httpClient: httpClient,
logger: logger,
}, nil
}
// Post performs a POST request to the Ollama API
func (c *Client) Post(ctx context.Context, url string, body interface{}, result interface{}) error {
return c.makeRequest(ctx, "POST", url, body, result)
}
// Get performs a GET request to the Ollama API
func (c *Client) Get(ctx context.Context, url string, result interface{}) error {
return c.makeRequest(ctx, "GET", url, nil, result)
}
// Delete performs a DELETE request to the Ollama API
func (c *Client) Delete(ctx context.Context, url string, body interface{}, result interface{}) error {
return c.makeRequest(ctx, "DELETE", url, body, result)
}
// makeRequest performs an HTTP request with retry logic and logging
func (c *Client) makeRequest(ctx context.Context, method, endpoint string, body interface{}, result interface{}) error {
url := c.config.BaseURL + endpoint
var bodyReader io.Reader
if body != nil {
bodyBytes, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("failed to marshal request body: %w", err)
}
bodyReader = bytes.NewReader(bodyBytes)
}
var lastErr error
for attempt := 0; attempt <= c.config.RetryAttempts; attempt++ {
if attempt > 0 {
// Wait before retry
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(c.config.RetryDelay):
}
}
err := c.doRequest(ctx, method, url, bodyReader, result)
if err == nil {
return nil
}
lastErr = err
// Don't retry on certain errors
if c.shouldNotRetry(err) {
break
}
if c.logger != nil {
c.logger.Warn("Request failed, retrying", map[string]interface{}{
"attempt": attempt + 1,
"max_attempts": c.config.RetryAttempts + 1,
"error": err.Error(),
"url": url,
"method": method,
})
}
}
return fmt.Errorf("request failed after %d attempts: %w", c.config.RetryAttempts+1, lastErr)
}
// doRequest performs a single HTTP request
func (c *Client) doRequest(ctx context.Context, method, url string, body io.Reader, result interface{}) error {
req, err := http.NewRequestWithContext(ctx, method, url, body)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", c.config.UserAgent)
if c.logger != nil && c.config.EnableLogging {
c.logger.Debug("Making HTTP request", map[string]interface{}{
"method": method,
"url": url,
"user_agent": c.config.UserAgent,
})
}
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to execute request: %w", err)
}
defer resp.Body.Close()
if c.logger != nil && c.config.EnableLogging {
c.logger.Debug("Received HTTP response", map[string]interface{}{
"status_code": resp.StatusCode,
"url": url,
})
}
if resp.StatusCode >= 400 {
bodyBytes, _ := io.ReadAll(resp.Body)
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(bodyBytes))
}
if result != nil {
if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
return fmt.Errorf("failed to decode response: %w", err)
}
}
return nil
}
// shouldNotRetry determines if an error should not be retried
func (c *Client) shouldNotRetry(err error) bool {
// Don't retry on context cancellation
if err == context.Canceled || err == context.DeadlineExceeded {
return true
}
// Don't retry on certain HTTP status codes
if err.Error() == "HTTP 400" || err.Error() == "HTTP 401" || err.Error() == "HTTP 403" || err.Error() == "HTTP 404" {
return true
}
return false
}
// StreamGenerate performs a streaming text generation request
func (c *Client) StreamGenerate(ctx context.Context, req *GenerateRequest, onChunk func(*StreamChunk) error) error {
url := c.config.BaseURL + "/api/generate"
// Force streaming
stream := true
req.Stream = &stream
bodyBytes, err := json.Marshal(req)
if err != nil {
return fmt.Errorf("failed to marshal request body: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(bodyBytes))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("User-Agent", c.config.UserAgent)
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("failed to execute request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
bodyBytes, _ := io.ReadAll(resp.Body)
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(bodyBytes))
}
decoder := json.NewDecoder(resp.Body)
for {
var chunk StreamChunk
if err := decoder.Decode(&chunk); err != nil {
if err == io.EOF {
break
}
return fmt.Errorf("failed to decode stream chunk: %w", err)
}
if err := onChunk(&chunk); err != nil {
return fmt.Errorf("error processing stream chunk: %w", err)
}
if chunk.Done {
break
}
}
return nil
}
// StreamChat performs a streaming chat request
func (c *Client) StreamChat(ctx context.Context, req *ChatRequest, onChunk func(*StreamChunk) error) error {
url := c.config.BaseURL + "/api/chat"
// Force streaming
stream := true
req.Stream = &stream
bodyBytes, err := json.Marshal(req)
if err != nil {
return fmt.Errorf("failed to marshal request body: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(bodyBytes))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("User-Agent", c.config.UserAgent)
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("failed to execute request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
bodyBytes, _ := io.ReadAll(resp.Body)
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(bodyBytes))
}
decoder := json.NewDecoder(resp.Body)
for {
var chunk StreamChunk
if err := decoder.Decode(&chunk); err != nil {
if err == io.EOF {
break
}
return fmt.Errorf("failed to decode stream chunk: %w", err)
}
if err := onChunk(&chunk); err != nil {
return fmt.Errorf("error processing stream chunk: %w", err)
}
if chunk.Done {
break
}
}
return nil
}