//! Embedder configuration use cases (LOT C2). Each is a single-responsibility //! struct carrying its ports as `Arc` and exposing one `execute`. //! //! - [`ListEmbedderProfiles`] / [`SaveEmbedderProfile`] / [`DeleteEmbedderProfile`] //! — CRUD over the persisted [`EmbedderProfile`]s through the //! [`EmbedderProfileStore`] port. //! - [`DescribeEmbedderEngines`] — a read-only view of the engines available to the //! "configure an embedder?" UI: the recommended ONNX model catalogue, a best-effort //! snapshot of the local environment ([`EmbedderEnvInspector`]), and which strategies //! are actually compiled into this binary. //! //! Hexagonal boundary: the application depends on the domain ports and on plain //! data only. The static engine catalogue and the compiled-capability flags are //! **injected** at the composition root (the infrastructure owns `reqwest`/`fastembed`, //! never the application). use std::sync::Arc; use domain::ports::{EmbedderEnvInspector, EmbedderProfileStore}; use domain::profile::{EmbedderProfile, EmbedderStrategy}; use crate::error::AppError; // --------------------------------------------------------------------------- // ListEmbedderProfiles // --------------------------------------------------------------------------- /// Output of [`ListEmbedderProfiles::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ListEmbedderProfilesOutput { /// All configured embedder profiles (empty when none configured ⇒ `none`). pub profiles: Vec, } /// Lists the configured embedder profiles from the store. pub struct ListEmbedderProfiles { store: Arc, } impl ListEmbedderProfiles { /// Builds the use case from the [`EmbedderProfileStore`] port. #[must_use] pub fn new(store: Arc) -> Self { Self { store } } /// Lists configured embedder profiles. /// /// # Errors /// [`AppError::Store`] on persistence failure. pub async fn execute(&self) -> Result { Ok(ListEmbedderProfilesOutput { profiles: self.store.list().await?, }) } } // --------------------------------------------------------------------------- // SaveEmbedderProfile // --------------------------------------------------------------------------- /// Input for [`SaveEmbedderProfile::execute`]: the raw fields of the profile to /// upsert (validated into an [`EmbedderProfile`] entity). #[derive(Debug, Clone, PartialEq, Eq)] pub struct SaveEmbedderProfileInput { /// Stable identifier (e.g. `"local-onnx-minilm"`). Non-empty. pub id: String, /// Display name. Non-empty. pub name: String, /// Embedding strategy driving which concrete adapter is used. pub strategy: EmbedderStrategy, /// Model identifier, when the strategy needs one. pub model: Option, /// Endpoint URL for a server/API strategy. pub endpoint: Option, /// Name of the env var carrying the API key (never the key itself). pub api_key_env: Option, /// Length of the vectors this engine produces. Non-zero. pub dimension: usize, } /// Output of [`SaveEmbedderProfile::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct SaveEmbedderProfileOutput { /// The saved (validated) profile, echoed back. pub profile: EmbedderProfile, } /// Persists (creates or replaces by id) a single embedder profile, after building /// and validating the entity. pub struct SaveEmbedderProfile { store: Arc, } impl SaveEmbedderProfile { /// Builds the use case from the [`EmbedderProfileStore`] port. #[must_use] pub fn new(store: Arc) -> Self { Self { store } } /// Validates then saves the profile. /// /// # Errors /// - [`AppError::Invalid`] if the profile's invariants are violated (empty /// `id`/`name`, zero `dimension`), /// - [`AppError::Store`] on persistence failure. pub async fn execute( &self, input: SaveEmbedderProfileInput, ) -> Result { let profile = EmbedderProfile::new( input.id, input.name, input.strategy, input.model, input.endpoint, input.api_key_env, input.dimension, ) .map_err(|e| AppError::Invalid(e.to_string()))?; self.store.save(&profile).await?; Ok(SaveEmbedderProfileOutput { profile }) } } // --------------------------------------------------------------------------- // DeleteEmbedderProfile // --------------------------------------------------------------------------- /// Input for [`DeleteEmbedderProfile::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct DeleteEmbedderProfileInput { /// Id of the embedder profile to delete. pub id: String, } /// Deletes an embedder profile by id. pub struct DeleteEmbedderProfile { store: Arc, } impl DeleteEmbedderProfile { /// Builds the use case from the [`EmbedderProfileStore`] port. #[must_use] pub fn new(store: Arc) -> Self { Self { store } } /// Deletes the profile. /// /// # Errors /// [`AppError::NotFound`] if the id is unknown, [`AppError::Store`] on /// persistence failure. pub async fn execute(&self, input: DeleteEmbedderProfileInput) -> Result<(), AppError> { self.store.delete(&input.id).await?; Ok(()) } } // --------------------------------------------------------------------------- // DescribeEmbedderEngines // --------------------------------------------------------------------------- /// A recommendable local ONNX model, as a plain application value (mirrors the /// infrastructure `OnnxModelInfo` data, injected at the composition root so the /// application never depends on the infrastructure crate). #[derive(Debug, Clone, PartialEq, Eq)] pub struct OnnxModelView { /// Stable model id accepted by a `localOnnx` profile's `model` field. pub id: String, /// Human-readable name for the UI. pub display_name: String, /// Length of the vectors this model produces. pub dimension: usize, /// Approximate download/disk size in megabytes. pub approx_size_mb: u32, /// Whether this is the recommended default model. pub recommended: bool, } /// A read-only description of the embedding engines available to the /// "configure an embedder?" UI (C2/C3). #[derive(Debug, Clone, PartialEq, Eq)] pub struct EmbedderEnginesView { /// The curated catalogue of recommendable local ONNX models. pub recommended_onnx: Vec, /// Whether an Ollama-style local embedding server was detected (best-effort). pub ollama_detected: bool, /// Ids of the recommended ONNX models already present in the local cache. pub onnx_cached_models: Vec, /// Whether the HTTP capability (`localServer`/`api`) is compiled into this binary. pub vector_http_enabled: bool, /// Whether the in-process ONNX capability (`localOnnx`) is compiled into this binary. pub vector_onnx_enabled: bool, } /// Describes the engines available to the embedder-configuration UI: the static /// ONNX catalogue + compiled-capability flags (injected at construction), enriched /// with a best-effort live snapshot of the local environment via the /// [`EmbedderEnvInspector`] port. Read-only — emits no event, never fails on the /// environment probe (the port is best-effort by contract). pub struct DescribeEmbedderEngines { inspector: Arc, recommended_onnx: Vec, vector_http_enabled: bool, vector_onnx_enabled: bool, } impl DescribeEmbedderEngines { /// Builds the use case from the [`EmbedderEnvInspector`] port plus the static /// engine catalogue and compiled-capability flags (data owned by infrastructure /// and injected at the composition root, so the application stays infra-free). #[must_use] pub fn new( inspector: Arc, recommended_onnx: Vec, vector_http_enabled: bool, vector_onnx_enabled: bool, ) -> Self { Self { inspector, recommended_onnx, vector_http_enabled, vector_onnx_enabled, } } /// Returns the engines view. Infallible in practice: the environment probe is /// best-effort and degrades to "nothing detected". /// /// # Errors /// Never; the `Result` keeps the call site uniform with the other use cases. #[allow(clippy::unused_async)] pub async fn execute(&self) -> Result { let report = self.inspector.inspect().await; Ok(EmbedderEnginesView { recommended_onnx: self.recommended_onnx.clone(), ollama_detected: report.ollama_detected, onnx_cached_models: report.onnx_cached_models, vector_http_enabled: self.vector_http_enabled, vector_onnx_enabled: self.vector_onnx_enabled, }) } }