⎇ d3649bfd1a80153652c2a028315369bb4b184024
crates/modules/nouser/nous-real/src/main.rs
History
Who touched what
raw
feat(nous-real): cache de embeddings + write-through al CAS de arje
1//! `nouser-nous-real` — proveedor Nous con LLM real (gated por feature).
2//!
3//! ## Build modes
4//!
5//! - `cargo build -p nouser-nous-real`
6//! Compila como **stub**: bin que arranca, sidecarea al brahman-init
7//! pero rechaza toda request con un error explicando que falta la
8//! feature. Útil para que `cargo build --workspace` no requiera ML
9//! deps.
10//!
11//! - `cargo build -p nouser-nous-real --features embeddings`
12//! Compila con `fastembed` + ONNX Runtime descargado por Cargo.
13//! Modelo default: `all-MiniLM-L6-v2` (384-d, ~80 MB descargado al
14//! primer run y cacheado en `~/.cache/fastembed`).
15//!
16//! ## Diseño
17//!
18//! Mismo contrato wire que `nouser-nous-mock` (`nouser-nous` crate). La
19//! diferencia operativa: real produce 384-d con semantic content
20//! (text-embedding del modelo); mock produce 32-d con metadata-hashing.
21//! No son intercambiables a media-deployment — los centroides de
22//! Mónadas calculadas con uno NO matchean con el otro.
23//!
24//! La Card declara `priority_contexts.prod = { priority_offset: +1 }`,
25//! contrapeso del mock que tiene `+1 en test`. Así el broker brahman
26//! elige automáticamente:
27//! - `BRAHMAN_BROKER_CONTEXT=test` → mock gana.
28//! - `BRAHMAN_BROKER_CONTEXT=prod` → real gana.
29//! - sin contexto → empate por label alfabético.
30
31#![forbid(unsafe_code)]
32
33use std::collections::BTreeMap;
34
35use brahman_card::{
36 ulid::Ulid, Card, CardKind, ContextBias, Flow, Flows, Lifecycle, Payload, Priority,
37 Supervision, TypeRef,
38};
39use nouser_nous::{transport, FLOW_EMBED_REQUEST, FLOW_EMBED_RESULT, FLOW_TYPE_NAME};
40use tokio::net::UnixListener;
41use tracing::info;
42
43#[cfg(feature = "embeddings")]
44mod cache;
45#[cfg(feature = "embeddings")]
46mod embeddings;
47#[cfg(not(feature = "embeddings"))]
48mod stub;
49
50#[cfg(feature = "embeddings")]
51const MODEL_ID: &str = "real-fastembed-allMiniLML6V2-384d";
52#[cfg(not(feature = "embeddings"))]
53const MODEL_ID: &str = "real-stub-no-feature";
54
55#[cfg(feature = "embeddings")]
56const EMBED_DIM: u32 = 384;
57#[cfg(not(feature = "embeddings"))]
58const EMBED_DIM: u32 = 0;
59
60#[tokio::main(flavor = "current_thread")]
61async fn main() -> std::io::Result<()> {
62 init_tracing();
63
64 #[cfg(not(feature = "embeddings"))]
65 info!(
66 "nouser-nous-real corriendo en modo STUB (compilá con \
67 --features embeddings para activar el modelo)"
68 );
69
70 // 1. Resolver socket del data-plane (default `nouser-nous-real.sock`,
71 // distinto del mock para coexistir).
72 let sock_path = transport::provider_socket_path("real");
73 if sock_path.exists() {
74 std::fs::remove_file(&sock_path)?;
75 }
76 if let Some(parent) = sock_path.parent() {
77 std::fs::create_dir_all(parent)?;
78 }
79 let listener = UnixListener::bind(&sock_path)?;
80 info!(socket = %sock_path.display(), "nouser-nous-real escuchando");
81
82 // 2. Sidecar al brahman-init con Card declarando el socket.
83 let card = build_card(sock_path.clone());
84 info!(label = %card.label, mode = MODEL_ID, "publicando Card al brahman-init");
85 brahman_sidecar::spawn(card);
86
87 // 3. Inicializar el modelo (sólo en modo embeddings).
88 #[cfg(feature = "embeddings")]
89 let backend = embeddings::Backend::init().map_err(|e| {
90 std::io::Error::other(format!("init modelo: {e}"))
91 })?;
92 #[cfg(feature = "embeddings")]
93 let backend = std::sync::Arc::new(backend);
94
95 // 4. Abrir el cache de embeddings (sled local, sha256-keyed).
96 // Si falla, seguimos sin cache — degrada a "siempre embed".
97 #[cfg(feature = "embeddings")]
98 let embed_cache = match cache::EmbedCache::open() {
99 Ok(c) => {
100 info!(entries = c.len(), "embed-cache abierto");
101 Some(c)
102 }
103 Err(e) => {
104 tracing::warn!(error = %e, "embed-cache no disponible — todas las requests irán al modelo");
105 None
106 }
107 };
108
109 // 5. Accept loop.
110 loop {
111 let (stream, _addr) = listener.accept().await?;
112
113 #[cfg(feature = "embeddings")]
114 {
115 let backend = backend.clone();
116 let cache = embed_cache.clone();
117 tokio::spawn(async move {
118 if let Err(e) = embeddings::handle_conn(stream, backend, cache).await {
119 tracing::warn!(error = %e, "conn falló");
120 }
121 });
122 }
123
124 #[cfg(not(feature = "embeddings"))]
125 {
126 tokio::spawn(async move {
127 if let Err(e) = stub::handle_conn(stream).await {
128 tracing::warn!(error = %e, "conn falló");
129 }
130 });
131 }
132 }
133}
134
135fn init_tracing() {
136 tracing_subscriber::fmt()
137 .with_env_filter(
138 tracing_subscriber::EnvFilter::try_from_default_env()
139 .unwrap_or_else(|_| "info".into()),
140 )
141 .with_target(false)
142 .compact()
143 .init();
144}
145
146/// Card que real-nous anuncia. Idéntica al mock excepto por:
147/// - label distinto (`nouser.nous_real`) para que coexistan en el broker.
148/// - `priority_contexts.prod = +1` (gana en contexto prod).
149/// - `service_socket` propio para que clientes lo descubran directo.
150fn build_card(service_socket: std::path::PathBuf) -> Card {
151 let mut priority_contexts = BTreeMap::new();
152 priority_contexts.insert(
153 "prod".into(),
154 ContextBias {
155 pin_to: None,
156 priority_offset: 1,
157 },
158 );
159
160 Card {
161 schema_version: brahman_card::CARD_SCHEMA_VERSION,
162 id: Ulid::new(),
163 label: "nouser.nous_real".into(),
164 payload: Payload::Virtual,
165 supervision: Supervision::Delegate,
166 lifecycle: Lifecycle::Daemon,
167 priority: Priority::Normal,
168 kind: CardKind::Ente,
169 service_socket: Some(service_socket),
170 flow: Flows {
171 input: vec![Flow {
172 name: FLOW_EMBED_REQUEST.into(),
173 ty: TypeRef::Primitive {
174 name: FLOW_TYPE_NAME.into(),
175 },
176 pin_to: None,
177 }],
178 output: vec![Flow {
179 name: FLOW_EMBED_RESULT.into(),
180 ty: TypeRef::Primitive {
181 name: FLOW_TYPE_NAME.into(),
182 },
183 pin_to: None,
184 }],
185 },
186 priority_contexts,
187 ..Default::default()
188 }
189}
190
191// Helpers compartidos. Anotados allow(dead_code) porque en stub mode
192// algunos quedan sin uso pero los queremos disponibles consistentemente.
193
194#[allow(dead_code)]
195pub(crate) fn model_id() -> &'static str {
196 MODEL_ID
197}
198
199#[allow(dead_code)]
200pub(crate) fn embed_dim() -> u32 {
201 EMBED_DIM
202}