mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-16 11:49:43 +02:00
Lot 6 : types partagés du chat en Rust et modules réservés
Message, ToolCall, Tool, événements de flux, statistiques, journal : sérialisation JSON identique aux types Go (omitempty, contenu any). Socle commun des ports parallèles qui suivent (charge utile, flux, compactage, journal). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c744b744ee
commit
b2fc40d48e
7 files changed
+278
No files matched your search
@@ -0,0 +1 @@
|
||||
//! Lot 6 : module en cours de portage.
|
||||
@@ -0,0 +1 @@
|
||||
//! Lot 6 : module en cours de portage.
|
||||
@@ -0,0 +1,15 @@
|
||||
//! Cœur chat en Rust (lot 6) — bibliothèque, pas encore branchée.
|
||||
//!
|
||||
//! Port de `llm_client.go`, `chat_conversation.go`, `chat_compact*.go` :
|
||||
//! - [`types`] : types partagés, JSON identique au Go ;
|
||||
//! - [`wire`] : ce qui part vers le moteur (messages, charge utile) ;
|
||||
//! - [`stream`] : analyse du flux SSE du moteur (texte, raisonnement, appels
|
||||
//! d'outils, statistiques) ;
|
||||
//! - [`compact`] : estimation du contexte et compactage de l'historique ;
|
||||
//! - [`journal`] : journal d'affichage (compactage, rejeu, pagination).
|
||||
|
||||
pub mod compact;
|
||||
pub mod journal;
|
||||
pub mod stream;
|
||||
pub mod types;
|
||||
pub mod wire;
|
||||
@@ -0,0 +1 @@
|
||||
//! Lot 6 : module en cours de portage.
|
||||
@@ -0,0 +1,257 @@
|
||||
//! Types partagés du chat — sérialisation JSON identique aux types Go
|
||||
//! (`llm_client.go`, `chat_conversation.go`, `chat_diff.go`).
|
||||
//!
|
||||
//! Règles de fidélité : `omitempty` Go → `skip_serializing_if` ; un champ Go
|
||||
//! `any` (`Content`) → `Option<Value>` (nil omis, "" gardé : en Go, omitempty
|
||||
//! n'omet une interface que si elle est nil).
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
fn is_false(b: &bool) -> bool {
|
||||
!*b
|
||||
}
|
||||
|
||||
/// Un message de l'historique (vue du modèle).
|
||||
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
|
||||
pub struct Message {
|
||||
pub role: String,
|
||||
/// Texte, ou tableau de parties (texte + image_url) pour un message multimodal.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub content: Option<Value>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub tool_calls: Vec<ToolCall>,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub tool_call_id: String,
|
||||
/// Raisonnement séparé gardé avec REASONING_ECHO=on.
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub reasoning_content: String,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub reasoning_model: String,
|
||||
/// Relais d'image d'un outil (KEEP_TURN_IMAGES) ; retiré à l'envoi.
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
pub img_relay: bool,
|
||||
/// Bloc <context_update> posé par Loki (PROJ_SNAPSHOT) ; retiré à l'envoi.
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
pub ctx_upd: bool,
|
||||
/// Coût mesuré d'une image gardée (KEEP_TURN_IMAGES) — en mémoire seulement.
|
||||
#[serde(skip)]
|
||||
pub img_tokens: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
|
||||
pub struct ToolCall {
|
||||
/// Position dans un flux (delta OpenAI) ; absente de l'historique.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub index: Option<i64>,
|
||||
#[serde(default)]
|
||||
pub id: String,
|
||||
#[serde(default, rename = "type")]
|
||||
pub kind: String,
|
||||
#[serde(default)]
|
||||
pub function: ToolCallFunc,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
|
||||
pub struct ToolCallFunc {
|
||||
#[serde(default)]
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub arguments: String,
|
||||
}
|
||||
|
||||
/// Définition d'un outil annoncée au modèle (schéma OpenAI).
|
||||
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
|
||||
pub struct Tool {
|
||||
#[serde(rename = "type")]
|
||||
pub kind: String,
|
||||
pub function: ToolFunction,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
|
||||
pub struct ToolFunction {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
/// `any` en Go : `null` si absent.
|
||||
pub parameters: Value,
|
||||
}
|
||||
|
||||
/// Question structurée posée à l'utilisateur (outil `ask`).
|
||||
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
|
||||
pub struct AskEvent {
|
||||
pub question: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub options: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
pub struct DiffLine {
|
||||
pub op: String,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
/// Appel d'outil annoncé, en cours d'écriture, ou terminé (`ToolUsedEvent`).
|
||||
/// Pas de JSON propre en Go : le journal en construit un objet à la main.
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct ToolUsedEvent {
|
||||
pub name: String,
|
||||
pub label: String,
|
||||
pub result: String,
|
||||
pub done: bool,
|
||||
pub typing: bool,
|
||||
pub body: String,
|
||||
pub body_tail: bool,
|
||||
pub body_lines: i64,
|
||||
pub diff: Vec<DiffLine>,
|
||||
pub added: i64,
|
||||
pub removed: i64,
|
||||
pub image: String,
|
||||
pub shot: String,
|
||||
pub result_chars: i64,
|
||||
pub result_id: String,
|
||||
}
|
||||
|
||||
/// Mesures llama.cpp d'une complétion (`StatsEvent`).
|
||||
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
|
||||
pub struct StatsEvent {
|
||||
#[serde(default, skip_serializing_if = "is_zero_i64")]
|
||||
pub prompt_tokens: i64,
|
||||
#[serde(default, skip_serializing_if = "is_zero_f64")]
|
||||
pub prompt_per_second: f64,
|
||||
#[serde(default, skip_serializing_if = "is_zero_f64")]
|
||||
pub prompt_ms: f64,
|
||||
#[serde(default, skip_serializing_if = "is_zero_i64")]
|
||||
pub gen_tokens: i64,
|
||||
#[serde(default, skip_serializing_if = "is_zero_f64")]
|
||||
pub gen_per_second: f64,
|
||||
#[serde(default, skip_serializing_if = "is_zero_f64")]
|
||||
pub gen_ms: f64,
|
||||
#[serde(default, skip_serializing_if = "is_zero_i64")]
|
||||
pub prompt_tokens_total: i64,
|
||||
#[serde(default, skip_serializing_if = "is_zero_i64")]
|
||||
pub reasoning_tokens: i64,
|
||||
/// Ce raisonnement repart dans la requête suivante (REASONING_ECHO).
|
||||
#[serde(skip)]
|
||||
pub echoed: bool,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub cache_hint: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cache_tokens: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub draft_n: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub draft_accepted: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ttft_ms: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub lost: Option<i64>,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub lost_after: String,
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
pub lost_alert: bool,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub kind: String,
|
||||
}
|
||||
|
||||
fn is_zero_i64(v: &i64) -> bool {
|
||||
*v == 0
|
||||
}
|
||||
fn is_zero_f64(v: &f64) -> bool {
|
||||
*v == 0.0
|
||||
}
|
||||
|
||||
impl StatsEvent {
|
||||
/// `ctxAfter` : contexte que la requête SUIVANTE renverra au moteur — le
|
||||
/// prompt plus ce qui a été généré, moins le raisonnement jamais renvoyé.
|
||||
pub fn ctx_after(&self) -> i64 {
|
||||
if self.echoed {
|
||||
return self.prompt_tokens_total + self.gen_tokens;
|
||||
}
|
||||
self.prompt_tokens_total + self.gen_tokens - self.gen_tokens.min(self.reasoning_tokens)
|
||||
}
|
||||
}
|
||||
|
||||
/// Raisonnement de la complétion finale, à ranger avec la réponse.
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct ReasoningEcho {
|
||||
pub text: String,
|
||||
pub model: String,
|
||||
}
|
||||
|
||||
/// Ce que reçoit le rappel d'une boucle de chat (`StreamEvent` Go, dont un
|
||||
/// seul champ est posé par appel).
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum StreamEvent {
|
||||
Content(String),
|
||||
Reasoning(String),
|
||||
ToolUsed(ToolUsedEvent),
|
||||
Stats(StatsEvent),
|
||||
Ask(AskEvent),
|
||||
Err(String),
|
||||
/// Retirer la dernière bulle de raisonnement (tour relancé).
|
||||
DropReasoning,
|
||||
/// Compactage en cours de tour : début (true) / fin (false).
|
||||
Compacting(bool),
|
||||
/// Vue modèle APRÈS un compactage en cours de tour — à substituer.
|
||||
NewHistory(Vec<Message>),
|
||||
Echo(ReasoningEcho),
|
||||
}
|
||||
|
||||
/// Événement du journal d'affichage (`LogEvent`).
|
||||
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
|
||||
pub struct LogEvent {
|
||||
pub seq: i64,
|
||||
pub ts: i64,
|
||||
/// Objet JSON (ordre des clés libre : l'UI les lit par nom).
|
||||
pub delta: Map<String, Value>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn message_json_comme_le_go() {
|
||||
let m = Message {
|
||||
role: "assistant".into(),
|
||||
tool_calls: vec![ToolCall {
|
||||
id: "c1".into(),
|
||||
kind: "function".into(),
|
||||
function: ToolCallFunc {
|
||||
name: "bash".into(),
|
||||
arguments: "{}".into(),
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_value(&m).unwrap(),
|
||||
json!({"role":"assistant","tool_calls":[{"id":"c1","type":"function","function":{"name":"bash","arguments":"{}"}}]})
|
||||
);
|
||||
// Contenu vide mais présent : gardé (interface non nil en Go).
|
||||
let u = Message {
|
||||
role: "user".into(),
|
||||
content: Some(json!("")),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_string(&u).unwrap(),
|
||||
r#"{"role":"user","content":""}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contexte_apres() {
|
||||
let s = StatsEvent {
|
||||
prompt_tokens_total: 1000,
|
||||
gen_tokens: 300,
|
||||
reasoning_tokens: 500,
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(s.ctx_after(), 1000);
|
||||
let e = StatsEvent { echoed: true, ..s };
|
||||
assert_eq!(e.ctx_after(), 1300);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
//! Lot 6 : module en cours de portage.
|
||||
@@ -6,10 +6,12 @@
|
||||
//! - `ui` : UI web statique embarquée (lot 2) ;
|
||||
//! - `auth` : authentification de l'API de pilotage (lot 2) ;
|
||||
//! - `oai` : surface compatible OpenAI `/v1` relayée au moteur (lot 5) ;
|
||||
//! - `chat` : cœur chat (lot 6, bibliothèque en cours de portage) ;
|
||||
//! - `update` : mise à jour des binaires au démarrage du conteneur.
|
||||
|
||||
pub mod api;
|
||||
pub mod auth;
|
||||
pub mod chat;
|
||||
pub mod child;
|
||||
pub mod engine;
|
||||
pub mod engstatus;
|
||||
|
||||
Reference in new issue
Block a user