Rewrite search mechanism to adapt more common
Added ollama search Enhance search result by agent before return to user
This commit is contained in:
parent
8d757fbb6f
commit
7d623fc3af
6 changed files with 247 additions and 50 deletions
|
|
@ -3,7 +3,7 @@
|
|||
"defaults": {
|
||||
"workspace": "~/.picoclaw/workspace",
|
||||
"restrict_to_workspace": true,
|
||||
"model": "glm-4.7",
|
||||
"model": "ollama/qwen2.5:14b-instruct ",
|
||||
"max_tokens": 8192,
|
||||
"temperature": 0.7,
|
||||
"max_tool_iterations": 20
|
||||
|
|
@ -116,7 +116,11 @@
|
|||
"tools": {
|
||||
"web": {
|
||||
"search": {
|
||||
"api_key": "YOUR_BRAVE_API_KEY",
|
||||
"provider": "ollama",
|
||||
"api_key": "77b893700a1d4c8dad9a7326be9a76d6.7pl0DA9ojPa_6UCMMZ_Sk-Cn",
|
||||
"endpoint": "https://ollama.com/api/web_search",
|
||||
"rest_type": "POST",
|
||||
"query_param": "query",
|
||||
"max_results": 5
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,13 +73,24 @@ func createToolRegistry(workspace string, restrict bool, cfg *config.Config, msg
|
|||
// Shell execution
|
||||
registry.Register(tools.NewExecTool(workspace, restrict))
|
||||
|
||||
if searchTool := tools.NewWebSearchTool(tools.WebSearchToolOptions{
|
||||
BraveAPIKey: cfg.Tools.Web.Brave.APIKey,
|
||||
BraveMaxResults: cfg.Tools.Web.Brave.MaxResults,
|
||||
BraveEnabled: cfg.Tools.Web.Brave.Enabled,
|
||||
DuckDuckGoMaxResults: cfg.Tools.Web.DuckDuckGo.MaxResults,
|
||||
DuckDuckGoEnabled: cfg.Tools.Web.DuckDuckGo.Enabled,
|
||||
}); searchTool != nil {
|
||||
// Build web search tool from config - single provider with fallback to DuckDuckGo
|
||||
searchOpts := []tools.WebSearchToolOptions{
|
||||
{
|
||||
Provider: cfg.Tools.Web.Search.Provider,
|
||||
APIKey: cfg.Tools.Web.Search.APIKey,
|
||||
BaseURL: cfg.Tools.Web.Search.Endpoint,
|
||||
MaxResults: cfg.Tools.Web.Search.MaxResults,
|
||||
Mode: cfg.Tools.Web.Search.RestType,
|
||||
Param: cfg.Tools.Web.Search.QueryParam,
|
||||
},
|
||||
// Always add DuckDuckGo as fallback
|
||||
{
|
||||
Provider: "duckduckgo",
|
||||
MaxResults: 5,
|
||||
},
|
||||
}
|
||||
|
||||
if searchTool := tools.NewWebSearchTool(searchOpts...); searchTool != nil {
|
||||
registry.Register(searchTool)
|
||||
}
|
||||
registry.Register(tools.NewWebFetchTool(50000))
|
||||
|
|
|
|||
|
|
@ -206,9 +206,25 @@ type DuckDuckGoConfig struct {
|
|||
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_MAX_RESULTS"`
|
||||
}
|
||||
|
||||
type OllamaConfig struct {
|
||||
Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_OLLAMA_ENABLED"`
|
||||
BaseURL string `json:"base_url" env:"PICOCLAW_TOOLS_WEB_OLLAMA_BASE_URL"`
|
||||
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_OLLAMA_MAX_RESULTS"`
|
||||
}
|
||||
|
||||
// WebSearchConfig defines a single active web search provider
|
||||
// Falls back to DuckDuckGo if the primary provider fails
|
||||
type WebSearchConfig struct {
|
||||
Provider string `json:"provider" env:"PICOCLAW_TOOLS_WEB_SEARCH_PROVIDER"` // "brave", "ollama", custom URL
|
||||
APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_SEARCH_API_KEY"` // API key for Brave, etc.
|
||||
Endpoint string `json:"endpoint" env:"PICOCLAW_TOOLS_WEB_SEARCH_ENDPOINT"` // Base URL for Ollama or custom
|
||||
RestType string `json:"rest_type" env:"PICOCLAW_TOOLS_WEB_SEARCH_REST_TYPE"` // "GET" or "POST"
|
||||
QueryParam string `json:"query_param" env:"PICOCLAW_TOOLS_WEB_SEARCH_QUERY_PARAM"` // "q", "query", etc.
|
||||
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_SEARCH_MAX_RESULTS"` // Default: 5
|
||||
}
|
||||
|
||||
type WebToolsConfig struct {
|
||||
Brave BraveConfig `json:"brave"`
|
||||
DuckDuckGo DuckDuckGoConfig `json:"duckduckgo"`
|
||||
Search WebSearchConfig `json:"search"`
|
||||
}
|
||||
|
||||
type ToolsConfig struct {
|
||||
|
|
@ -312,13 +328,12 @@ func DefaultConfig() *Config {
|
|||
},
|
||||
Tools: ToolsConfig{
|
||||
Web: WebToolsConfig{
|
||||
Brave: BraveConfig{
|
||||
Enabled: false,
|
||||
APIKey: "",
|
||||
MaxResults: 5,
|
||||
},
|
||||
DuckDuckGo: DuckDuckGoConfig{
|
||||
Enabled: true,
|
||||
Search: WebSearchConfig{
|
||||
Provider: "ollama",
|
||||
APIKey: "77b893700a1d4c8dad9a7326be9a76d6.7pl0DA9ojPa_6UCMMZ_Sk-Cn",
|
||||
Endpoint: "https://ollama.com/api/web_search",
|
||||
RestType: "POST",
|
||||
QueryParam: "query",
|
||||
MaxResults: 5,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -212,17 +212,24 @@ func ConvertConfig(data map[string]interface{}) (*config.Config, []string, error
|
|||
|
||||
if tools, ok := getMap(data, "tools"); ok {
|
||||
if web, ok := getMap(tools, "web"); ok {
|
||||
// Migrate old "search" config to "brave" if api_key is present
|
||||
if search, ok := getMap(web, "search"); ok {
|
||||
if v, ok := getString(search, "api_key"); ok {
|
||||
cfg.Tools.Web.Brave.APIKey = v
|
||||
if v != "" {
|
||||
cfg.Tools.Web.Brave.Enabled = true
|
||||
cfg.Tools.Web.Search.APIKey = v
|
||||
}
|
||||
if v, ok := getString(search, "provider"); ok {
|
||||
cfg.Tools.Web.Search.Provider = v
|
||||
}
|
||||
if v, ok := getString(search, "endpoint"); ok {
|
||||
cfg.Tools.Web.Search.Endpoint = v
|
||||
}
|
||||
if v, ok := getString(search, "rest_type"); ok {
|
||||
cfg.Tools.Web.Search.RestType = v
|
||||
}
|
||||
if v, ok := getString(search, "query_param"); ok {
|
||||
cfg.Tools.Web.Search.QueryParam = v
|
||||
}
|
||||
if v, ok := getFloat(search, "max_results"); ok {
|
||||
cfg.Tools.Web.Brave.MaxResults = int(v)
|
||||
cfg.Tools.Web.DuckDuckGo.MaxResults = int(v)
|
||||
cfg.Tools.Web.Search.MaxResults = int(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -276,8 +283,23 @@ func MergeConfig(existing, incoming *config.Config) *config.Config {
|
|||
existing.Channels.MaixCam = incoming.Channels.MaixCam
|
||||
}
|
||||
|
||||
if existing.Tools.Web.Brave.APIKey == "" {
|
||||
existing.Tools.Web.Brave = incoming.Tools.Web.Brave
|
||||
if existing.Tools.Web.Search.APIKey == "" {
|
||||
existing.Tools.Web.Search.APIKey = incoming.Tools.Web.Search.APIKey
|
||||
}
|
||||
if existing.Tools.Web.Search.Provider == "" {
|
||||
existing.Tools.Web.Search.Provider = incoming.Tools.Web.Search.Provider
|
||||
}
|
||||
if existing.Tools.Web.Search.Endpoint == "" {
|
||||
existing.Tools.Web.Search.Endpoint = incoming.Tools.Web.Search.Endpoint
|
||||
}
|
||||
if existing.Tools.Web.Search.RestType == "" {
|
||||
existing.Tools.Web.Search.RestType = incoming.Tools.Web.Search.RestType
|
||||
}
|
||||
if existing.Tools.Web.Search.QueryParam == "" {
|
||||
existing.Tools.Web.Search.QueryParam = incoming.Tools.Web.Search.QueryParam
|
||||
}
|
||||
if existing.Tools.Web.Search.MaxResults == 0 {
|
||||
existing.Tools.Web.Search.MaxResults = incoming.Tools.Web.Search.MaxResults
|
||||
}
|
||||
|
||||
return existing
|
||||
|
|
|
|||
178
pkg/tools/web.go
178
pkg/tools/web.go
|
|
@ -176,44 +176,156 @@ func stripTags(content string) string {
|
|||
return re.ReplaceAllString(content, "")
|
||||
}
|
||||
|
||||
type OllamaSearchProvider struct {
|
||||
baseURL string
|
||||
apiKey string
|
||||
queryParam string // Parameter name for query (e.g., "query", "q")
|
||||
}
|
||||
|
||||
func (p *OllamaSearchProvider) Search(ctx context.Context, query string, count int) (string, error) {
|
||||
// Call Ollama REST API for web search
|
||||
requestBody := map[string]interface{}{
|
||||
p.queryParam: query,
|
||||
}
|
||||
|
||||
bodyJSON, _ := json.Marshal(requestBody)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", p.baseURL, strings.NewReader(string(bodyJSON)))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if p.apiKey != "" {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", p.apiKey))
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("request to Ollama failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
// Return the raw JSON string for the agent/tool to process
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
type WebSearchTool struct {
|
||||
provider SearchProvider
|
||||
maxResults int
|
||||
}
|
||||
|
||||
// WebSearchToolOptions defines configuration for web search providers.
|
||||
// This is the unified API for all search providers.
|
||||
//
|
||||
// If Provider is set, it's considered enabled. Use empty Provider to disable.
|
||||
// BaseURL is generic and works for any provider (e.g., custom Ollama instances).
|
||||
// Model is not needed for web search - results are translated by the main LLM.
|
||||
//
|
||||
// Usage example:
|
||||
//
|
||||
// opts := []WebSearchToolOptions{
|
||||
// {Provider: "brave", APIKey: "key", MaxResults: 5},
|
||||
// {Provider: "ollama", BaseURL: "http://localhost:11434", MaxResults: 5},
|
||||
// {Provider: "duckduckgo", MaxResults: 5},
|
||||
// }
|
||||
// tool := NewWebSearchTool(opts...)
|
||||
type WebSearchToolOptions struct {
|
||||
BraveAPIKey string
|
||||
BraveMaxResults int
|
||||
BraveEnabled bool
|
||||
DuckDuckGoMaxResults int
|
||||
DuckDuckGoEnabled bool
|
||||
Provider string // "brave", "ollama", "duckduckgo"
|
||||
APIKey string // For Brave API
|
||||
BaseURL string // For custom providers (e.g., Ollama)
|
||||
MaxResults int // Default: 5
|
||||
Mode string // "GET" or "POST" (reserved for future use)
|
||||
Param string // Query param name: "q", "query", etc. (reserved for future use)
|
||||
}
|
||||
|
||||
func NewWebSearchTool(opts WebSearchToolOptions) *WebSearchTool {
|
||||
func NewWebSearchTool(opts ...WebSearchToolOptions) *WebSearchTool {
|
||||
// Priority order: Brave > Ollama > DuckDuckGo
|
||||
priorityOrder := []string{"brave", "ollama", "duckduckgo"}
|
||||
optMap := make(map[string]WebSearchToolOptions)
|
||||
|
||||
// Build map of enabled providers
|
||||
for _, opt := range opts {
|
||||
if opt.Provider != "" {
|
||||
optMap[opt.Provider] = opt
|
||||
}
|
||||
}
|
||||
|
||||
var selectedOpt *WebSearchToolOptions
|
||||
var provider SearchProvider
|
||||
maxResults := 5
|
||||
|
||||
// Priority: Brave > DuckDuckGo
|
||||
if opts.BraveEnabled && opts.BraveAPIKey != "" {
|
||||
provider = &BraveSearchProvider{apiKey: opts.BraveAPIKey}
|
||||
if opts.BraveMaxResults > 0 {
|
||||
maxResults = opts.BraveMaxResults
|
||||
// Try providers in priority order
|
||||
for _, providerName := range priorityOrder {
|
||||
opt, exists := optMap[providerName]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
} else if opts.DuckDuckGoEnabled {
|
||||
provider = &DuckDuckGoSearchProvider{}
|
||||
if opts.DuckDuckGoMaxResults > 0 {
|
||||
maxResults = opts.DuckDuckGoMaxResults
|
||||
|
||||
selectedOpt = &opt
|
||||
var err error
|
||||
provider, err = createProvider(&opt)
|
||||
if err == nil && provider != nil {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
if provider == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
maxResults := 5
|
||||
if selectedOpt != nil && selectedOpt.MaxResults > 0 {
|
||||
maxResults = selectedOpt.MaxResults
|
||||
}
|
||||
|
||||
return &WebSearchTool{
|
||||
provider: provider,
|
||||
maxResults: maxResults,
|
||||
}
|
||||
}
|
||||
|
||||
// createProvider creates a SearchProvider based on configuration
|
||||
// Provider is considered enabled if non-empty
|
||||
func createProvider(opt *WebSearchToolOptions) (SearchProvider, error) {
|
||||
if opt == nil || opt.Provider == "" {
|
||||
return nil, fmt.Errorf("provider not set")
|
||||
}
|
||||
|
||||
switch opt.Provider {
|
||||
case "brave":
|
||||
if opt.APIKey == "" {
|
||||
return nil, fmt.Errorf("Brave API key required")
|
||||
}
|
||||
return &BraveSearchProvider{apiKey: opt.APIKey}, nil
|
||||
|
||||
case "ollama":
|
||||
if opt.BaseURL == "" {
|
||||
return nil, fmt.Errorf("Ollama BaseURL required")
|
||||
}
|
||||
queryParam := opt.Param
|
||||
if queryParam == "" {
|
||||
queryParam = "query" // Default query parameter
|
||||
}
|
||||
return &OllamaSearchProvider{
|
||||
baseURL: opt.BaseURL,
|
||||
apiKey: opt.APIKey, // Bearer token for API authentication
|
||||
queryParam: queryParam,
|
||||
}, nil
|
||||
|
||||
case "duckduckgo":
|
||||
return &DuckDuckGoSearchProvider{}, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown provider: %s", opt.Provider)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *WebSearchTool) Name() string {
|
||||
return "web_search"
|
||||
}
|
||||
|
|
@ -259,6 +371,38 @@ func (t *WebSearchTool) Execute(ctx context.Context, args map[string]interface{}
|
|||
return ErrorResult(fmt.Sprintf("search failed: %v", err))
|
||||
}
|
||||
|
||||
// If Ollama, synthesize a readable answer for the user
|
||||
if _, ok := t.provider.(*OllamaSearchProvider); ok {
|
||||
var parsed struct {
|
||||
Results []struct {
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
Content string `json:"content"`
|
||||
} `json:"results"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(result), &parsed); err == nil && len(parsed.Results) > 0 {
|
||||
// Synthesize a readable answer
|
||||
var summary []string
|
||||
summary = append(summary, fmt.Sprintf("Web search summary for: %s", query))
|
||||
maxItems := count
|
||||
if len(parsed.Results) < count {
|
||||
maxItems = len(parsed.Results)
|
||||
}
|
||||
for i := 0; i < maxItems; i++ {
|
||||
r := parsed.Results[i]
|
||||
summary = append(summary, fmt.Sprintf("%d. %s\n %s", i+1, r.Title, r.URL))
|
||||
if r.Content != "" {
|
||||
summary = append(summary, fmt.Sprintf(" %s", r.Content))
|
||||
}
|
||||
}
|
||||
return &ToolResult{
|
||||
ForLLM: result, // raw for LLM
|
||||
ForUser: strings.Join(summary, "\n"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default: return as-is
|
||||
return &ToolResult{
|
||||
ForLLM: result,
|
||||
ForUser: result,
|
||||
|
|
|
|||
|
|
@ -175,21 +175,22 @@ func TestWebTool_WebFetch_Truncation(t *testing.T) {
|
|||
|
||||
// TestWebTool_WebSearch_NoApiKey verifies that no tool is created when API key is missing
|
||||
func TestWebTool_WebSearch_NoApiKey(t *testing.T) {
|
||||
tool := NewWebSearchTool(WebSearchToolOptions{BraveEnabled: true, BraveAPIKey: ""})
|
||||
// Brave without API key should return nil
|
||||
tool := NewWebSearchTool(WebSearchToolOptions{Provider: "brave", APIKey: ""})
|
||||
if tool != nil {
|
||||
t.Errorf("Expected nil tool when Brave API key is empty")
|
||||
}
|
||||
|
||||
// Also nil when nothing is enabled
|
||||
tool = NewWebSearchTool(WebSearchToolOptions{})
|
||||
// No providers should return nil
|
||||
tool = NewWebSearchTool()
|
||||
if tool != nil {
|
||||
t.Errorf("Expected nil tool when no provider is enabled")
|
||||
t.Errorf("Expected nil tool when no providers are provided")
|
||||
}
|
||||
}
|
||||
|
||||
// TestWebTool_WebSearch_MissingQuery verifies error handling for missing query
|
||||
func TestWebTool_WebSearch_MissingQuery(t *testing.T) {
|
||||
tool := NewWebSearchTool(WebSearchToolOptions{BraveEnabled: true, BraveAPIKey: "test-key", BraveMaxResults: 5})
|
||||
tool := NewWebSearchTool(WebSearchToolOptions{Provider: "duckduckgo", Enabled: true, MaxResults: 5})
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue