Files
2026-02-08 17:01:36 +03:00

149 lines
3.9 KiB
Go

package ollama
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
const defaultTimeout = 10 * time.Minute
// Client calls Ollama /api/chat.
type Client struct {
BaseURL string
HTTPClient *http.Client
}
// NewClient creates an Ollama client. baseURL is e.g. "http://localhost:11434".
func NewClient(baseURL string) *Client {
return &Client{
BaseURL: baseURL,
HTTPClient: &http.Client{
Timeout: defaultTimeout,
},
}
}
// ChatRequest matches Ollama POST /api/chat body.
type ChatRequest struct {
Model string `json:"model"`
Messages []ChatMessage `json:"messages"`
Stream bool `json:"stream"`
Format interface{} `json:"format,omitempty"` // "json" or JSON schema object
Tools []Tool `json:"tools,omitempty"`
}
// ChatMessage is one message in the conversation.
type ChatMessage struct {
Role string `json:"role"` // "user", "assistant", "system", "tool"
Content string `json:"content,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolName string `json:"tool_name,omitempty"` // for role "tool"
}
// Tool defines a function the model may call.
type Tool struct {
Type string `json:"type"`
Function ToolFunc `json:"function"`
}
// ToolFunc describes the function.
type ToolFunc struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters interface{} `json:"parameters"`
}
// ToolCall is a model request to call a tool.
type ToolCall struct {
Type string `json:"type"`
Function ToolCallFn `json:"function"`
}
// ToolCallFn holds name and arguments.
// Arguments may come from Ollama as a JSON object or as a JSON string.
type ToolCallFn struct {
Name string `json:"name"`
Arguments interface{} `json:"arguments"` // object or string
}
// QueryFromToolCall returns the "query" argument from a web_search tool call.
// Ollama may send arguments as a map or as a JSON string.
func QueryFromToolCall(tc ToolCall) string {
switch v := tc.Function.Arguments.(type) {
case map[string]interface{}:
if q, _ := v["query"].(string); q != "" {
return q
}
case string:
var m map[string]interface{}
if json.Unmarshal([]byte(v), &m) == nil {
if q, _ := m["query"].(string); q != "" {
return q
}
}
}
return ""
}
// ChatResponse is the Ollama /api/chat response.
type ChatResponse struct {
Message ChatMessage `json:"message"`
Done bool `json:"done"`
}
// Chat sends a chat request and returns the response.
func (c *Client) Chat(req *ChatRequest) (*ChatResponse, error) {
body, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("marshal request: %w", err)
}
url := c.BaseURL + "/api/chat"
httpReq, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("new request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.HTTPClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("do request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("ollama returned %d: %s", resp.StatusCode, string(b))
}
var out ChatResponse
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return nil, fmt.Errorf("decode response: %w", err)
}
return &out, nil
}
// WebSearchTool returns the tool definition for web_search (Tavily).
func WebSearchTool() Tool {
return Tool{
Type: "function",
Function: ToolFunc{
Name: "web_search",
Description: "Search the web for current information. Use when you need up-to-date or factual information from the internet.",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"query": map[string]interface{}{
"type": "string",
"description": "Search query",
},
},
"required": []string{"query"},
},
},
}
}