// LLM utilities: model invocation via direct API use crate::store::Store; use regex::Regex; use std::fs; /// Simple LLM call for non-agent uses (audit, digest, compare). /// Logs to llm-logs/{caller}/ file. pub(crate) fn call_simple(caller: &str, prompt: &str) -> Result { let log_dir = crate::store::memory_dir().join("llm-logs").join(caller); fs::create_dir_all(&log_dir).ok(); let log_path = log_dir.join(format!("{}.txt", crate::store::compact_timestamp())); use std::io::Write; let log = move |msg: &str| { if let Ok(mut f) = fs::OpenOptions::new() .create(true).append(true).open(&log_path) { let _ = writeln!(f, "{}", msg); } }; let prompts = vec![prompt.to_string()]; super::api::call_api_with_tools_sync(caller, &prompts, None, &log) } /// Call a model using an agent definition's configuration (multi-step). pub(crate) fn call_for_def_multi( def: &super::defs::AgentDef, prompts: &[String], log: &(dyn Fn(&str) + Sync), ) -> Result { super::api::call_api_with_tools_sync(&def.agent, prompts, def.temperature, log) } /// Parse a JSON response, handling markdown fences. pub(crate) fn parse_json_response(response: &str) -> Result { let cleaned = response.trim(); let cleaned = cleaned.strip_prefix("```json").unwrap_or(cleaned); let cleaned = cleaned.strip_prefix("```").unwrap_or(cleaned); let cleaned = cleaned.strip_suffix("```").unwrap_or(cleaned); let cleaned = cleaned.trim(); if let Ok(v) = serde_json::from_str(cleaned) { return Ok(v); } // Try to find JSON object or array let re_obj = Regex::new(r"\{[\s\S]*\}").unwrap(); let re_arr = Regex::new(r"\[[\s\S]*\]").unwrap(); if let Some(m) = re_obj.find(cleaned) && let Ok(v) = serde_json::from_str(m.as_str()) { return Ok(v); } if let Some(m) = re_arr.find(cleaned) && let Ok(v) = serde_json::from_str(m.as_str()) { return Ok(v); } let preview = crate::util::first_n_chars(cleaned, 200); Err(format!("no valid JSON in response: {preview}...")) } /// Get all keys for prompt context. pub(crate) fn semantic_keys(store: &Store) -> Vec { let mut keys: Vec = store.nodes.keys() .cloned() .collect(); keys.sort(); keys.truncate(200); keys }