mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-16 19:59:56 +02:00
Les outils web des tours de la façade (web_search, web_images, web_open,
web_read, web_grep) ne passent plus par /tool/exec du Go : src/webtools.rs
porte chat_internet_tools.go, chat_internet.go et chat_web_images.go. Le
cache des pages (10 min, clé MD5 des options comme le Go) est dans la
façade ; client Crawl4AI au corps près, réponses lues comme json.Unmarshal ;
résultats DuckDuckGo tirés du markdown (uddg, entités, extraits) ;
recherche d'images avec le jeton vqd et les cookies gardés par hôte ;
web_grep sur les regex du Go, erreurs au texte de regexp.
Le moteur intégré lit le HTML avec go-readability et html-to-markdown, que
rien en Rust ne reproduit au caractère près : la conversion d'une page et la
lecture des résultats restent au Go, derrière /internal/v1/web/fetch et
/internal/v1/web/search (sans état). web_screenshot reste au Go.
Le traducteur go_regex refuse la répétition d'une répétition et les comptes
au-delà de 1000, et rend littéral un « { » qui n'ouvre pas de répétition,
comme le Go (motifs retry_patterns compris).
Parité : journal de 74 appels tiré du Go (parity_web_tools_test.go →
tests/parity_web/outils.json : résultats, requêtes reçues par les faux
Crawl4AI et DuckDuckGo, pages demandées au moteur intégré), rejoué par
tests/webtools.rs ; un tour de la façade qui ouvre une page (tests/lot6.rs) ;
TestInternalMoteurWebIntegre côté Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
143 lines
4.0 KiB
Go
143 lines
4.0 KiB
Go
package loki
|
|
|
|
// web_images : la recherche d'images DuckDuckGo, pendant de web_search. Sans
|
|
// elle, « montre-moi des photos de X » obligeait le modèle à ouvrir des pages
|
|
// dans le navigateur et à en renvoyer des captures (lent, et des images de
|
|
// pages au lieu de photos).
|
|
//
|
|
// DuckDuckGo sert les images par i.js, qui exige un jeton « vqd » lu dans la
|
|
// page de recherche. Même client HTTP que le moteur intégré, quel que soit le
|
|
// moteur web actif : Crawl4AI n'apporterait rien à un appel JSON.
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
func webImagesTool() Tool {
|
|
return Tool{Type: "function", Function: ToolFunction{
|
|
// Schéma minimal : le préambule a un budget (chat_prompt_size_test.go).
|
|
// limit n'est pas annoncé ; toolWebImages l'accepte encore (6 par défaut).
|
|
Name: "web_images",
|
|
Description: "Find images.",
|
|
Parameters: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"query": map[string]any{"type": "string"},
|
|
},
|
|
"required": []string{"query"},
|
|
},
|
|
}}
|
|
}
|
|
|
|
type imageResult struct {
|
|
Image string `json:"image"`
|
|
Title string `json:"title"`
|
|
URL string `json:"url"`
|
|
Width int `json:"width"`
|
|
Height int `json:"height"`
|
|
}
|
|
|
|
var ddgVqdRe = regexp.MustCompile(`vqd=["']?([0-9-]+)`)
|
|
|
|
// ddgImagesBase : DuckDuckGo (un faux pour les tests).
|
|
var ddgImagesBase = "https://duckduckgo.com"
|
|
|
|
func ddgGet(ctx context.Context, target, referer string) ([]byte, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("User-Agent", goFetchUA)
|
|
req.Header.Set("Accept-Language", "fr-FR,fr;q=0.9,en;q=0.8")
|
|
if referer != "" {
|
|
req.Header.Set("Referer", referer)
|
|
req.Header.Set("Accept", "application/json, text/javascript, */*; q=0.01")
|
|
}
|
|
resp, err := goHTTPClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("échec de la requête : %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
b, err := io.ReadAll(io.LimitReader(resp.Body, goFetchMaxBytes))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode >= 400 {
|
|
return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
func duckduckgoImages(query string, limit int) ([]imageResult, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), goFetchTimeout)
|
|
defer cancel()
|
|
page := ddgImagesBase + "/?q=" + url.QueryEscape(query) + "&iax=images&ia=images"
|
|
body, err := ddgGet(ctx, page, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
m := ddgVqdRe.FindSubmatch(body)
|
|
if m == nil {
|
|
return nil, fmt.Errorf("jeton DuckDuckGo introuvable (défi anti-bot ?)")
|
|
}
|
|
api := ddgImagesBase + "/i.js?l=fr-fr&o=json&f=,,,,,&p=1&q=" + url.QueryEscape(query) + "&vqd=" + string(m[1])
|
|
body, err = ddgGet(ctx, api, "https://duckduckgo.com/")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var r struct {
|
|
Results []imageResult `json:"results"`
|
|
}
|
|
if err := json.Unmarshal(body, &r); err != nil {
|
|
return nil, fmt.Errorf("réponse illisible : %v", err)
|
|
}
|
|
var out []imageResult
|
|
seen := map[string]bool{}
|
|
for _, it := range r.Results {
|
|
if it.Image == "" || seen[it.Image] || !strings.HasPrefix(it.Image, "http") {
|
|
continue
|
|
}
|
|
seen[it.Image] = true
|
|
out = append(out, it)
|
|
if len(out) >= limit {
|
|
break
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func toolWebImages(args map[string]any) string {
|
|
query, _ := args["query"].(string)
|
|
limit := 6
|
|
if v, ok := args["limit"].(float64); ok {
|
|
limit = int(v)
|
|
}
|
|
if limit < 1 {
|
|
limit = 1
|
|
}
|
|
if limit > 20 {
|
|
limit = 20
|
|
}
|
|
results, err := duckduckgoImages(query, limit)
|
|
if err != nil {
|
|
return "❌ Recherche d'images échouée : " + err.Error()
|
|
}
|
|
if len(results) == 0 {
|
|
return fmt.Sprintf("Aucune image pour « %s »", query)
|
|
}
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "# Images : %s\n%d résultat(s) DuckDuckGo\n\n", query, len(results))
|
|
for i, r := range results {
|
|
fmt.Fprintf(&b, "%d. %s (%dx%d)\n image : %s\n page : %s\n\n", i+1, r.Title, r.Width, r.Height, r.Image, r.URL)
|
|
}
|
|
b.WriteString("To show an image to the user, write  in your answer.")
|
|
return b.String()
|
|
}
|