Files

152 lines
3.8 KiB
Go

package searxng
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"net/url"
"strings"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
type SearxngConfig struct {
URL string `json:"url"`
}
type SearxngTool struct {
}
func (t *SearxngTool) Name() string {
return "searxng"
}
func (t *SearxngTool) Description() string {
return "Search the web using a SearXNG instance. Returns a list of relevant results with title, URL, and snippet."
}
func (t *SearxngTool) Params() []domain.ToolParam {
return []domain.ToolParam{
{JsonName: "url", Type: "string", Mandatory: true, Description: "Searxng URL"},
}
}
func (t *SearxngTool) Definition(ctx context.Context) *domain.ToolDefinition {
return &domain.ToolDefinition{
Type: "function",
Function: &domain.ToolFunction{
Name: t.Name(),
Description: t.Description(),
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"query": map[string]any{
"type": "string",
"description": "The search query",
},
"categories": map[string]any{
"type": "string",
"description": "Comma-separated list of categories: general, news, images, science, it, social_media. Defaults to 'general'.",
},
"language": map[string]any{
"type": "string",
"description": "Language code for results, e.g. 'fr', 'en'. Defaults to 'all'.",
},
},
"required": []string{"query"},
},
},
}
}
type searxngResponse struct {
Results []searxngResult `json:"results"`
}
type searxngResult struct {
Title string `json:"title"`
URL string `json:"url"`
Content string `json:"content"`
Engine string `json:"engine"`
}
func (t *SearxngTool) Execute(ctx context.Context, input []byte, configuration string) ([]byte, error) {
slog.Debug("[searxng]", "input", string(input))
var params struct {
Query string `json:"query"`
Categories string `json:"categories"`
Language string `json:"language"`
}
if err := json.Unmarshal(input, &params); err != nil {
return nil, fmt.Errorf("invalid parameters: %w", err)
}
var toolCfg SearxngConfig
err := json.Unmarshal([]byte(configuration), &toolCfg)
if err != nil {
return nil, fmt.Errorf("searxng tool not configured: %w", err)
}
if toolCfg.URL == "" {
return nil, fmt.Errorf("searxng tool config missing 'url' field")
}
baseURL := toolCfg.URL
categories := "general"
if params.Categories != "" {
categories = params.Categories
}
language := "all"
if params.Language != "" {
language = params.Language
}
reqURL := fmt.Sprintf("%s/search?q=%s&format=json&categories=%s&language=%s",
strings.TrimRight(baseURL, "/"),
url.QueryEscape(params.Query),
url.QueryEscape(categories),
url.QueryEscape(language),
)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to build request: %w", err)
}
req.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("searxng request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("searxng returned status %d", resp.StatusCode)
}
var searchResp searxngResponse
if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
if len(searchResp.Results) == 0 {
return []byte("No results found."), nil
}
var sb strings.Builder
fmt.Fprintf(&sb, "Search results for \"%s\":\n\n", params.Query)
for i, r := range searchResp.Results {
fmt.Fprintf(&sb, "%d. %s\n", i+1, r.Title)
fmt.Fprintf(&sb, " URL: %s\n", r.URL)
if r.Content != "" {
fmt.Fprintf(&sb, " %s\n", r.Content)
}
fmt.Fprintf(&sb, "\n")
}
return []byte(sb.String()), nil
}