1use std::path::PathBuf;
7
8use anyhow::Result;
9#[cfg(feature = "llama-cpp")]
10use anyhow::bail;
11use clap::{Args, ValueEnum};
12use memvid_core::{EnrichmentEngine, Memvid, RulesEngine};
13use serde::Serialize;
14
15#[cfg(feature = "llama-cpp")]
16use crate::commands::{default_enrichment_model, get_installed_model_path, LlmModel};
17use crate::config::CliConfig;
18#[cfg(feature = "candle-llm")]
19use crate::enrich::CandlePhiEngine;
20#[cfg(feature = "llama-cpp")]
21use crate::enrich::LlmEngine;
22use crate::enrich::OpenAiEngine;
23
24#[derive(Debug, Clone, Copy, ValueEnum, Default)]
26pub enum EnrichEngine {
27 #[default]
29 Rules,
30 #[cfg(feature = "llama-cpp")]
32 Llm,
33 #[cfg(feature = "candle-llm")]
35 Candle,
36 Openai,
38}
39
40#[derive(Args)]
42pub struct EnrichArgs {
43 #[arg(value_name = "FILE", value_parser = clap::value_parser!(PathBuf))]
45 pub file: PathBuf,
46
47 #[arg(long, value_enum, default_value_t = EnrichEngine::Rules)]
49 pub engine: EnrichEngine,
50
51 #[arg(long, default_value_t = true)]
53 pub incremental: bool,
54
55 #[arg(long, conflicts_with = "incremental")]
57 pub force: bool,
58
59 #[arg(long)]
61 pub json: bool,
62
63 #[arg(long)]
65 pub verbose: bool,
66}
67
68#[derive(Debug, Serialize)]
70pub struct EnrichResult {
71 pub engine: String,
72 pub version: String,
73 pub frames_processed: usize,
74 pub cards_extracted: usize,
75 pub total_cards: usize,
76 pub total_entities: usize,
77}
78
79#[allow(unused_variables)]
81pub fn handle_enrich(config: &CliConfig, args: EnrichArgs) -> Result<()> {
82 let mut mem = Memvid::open(&args.file)?;
83
84 let initial_stats = mem.memories_stats();
86
87 if args.force {
89 mem.clear_memories();
90 }
91
92 let (engine_kind, engine_version, frames, cards) = match args.engine {
94 EnrichEngine::Rules => {
95 let engine = RulesEngine::new();
96 let kind = engine.kind().to_string();
97 let version = engine.version().to_string();
98 let (frames, cards) = mem.run_enrichment(&engine)?;
99 (kind, version, frames, cards)
100 }
101 #[cfg(feature = "llama-cpp")]
102 EnrichEngine::Llm => {
103 let model = default_enrichment_model();
105 let model_path = match get_installed_model_path(config, model) {
106 Some(path) => path,
107 None => {
108 bail!(
109 "LLM model not installed. Run `memvid models install {}` first.",
110 match model {
111 LlmModel::Phi35Mini => "phi-3.5-mini",
112 LlmModel::Phi35MiniQ8 => "phi-3.5-mini-q8",
113 }
114 );
115 }
116 };
117
118 let mut engine = LlmEngine::new(model_path);
120 eprintln!("Loading LLM model...");
121 engine.init()?;
122
123 let kind = engine.kind().to_string();
124 let version = engine.version().to_string();
125 let (frames, cards) = mem.run_enrichment(&engine)?;
126 (kind, version, frames, cards)
127 }
128 #[cfg(feature = "candle-llm")]
129 EnrichEngine::Candle => {
130 eprintln!("Loading Phi-3-mini Q4 model via Candle (first run downloads ~2.4GB to ~/.memvid/models/llm/)...");
133 let mut engine = CandlePhiEngine::from_memvid_models(config.models_dir.clone());
134 engine.init()?;
135
136 let kind = engine.kind().to_string();
137 let version = engine.version().to_string();
138 let (frames, cards) = mem.run_enrichment(&engine)?;
139 (kind, version, frames, cards)
140 }
141 EnrichEngine::Openai => {
142 eprintln!("Using OpenAI GPT-4o-mini for enrichment (parallel mode)...");
144 let mut engine = OpenAiEngine::new();
145 engine.init()?;
146
147 let kind = engine.kind().to_string();
148 let version = engine.version().to_string();
149
150 let (frames, cards) = run_openai_parallel(&mut mem, &engine)?;
152 (kind, version, frames, cards)
153 }
154 };
155
156 mem.commit()?;
158
159 let final_stats = mem.memories_stats();
161
162 if args.json {
163 let result = EnrichResult {
164 engine: engine_kind,
165 version: engine_version,
166 frames_processed: frames,
167 cards_extracted: cards,
168 total_cards: final_stats.card_count,
169 total_entities: final_stats.entity_count,
170 };
171 println!("{}", serde_json::to_string_pretty(&result)?);
172 } else {
173 println!("Enrichment complete:");
174 println!(" Engine: {} v{}", engine_kind, engine_version);
175 println!(" Frames processed: {}", frames);
176 println!(" Cards extracted: {}", cards);
177 println!(
178 " Total cards: {} (+{})",
179 final_stats.card_count,
180 final_stats
181 .card_count
182 .saturating_sub(initial_stats.card_count)
183 );
184 println!(" Entities: {}", final_stats.entity_count);
185
186 if args.verbose && cards > 0 {
187 println!("\nExtracted memory cards:");
188 for entity in mem.memory_entities() {
189 println!(" {}:", entity);
190 for card in mem.get_entity_memories(&entity) {
191 println!(" - {}: {} = \"{}\"", card.kind, card.slot, card.value);
192 }
193 }
194 }
195 }
196
197 Ok(())
198}
199
200fn run_openai_parallel(
205 mem: &mut memvid_core::Memvid,
206 engine: &OpenAiEngine,
207) -> Result<(usize, usize)> {
208 use memvid_core::enrich::EnrichmentContext;
209 use memvid_core::EnrichmentEngine;
210
211 let kind = engine.kind();
212 let version = engine.version();
213
214 let unenriched = mem.get_unenriched_frames(kind, version);
216 let total_frames = unenriched.len();
217
218 if total_frames == 0 {
219 eprintln!("No unenriched frames found.");
220 return Ok((0, 0));
221 }
222
223 eprintln!(
224 "Gathering {} frames for parallel enrichment...",
225 total_frames
226 );
227
228 let mut contexts = Vec::with_capacity(total_frames);
230 for frame_id in &unenriched {
231 let frame = match mem.frame_by_id(*frame_id) {
232 Ok(f) => f,
233 Err(_) => continue,
234 };
235
236 let text = match mem.frame_preview_by_id(*frame_id) {
238 Ok(t) => t,
239 Err(_) => continue,
240 };
241
242 let uri = frame
243 .uri
244 .clone()
245 .unwrap_or_else(|| format!("mv2://frame/{}", frame_id));
246 let metadata_json = frame
247 .metadata
248 .as_ref()
249 .and_then(|m| serde_json::to_string(m).ok());
250
251 let ctx = EnrichmentContext::new(
252 *frame_id,
253 uri,
254 text,
255 frame.title.clone(),
256 frame.timestamp,
257 metadata_json,
258 );
259
260 contexts.push(ctx);
261 }
262
263 eprintln!(
264 "Starting parallel enrichment of {} frames with 20 workers...",
265 contexts.len()
266 );
267
268 let results = engine.enrich_batch(contexts);
270
271 let mut total_cards = 0;
273 for (frame_id, cards) in results {
274 let card_count = cards.len();
275
276 let card_ids = if !cards.is_empty() {
278 mem.put_memory_cards(cards)?
279 } else {
280 Vec::new()
281 };
282
283 mem.record_enrichment(frame_id, kind, version, card_ids)?;
285
286 total_cards += card_count;
287 }
288
289 Ok((total_frames, total_cards))
290}
291
292#[derive(Args)]
294pub struct MemoriesArgs {
295 #[arg(value_name = "FILE", value_parser = clap::value_parser!(PathBuf))]
297 pub file: PathBuf,
298
299 #[arg(long)]
301 pub entity: Option<String>,
302
303 #[arg(long)]
305 pub slot: Option<String>,
306
307 #[arg(long)]
309 pub json: bool,
310}
311
312#[derive(Debug, Serialize)]
314pub struct MemoryOutput {
315 pub id: u64,
316 pub kind: String,
317 pub entity: String,
318 pub slot: String,
319 pub value: String,
320 pub polarity: Option<String>,
321 pub document_date: Option<i64>,
322 pub source_frame_id: u64,
323}
324
325pub fn handle_memories(_config: &CliConfig, args: MemoriesArgs) -> Result<()> {
326 let mem = Memvid::open(&args.file)?;
327
328 let stats = mem.memories_stats();
329
330 if args.json {
331 let mut cards: Vec<MemoryOutput> = Vec::new();
332
333 if let Some(entity) = &args.entity {
334 if let Some(slot) = &args.slot {
335 if let Some(card) = mem.get_current_memory(entity, slot) {
337 cards.push(card_to_output(card));
338 }
339 } else {
340 for card in mem.get_entity_memories(entity) {
342 cards.push(card_to_output(card));
343 }
344 }
345 } else {
346 for entity in mem.memory_entities() {
348 for card in mem.get_entity_memories(&entity) {
349 cards.push(card_to_output(card));
350 }
351 }
352 }
353
354 println!("{}", serde_json::to_string_pretty(&cards)?);
355 } else {
356 println!(
357 "Memory cards: {} total, {} entities",
358 stats.card_count, stats.entity_count
359 );
360 println!();
361
362 if let Some(entity) = &args.entity {
363 if let Some(slot) = &args.slot {
364 if let Some(card) = mem.get_current_memory(entity, slot) {
366 println!("{}:{} = \"{}\"", entity, slot, card.value);
367 } else {
368 println!("No memory found for {}:{}", entity, slot);
369 }
370 } else {
371 println!("{}:", entity);
373 for card in mem.get_entity_memories(entity) {
374 println!(" {}: {} = \"{}\"", card.kind, card.slot, card.value);
375 }
376 }
377 } else {
378 for entity in mem.memory_entities() {
380 println!("{}:", entity);
381 for card in mem.get_entity_memories(&entity) {
382 let polarity = card
383 .polarity
384 .as_ref()
385 .map(|p| format!(" [{}]", p))
386 .unwrap_or_default();
387 println!(
388 " {}: {} = \"{}\"{}",
389 card.kind, card.slot, card.value, polarity
390 );
391 }
392 println!();
393 }
394 }
395 }
396
397 Ok(())
398}
399
400fn card_to_output(card: &memvid_core::MemoryCard) -> MemoryOutput {
401 MemoryOutput {
402 id: card.id,
403 kind: card.kind.to_string(),
404 entity: card.entity.clone(),
405 slot: card.slot.clone(),
406 value: card.value.clone(),
407 polarity: card.polarity.as_ref().map(|p| p.to_string()),
408 document_date: card.document_date,
409 source_frame_id: card.source_frame_id,
410 }
411}