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
This commit is contained in:
Claude committed 2026-10-16 05:29:35 +00:00
1 parent bed6d07739
commit f66d1b7621
19 files changed
+4561 -7

No files matched your search

+84
View File
@@ -968,8 +968,67 @@ pub fn go_regex(p: &str) -> Option<String> {
let cs: Vec<char> = p.chars().collect();
let mut out = String::with_capacity(p.len() + 16);
let (mut i, mut class, mut class_start) = (0, false, false);
// Répétition qui précède : 0 aucune, 1 une répétition (un « ? » la
// rend paresseuse), 2 déjà paresseuse. Le Go refuse d'en répéter une.
let mut rep = 0u8;
while i < cs.len() {
let c = cs[i];
if !class {
match c {
'*' | '+' | '?' => {
if rep == 1 && c == '?' {
rep = 2;
} else if rep != 0 {
return None;
} else {
rep = 1;
}
out.push(c);
i += 1;
continue;
}
'{' => {
// `{n}`, `{n,}`, `{n,m}` (1000 au plus) ; sinon un « { »
// littéral pour le Go.
let rest: String = cs[i + 1..].iter().collect();
let body = rest.split_once('}').map(|(b, _)| b);
let parsed = body.and_then(|b| {
let (lo, hi) = match b.split_once(',') {
Some((lo, hi)) => (lo, Some(hi)),
None => (b, None),
};
let num = |t: &str| {
(!t.is_empty() && t.bytes().all(|d| d.is_ascii_digit()))
.then(|| t.parse::<u64>().unwrap_or(u64::MAX))
};
let lo = num(lo)?;
let hi = match hi {
None => Some(lo),
Some("") => None,
Some(h) => Some(num(h)?),
};
Some((b.chars().count(), lo, hi))
});
match parsed {
Some((len, lo, hi)) => {
if lo > 1000 || hi.is_some_and(|h| h > 1000 || h < lo) || rep != 0 {
return None;
}
rep = 1;
out.extend(&cs[i..i + len + 2]);
i += len + 2;
}
None => {
rep = 0;
out.push_str(r"\{");
i += 1;
}
}
continue;
}
_ => rep = 0,
}
}
if c == '\\' && i + 1 < cs.len() {
let n = cs[i + 1];
i += 2;
@@ -990,6 +1049,18 @@ pub fn go_regex(p: &str) -> Option<String> {
class_start = false;
continue;
}
// `\p{…}`, `\P{…}`, `\x{…}` : les accolades sont à l'échappement.
if matches!(n, 'p' | 'P' | 'x')
&& cs.get(i) == Some(&'{')
&& let Some(k) = cs[i..].iter().position(|&c| c == '}')
{
out.push('\\');
out.push(n);
out.extend(&cs[i..=i + k]);
i += k + 1;
class_start = false;
continue;
}
// Un chiffre seul est une référence arrière pour le Go, qui la
// refuse ; avec l'octal permis, `regex` l'accepterait.
if ('1'..='7').contains(&n) && !cs.get(i).is_some_and(|d| ('0'..='7').contains(d)) {
@@ -2872,6 +2943,19 @@ mod tests {
assert_eq!(m(r"\Q(x", "a(x"), Some("(x".into()));
// Le Go refuse \Q dans une classe.
assert_eq!(go_regex(r"[\Q]-\E]+"), None);
// Répétition d'une répétition : refusée par le Go ; « ? » paresseux
// permis une fois ; « { » hors répétition littéral ; bornes ≤ 1000.
for p in ["a**", "a+*", "a*??", "a{2}*", "a*{2}", "a{1001}", "a{3,2}"] {
assert_eq!(go_regex(p), None, "{p}");
}
assert_eq!(go_regex("a*?b+?c??").as_deref(), Some("a*?b+?c??"));
assert_eq!(go_regex("a{2,}{").as_deref(), Some(r"a{2,}\{"));
assert_eq!(go_regex("x{a}").as_deref(), Some(r"x\{a}"));
assert!(compile_go_regex("x{,3}").unwrap().is_match("x{,3}"));
assert_eq!(
go_regex(r"\p{Greek}+\x{41}{2}").as_deref(),
Some(r"\p{Greek}+\x{41}{2}")
);
// Dans une classe, « [ », « && », « -- » et « ~~ » sont littéraux.
assert_eq!(m(r"[a[]+", "x[a["), Some("[a[".into()));
assert_eq!(m(r"[a&&b]+", "x&ab"), Some("&ab".into()));