mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-20 05:28:15 +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) ;
|
//! - `ui` : UI web statique embarquée (lot 2) ;
|
||||||
//! - `auth` : authentification de l'API de pilotage (lot 2) ;
|
//! - `auth` : authentification de l'API de pilotage (lot 2) ;
|
||||||
//! - `oai` : surface compatible OpenAI `/v1` relayée au moteur (lot 5) ;
|
//! - `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.
|
//! - `update` : mise à jour des binaires au démarrage du conteneur.
|
||||||
|
|
||||||
pub mod api;
|
pub mod api;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
|
pub mod chat;
|
||||||
pub mod child;
|
pub mod child;
|
||||||
pub mod engine;
|
pub mod engine;
|
||||||
pub mod engstatus;
|
pub mod engstatus;
|
||||||
|
|||||||
Reference in new issue
Block a user