package websearch import ( "context" "encoding/json" "fmt" "net/http" "net/url" "strings" "time" "github.com/yourname/cyrene-ai/plugin-manager/internal/sdk" ) type WebSearchPlugin struct { sdk.BasePlugin client *http.Client } func NewWebSearchPlugin() *WebSearchPlugin { return &WebSearchPlugin{client: &http.Client{Timeout: 10 * time.Second}} } func (p *WebSearchPlugin) Metadata() sdk.PluginMetadata { return sdk.PluginMetadata{ Name: "web_search", DisplayName: "Web Search", Version: "1.0.0", Description: "Search the internet via DuckDuckGo Instant Answer API", Category: "network", Author: sdk.PluginAuthor{Name: "Cyrene Team"}, } } func (p *WebSearchPlugin) Tools() []sdk.Tool { return []sdk.Tool{&WebSearchTool{client: p.client}} } type WebSearchTool struct { sdk.BaseTool client *http.Client } type ddgResponse struct { Abstract string `json:"Abstract"` AbstractText string `json:"AbstractText"` Answer string `json:"Answer"` Heading string `json:"Heading"` Results []ddgTopic `json:"Results"` RelatedTopics []ddgTopic `json:"RelatedTopics"` } type ddgTopic struct { FirstURL string `json:"FirstURL"` Text string `json:"Text"` } func (t *WebSearchTool) Definition() sdk.ToolDefinition { return sdk.ToolDefinition{ ID: "web_search", Name: "web_search", DisplayName: "Web Search", Description: "Search the internet using DuckDuckGo Instant Answer API. Returns up to 5 results.", Category: "network", Complexity: sdk.ComplexitySimple, Parameters: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{"query": map[string]interface{}{"type": "string"}}, "required": []string{"query"}, }, } } func (t *WebSearchTool) Validate(args map[string]interface{}) error { if _, ok := args["query"]; !ok { return fmt.Errorf("missing required parameter: query") } return nil } func (t *WebSearchTool) Execute(_ context.Context, args map[string]interface{}) (*sdk.ToolResult, error) { query, _ := args["query"].(string) apiURL := fmt.Sprintf("https://api.duckduckgo.com/?q=%s&format=json&no_html=1", url.QueryEscape(query)) resp, err := t.client.Get(apiURL) if err != nil { return &sdk.ToolResult{ToolName: "web_search", Success: false, Error: err.Error()}, nil } defer resp.Body.Close() var result ddgResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return &sdk.ToolResult{ToolName: "web_search", Success: false, Error: err.Error()}, nil } var out strings.Builder if result.Answer != "" { out.WriteString(fmt.Sprintf("Answer: %s\n\n", result.Answer)) } if result.AbstractText != "" { text := result.AbstractText if len([]rune(text)) > 500 { text = string([]rune(text)[:500]) + "..." } out.WriteString(fmt.Sprintf("Abstract: %s\n\n", stripHTML(text))) } topics := result.Results if len(topics) == 0 { topics = result.RelatedTopics } count := 0 for _, topic := range topics { if count >= 5 { break } if topic.Text == "" { continue } out.WriteString(fmt.Sprintf("%d. %s (%s)\n", count+1, stripHTML(topic.Text), topic.FirstURL)) count++ } if out.Len() == 0 { return &sdk.ToolResult{ToolName: "web_search", Success: true, Output: "No results found for: " + query}, nil } return &sdk.ToolResult{ToolName: "web_search", Success: true, Output: out.String()}, nil } func stripHTML(s string) string { result := make([]rune, 0, len([]rune(s))) inTag := false for _, r := range s { if r == '<' { inTag = true continue } if r == '>' { inTag = false continue } if !inTag { result = append(result, r) } } return strings.TrimSpace(string(result)) }