ollama source for Momentry Core verification

This commit is contained in:
Accusys
2026-05-22 17:19:10 +08:00
commit 0b31ff9135
2020 changed files with 1413145 additions and 0 deletions

114
x/tools/bash.go Normal file
View File

@@ -0,0 +1,114 @@
package tools
import (
"bytes"
"context"
"fmt"
"os/exec"
"strings"
"time"
"github.com/ollama/ollama/api"
)
const (
// bashTimeout is the maximum execution time for a command.
bashTimeout = 60 * time.Second
// maxOutputSize is the maximum output size in bytes.
maxOutputSize = 50000
)
// BashTool implements shell command execution.
type BashTool struct{}
// Name returns the tool name.
func (b *BashTool) Name() string {
return "bash"
}
// Description returns a description of the tool.
func (b *BashTool) Description() string {
return "Execute a bash command on the system. Use this to run shell commands, check files, run programs, etc."
}
// Schema returns the tool's parameter schema.
func (b *BashTool) Schema() api.ToolFunction {
props := api.NewToolPropertiesMap()
props.Set("command", api.ToolProperty{
Type: api.PropertyType{"string"},
Description: "The bash command to execute",
})
return api.ToolFunction{
Name: b.Name(),
Description: b.Description(),
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: props,
Required: []string{"command"},
},
}
}
// Execute runs the bash command.
func (b *BashTool) Execute(args map[string]any) (string, error) {
command, ok := args["command"].(string)
if !ok || command == "" {
return "", fmt.Errorf("command parameter is required")
}
// Create context with timeout
ctx, cancel := context.WithTimeout(context.Background(), bashTimeout)
defer cancel()
// Execute command
cmd := exec.CommandContext(ctx, "bash", "-c", command)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
// Build output
var sb strings.Builder
// Add stdout
if stdout.Len() > 0 {
output := stdout.String()
if len(output) > maxOutputSize {
output = output[:maxOutputSize] + "\n... (output truncated)"
}
sb.WriteString(output)
}
// Add stderr if present
if stderr.Len() > 0 {
stderrOutput := stderr.String()
if len(stderrOutput) > maxOutputSize {
stderrOutput = stderrOutput[:maxOutputSize] + "\n... (stderr truncated)"
}
if sb.Len() > 0 {
sb.WriteString("\n")
}
sb.WriteString("stderr:\n")
sb.WriteString(stderrOutput)
}
// Handle errors
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
return sb.String() + "\n\nError: command timed out after 60 seconds", nil
}
// Include exit code in output but don't return as error
if exitErr, ok := err.(*exec.ExitError); ok {
return sb.String() + fmt.Sprintf("\n\nExit code: %d", exitErr.ExitCode()), nil
}
return sb.String(), fmt.Errorf("executing command: %w", err)
}
if sb.Len() == 0 {
return "(no output)", nil
}
return sb.String(), nil
}

131
x/tools/registry.go Normal file
View File

@@ -0,0 +1,131 @@
// Package tools provides built-in tool implementations for the agent loop.
package tools
import (
"fmt"
"os"
"sort"
"github.com/ollama/ollama/api"
)
// Tool defines the interface for agent tools.
type Tool interface {
// Name returns the tool's unique identifier.
Name() string
// Description returns a human-readable description of what the tool does.
Description() string
// Schema returns the tool's parameter schema for the LLM.
Schema() api.ToolFunction
// Execute runs the tool with the given arguments.
Execute(args map[string]any) (string, error)
}
// Registry manages available tools.
type Registry struct {
tools map[string]Tool
}
// NewRegistry creates a new tool registry.
func NewRegistry() *Registry {
return &Registry{
tools: make(map[string]Tool),
}
}
// Register adds a tool to the registry.
func (r *Registry) Register(tool Tool) {
r.tools[tool.Name()] = tool
}
// Unregister removes a tool from the registry by name.
func (r *Registry) Unregister(name string) {
delete(r.tools, name)
}
// Has checks if a tool with the given name is registered.
func (r *Registry) Has(name string) bool {
_, ok := r.tools[name]
return ok
}
// RegisterBash adds the bash tool to the registry.
func (r *Registry) RegisterBash() {
r.Register(&BashTool{})
}
// RegisterWebSearch adds the web search tool to the registry.
func (r *Registry) RegisterWebSearch() {
r.Register(&WebSearchTool{})
}
// RegisterWebFetch adds the web fetch tool to the registry.
func (r *Registry) RegisterWebFetch() {
r.Register(&WebFetchTool{})
}
// Get retrieves a tool by name.
func (r *Registry) Get(name string) (Tool, bool) {
tool, ok := r.tools[name]
return tool, ok
}
// Tools returns all registered tools in Ollama API format, sorted by name.
func (r *Registry) Tools() api.Tools {
// Get sorted names for deterministic ordering
names := make([]string, 0, len(r.tools))
for name := range r.tools {
names = append(names, name)
}
sort.Strings(names)
var tools api.Tools
for _, name := range names {
tool := r.tools[name]
tools = append(tools, api.Tool{
Type: "function",
Function: tool.Schema(),
})
}
return tools
}
// Execute runs a tool call and returns the result.
func (r *Registry) Execute(call api.ToolCall) (string, error) {
tool, ok := r.tools[call.Function.Name]
if !ok {
return "", fmt.Errorf("unknown tool: %s", call.Function.Name)
}
return tool.Execute(call.Function.Arguments.ToMap())
}
// Names returns the names of all registered tools, sorted alphabetically.
func (r *Registry) Names() []string {
names := make([]string, 0, len(r.tools))
for name := range r.tools {
names = append(names, name)
}
sort.Strings(names)
return names
}
// Count returns the number of registered tools.
func (r *Registry) Count() int {
return len(r.tools)
}
// DefaultRegistry creates a registry with all built-in tools.
// Tools can be disabled via environment variables:
// - OLLAMA_AGENT_DISABLE_WEBSEARCH=1 disables web_search
// - OLLAMA_AGENT_DISABLE_BASH=1 disables bash
func DefaultRegistry() *Registry {
r := NewRegistry()
// TODO(parthsareen): re-enable web search once it's ready for release
// if os.Getenv("OLLAMA_AGENT_DISABLE_WEBSEARCH") == "" {
// r.Register(&WebSearchTool{})
// }
if os.Getenv("OLLAMA_AGENT_DISABLE_BASH") == "" {
r.Register(&BashTool{})
}
return r
}

223
x/tools/registry_test.go Normal file
View File

@@ -0,0 +1,223 @@
package tools
import (
"testing"
"github.com/ollama/ollama/api"
)
func TestRegistry_Register(t *testing.T) {
r := NewRegistry()
r.Register(&BashTool{})
r.Register(&WebSearchTool{})
if r.Count() != 2 {
t.Errorf("expected 2 tools, got %d", r.Count())
}
names := r.Names()
if len(names) != 2 {
t.Errorf("expected 2 names, got %d", len(names))
}
}
func TestRegistry_Get(t *testing.T) {
r := NewRegistry()
r.Register(&BashTool{})
tool, ok := r.Get("bash")
if !ok {
t.Fatal("expected to find bash tool")
}
if tool.Name() != "bash" {
t.Errorf("expected name 'bash', got '%s'", tool.Name())
}
_, ok = r.Get("nonexistent")
if ok {
t.Error("expected not to find nonexistent tool")
}
}
func TestRegistry_Tools(t *testing.T) {
r := NewRegistry()
r.Register(&BashTool{})
r.Register(&WebSearchTool{})
tools := r.Tools()
if len(tools) != 2 {
t.Errorf("expected 2 tools, got %d", len(tools))
}
for _, tool := range tools {
if tool.Type != "function" {
t.Errorf("expected type 'function', got '%s'", tool.Type)
}
}
}
func TestRegistry_Execute(t *testing.T) {
r := NewRegistry()
r.Register(&BashTool{})
// Test successful execution
args := api.NewToolCallFunctionArguments()
args.Set("command", "echo hello")
result, err := r.Execute(api.ToolCall{
Function: api.ToolCallFunction{
Name: "bash",
Arguments: args,
},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result != "hello\n" {
t.Errorf("expected 'hello\\n', got '%s'", result)
}
// Test unknown tool
_, err = r.Execute(api.ToolCall{
Function: api.ToolCallFunction{
Name: "unknown",
Arguments: api.NewToolCallFunctionArguments(),
},
})
if err == nil {
t.Error("expected error for unknown tool")
}
}
func TestDefaultRegistry(t *testing.T) {
r := DefaultRegistry()
if r.Count() != 1 {
t.Errorf("expected 1 tool in default registry, got %d", r.Count())
}
_, ok := r.Get("bash")
if !ok {
t.Error("expected bash tool in default registry")
}
}
func TestDefaultRegistry_DisableWebsearch(t *testing.T) {
t.Setenv("OLLAMA_AGENT_DISABLE_WEBSEARCH", "1")
r := DefaultRegistry()
if r.Count() != 1 {
t.Errorf("expected 1 tool with websearch disabled, got %d", r.Count())
}
_, ok := r.Get("bash")
if !ok {
t.Error("expected bash tool in registry")
}
_, ok = r.Get("web_search")
if ok {
t.Error("expected web_search to be disabled")
}
}
func TestDefaultRegistry_DisableBash(t *testing.T) {
t.Setenv("OLLAMA_AGENT_DISABLE_BASH", "1")
r := DefaultRegistry()
if r.Count() != 0 {
t.Errorf("expected 0 tools with bash disabled, got %d", r.Count())
}
}
func TestDefaultRegistry_DisableBoth(t *testing.T) {
t.Setenv("OLLAMA_AGENT_DISABLE_WEBSEARCH", "1")
t.Setenv("OLLAMA_AGENT_DISABLE_BASH", "1")
r := DefaultRegistry()
if r.Count() != 0 {
t.Errorf("expected 0 tools with both disabled, got %d", r.Count())
}
}
func TestBashTool_Schema(t *testing.T) {
tool := &BashTool{}
schema := tool.Schema()
if schema.Name != "bash" {
t.Errorf("expected name 'bash', got '%s'", schema.Name)
}
if schema.Parameters.Type != "object" {
t.Errorf("expected parameters type 'object', got '%s'", schema.Parameters.Type)
}
if _, ok := schema.Parameters.Properties.Get("command"); !ok {
t.Error("expected 'command' property in schema")
}
}
func TestWebSearchTool_Schema(t *testing.T) {
tool := &WebSearchTool{}
schema := tool.Schema()
if schema.Name != "web_search" {
t.Errorf("expected name 'web_search', got '%s'", schema.Name)
}
if schema.Parameters.Type != "object" {
t.Errorf("expected parameters type 'object', got '%s'", schema.Parameters.Type)
}
if _, ok := schema.Parameters.Properties.Get("query"); !ok {
t.Error("expected 'query' property in schema")
}
}
func TestRegistry_Unregister(t *testing.T) {
r := NewRegistry()
r.Register(&BashTool{})
if r.Count() != 1 {
t.Errorf("expected 1 tool, got %d", r.Count())
}
r.Unregister("bash")
if r.Count() != 0 {
t.Errorf("expected 0 tools after unregister, got %d", r.Count())
}
_, ok := r.Get("bash")
if ok {
t.Error("expected bash tool to be removed")
}
}
func TestRegistry_Has(t *testing.T) {
r := NewRegistry()
if r.Has("bash") {
t.Error("expected Has to return false for unregistered tool")
}
r.Register(&BashTool{})
if !r.Has("bash") {
t.Error("expected Has to return true for registered tool")
}
}
func TestRegistry_RegisterBash(t *testing.T) {
r := NewRegistry()
r.RegisterBash()
if !r.Has("bash") {
t.Error("expected bash tool to be registered")
}
}

167
x/tools/webfetch.go Normal file
View File

@@ -0,0 +1,167 @@
package tools
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/ollama/ollama/api"
"github.com/ollama/ollama/auth"
internalcloud "github.com/ollama/ollama/internal/cloud"
)
const (
webFetchAPI = "https://ollama.com/api/web_fetch"
webFetchTimeout = 30 * time.Second
)
// ErrWebFetchAuthRequired is returned when web fetch requires authentication
var ErrWebFetchAuthRequired = errors.New("web fetch requires authentication")
// WebFetchTool implements web page fetching using Ollama's hosted API.
type WebFetchTool struct{}
// Name returns the tool name.
func (w *WebFetchTool) Name() string {
return "web_fetch"
}
// Description returns a description of the tool.
func (w *WebFetchTool) Description() string {
return "Fetch and extract text content from a web page. Use this to read the full content of a URL found in search results or provided by the user."
}
// Schema returns the tool's parameter schema.
func (w *WebFetchTool) Schema() api.ToolFunction {
props := api.NewToolPropertiesMap()
props.Set("url", api.ToolProperty{
Type: api.PropertyType{"string"},
Description: "The URL to fetch and extract content from",
})
return api.ToolFunction{
Name: w.Name(),
Description: w.Description(),
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: props,
Required: []string{"url"},
},
}
}
// webFetchRequest is the request body for the web fetch API.
type webFetchRequest struct {
URL string `json:"url"`
}
// webFetchResponse is the response from the web fetch API.
type webFetchResponse struct {
Title string `json:"title"`
Content string `json:"content"`
Links []string `json:"links,omitempty"`
}
// Execute fetches content from a web page.
// Uses Ollama key signing for authentication - this makes requests via ollama.com API.
func (w *WebFetchTool) Execute(args map[string]any) (string, error) {
if internalcloud.Disabled() {
return "", errors.New(internalcloud.DisabledError("web fetch is unavailable"))
}
urlStr, ok := args["url"].(string)
if !ok || urlStr == "" {
return "", fmt.Errorf("url parameter is required")
}
// Validate URL
if _, err := url.Parse(urlStr); err != nil {
return "", fmt.Errorf("invalid URL: %w", err)
}
// Prepare request
reqBody := webFetchRequest{
URL: urlStr,
}
jsonBody, err := json.Marshal(reqBody)
if err != nil {
return "", fmt.Errorf("marshaling request: %w", err)
}
// Parse URL and add timestamp for signing
fetchURL, err := url.Parse(webFetchAPI)
if err != nil {
return "", fmt.Errorf("parsing fetch URL: %w", err)
}
q := fetchURL.Query()
q.Add("ts", strconv.FormatInt(time.Now().Unix(), 10))
fetchURL.RawQuery = q.Encode()
// Sign the request using Ollama key (~/.ollama/id_ed25519)
ctx := context.Background()
data := fmt.Appendf(nil, "%s,%s", http.MethodPost, fetchURL.RequestURI())
signature, err := auth.Sign(ctx, data)
if err != nil {
return "", fmt.Errorf("signing request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fetchURL.String(), bytes.NewBuffer(jsonBody))
if err != nil {
return "", fmt.Errorf("creating request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
if signature != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", signature))
}
// Send request
client := &http.Client{Timeout: webFetchTimeout}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("sending request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("reading response: %w", err)
}
if resp.StatusCode == http.StatusUnauthorized {
return "", ErrWebFetchAuthRequired
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("web fetch API returned status %d: %s", resp.StatusCode, string(body))
}
// Parse response
var fetchResp webFetchResponse
if err := json.Unmarshal(body, &fetchResp); err != nil {
return "", fmt.Errorf("parsing response: %w", err)
}
// Format result
var sb strings.Builder
if fetchResp.Title != "" {
sb.WriteString(fmt.Sprintf("Title: %s\n\n", fetchResp.Title))
}
if fetchResp.Content != "" {
sb.WriteString("Content:\n")
sb.WriteString(fetchResp.Content)
} else {
sb.WriteString("No content could be extracted from the page.")
}
return sb.String(), nil
}

180
x/tools/websearch.go Normal file
View File

@@ -0,0 +1,180 @@
package tools
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/ollama/ollama/api"
"github.com/ollama/ollama/auth"
internalcloud "github.com/ollama/ollama/internal/cloud"
)
const (
webSearchAPI = "https://ollama.com/api/web_search"
webSearchTimeout = 15 * time.Second
)
// ErrWebSearchAuthRequired is returned when web search requires authentication
var ErrWebSearchAuthRequired = errors.New("web search requires authentication")
// WebSearchTool implements web search using Ollama's hosted API.
type WebSearchTool struct{}
// Name returns the tool name.
func (w *WebSearchTool) Name() string {
return "web_search"
}
// Description returns a description of the tool.
func (w *WebSearchTool) Description() string {
return "Search the web for current information. Use this when you need up-to-date information that may not be in your training data."
}
// Schema returns the tool's parameter schema.
func (w *WebSearchTool) Schema() api.ToolFunction {
props := api.NewToolPropertiesMap()
props.Set("query", api.ToolProperty{
Type: api.PropertyType{"string"},
Description: "The search query to look up on the web",
})
return api.ToolFunction{
Name: w.Name(),
Description: w.Description(),
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: props,
Required: []string{"query"},
},
}
}
// webSearchRequest is the request body for the web search API.
type webSearchRequest struct {
Query string `json:"query"`
MaxResults int `json:"max_results,omitempty"`
}
// webSearchResponse is the response from the web search API.
type webSearchResponse struct {
Results []webSearchResult `json:"results"`
}
// webSearchResult is a single search result.
type webSearchResult struct {
Title string `json:"title"`
URL string `json:"url"`
Content string `json:"content"`
}
// Execute performs the web search.
// Uses Ollama key signing for authentication - this makes requests via ollama.com API.
func (w *WebSearchTool) Execute(args map[string]any) (string, error) {
if internalcloud.Disabled() {
return "", errors.New(internalcloud.DisabledError("web search is unavailable"))
}
query, ok := args["query"].(string)
if !ok || query == "" {
return "", fmt.Errorf("query parameter is required")
}
// Prepare request
reqBody := webSearchRequest{
Query: query,
MaxResults: 5,
}
jsonBody, err := json.Marshal(reqBody)
if err != nil {
return "", fmt.Errorf("marshaling request: %w", err)
}
// Parse URL and add timestamp for signing
searchURL, err := url.Parse(webSearchAPI)
if err != nil {
return "", fmt.Errorf("parsing search URL: %w", err)
}
q := searchURL.Query()
q.Add("ts", strconv.FormatInt(time.Now().Unix(), 10))
searchURL.RawQuery = q.Encode()
// Sign the request using Ollama key (~/.ollama/id_ed25519)
// This authenticates with ollama.com using the local signing key
ctx := context.Background()
data := fmt.Appendf(nil, "%s,%s", http.MethodPost, searchURL.RequestURI())
signature, err := auth.Sign(ctx, data)
if err != nil {
return "", fmt.Errorf("signing request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, searchURL.String(), bytes.NewBuffer(jsonBody))
if err != nil {
return "", fmt.Errorf("creating request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
if signature != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", signature))
}
// Send request
client := &http.Client{Timeout: webSearchTimeout}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("sending request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("reading response: %w", err)
}
if resp.StatusCode == http.StatusUnauthorized {
return "", ErrWebSearchAuthRequired
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("web search API returned status %d: %s", resp.StatusCode, string(body))
}
// Parse response
var searchResp webSearchResponse
if err := json.Unmarshal(body, &searchResp); err != nil {
return "", fmt.Errorf("parsing response: %w", err)
}
// Format results
if len(searchResp.Results) == 0 {
return "No results found for query: " + query, nil
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Search results for: %s\n\n", query))
for i, result := range searchResp.Results {
sb.WriteString(fmt.Sprintf("%d. %s\n", i+1, result.Title))
sb.WriteString(fmt.Sprintf(" URL: %s\n", result.URL))
if result.Content != "" {
// Truncate long content (UTF-8 safe)
content := result.Content
runes := []rune(content)
if len(runes) > 300 {
content = string(runes[:300]) + "..."
}
sb.WriteString(fmt.Sprintf(" %s\n", content))
}
sb.WriteString("\n")
}
return sb.String(), nil
}

58
x/tools/websearch_test.go Normal file
View File

@@ -0,0 +1,58 @@
package tools
import (
"errors"
"testing"
)
func TestWebSearchTool_Name(t *testing.T) {
tool := &WebSearchTool{}
if tool.Name() != "web_search" {
t.Errorf("expected name 'web_search', got '%s'", tool.Name())
}
}
func TestWebSearchTool_Description(t *testing.T) {
tool := &WebSearchTool{}
if tool.Description() == "" {
t.Error("expected non-empty description")
}
}
func TestWebSearchTool_Execute_MissingQuery(t *testing.T) {
tool := &WebSearchTool{}
// Test with no query
_, err := tool.Execute(map[string]any{})
if err == nil {
t.Error("expected error for missing query")
}
// Test with empty query
_, err = tool.Execute(map[string]any{"query": ""})
if err == nil {
t.Error("expected error for empty query")
}
}
func TestErrWebSearchAuthRequired(t *testing.T) {
// Test that the error type exists and can be checked with errors.Is
err := ErrWebSearchAuthRequired
if err == nil {
t.Fatal("ErrWebSearchAuthRequired should not be nil")
}
if err.Error() != "web search requires authentication" {
t.Errorf("unexpected error message: %s", err.Error())
}
// Test that errors.Is works
wrappedErr := errors.New("wrapped: " + err.Error())
if errors.Is(wrappedErr, ErrWebSearchAuthRequired) {
t.Error("wrapped error should not match with errors.Is")
}
if !errors.Is(ErrWebSearchAuthRequired, ErrWebSearchAuthRequired) {
t.Error("ErrWebSearchAuthRequired should match itself with errors.Is")
}
}