Files
Loki/internal/loki/chat_web_images.go
T
Claude f66d1b7621 Lot 42 : outils web de l'agent exécutés par la façade
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
2026-10-16 05:29:35 +00:00

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 ![title](image URL) in your answer.")
return b.String()
}