- LOT C2 (§14.5.3) : use cases de configuration des embedders déclaratifs (List/Save/Delete + DescribeEmbedderEngines : modèles ONNX recommandés, environnement local détecté, stratégies compilées). UI EmbedderSettings. - LOT C3 (§14.5.5) : suggestion contextuelle best-effort à l'activation quand la mémoire dépasse le budget de recall sans embedder configuré (event EmbedderSuggested, anti-spam 1×/session, « ne plus demander »). - Contexte projet partagé .ideai/CONTEXT.md (model-agnostic) injecté à tous les agents/profils au lancement, avant la persona. UI ProjectContextPanel. Tests : backend workspace vert (0 échec) ; frontend 306/306. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
246 lines
9.1 KiB
Rust
246 lines
9.1 KiB
Rust
//! Embedder configuration use cases (LOT C2). Each is a single-responsibility
|
|
//! struct carrying its ports as `Arc<dyn Port>` 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<EmbedderProfile>,
|
|
}
|
|
|
|
/// Lists the configured embedder profiles from the store.
|
|
pub struct ListEmbedderProfiles {
|
|
store: Arc<dyn EmbedderProfileStore>,
|
|
}
|
|
|
|
impl ListEmbedderProfiles {
|
|
/// Builds the use case from the [`EmbedderProfileStore`] port.
|
|
#[must_use]
|
|
pub fn new(store: Arc<dyn EmbedderProfileStore>) -> Self {
|
|
Self { store }
|
|
}
|
|
|
|
/// Lists configured embedder profiles.
|
|
///
|
|
/// # Errors
|
|
/// [`AppError::Store`] on persistence failure.
|
|
pub async fn execute(&self) -> Result<ListEmbedderProfilesOutput, AppError> {
|
|
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<String>,
|
|
/// Endpoint URL for a server/API strategy.
|
|
pub endpoint: Option<String>,
|
|
/// Name of the env var carrying the API key (never the key itself).
|
|
pub api_key_env: Option<String>,
|
|
/// 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<dyn EmbedderProfileStore>,
|
|
}
|
|
|
|
impl SaveEmbedderProfile {
|
|
/// Builds the use case from the [`EmbedderProfileStore`] port.
|
|
#[must_use]
|
|
pub fn new(store: Arc<dyn EmbedderProfileStore>) -> 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<SaveEmbedderProfileOutput, AppError> {
|
|
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<dyn EmbedderProfileStore>,
|
|
}
|
|
|
|
impl DeleteEmbedderProfile {
|
|
/// Builds the use case from the [`EmbedderProfileStore`] port.
|
|
#[must_use]
|
|
pub fn new(store: Arc<dyn EmbedderProfileStore>) -> 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<OnnxModelView>,
|
|
/// 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<String>,
|
|
/// 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<dyn EmbedderEnvInspector>,
|
|
recommended_onnx: Vec<OnnxModelView>,
|
|
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<dyn EmbedderEnvInspector>,
|
|
recommended_onnx: Vec<OnnxModelView>,
|
|
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<EmbedderEnginesView, AppError> {
|
|
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,
|
|
})
|
|
}
|
|
}
|