mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-21 05:58:13 +02:00
Lot 6 : le tour branché sur le moteur, les outils et la base (chat::host)
GoTurnHost est le TurnHost de la façade : envoi par EngineHttp (clé de l'endpoint, pannes classées comme friendlyLLMError), outils par l'API du Go avec leurs suites (capture, aperçu, image relayée), résultats complets confiés à l'écrivain des discussions (id « <discussion>.<aléa> »), compaction par Compactor avec un résumeur qui interroge le moteur (EngineSummarizer) et l'archive recall du Go (StoreRecall), images jointes lues sur disque et ouvertes par le Go, en cache borné à 64 Mio. RecallArchive devient asynchrone ; la boucle consulte la mémoire des refus de compaction avant d'annoncer une compaction en cours de tour. Écarts : pas de résumé en continuation (état du slot tenu par le Go), ni conseil de cache, ni garde d'écriture du mode Code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
This commit is contained in:
7 files changed
+843
-7
No files matched your search
@@ -37,6 +37,9 @@ pub struct FakeGo {
|
||||
pub lease_stop: Mutex<bool>,
|
||||
pub go_busy: Mutex<Option<String>>,
|
||||
pub lease_log: Mutex<Vec<String>>,
|
||||
/// Résultat imposé à tout outil (sinon « exec:<nom> »), et archive recall.
|
||||
pub tool_result: Mutex<Option<String>>,
|
||||
pub recall: Mutex<Vec<String>>,
|
||||
/// Adresse d'écoute du faux Go (pour un client `Store` direct).
|
||||
pub backend: Mutex<Option<SocketAddr>>,
|
||||
}
|
||||
@@ -166,6 +169,16 @@ async fn handle(st: Arc<FakeGo>, req: Request<Incoming>) -> Response<Full<Bytes>
|
||||
let stop = *st.lease_stop.lock().unwrap();
|
||||
resp(200, serde_json::json!({"stop": stop}).to_string())
|
||||
}
|
||||
"/internal/v1/recall/archive" => {
|
||||
let body = req.into_body().collect().await.unwrap().to_bytes();
|
||||
let v: serde_json::Value = serde_json::from_slice(&body).unwrap_or_default();
|
||||
let mut r = st.recall.lock().unwrap();
|
||||
r.push(v["content"].as_str().unwrap_or_default().to_string());
|
||||
resp(
|
||||
200,
|
||||
serde_json::json!({"id": format!("r{}", r.len())}).to_string(),
|
||||
)
|
||||
}
|
||||
"/internal/v1/engine/stop" => {
|
||||
st.lease_log.lock().unwrap().push("stop".into());
|
||||
*st.go_busy.lock().unwrap() = None;
|
||||
@@ -174,9 +187,15 @@ async fn handle(st: Arc<FakeGo>, req: Request<Incoming>) -> Response<Full<Bytes>
|
||||
"/internal/v1/tool/exec" => {
|
||||
let body = req.into_body().collect().await.unwrap().to_bytes();
|
||||
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||
let result = st
|
||||
.tool_result
|
||||
.lock()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.unwrap_or_else(|| format!("exec:{}", v["name"].as_str().unwrap()));
|
||||
let out = serde_json::json!({
|
||||
"label": v["args"]["command"],
|
||||
"result": format!("exec:{}", v["name"].as_str().unwrap()),
|
||||
"result": result,
|
||||
"caps_agent": v["caps"]["Agent"],
|
||||
});
|
||||
resp(200, out.to_string())
|
||||
|
||||
@@ -415,3 +415,198 @@ async fn supprimer_l_ouverte_puis_la_derniere() {
|
||||
assert_eq!(idx[0].id, c);
|
||||
assert_eq!(s.delete("inconnue").await, Err(SessionError::NotFound));
|
||||
}
|
||||
|
||||
// ---- un tour complet, branché ----
|
||||
|
||||
use loki_rs::chat::compact::{CompactCaps, CompactConfig, CompactMemory};
|
||||
use loki_rs::chat::engine::EngineHttp;
|
||||
use loki_rs::chat::host::{ChatShared, EngineSummarizer, GoTurnHost, ImageCache, TurnCompact};
|
||||
use loki_rs::chat::stream::RetryPatterns;
|
||||
use loki_rs::chat::turn::{Cancel, TurnConfig, run_turn};
|
||||
use loki_rs::chat::types::{Message, StreamEvent, Tool, ToolFunction};
|
||||
use loki_rs::chat::wire::{ChatEndpoint, EchoRefusals, EffortFallbacks, TurnReasoning};
|
||||
|
||||
/// Faux moteur : rend les réponses SSE dans l'ordre, garde les requêtes.
|
||||
async fn fake_engine(
|
||||
replies: Vec<String>,
|
||||
) -> (String, Arc<StdMutex<Vec<(String, serde_json::Value)>>>) {
|
||||
use http_body_util::{BodyExt, Full};
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let seen = Arc::new(StdMutex::new(Vec::new()));
|
||||
let replies = Arc::new(StdMutex::new(std::collections::VecDeque::from(replies)));
|
||||
let s = seen.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let Ok((stream, _)) = listener.accept().await else {
|
||||
return;
|
||||
};
|
||||
let (s, replies) = (s.clone(), replies.clone());
|
||||
tokio::spawn(async move {
|
||||
let svc = hyper::service::service_fn(
|
||||
move |req: hyper::Request<hyper::body::Incoming>| {
|
||||
let (s, replies) = (s.clone(), replies.clone());
|
||||
async move {
|
||||
let auth = req
|
||||
.headers()
|
||||
.get("authorization")
|
||||
.map(|v| v.to_str().unwrap().to_string())
|
||||
.unwrap_or_default();
|
||||
let body = req.into_body().collect().await.unwrap().to_bytes();
|
||||
s.lock()
|
||||
.unwrap()
|
||||
.push((auth, serde_json::from_slice(&body).unwrap()));
|
||||
let out = replies.lock().unwrap().pop_front().unwrap_or_default();
|
||||
Ok::<_, std::convert::Infallible>(hyper::Response::new(Full::new(
|
||||
bytes::Bytes::from(out),
|
||||
)))
|
||||
}
|
||||
},
|
||||
);
|
||||
let _ = hyper::server::conn::http1::Builder::new()
|
||||
.serve_connection(hyper_util::rt::TokioIo::new(stream), svc)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
});
|
||||
(format!("http://{addr}/v1/chat/completions"), seen)
|
||||
}
|
||||
|
||||
fn sse(chunks: &[serde_json::Value]) -> String {
|
||||
chunks
|
||||
.iter()
|
||||
.map(|c| format!("data: {c}\n\n"))
|
||||
.collect::<String>()
|
||||
+ "data: [DONE]\n\n"
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tour_complet_par_le_go() {
|
||||
let (st, go, _stop, t) = store().await;
|
||||
let long = "ligne\n".repeat(400); // > aperçu : rangé pour « voir plus »
|
||||
*go.tool_result.lock().unwrap() = Some(long.clone());
|
||||
let imgdir = t.path().join("chatimg");
|
||||
std::fs::create_dir_all(&imgdir).unwrap();
|
||||
std::fs::write(imgdir.join("p.png"), b"SCELLE:PNG").unwrap();
|
||||
let (url, seen) = fake_engine(vec![
|
||||
sse(&[
|
||||
serde_json::json!({"choices":[{"delta":{"tool_calls":[{"index":0,"id":"c1","type":"function","function":{"name":"bash","arguments":"{\"command\":\"ls\"}"}}]}}]}),
|
||||
serde_json::json!({"choices":[{"delta":{},"finish_reason":"tool_calls"}]}),
|
||||
]),
|
||||
sse(&[
|
||||
serde_json::json!({"choices":[{"delta":{"content":"Voilà."}}]}),
|
||||
serde_json::json!({"choices":[{"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":50,"completion_tokens":2}}),
|
||||
]),
|
||||
])
|
||||
.await;
|
||||
let ep = ChatEndpoint {
|
||||
url,
|
||||
model: "loki".into(),
|
||||
key: "cle-locale".into(),
|
||||
external: false,
|
||||
};
|
||||
let shared = Arc::new(ChatShared {
|
||||
http: EngineHttp::new(),
|
||||
store: st.clone(),
|
||||
persister: Persister::new(StoreSink::new(st.clone())),
|
||||
effort: EffortFallbacks::new(),
|
||||
echo_refusals: EchoRefusals::new(),
|
||||
compact_memory: CompactMemory::new(),
|
||||
images: ImageCache::new(imgdir),
|
||||
patterns: RetryPatterns::builtin(),
|
||||
});
|
||||
let cfg = std::collections::HashMap::new();
|
||||
let host = GoTurnHost {
|
||||
shared: shared.clone(),
|
||||
conv_id: "c7".into(),
|
||||
caps: serde_json::json!({"Agent": true}),
|
||||
cfg: cfg.clone(),
|
||||
port: 8080,
|
||||
summarizer: EngineSummarizer {
|
||||
http: shared.http.clone(),
|
||||
ep: ep.clone(),
|
||||
cfg: cfg.clone(),
|
||||
patterns: shared.patterns.clone(),
|
||||
port: 8080,
|
||||
},
|
||||
compact: TurnCompact {
|
||||
cfg: CompactConfig::default(),
|
||||
caps: CompactCaps::default(),
|
||||
criteria: String::new(),
|
||||
main_conv: "c7".into(),
|
||||
tools: vec![],
|
||||
effort: String::new(),
|
||||
kwargs: Default::default(),
|
||||
},
|
||||
queued: Box::new(Vec::new),
|
||||
};
|
||||
let bash = Tool {
|
||||
kind: "function".into(),
|
||||
function: ToolFunction {
|
||||
name: "bash".into(),
|
||||
description: String::new(),
|
||||
parameters: serde_json::json!({"type":"object"}),
|
||||
},
|
||||
};
|
||||
let tc = TurnConfig {
|
||||
cfg,
|
||||
ep,
|
||||
tools: vec![bash],
|
||||
temperature: 0.7,
|
||||
code: false,
|
||||
reasoning: TurnReasoning::default(),
|
||||
echo: Default::default(),
|
||||
compact: CompactConfig::default(),
|
||||
patterns: shared.patterns.clone(),
|
||||
};
|
||||
let question = Message {
|
||||
role: "user".into(),
|
||||
content: Some(serde_json::json!([
|
||||
{"type": "text", "text": "Regarde"},
|
||||
{"type": "image_url", "image_url": {"url": "loki-img:p.png"}}
|
||||
])),
|
||||
..Default::default()
|
||||
};
|
||||
let mut evs = Vec::new();
|
||||
let mut cb = |e: StreamEvent| {
|
||||
evs.push(e);
|
||||
true
|
||||
};
|
||||
let r = run_turn(&host, &tc, vec![question], &Cancel::never(), &mut cb).await;
|
||||
assert_eq!(r.err, None);
|
||||
assert_eq!(r.extra.len(), 2);
|
||||
|
||||
let seen = seen.lock().unwrap().clone();
|
||||
assert_eq!(seen.len(), 2);
|
||||
assert_eq!(seen[0].0, "Bearer cle-locale");
|
||||
// Image jointe : lue sur disque, ouverte par le Go, envoyée en data-URL.
|
||||
assert_eq!(
|
||||
seen[0].1["messages"][0]["content"][1]["image_url"]["url"],
|
||||
"data:image/png;base64,UE5H"
|
||||
);
|
||||
// Le modèle reçoit le résultat (plafonné), l'UI un aperçu et un id.
|
||||
assert_eq!(seen[1].1["messages"][2]["role"], "tool");
|
||||
let done = evs
|
||||
.iter()
|
||||
.find_map(|e| match e {
|
||||
StreamEvent::ToolUsed(t) if t.done => Some(t.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(done.result_chars, long.chars().count() as i64);
|
||||
assert!(done.result.len() < long.len());
|
||||
assert!(done.result_id.starts_with("c7."));
|
||||
shared.persister.flush().await;
|
||||
assert_eq!(
|
||||
opened(&go, BK_TOOL_RES, &done.result_id).unwrap(),
|
||||
long.as_bytes()
|
||||
);
|
||||
let answer: String = evs
|
||||
.iter()
|
||||
.filter_map(|e| match e {
|
||||
StreamEvent::Content(s) => Some(s.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(answer, "Voilà.");
|
||||
}
|
||||
Reference in new issue
Block a user