2026-03-22 02:09:40 -04:00
|
|
|
// LLM utilities: model invocation via direct API
|
2026-03-03 17:18:18 -05:00
|
|
|
|
|
|
|
|
use crate::store::Store;
|
|
|
|
|
|
|
|
|
|
use regex::Regex;
|
|
|
|
|
use std::fs;
|
2026-03-05 15:30:57 -05:00
|
|
|
|
2026-03-22 02:08:32 -04:00
|
|
|
/// 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<String, String> {
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
super::api::call_api_with_tools_sync(caller, prompt, &log)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Call a model using an agent definition's configuration.
|
2026-03-22 01:57:47 -04:00
|
|
|
pub(crate) fn call_for_def(
|
|
|
|
|
def: &super::defs::AgentDef,
|
|
|
|
|
prompt: &str,
|
|
|
|
|
log: &(dyn Fn(&str) + Sync),
|
|
|
|
|
) -> Result<String, String> {
|
|
|
|
|
super::api::call_api_with_tools_sync(&def.agent, prompt, log)
|
2026-03-13 18:50:06 -04:00
|
|
|
}
|
|
|
|
|
|
2026-03-05 15:30:57 -05:00
|
|
|
/// Parse a JSON response, handling markdown fences.
|
2026-03-03 17:18:18 -05:00
|
|
|
pub(crate) fn parse_json_response(response: &str) -> Result<serde_json::Value, String> {
|
|
|
|
|
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();
|
|
|
|
|
|
2026-03-21 19:42:38 -04:00
|
|
|
if let Some(m) = re_obj.find(cleaned)
|
|
|
|
|
&& let Ok(v) = serde_json::from_str(m.as_str()) {
|
2026-03-03 17:18:18 -05:00
|
|
|
return Ok(v);
|
|
|
|
|
}
|
2026-03-21 19:42:38 -04:00
|
|
|
if let Some(m) = re_arr.find(cleaned)
|
|
|
|
|
&& let Ok(v) = serde_json::from_str(m.as_str()) {
|
2026-03-03 17:18:18 -05:00
|
|
|
return Ok(v);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-08 21:13:02 -04:00
|
|
|
let preview = crate::util::first_n_chars(cleaned, 200);
|
2026-03-05 21:15:57 -05:00
|
|
|
Err(format!("no valid JSON in response: {preview}..."))
|
2026-03-03 17:18:18 -05:00
|
|
|
}
|
|
|
|
|
|
2026-03-08 20:07:07 -04:00
|
|
|
/// Get all keys for prompt context.
|
2026-03-03 17:18:18 -05:00
|
|
|
pub(crate) fn semantic_keys(store: &Store) -> Vec<String> {
|
|
|
|
|
let mut keys: Vec<String> = store.nodes.keys()
|
|
|
|
|
.cloned()
|
|
|
|
|
.collect();
|
|
|
|
|
keys.sort();
|
|
|
|
|
keys.truncate(200);
|
|
|
|
|
keys
|
|
|
|
|
}
|