tawasuyu

tawasuyu/brahman

Rust 326TOML 123JSON 17Markdown 144606 Definiciones

⎇ d3649bfd1a80153652c2a028315369bb4b184024 crates/modules/nouser/nous/src/lib.rs Rust · 196 líneas · 6.5 KB Historia Quién tocó qué crudo

feat: Phase D-3 + D-4 — service_socket en Card, providers coexisten 5edc912ed · Sergio ·

1//! `nouser-nous` — el contrato del proveedor de embeddings.
2//!
3//! Define el wire-format compartido entre `nouser-core` (consumidor) y
4//! cualquier implementación de Nous (mock determinista o LLM real). El
5//! protocolo es **line-delimited JSON** sobre Unix socket: cada conexión
6//! envía una request, recibe una response, y cierra. Single-shot por
7//! conexión, igual al admin de brahman.
8//!
9//! ## Contrato
10//!
11//! ```text
12//! C → S: {"kind":"embed_file","payload":{...}}\n
13//! S → C: {"embedding":[...],"model":"mock-pseudo-32d","elapsed_ms":1}\n
14//! ```
15//!
16//! En caso de error:
17//!
18//! ```text
19//! S → C: {"error":"unsupported kind"}\n
20//! ```
21//!
22//! ## Por qué un crate aparte
23//!
24//! El consumidor (nouser-core) y el proveedor (nouser-nous-mock,
25//! nouser-nous-real) deben acordar en types EXACTOS. Tener el contrato
26//! en su crate evita que cada lado declare structs paralelos que se
27//! desincronizan. Si bumpeás el wire, bumpeás aquí.
28//!
29//! ## Swap por priority_contexts
30//!
31//! Cuando existan dos proveedores (mock-nous y real-nous), ambos declaran
32//! el mismo `flow.output: { name: "embed-result", type: ... }` y
33//! `flow.input: "embed-request"`. El broker brahman los matchea contra
34//! los consumidores; el `priority_offset` per-contexto del Card hace que
35//! mock-nous gane en `test` y real-nous en `prod`. nouser-core sólo
36//! consume el flow, sin saber cuál implementación corre.
37
38#![forbid(unsafe_code)]
39#![warn(rust_2018_idioms)]
40
41use serde::{Deserialize, Serialize};
42use thiserror::Error;
43
44// =====================================================================
45// Wire types
46// =====================================================================
47
48/// Request al proveedor Nous.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct EmbedRequest {
51    pub kind: RequestKind,
52    pub payload: serde_json::Value,
53}
54
55/// Tipo de request. El payload se interpreta según el kind.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "snake_case")]
58pub enum RequestKind {
59    /// payload = `EmbedFilePayload` (path + metadata mínima).
60    EmbedFile,
61    /// payload = `EmbedTextPayload` (string libre).
62    EmbedText,
63    /// payload = `{}`. Devuelve `PingResponse`.
64    Ping,
65}
66
67/// Payload para `EmbedFile`. Es la información mínima que el proveedor
68/// necesita para producir un embedding de archivo determinista.
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct EmbedFilePayload {
71    pub path: String,
72    pub extension: Option<String>,
73    pub size: u64,
74    /// `mtime` en ms desde UNIX_EPOCH.
75    pub mtime_ms: u64,
76}
77
78/// Payload para `EmbedText`.
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct EmbedTextPayload {
81    pub text: String,
82}
83
84/// Response exitosa con un embedding.
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct EmbedResponse {
87    /// Vector. Su longitud depende del modelo (mock=32, llama=384, etc.).
88    pub embedding: Vec<f32>,
89    /// Identificador del modelo que produjo el embedding (útil para logs
90    /// y para invalidar caches al cambiar de proveedor).
91    pub model: String,
92    /// Tiempo de cómputo en ms (proveedor lo reporta).
93    pub elapsed_ms: u64,
94}
95
96/// Response a Ping. Útil para health-checks y discovery.
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct PingResponse {
99    pub model: String,
100    pub embed_dim: u32,
101}
102
103/// Error retornado por el proveedor en lugar de una response normal.
104#[derive(Debug, Clone, Serialize, Deserialize, Error)]
105#[error("nous: {error}")]
106pub struct ErrorResponse {
107    pub error: String,
108}
109
110// =====================================================================
111// Transport
112// =====================================================================
113
114pub mod transport {
115    use std::path::PathBuf;
116
117    /// Variable de entorno para sobreescribir la ruta del socket.
118    pub const SOCKET_ENV: &str = "NOUSER_NOUS_SOCKET";
119
120    /// Nombre genérico del socket cuando hay un solo proveedor.
121    pub const SOCKET_NAME: &str = "nouser-nous.sock";
122
123    /// Ruta canónica al socket cuando un único proveedor está activo
124    /// (consumidores que no quieren elegir).
125    pub fn default_socket_path() -> PathBuf {
126        if let Ok(p) = std::env::var(SOCKET_ENV) {
127            return PathBuf::from(p);
128        }
129        runtime_base().join(SOCKET_NAME)
130    }
131
132    /// Ruta default para un proveedor identificado (`"mock"`, `"real"`,
133    /// etc). Permite que mock y real coexistan sin clash de socket.
134    /// `NOUSER_NOUS_SOCKET` igual override esta función si está set.
135    pub fn provider_socket_path(provider: &str) -> PathBuf {
136        if let Ok(p) = std::env::var(SOCKET_ENV) {
137            return PathBuf::from(p);
138        }
139        runtime_base().join(format!("nouser-nous-{}.sock", provider))
140    }
141
142    fn runtime_base() -> PathBuf {
143        std::env::var_os("XDG_RUNTIME_DIR")
144            .map(PathBuf::from)
145            .unwrap_or_else(std::env::temp_dir)
146    }
147}
148
149// =====================================================================
150// Names compartidos para el broker brahman
151// =====================================================================
152
153/// Nombre del flow output del proveedor (entrada del consumidor).
154pub const FLOW_EMBED_RESULT: &str = "embed-result";
155
156/// Nombre del flow input del proveedor (salida del consumidor).
157pub const FLOW_EMBED_REQUEST: &str = "embed-request";
158
159/// Tipo del flow: el wire es JSON serializado, así que el TypeRef
160/// declarado en la Card es `primitive::json`.
161pub const FLOW_TYPE_NAME: &str = "json";
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn request_roundtrip_json() {
169        let req = EmbedRequest {
170            kind: RequestKind::EmbedFile,
171            payload: serde_json::to_value(EmbedFilePayload {
172                path: "/x/y.rs".into(),
173                extension: Some("rs".into()),
174                size: 1024,
175                mtime_ms: 1_700_000_000_000,
176            })
177            .unwrap(),
178        };
179        let s = serde_json::to_string(&req).unwrap();
180        let parsed: EmbedRequest = serde_json::from_str(&s).unwrap();
181        assert_eq!(parsed.kind, RequestKind::EmbedFile);
182    }
183
184    #[test]
185    fn response_roundtrip() {
186        let resp = EmbedResponse {
187            embedding: vec![0.1, 0.2, 0.3],
188            model: "mock-pseudo-32d".into(),
189            elapsed_ms: 1,
190        };
191        let s = serde_json::to_string(&resp).unwrap();
192        let parsed: EmbedResponse = serde_json::from_str(&s).unwrap();
193        assert_eq!(parsed.model, "mock-pseudo-32d");
194        assert_eq!(parsed.embedding.len(), 3);
195    }
196}