mirror of
https://github.com/ollama/ollama.git
synced 2026-03-27 02:58:43 +07:00
x/cmd: enable web search and web fetch with flag (#13690)
This commit is contained in:
@@ -537,6 +537,7 @@ func RunHandler(cmd *cobra.Command, args []string) error {
|
|||||||
// Check for experimental flag
|
// Check for experimental flag
|
||||||
isExperimental, _ := cmd.Flags().GetBool("experimental")
|
isExperimental, _ := cmd.Flags().GetBool("experimental")
|
||||||
yoloMode, _ := cmd.Flags().GetBool("experimental-yolo")
|
yoloMode, _ := cmd.Flags().GetBool("experimental-yolo")
|
||||||
|
enableWebsearch, _ := cmd.Flags().GetBool("experimental-websearch")
|
||||||
|
|
||||||
if interactive {
|
if interactive {
|
||||||
if err := loadOrUnloadModel(cmd, &opts); err != nil {
|
if err := loadOrUnloadModel(cmd, &opts); err != nil {
|
||||||
@@ -566,7 +567,7 @@ func RunHandler(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
// Use experimental agent loop with tools
|
// Use experimental agent loop with tools
|
||||||
if isExperimental {
|
if isExperimental {
|
||||||
return xcmd.GenerateInteractive(cmd, opts.Model, opts.WordWrap, opts.Options, opts.Think, opts.HideThinking, opts.KeepAlive, yoloMode)
|
return xcmd.GenerateInteractive(cmd, opts.Model, opts.WordWrap, opts.Options, opts.Think, opts.HideThinking, opts.KeepAlive, yoloMode, enableWebsearch)
|
||||||
}
|
}
|
||||||
|
|
||||||
return generateInteractive(cmd, opts)
|
return generateInteractive(cmd, opts)
|
||||||
@@ -1786,6 +1787,7 @@ func NewCLI() *cobra.Command {
|
|||||||
runCmd.Flags().Int("dimensions", 0, "Truncate output embeddings to specified dimension (embedding models only)")
|
runCmd.Flags().Int("dimensions", 0, "Truncate output embeddings to specified dimension (embedding models only)")
|
||||||
runCmd.Flags().Bool("experimental", false, "Enable experimental agent loop with tools")
|
runCmd.Flags().Bool("experimental", false, "Enable experimental agent loop with tools")
|
||||||
runCmd.Flags().Bool("experimental-yolo", false, "Skip all tool approval prompts (use with caution)")
|
runCmd.Flags().Bool("experimental-yolo", false, "Skip all tool approval prompts (use with caution)")
|
||||||
|
runCmd.Flags().Bool("experimental-websearch", false, "Enable web search tool in experimental mode")
|
||||||
|
|
||||||
// Image generation flags (width, height, steps, seed, etc.)
|
// Image generation flags (width, height, steps, seed, etc.)
|
||||||
imagegen.RegisterFlags(runCmd)
|
imagegen.RegisterFlags(runCmd)
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ var optionLabels = []string{
|
|||||||
var toolDisplayNames = map[string]string{
|
var toolDisplayNames = map[string]string{
|
||||||
"bash": "Bash",
|
"bash": "Bash",
|
||||||
"web_search": "Web Search",
|
"web_search": "Web Search",
|
||||||
|
"web_fetch": "Web Fetch",
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToolDisplayName returns the human-readable display name for a tool.
|
// ToolDisplayName returns the human-readable display name for a tool.
|
||||||
@@ -565,6 +566,16 @@ func formatToolDisplay(toolName string, args map[string]any) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For web fetch, show URL and internet notice
|
||||||
|
if toolName == "web_fetch" {
|
||||||
|
if url, ok := args["url"].(string); ok {
|
||||||
|
sb.WriteString(fmt.Sprintf("Tool: %s\n", displayName))
|
||||||
|
sb.WriteString(fmt.Sprintf("URL: %s\n", url))
|
||||||
|
sb.WriteString("Uses internet via ollama.com")
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Generic display
|
// Generic display
|
||||||
sb.WriteString(fmt.Sprintf("Tool: %s", displayName))
|
sb.WriteString(fmt.Sprintf("Tool: %s", displayName))
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
@@ -1017,6 +1028,16 @@ func FormatApprovalResult(toolName string, args map[string]any, result ApprovalR
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if toolName == "web_fetch" {
|
||||||
|
if url, ok := args["url"].(string); ok {
|
||||||
|
// Truncate long URLs
|
||||||
|
if len(url) > 50 {
|
||||||
|
url = url[:47] + "..."
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("\033[1m%s:\033[0m %s: %s", label, displayName, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("\033[1m%s:\033[0m %s", label, displayName)
|
return fmt.Sprintf("\033[1m%s:\033[0m %s", label, displayName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
14
x/cmd/run.go
14
x/cmd/run.go
@@ -650,7 +650,8 @@ func checkModelCapabilities(ctx context.Context, modelName string) (supportsTool
|
|||||||
// GenerateInteractive runs an interactive agent session.
|
// GenerateInteractive runs an interactive agent session.
|
||||||
// This is called from cmd.go when --experimental flag is set.
|
// This is called from cmd.go when --experimental flag is set.
|
||||||
// If yoloMode is true, all tool approvals are skipped.
|
// If yoloMode is true, all tool approvals are skipped.
|
||||||
func GenerateInteractive(cmd *cobra.Command, modelName string, wordWrap bool, options map[string]any, think *api.ThinkValue, hideThinking bool, keepAlive *api.Duration, yoloMode bool) error {
|
// If enableWebsearch is true, the web search tool is registered.
|
||||||
|
func GenerateInteractive(cmd *cobra.Command, modelName string, wordWrap bool, options map[string]any, think *api.ThinkValue, hideThinking bool, keepAlive *api.Duration, yoloMode bool, enableWebsearch bool) error {
|
||||||
scanner, err := readline.New(readline.Prompt{
|
scanner, err := readline.New(readline.Prompt{
|
||||||
Prompt: ">>> ",
|
Prompt: ">>> ",
|
||||||
AltPrompt: "... ",
|
AltPrompt: "... ",
|
||||||
@@ -676,6 +677,12 @@ func GenerateInteractive(cmd *cobra.Command, modelName string, wordWrap bool, op
|
|||||||
if supportsTools {
|
if supportsTools {
|
||||||
toolRegistry = tools.DefaultRegistry()
|
toolRegistry = tools.DefaultRegistry()
|
||||||
|
|
||||||
|
// Register web search and web fetch tools if enabled via flag
|
||||||
|
if enableWebsearch {
|
||||||
|
toolRegistry.RegisterWebSearch()
|
||||||
|
toolRegistry.RegisterWebFetch()
|
||||||
|
}
|
||||||
|
|
||||||
if toolRegistry.Has("bash") {
|
if toolRegistry.Has("bash") {
|
||||||
fmt.Fprintln(os.Stderr)
|
fmt.Fprintln(os.Stderr)
|
||||||
fmt.Fprintln(os.Stderr, "This experimental version of Ollama has the \033[1mbash\033[0m tool enabled.")
|
fmt.Fprintln(os.Stderr, "This experimental version of Ollama has the \033[1mbash\033[0m tool enabled.")
|
||||||
@@ -683,6 +690,11 @@ func GenerateInteractive(cmd *cobra.Command, modelName string, wordWrap bool, op
|
|||||||
fmt.Fprintln(os.Stderr)
|
fmt.Fprintln(os.Stderr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if toolRegistry.Has("web_search") || toolRegistry.Has("web_fetch") {
|
||||||
|
fmt.Fprintln(os.Stderr, "The \033[1mWeb Search\033[0m and \033[1mWeb Fetch\033[0m tools are enabled. Models can search and fetch web content via ollama.com.")
|
||||||
|
fmt.Fprintln(os.Stderr)
|
||||||
|
}
|
||||||
|
|
||||||
if yoloMode {
|
if yoloMode {
|
||||||
fmt.Fprintf(os.Stderr, "\033[1mwarning:\033[0m yolo mode - all tool approvals will be skipped\n")
|
fmt.Fprintf(os.Stderr, "\033[1mwarning:\033[0m yolo mode - all tool approvals will be skipped\n")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,16 @@ func (r *Registry) RegisterBash() {
|
|||||||
r.Register(&BashTool{})
|
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.
|
// Get retrieves a tool by name.
|
||||||
func (r *Registry) Get(name string) (Tool, bool) {
|
func (r *Registry) Get(name string) (Tool, bool) {
|
||||||
tool, ok := r.tools[name]
|
tool, ok := r.tools[name]
|
||||||
|
|||||||
162
x/tools/webfetch.go
Normal file
162
x/tools/webfetch.go
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user