//! El cliente: consume un daemon presentándose como un `Provider`. //! //! Un [`DaemonClient`] implementa `verbo_core::Provider`, así que //! cualquier consumidor (`fana-semantic`, `badu`, `chasqui`) lo usa sin //! saber que el modelo vive en otro proceso. Cada llamada es un //! round-trip independiente: sin estado de conexión que reparar. use std::path::{Path, PathBuf}; use async_trait::async_trait; use tokio::net::UnixStream; use verbo_core::{EmbedError, EmbeddingVector, ModelId, Provider}; use crate::wire::{read_frame, write_frame, Request, Response}; /// Cliente de un [`crate::Daemon`]. Se comporta como un `Provider` /// local — los consumidores no notan que el modelo es remoto. pub struct DaemonClient { path: PathBuf, model: ModelId, } impl DaemonClient { /// Conecta a un daemon y hace el handshake del modelo. El `ModelId` /// queda cacheado: marca los vectores y nunca cambia en vida del /// daemon. pub async fn connect(path: impl AsRef) -> Result { let path = path.as_ref().to_path_buf(); let model = match round_trip(&path, &Request::ModelId).await? { Response::ModelId(m) => m, other => return Err(unexpected(other)), }; Ok(Self { path, model }) } } /// Mapea una respuesta fuera de contrato a un `EmbedError`. fn unexpected(r: Response) -> EmbedError { match r { Response::Error(e) => EmbedError::Backend(e), _ => EmbedError::Backend("respuesta del daemon verbo inesperada".into()), } } /// Un round-trip completo: conecta, manda el request, lee la respuesta. async fn round_trip(path: &Path, req: &Request) -> Result { let mut stream = UnixStream::connect(path) .await .map_err(|e| EmbedError::Backend(format!("conexión al daemon verbo: {e}")))?; write_frame(&mut stream, req) .await .map_err(|e| EmbedError::Backend(format!("envío al daemon verbo: {e}")))?; match read_frame::<_, Response>(&mut stream).await { Ok(Some(resp)) => Ok(resp), Ok(None) => Err(EmbedError::Backend( "el daemon verbo cerró la conexión sin responder".into(), )), Err(e) => Err(EmbedError::Backend(format!("lectura del daemon verbo: {e}"))), } } #[async_trait] impl Provider for DaemonClient { fn model_id(&self) -> &ModelId { &self.model } async fn embed(&self, text: &str) -> Result { match round_trip(&self.path, &Request::Embed(text.to_string())).await? { Response::Embed(v) => Ok(v), other => Err(unexpected(other)), } } async fn embed_batch(&self, texts: &[String]) -> Result, EmbedError> { match round_trip(&self.path, &Request::EmbedBatch(texts.to_vec())).await? { Response::EmbedBatch(v) => Ok(v), other => Err(unexpected(other)), } } }