mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
- web_images (amont v0.17.7) : recherche d'images DuckDuckGo, pendant de web_search ; les images du web s'affichent dans le fil avec la loupe, sans Referer, et disparaissent si le site les refuse. Schéma minimal pour tenir le budget du préambule. - Aperçu en direct du navigateur piloté (cu_autoshot.go) : une capture après chaque action browser_* qui change la page, gardée en RAM, jamais envoyée au modèle, retirée du journal en fin de tour ; une carte dans le fil, en fondu. - Vignettes calculées par le serveur (?thumb=, amont v0.17.9) : ~560 px au lieu de l'original (7 Ko au lieu de 550 Ko sur une capture), chargées deux par deux avec un nouvel essai ; la loupe ouvre l'original. - Un lien vers une image ([graphique](graph.png)) l'affiche au lieu d'un bouton de téléchargement. - Chrome du computer use tué avec Loki sous Linux (Pdeathsig), même en cas d'arrêt brutal : plus de navigateur orphelin dans le conteneur. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
140 lines
3.9 KiB
Go
140 lines
3.9 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-]+)`)
|
|
|
|
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 := "https://duckduckgo.com/?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 := "https://duckduckgo.com/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()
|
|
}
|