mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-19 05:07:20 +02:00
KEEP_TURN_IMAGES ne garde plus la conversation au Go. L'état du tour vit dans la façade ; les fichiers et la vision restent au Go. Go (facade_keepimg.go) : - /internal/v1/turn/keepimg : vision réellement active, budget des images gardées ; - /internal/v1/turn/keepimg/store : une image relayée rangée par référence (refRelayImage), relue à l'octet près. Rust : - chat/keepimg.rs : port de keepImages (image à l'essai, mesure par le moteur répartie au prorata des octets, budget, publication, fin de tour). - La boucle du tour : budget demandé pour le fil principal, image rangée par le Go, historique réécrit publié sans les relais non gardés ; les images gardées partent avant tout résumé (filet réactif, en cours de tour, fenêtre pleine). - Début et fin de tour : retirées si le contexte le demande ou si elles ne doivent plus partir (clé retirée, vision absente). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
299 lines
8.5 KiB
Rust
299 lines
8.5 KiB
Rust
//! Tests de l'état KEEP_TURN_IMAGES d'un tour (cas de
|
|
//! `chat_keep_images_test.go`).
|
|
|
|
use super::*;
|
|
use crate::chat::compact::IMAGE_LOST_MARKER;
|
|
use serde_json::{Value, json};
|
|
|
|
fn um(s: &str) -> Message {
|
|
Message {
|
|
role: "user".into(),
|
|
content: Some(json!(s)),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
fn tm(s: &str) -> Message {
|
|
Message {
|
|
role: "tool".into(),
|
|
tool_call_id: "v1".into(),
|
|
content: Some(json!(s)),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
fn img(url: &str) -> Message {
|
|
Message {
|
|
role: "user".into(),
|
|
content: Some(json!([
|
|
{"type": "text", "text": "Voici la capture demandée."},
|
|
{"type": "image_url", "image_url": {"url": url}}
|
|
])),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
fn stats(prompt: i64, gen_tokens: i64) -> StatsEvent {
|
|
StatsEvent {
|
|
prompt_tokens_total: prompt,
|
|
gen_tokens,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
fn kept(tokens: i64) -> Message {
|
|
let mut m = img("loki-img:aa.png");
|
|
m.img_relay = true;
|
|
m.img_tokens = tokens;
|
|
m
|
|
}
|
|
|
|
/// Une étape `see_image` : première complétion (prompt 1000, 20 générés),
|
|
/// résultat d'outil, relais rangé ; rend (état, messages, extra, base du
|
|
/// texte ajouté).
|
|
fn step(k: &mut KeepImages, stored: bool) -> (Vec<Message>, Vec<Message>, i64) {
|
|
let mut messages = vec![um("regarde img.png")];
|
|
let mut extra = Vec::new();
|
|
k.sending(messages.len());
|
|
k.observe(true, &stats(1000, 20), &mut messages, &mut extra);
|
|
let from = messages.len();
|
|
let t = tm("[ok] image chargée : img.png");
|
|
let text = msg_tokens(&t);
|
|
messages.push(t.clone());
|
|
extra.push(t);
|
|
let s = stored.then(|| (img("loki-img:aa.png"), 1000));
|
|
k.relay(
|
|
&mut messages,
|
|
&mut extra,
|
|
img("data:image/png;base64,AA=="),
|
|
from,
|
|
s,
|
|
);
|
|
k.sending(messages.len());
|
|
(messages, extra, text)
|
|
}
|
|
|
|
#[test]
|
|
fn cle() {
|
|
for (v, want) in [
|
|
("on", true),
|
|
(" OUI ", true),
|
|
("1", true),
|
|
("true", true),
|
|
("yes", true),
|
|
("off", false),
|
|
("", false),
|
|
("2", false),
|
|
] {
|
|
let cfg = HashMap::from([("KEEP_TURN_IMAGES".to_string(), v.to_string())]);
|
|
assert_eq!(keep_turn_images_on(&cfg), want, "{v:?}");
|
|
}
|
|
assert!(!keep_turn_images_on(&HashMap::new()));
|
|
}
|
|
|
|
/// Coût mesuré par le moteur : écart de prompt moins le texte ajouté ; gardée
|
|
/// par référence, dans `messages` comme dans `extra`.
|
|
#[test]
|
|
fn gardee_et_mesuree() {
|
|
let mut k = KeepImages::new(3200);
|
|
let (mut messages, mut extra, text) = step(&mut k, true);
|
|
assert_eq!(extra.len(), 2);
|
|
k.observe(
|
|
true,
|
|
&stats(1020 + text + 300, 5),
|
|
&mut messages,
|
|
&mut extra,
|
|
);
|
|
assert_eq!(extra.len(), 2);
|
|
let r = &extra[1];
|
|
assert!(r.img_relay && r.img_tokens == 300, "{r:?}");
|
|
assert_eq!(
|
|
r.content.as_ref().unwrap()[1]["image_url"]["url"],
|
|
"loki-img:aa.png"
|
|
);
|
|
assert_eq!(messages[2].img_tokens, 300);
|
|
assert_eq!(kept_image_tokens(&messages), 300);
|
|
}
|
|
|
|
/// Au-delà du budget, l'image reste éphémère : retirée d'`extra`, gardée
|
|
/// dans `messages` (le modèle l'a vue), et le tour s'arrête là.
|
|
#[test]
|
|
fn budget() {
|
|
let mut k = KeepImages::new(200);
|
|
let (mut messages, mut extra, text) = step(&mut k, true);
|
|
k.observe(
|
|
true,
|
|
&stats(1020 + text + 300, 5),
|
|
&mut messages,
|
|
&mut extra,
|
|
);
|
|
assert_eq!(extra.len(), 1);
|
|
assert_eq!(messages.len(), 3);
|
|
assert!(messages[2].img_relay && messages[2].img_tokens == 0);
|
|
assert!(k.stopped);
|
|
// Une image suivante n'est même pas rangée.
|
|
assert!(!k.wants(&messages));
|
|
}
|
|
|
|
/// Moteur sans usage, ou complétion coupée : rien à mesurer.
|
|
#[test]
|
|
fn sans_mesure() {
|
|
for (complete, prompt) in [(true, 0), (false, 2000)] {
|
|
let mut k = KeepImages::new(3200);
|
|
let (mut messages, mut extra, _) = step(&mut k, true);
|
|
k.observe(complete, &stats(prompt, 5), &mut messages, &mut extra);
|
|
assert_eq!(extra.len(), 1, "complète={complete} prompt={prompt}");
|
|
assert_eq!(kept_image_tokens(&messages), 0);
|
|
}
|
|
// Première complétion sans usage : pas de base.
|
|
let mut k = KeepImages::new(3200);
|
|
let mut messages = vec![um("q")];
|
|
let mut extra = Vec::new();
|
|
k.sending(1);
|
|
k.observe(true, &stats(0, 0), &mut messages, &mut extra);
|
|
let t = tm("[ok]");
|
|
messages.push(t.clone());
|
|
extra.push(t);
|
|
k.relay(
|
|
&mut messages,
|
|
&mut extra,
|
|
img("data:x"),
|
|
1,
|
|
Some((img("loki-img:b.png"), 9)),
|
|
);
|
|
k.sending(messages.len());
|
|
k.observe(true, &stats(5000, 5), &mut messages, &mut extra);
|
|
assert_eq!(extra.len(), 1);
|
|
}
|
|
|
|
/// Requête relancée entre-temps (reprise réseau) : la mesure ne vaut plus.
|
|
#[test]
|
|
fn requete_relancee() {
|
|
let mut k = KeepImages::new(3200);
|
|
let (mut messages, mut extra, text) = step(&mut k, true);
|
|
k.sending(messages.len());
|
|
k.observe(
|
|
true,
|
|
&stats(1020 + text + 300, 5),
|
|
&mut messages,
|
|
&mut extra,
|
|
);
|
|
assert_eq!(extra.len(), 1);
|
|
}
|
|
|
|
/// Rangement refusé : éphémère, marqué relais, et les suivantes aussi.
|
|
#[test]
|
|
fn rangement_refuse() {
|
|
let mut k = KeepImages::new(3200);
|
|
let (mut messages, extra, _) = step(&mut k, false);
|
|
assert_eq!(extra.len(), 1);
|
|
assert!(messages[2].img_relay);
|
|
assert!(k.stopped);
|
|
let mut extra = extra;
|
|
k.relay(
|
|
&mut messages,
|
|
&mut extra,
|
|
img("data:y"),
|
|
1,
|
|
Some((img("loki-img:c.png"), 5)),
|
|
);
|
|
assert_eq!(extra.len(), 1, "tour arrêté : plus rien de gardé");
|
|
assert!(messages[3].img_relay);
|
|
}
|
|
|
|
/// Deux images dans l'étape : le coût réparti au prorata des octets, la
|
|
/// dernière prend le reste ; une part sous le seuil, tout reste éphémère.
|
|
#[test]
|
|
fn partage_entre_images() {
|
|
let mut k = KeepImages::new(3200);
|
|
let mut messages = vec![um("q")];
|
|
let mut extra = Vec::new();
|
|
k.sending(1);
|
|
k.observe(true, &stats(1000, 20), &mut messages, &mut extra);
|
|
k.relay(
|
|
&mut messages,
|
|
&mut extra,
|
|
img("data:a"),
|
|
1,
|
|
Some((img("loki-img:a.png"), 3000)),
|
|
);
|
|
k.relay(
|
|
&mut messages,
|
|
&mut extra,
|
|
img("data:b"),
|
|
1,
|
|
Some((img("loki-img:b.png"), 1000)),
|
|
);
|
|
k.sending(messages.len());
|
|
k.observe(true, &stats(1020 + 401, 5), &mut messages, &mut extra);
|
|
assert_eq!(
|
|
(extra[0].img_tokens, extra[1].img_tokens),
|
|
(300, 101),
|
|
"{extra:?}"
|
|
);
|
|
let mut k = KeepImages::new(3200);
|
|
let mut messages = vec![um("q")];
|
|
let mut extra = Vec::new();
|
|
k.sending(1);
|
|
k.observe(true, &stats(1000, 20), &mut messages, &mut extra);
|
|
k.relay(
|
|
&mut messages,
|
|
&mut extra,
|
|
img("data:a"),
|
|
1,
|
|
Some((img("loki-img:a.png"), 3000)),
|
|
);
|
|
k.relay(
|
|
&mut messages,
|
|
&mut extra,
|
|
img("data:b"),
|
|
1,
|
|
Some((img("loki-img:b.png"), 10)),
|
|
);
|
|
k.sending(messages.len());
|
|
k.observe(true, &stats(1020 + 400, 5), &mut messages, &mut extra);
|
|
assert!(extra.is_empty(), "part de 1 jeton : {extra:?}");
|
|
}
|
|
|
|
/// Réécriture en cours de tour : une image non gardée n'entre pas dans
|
|
/// l'historique publié ; gardée ou déjà retirée, si. Les images à l'essai ne
|
|
/// sont plus suivies.
|
|
#[test]
|
|
fn publication() {
|
|
let mut eph = img("data:image/png;base64,AA==");
|
|
eph.img_relay = true;
|
|
let mut stub = um(&format!("Image :{IMAGE_LOST_MARKER}"));
|
|
stub.img_relay = true;
|
|
let msgs = vec![um("q"), tm("a"), eph, tm("b"), kept(50), stub];
|
|
let k = KeepImages::new(3200);
|
|
let got = publishable(Some(&k), &msgs);
|
|
assert_eq!(got.len(), 5);
|
|
assert_eq!(got[2].content, Some(Value::from("b")));
|
|
assert_eq!(publishable(None, &msgs).len(), 6, "sans la clé : tel quel");
|
|
let mut k = KeepImages::new(3200);
|
|
let (_, _, _) = step(&mut k, true);
|
|
k.rewritten();
|
|
assert!(k.stopped && k.pend.is_empty());
|
|
let mut k = KeepImages::new(3200);
|
|
k.rewritten();
|
|
assert!(!k.stopped, "rien à l'essai : le tour continue");
|
|
}
|
|
|
|
/// Fin du tour avec une image à l'essai : éphémère.
|
|
#[test]
|
|
fn fin_de_tour() {
|
|
let mut k = KeepImages::new(3200);
|
|
let (_, mut extra, _) = step(&mut k, true);
|
|
k.finish(&mut extra);
|
|
assert_eq!(extra.len(), 1);
|
|
assert_eq!(extra[0].role, "tool");
|
|
}
|
|
|
|
/// Budget déjà plein : rien à ranger.
|
|
#[test]
|
|
fn budget_plein_avant_rangement() {
|
|
let k = KeepImages::new(200);
|
|
assert!(!k.wants(&[um("q"), kept(200)]));
|
|
assert!(k.wants(&[um("q"), kept(199)]));
|
|
}
|