tawasuyu

tawasuyu/brahman

Rust 326TOML 123JSON 17Markdown 144606 Definitions

⎇ 72758e75ce515f355f27615671c95ecaf8c2a8bc crates/modules/nouser/nous-real/src/main.rs Rust · 202 lines · 6.5 KB History Who touched what raw

feat(nous-real): cache de embeddings + write-through al CAS de arje b23ddf298 · Sergio ·

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}