use crate::config::Config;
use crate::llm::Llm;
use anyhow::Result;
use indicatif::ProgressBar;
use serde_json::Value;
use std::fs;
const FINAL_SYSTEM: &str = r#"You are producing the final persona profile of a person from per-file reviews of their filesystem.
Each review below was produced by reading one file from the person's computer and analyzing what it reveals about them.
Write a single well-structured Markdown document with these sections (omit empty ones):
# Persona Profile
## Overview
## Identity & Background
## Skills & Expertise
## Projects & Work
## Interests & Hobbies
## Tools, Environment & Workflow
## Habits & Routine
## Social & Relationships
## Values, Beliefs & Personality
## Notable Artifacts & Evidence
## Timeline / Recent Activity
## Gaps & Uncertainties
Requirements:
- Ground every claim in cited file paths where possible.
- Be specific and concrete (names, technologies, places, dates, metrics, numbers).
- Capture quantitative results (accuracy, F1, precision, recall, etc.) when present in the reviews.
- Distinguish facts vs. inferences (mark inferences as "likely").
- Do not repeat facts or bullet points.
- For "Values, Beliefs & Personality": analyze the person's traits, work style, communication style, values, and personality based on evidence from their files. Infer personality from coding style, project choices, documentation habits, file organization, tool preferences, and any personal notes. Mark all personality claims as inferences ("likely", "appears to", "suggests").
- Include an "Educated Guesses" subsection (or integrate throughout): based on the evidence, make reasonable inferences about what the person has likely said, done, experienced, or believes. Examples: "likely has said X in standups", "probably debugged Y at 2am", "appears to have experienced Z". Mark all as educated guesses ("likely", "probably", "appears to", "I'd guess"). These are informed speculation, not facts.
- Length: as thorough as needed to capture all accurate information. No artificial length limit.
- No preamble outside the document.
"#;
pub async fn synthesize(cfg: &Config, llm: &Llm, facts_path: &std::path::Path) -> Result<()> {
// load per-file reviews
let content = fs::read_to_string(facts_path)?;
let mut records: Vec<Value> = Vec::new();
for line in content.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
if let Ok(v) = serde_json::from_str::<Value>(line) {
records.push(v);
}
}
tracing::info!("loaded {} file records", records.len());
// filter to those with reviews (skip empty and file-tree)
let reviews: Vec<(String, String)> = records
.iter()
.filter_map(|r| {
let path = r.get("path").and_then(|x| x.as_str()).unwrap_or("");
if path == "system:file-tree" {
return None;
}
let notes = r.get("notes").and_then(|x| x.as_str()).unwrap_or("");
if notes.trim().is_empty() || notes.trim().eq_ignore_ascii_case("NONE") {
return None;
}
Some((path.to_string(), notes.to_string()))
})
.collect();
if reviews.is_empty() {
let msg = "# Persona Profile\n\nNo relevant reviews extracted.\n";
fs::write(&cfg.out, msg)?;
return Ok(());
}
tracing::info!("{} per-file reviews to synthesize", reviews.len());
// Build the combined input from all per-file reviews
let mut combined = String::new();
for (path, review) in &reviews {
combined.push_str(&format!("FILE: {}\nREVIEW: {}\n\n", path, review));
}
// Cap at 200k chars to leave room for output
let combined = if combined.chars().count() > 200_000 {
combined.chars().take(200_000).collect()
} else {
combined
};
let user = format!(
"Here are {} per-file reviews from the person's filesystem. Synthesize the final profile.\n\n{}",
reviews.len(),
combined
);
let sp = ProgressBar::new_spinner();
sp.set_message("final synthesis...");
sp.enable_steady_tick(std::time::Duration::from_millis(100));
let preview: String = combined.chars().take(150).collect::<String>().replace('\n', " ");
tracing::info!("sending to LLM: final synthesis ({} chars from {} reviews): {}...", combined.len(), reviews.len(), preview);
let (final_md, thinking) = match llm.chat_full(FINAL_SYSTEM, &user, 8192, 0.3, Some(0)).await {
Ok((m, t)) => (m, t),
Err(e) => {
tracing::error!("final synthesis failed: {}", e);
// fallback: concatenate reviews
(format!("# Persona Profile (synthesis failed)\n\n{}", combined), String::new())
}
};
sp.finish_and_clear();
// Append thinking as an annotated section if present
let mut output = final_md.clone();
if !thinking.trim().is_empty() {
output.push_str("\n\n---\n\n## Model Thinking (annotated)\n\n");
output.push_str(&thinking);
}
fs::write(&cfg.out, output)?;
tracing::info!("wrote profile to {}", cfg.out.display());
Ok(())
}