feat(memory): config embedders (LOT C2) + suggestion contextuelle (LOT C3) + contexte projet partagé
- 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>
This commit is contained in:
245
crates/application/src/embedder/usecases.rs
Normal file
245
crates/application/src/embedder/usecases.rs
Normal file
@ -0,0 +1,245 @@
|
||||
//! 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user