feat(nouser): Phase D-2 — proveedor Nous real (LLM) detrás de feature

Cierra el ciclo del módulo Nous: existe un proveedor que produce
embeddings reales con un modelo LLM, mientras que `cargo build` sin
features sigue siendo liviano (no descarga ni compila ML deps).

Crate nuevo crates/modules/nouser/nous-real con dos modos según feature:

- Sin feature (default): stub.
  cargo build -p nouser-nous-real (~10s, sin ML deps).
  Bin arranca, sidecarea a brahman-init declarando la Card,
  escucha en el socket Nous, rechaza requests con un ErrorResponse
  explicativo: "compilado sin la feature embeddings, rebuild con
  cargo build -p nouser-nous-real --features embeddings".
  cargo build --workspace SIGUE siendo limpio.

- Con --features embeddings: real.
  Pulls fastembed = "4" → ort 2.0.0-rc.9 (ONNX Runtime con binarios
  descargados por Cargo) + tokenizers 0.21 + ~30 transitive deps.
  Compila en ~50s.
  Modelo default: all-MiniLM-L6-v2 (384-d, descargado a
  ~/.cache/fastembed la primera vez).
  EmbedText: pasa el texto al modelo → vector 384-d.
  EmbedFile: lee primeros 8KiB UTF-8 lossy, embed como texto.
  Ping: devuelve model_id + embed_dim reales.

Card declara label "nouser.nous_real" + priority_contexts.prod = +1.
En contexto prod gana sobre el mock; en test el mock gana por su +1
en test. Sin contexto, empate alfabético.

Validación end-to-end con modelo real:
  $ ente-zero & nouser-nous-real &
  $ python3 socket-probe '{"kind":"embed_text","payload":{"text":"..."}}'
    model: real-fastembed-allMiniLML6V2-384d
    elapsed_ms: 8
    embed_dim: 384

Tradeoff: dim mock (32) vs real (384) son incompatibles. Cambiar
proveedor invalida centroides cacheados — documentar "limpiar DB al
swap".

Workspace state:
- cargo build --workspace limpio sin features (no ML deps pulled).
- cargo build -p nouser-nous-real --features embeddings funciona.
- 0 errores, 0 warnings en ambos modos.

Pendientes para D-3 / futuro:
- Discovery de socket: el consumer hoy usa NOUSER_NOUS_SOCKET hardcoded.
  Para que el broker elija real vs mock per-contexto, falta o un campo
  socket en el MatchEvent o un broker query "dame socket de session X".
- Coexistencia: ambos providers compiten por el mismo socket path por
  default. Parametrizarlos cuando se quiera correrlos juntos.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-05-08 19:08:27 +00:00
parent b3c3c00cf2
commit 11fc95629c
23 changed files with 31943 additions and 22 deletions
@@ -0,0 +1,35 @@
[package]
name = "nouser-nous-real"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
publish.workspace = true
description = "Nouser — proveedor Nous con LLM real (text-embedding via ONNX). El soporte AI vive detrás del feature `embeddings`; sin él, este crate compila como stub mínimo."
[features]
# Sin features = stub que arranca y rechaza requests. Compila en
# segundos, sin descargar nada.
default = []
# Con feature embeddings: pulls fastembed + ONNX Runtime descargado.
# Modelo default: all-MiniLM-L6-v2 (384-d, ~80MB descargado al primer
# run y cacheado).
embeddings = ["dep:fastembed"]
[dependencies]
brahman-card = { path = "../../../core/brahman-card" }
brahman-sidecar = { path = "../../../shared/brahman-sidecar" }
nouser-nous = { path = "../nous" }
serde_json = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
ulid = { workspace = true }
# Opcional: gateado por feature `embeddings`.
fastembed = { version = "4", optional = true }
[[bin]]
name = "nouser-nous-real"
path = "src/main.rs"