Skip to main content

qualia_core_db/inference/inference_agent/
local_agent.rs

1// The concrete `LocalLlmAgent` struct, its constructors, LoRA adapter
2// management, sieve-lexicon wiring, and DID hashing. The Phase-8 decode path
3// lives in `decode.rs`; intent validation in `validation.rs`; the
4// `AgentRuntime` impl in `runtime.rs` — all separate impl blocks on this type.
5
6use crate::q_hash;
7
8use super::types::{default_local_modality, AgentBackend};
9
10// ─── LocalLlmAgent ───────────────────────────────────────────────────────────
11/// The concrete local inference agent. Uses a mock inference path for now;
12/// swap `infer_local_model` for an actual llama.cpp FFI call.
13pub struct LocalLlmAgent {
14    pub agent_did: String,
15    pub backend: AgentBackend,
16    pub memory_used_bytes: std::sync::atomic::AtomicU64,
17    // NOTE: the four fields below were private in the monolith. They are now
18    // `pub(super)` widenings so the decode (`decode.rs`) and runtime
19    // (`runtime.rs`) impl blocks can read/write them across the new submodule
20    // boundary. Visibility stays crate-internal — the external API is unchanged.
21    /// Set by `validate_intent` when `output_mode` requires graph-structured emission.
22    pub(super) use_sieve_output: std::sync::atomic::AtomicBool,
23    /// Memory-mapped `.q42.lex` sidecar for dynamic sieve masks.
24    pub(super) sieve_lex_path: std::sync::Mutex<Option<String>>,
25    /// IRI hashes to resolve through the lexicon for Subject / Predicate / Object slots.
26    pub(super) sieve_spec: std::sync::Mutex<crate::neuro_symbolic_sieve::SieveLexSpec>,
27    /// Optional LoRA adapter manager for zero-copy context-driven neural adaptation.
28    /// When set, the prompt is classified into a domain (Medical / Legal / Chemical / …)
29    /// and the matching adapter's delta is applied to the embedding hidden state before
30    /// the autoregressive decode loop.
31    pub(super) lora_manager: std::sync::Mutex<Option<crate::lora::LoRAAdapterManager>>,
32}
33
34impl LocalLlmAgent {
35    pub fn new(agent_did: impl Into<String>, model_path: impl Into<String>) -> Self {
36        Self::with_local_backend(
37            agent_did,
38            AgentBackend::Local {
39                model_path: model_path.into(),
40                context_window: 4096,
41                quantization: "Q4_K_M".into(),
42                vision_projector_path: None,
43                modality: default_local_modality(),
44                architecture: None,
45            },
46        )
47    }
48
49    /// Construct an agent with a fully specified backend (e.g. catalog multimodal profile).
50    pub fn with_local_backend(agent_did: impl Into<String>, backend: AgentBackend) -> Self {
51        Self {
52            agent_did: agent_did.into(),
53            backend,
54            memory_used_bytes: std::sync::atomic::AtomicU64::new(0),
55            use_sieve_output: std::sync::atomic::AtomicBool::new(false),
56            sieve_lex_path: std::sync::Mutex::new(None),
57            sieve_spec: std::sync::Mutex::new(
58                crate::neuro_symbolic_sieve::SieveLexSpec::graph_mutation_default(),
59            ),
60            lora_manager: std::sync::Mutex::new(None),
61        }
62    }
63
64    // ── LoRA adapter management ───────────────────────────────────────────────
65
66    /// Attach a LoRA adapter directory to this agent.
67    ///
68    /// Adapters are loaded lazily on the first prompt that triggers a domain
69    /// switch.  The directory must contain `*.lora` files named after
70    /// `ContextType::adapter_filename()` (e.g. `medical_v1.lora`).
71    pub fn attach_lora_adapters(&self, adapter_dir: impl Into<std::path::PathBuf>) {
72        let mgr = crate::lora::LoRAAdapterManager::new(adapter_dir);
73        *self.lora_manager.lock().unwrap_or_else(|e| e.into_inner()) = Some(mgr);
74    }
75
76    /// Attach a LoRA manager pre-configured with expected embedding dimensions.
77    ///
78    /// `n_in` should match the model's embedding dimension (e.g. 4096 for 7B models).
79    pub fn attach_lora_adapters_with_dims(
80        &self,
81        adapter_dir: impl Into<std::path::PathBuf>,
82        n_in: usize,
83        n_out: usize,
84    ) {
85        let mut mgr = crate::lora::LoRAAdapterManager::new(adapter_dir);
86        mgr.set_expected_dims(n_in, n_out);
87        *self.lora_manager.lock().unwrap_or_else(|e| e.into_inner()) = Some(mgr);
88    }
89
90    /// Remove the LoRA manager and revert to base-model-only inference.
91    pub fn detach_lora_adapters(&self) {
92        *self.lora_manager.lock().unwrap_or_else(|e| e.into_inner()) = None;
93    }
94
95    /// Detect context from `prompt` and pre-warm the LoRA adapter cache.
96    ///
97    /// Call this before a batch of related prompts to avoid cold-load latency
98    /// on the first inference.
99    pub fn warm_lora_for_prompt(&self, prompt: &str) {
100        let mut guard = self.lora_manager.lock().unwrap_or_else(|e| e.into_inner());
101        if let Some(mgr) = guard.as_mut() {
102            let (ctx, conf) = mgr.detector.analyze_text(prompt);
103            if conf >= mgr.detector.confidence_threshold {
104                let _ = mgr.switch_to(ctx);
105            }
106        }
107    }
108
109    /// Return the currently active LoRA context type, if any.
110    pub fn active_lora_context(&self) -> Option<crate::lora::ContextType> {
111        let guard = self.lora_manager.lock().unwrap_or_else(|e| e.into_inner());
112        guard
113            .as_ref()
114            .and_then(|m| m.active())
115            .map(|a| a.context_type)
116    }
117
118    /// Wire the `.q42.lex` sidecar used to populate FSM sieve masks at inference time.
119    pub fn configure_sieve_lex(&self, path: impl Into<String>) {
120        *self
121            .sieve_lex_path
122            .lock()
123            .unwrap_or_else(|e| e.into_inner()) = Some(path.into());
124    }
125
126    pub fn agent_did_hash(&self) -> u64 {
127        q_hash(&self.agent_did)
128    }
129}