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:
24
crates/application/src/embedder/mod.rs
Normal file
24
crates/application/src/embedder/mod.rs
Normal file
@ -0,0 +1,24 @@
|
||||
//! Embedder configuration use cases (LOT C2 — §14.5.3).
|
||||
//!
|
||||
//! CRUD over the declarative [`domain::profile::EmbedderProfile`]s
|
||||
//! ([`ListEmbedderProfiles`], [`SaveEmbedderProfile`], [`DeleteEmbedderProfile`])
|
||||
//! plus a read-only description of the available engines ([`DescribeEmbedderEngines`]):
|
||||
//! the recommended local ONNX models, the detected local environment, and which
|
||||
//! strategies are actually compiled into this binary.
|
||||
//!
|
||||
//! A changed embedder takes effect at the **next IDE start** (the composition root
|
||||
//! freezes the recall for the session): these use cases only persist/inspect config,
|
||||
//! they never reconfigure a live recall.
|
||||
|
||||
mod suggestion;
|
||||
mod usecases;
|
||||
|
||||
pub use suggestion::{
|
||||
CheckEmbedderSuggestion, CheckEmbedderSuggestionInput, CheckEmbedderSuggestionOutput,
|
||||
DismissChoice, DismissEmbedderSuggestion, DismissEmbedderSuggestionInput, SuggestedThisSession,
|
||||
};
|
||||
pub use usecases::{
|
||||
DeleteEmbedderProfile, DeleteEmbedderProfileInput, DescribeEmbedderEngines,
|
||||
EmbedderEnginesView, ListEmbedderProfiles, ListEmbedderProfilesOutput, OnnxModelView,
|
||||
SaveEmbedderProfile, SaveEmbedderProfileInput, SaveEmbedderProfileOutput,
|
||||
};
|
||||
269
crates/application/src/embedder/suggestion.rs
Normal file
269
crates/application/src/embedder/suggestion.rs
Normal file
@ -0,0 +1,269 @@
|
||||
//! Contextual embedder-suggestion use cases (LOT C3, ARCHITECTURE §14.5.5).
|
||||
//!
|
||||
//! - [`CheckEmbedderSuggestion`] — the **best-effort** check run when an agent
|
||||
//! reads the project memory at activation: the *first* time a project's memory
|
||||
//! outgrows the recall budget while no embedder is configured (strategy `none`),
|
||||
//! it publishes a one-time [`DomainEvent::EmbedderSuggested`]. Anti-spam: at most
|
||||
//! once per session per project, and never again once the user chose
|
||||
//! [`EmbedderPromptDismissal::Never`].
|
||||
//! - [`DismissEmbedderSuggestion`] — persists the user's "plus tard" / "ne plus
|
||||
//! demander" response.
|
||||
//!
|
||||
//! By construction this never blocks the activation flow: every error degrades to
|
||||
//! "no suggestion", and a `none`-strategy/under-budget project is a cheap no-op.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use domain::ports::{
|
||||
EmbedderEnvInspector, EmbedderProfileStore, EmbedderPromptDismissal, EmbedderPromptStore,
|
||||
EventBus, MemoryStore,
|
||||
};
|
||||
use domain::profile::EmbedderStrategy;
|
||||
use domain::{DomainEvent, ProjectId, ProjectPath};
|
||||
|
||||
use crate::error::AppError;
|
||||
|
||||
/// In-memory "already suggested this session" guard, shared with the composition
|
||||
/// root (held in `AppState`). One [`ProjectId`] per project that has already seen
|
||||
/// the suggestion since the IDE started. Plain std types so the application stays
|
||||
/// dependency-free and the guard is trivially testable.
|
||||
pub type SuggestedThisSession = Arc<Mutex<HashSet<ProjectId>>>;
|
||||
|
||||
/// Sums the approximate token size of the index, mirroring the infrastructure
|
||||
/// `index_token_size` so the application does not depend on the infra crate. One
|
||||
/// entry's payload is its rendered index line (title + hook + slug), and the rule
|
||||
/// only needs a *monotone* measure consistent with the recall's budget unit
|
||||
/// (~1 token / 4 chars), so we count characters / 4 (min 1 per entry).
|
||||
fn index_token_size(entries: &[domain::MemoryIndexEntry]) -> usize {
|
||||
entries
|
||||
.iter()
|
||||
.map(|e| {
|
||||
let chars = e.title.len() + e.hook.len() + e.slug.as_str().len();
|
||||
(chars / 4).max(1)
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CheckEmbedderSuggestion
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Input for [`CheckEmbedderSuggestion::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CheckEmbedderSuggestionInput {
|
||||
/// The project whose memory is being read at agent activation.
|
||||
pub project_id: ProjectId,
|
||||
/// That project's root (where its `.ideai/memory/` lives).
|
||||
pub project_root: ProjectPath,
|
||||
}
|
||||
|
||||
/// Output of [`CheckEmbedderSuggestion::execute`]: whether a suggestion event was
|
||||
/// published on this call (useful for tests; the caller ignores it — the flow is
|
||||
/// best-effort).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct CheckEmbedderSuggestionOutput {
|
||||
/// `true` iff an [`DomainEvent::EmbedderSuggested`] was published.
|
||||
pub suggested: bool,
|
||||
}
|
||||
|
||||
/// Publishes a one-time embedder suggestion the first time a project's memory
|
||||
/// outgrows the recall budget while no embedder is configured (LOT C3).
|
||||
///
|
||||
/// Composes — each port only for its slice (Interface Segregation):
|
||||
/// - [`EmbedderProfileStore`] to read the active strategy (no non-`none` profile ⇒
|
||||
/// strategy `none`),
|
||||
/// - [`MemoryStore`] to measure the memory index size,
|
||||
/// - [`EmbedderPromptStore`] for the persistent dismissal (`never` ⇒ silence),
|
||||
/// - [`EmbedderEnvInspector`] to enrich the event with the detected environment,
|
||||
/// - [`EventBus`] to publish, plus the shared [`SuggestedThisSession`] guard.
|
||||
///
|
||||
/// **Never blocks the caller**: any port error degrades to "no suggestion".
|
||||
pub struct CheckEmbedderSuggestion {
|
||||
profiles: Arc<dyn EmbedderProfileStore>,
|
||||
memories: Arc<dyn MemoryStore>,
|
||||
prompts: Arc<dyn EmbedderPromptStore>,
|
||||
inspector: Arc<dyn EmbedderEnvInspector>,
|
||||
events: Arc<dyn EventBus>,
|
||||
suggested_this_session: SuggestedThisSession,
|
||||
/// Recall token budget the memory must exceed (injected so it stays aligned
|
||||
/// with [`crate::AGENT_MEMORY_RECALL_BUDGET`] without re-hardcoding it).
|
||||
budget: usize,
|
||||
/// Compiled-in HTTP capability flag (carried into the event).
|
||||
vector_http_enabled: bool,
|
||||
/// Compiled-in ONNX capability flag (carried into the event).
|
||||
vector_onnx_enabled: bool,
|
||||
}
|
||||
|
||||
impl CheckEmbedderSuggestion {
|
||||
/// Builds the use case from its ports + the session guard + the budget and
|
||||
/// compiled-capability flags (the latter injected at the composition root, the
|
||||
/// application stays infra-free).
|
||||
#[must_use]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
profiles: Arc<dyn EmbedderProfileStore>,
|
||||
memories: Arc<dyn MemoryStore>,
|
||||
prompts: Arc<dyn EmbedderPromptStore>,
|
||||
inspector: Arc<dyn EmbedderEnvInspector>,
|
||||
events: Arc<dyn EventBus>,
|
||||
suggested_this_session: SuggestedThisSession,
|
||||
budget: usize,
|
||||
vector_http_enabled: bool,
|
||||
vector_onnx_enabled: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
profiles,
|
||||
memories,
|
||||
prompts,
|
||||
inspector,
|
||||
events,
|
||||
suggested_this_session,
|
||||
budget,
|
||||
vector_http_enabled,
|
||||
vector_onnx_enabled,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether any configured profile selects a non-`none` strategy. No profile (or
|
||||
/// a store error) ⇒ `none` (the dependency-free default posture).
|
||||
async fn strategy_is_none(&self) -> bool {
|
||||
match self.profiles.list().await {
|
||||
Ok(profiles) => !profiles
|
||||
.iter()
|
||||
.any(|p| p.strategy != EmbedderStrategy::None),
|
||||
// A store failure must not turn into a suggestion: behave as configured
|
||||
// (i.e. *not* `none`) so we stay silent.
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs the check best-effort.
|
||||
///
|
||||
/// Order (each step short-circuits to "no suggestion"):
|
||||
/// 1. already suggested this session ⇒ stop (no I/O beyond the guard);
|
||||
/// 2. a non-`none` strategy is configured ⇒ stop;
|
||||
/// 3. the persisted dismissal is `never` ⇒ stop;
|
||||
/// 4. the memory index size does not exceed the budget ⇒ stop;
|
||||
/// 5. otherwise mark the session guard, probe the environment, and publish
|
||||
/// [`DomainEvent::EmbedderSuggested`].
|
||||
///
|
||||
/// # Errors
|
||||
/// Never in practice — the signature keeps a uniform `Result` with the other
|
||||
/// use cases; every failing port read degrades to `Ok(suggested: false)`.
|
||||
pub async fn execute(
|
||||
&self,
|
||||
input: CheckEmbedderSuggestionInput,
|
||||
) -> Result<CheckEmbedderSuggestionOutput, AppError> {
|
||||
let not_suggested = Ok(CheckEmbedderSuggestionOutput { suggested: false });
|
||||
|
||||
// 1. Once per session per project (cheap guard check before any I/O).
|
||||
if self
|
||||
.suggested_this_session
|
||||
.lock()
|
||||
.map(|set| set.contains(&input.project_id))
|
||||
.unwrap_or(true)
|
||||
{
|
||||
return not_suggested;
|
||||
}
|
||||
|
||||
// 2. Only when no embedder is configured (strategy `none`).
|
||||
if !self.strategy_is_none().await {
|
||||
return not_suggested;
|
||||
}
|
||||
|
||||
// 3. Persistent "ne plus demander" silences the suggestion forever.
|
||||
if matches!(
|
||||
self.prompts.read(&input.project_root).await,
|
||||
Ok(Some(EmbedderPromptDismissal::Never))
|
||||
) {
|
||||
return not_suggested;
|
||||
}
|
||||
|
||||
// 4. Memory must have outgrown the recall budget (a fresh project has 0).
|
||||
let size = match self.memories.read_index(&input.project_root).await {
|
||||
Ok(entries) => index_token_size(&entries),
|
||||
Err(_) => return not_suggested,
|
||||
};
|
||||
if size <= self.budget {
|
||||
return not_suggested;
|
||||
}
|
||||
|
||||
// 5. Mark the guard *before* publishing so a concurrent activation cannot
|
||||
// double-fire; if another thread won the race, stay silent.
|
||||
match self.suggested_this_session.lock() {
|
||||
Ok(mut set) => {
|
||||
if !set.insert(input.project_id) {
|
||||
return not_suggested;
|
||||
}
|
||||
}
|
||||
Err(_) => return not_suggested,
|
||||
}
|
||||
|
||||
let report = self.inspector.inspect().await;
|
||||
self.events.publish(DomainEvent::EmbedderSuggested {
|
||||
project_id: input.project_id,
|
||||
ollama_detected: report.ollama_detected,
|
||||
onnx_cached: report.onnx_cached_models,
|
||||
vector_http_enabled: self.vector_http_enabled,
|
||||
vector_onnx_enabled: self.vector_onnx_enabled,
|
||||
});
|
||||
Ok(CheckEmbedderSuggestionOutput { suggested: true })
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DismissEmbedderSuggestion
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// The user's response to the embedder suggestion popup.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DismissChoice {
|
||||
/// "Plus tard" — re-proposable on a future session.
|
||||
Later,
|
||||
/// "Ne plus demander" — never again (persistent).
|
||||
Never,
|
||||
}
|
||||
|
||||
impl From<DismissChoice> for EmbedderPromptDismissal {
|
||||
fn from(c: DismissChoice) -> Self {
|
||||
match c {
|
||||
DismissChoice::Later => Self::Later,
|
||||
DismissChoice::Never => Self::Never,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Input for [`DismissEmbedderSuggestion::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DismissEmbedderSuggestionInput {
|
||||
/// The project the suggestion concerned.
|
||||
pub project_root: ProjectPath,
|
||||
/// The user's choice.
|
||||
pub choice: DismissChoice,
|
||||
}
|
||||
|
||||
/// Persists the user's dismissal of the embedder suggestion (LOT C3).
|
||||
pub struct DismissEmbedderSuggestion {
|
||||
prompts: Arc<dyn EmbedderPromptStore>,
|
||||
}
|
||||
|
||||
impl DismissEmbedderSuggestion {
|
||||
/// Builds the use case from the [`EmbedderPromptStore`] port.
|
||||
#[must_use]
|
||||
pub fn new(prompts: Arc<dyn EmbedderPromptStore>) -> Self {
|
||||
Self { prompts }
|
||||
}
|
||||
|
||||
/// Writes the dismissal state. A `later` keeps the suggestion re-proposable on
|
||||
/// a future session; a `never` silences it for good.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`AppError::Store`] on a persistence failure.
|
||||
pub async fn execute(&self, input: DismissEmbedderSuggestionInput) -> Result<(), AppError> {
|
||||
self.prompts
|
||||
.write(&input.project_root, input.choice.into())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
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