synops/maskinrommet/src/summarize.rs
vegard 08ff14028b Implementer synops-summarize CLI-verktøy (oppgave 21.6)
Ekstraherer AI-oppsummeringslogikk fra maskinrommet til standalone
CLI-verktøy, i tråd med unix_filosofi.md-prinsippet om at maskinrommet
orkestrerer og CLI-verktøy gjør jobben.

synops-summarize:
- Henter meldinger og deltakere fra kommunikasjonsnode i PG
- Sender samtalelogg til LiteLLM for oppsummering
- Med --write: oppretter sammendrag-node, belongs_to/summary-edges,
  logger AI-ressursforbruk
- Uten --write: dry-run som skriver JSON til stdout

maskinrommet/src/summarize.rs er nå en tynn dispatcher som spawner
synops-summarize med --write, tilsvarende transcribe.rs-mønsteret.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 09:36:51 +00:00

120 lines
3.7 KiB
Rust

// Oppsummerings-dispatcher — delegerer til synops-summarize CLI.
//
// Maskinrommet orkestrerer, CLI-verktøyet gjør jobben.
// Ref: docs/retninger/unix_filosofi.md
//
// Jobbtype: "summarize_communication"
// Payload: { "communication_id": "<uuid>", "requested_by": "<uuid>" }
use std::process::Stdio;
use uuid::Uuid;
use crate::jobs::JobRow;
use crate::stdb::StdbClient;
/// Synops-summarize binary path.
/// Søker i PATH, men kan overrides med SYNOPS_SUMMARIZE_BIN.
fn summarize_bin() -> String {
std::env::var("SYNOPS_SUMMARIZE_BIN")
.unwrap_or_else(|_| "synops-summarize".to_string())
}
/// Handler for summarize_communication-jobber.
///
/// Spawner synops-summarize med --write for å gjøre alt arbeidet:
/// LLM-kall, node-opprettelse, edge-skriving, ressurslogging.
///
/// Payload forventer:
/// - communication_id: UUID — kommunikasjonsnoden som skal oppsummeres
/// - requested_by: UUID — brukeren som utløste oppsummeringen
pub async fn handle_summarize_communication(
job: &JobRow,
_db: &sqlx::PgPool,
_stdb: &StdbClient,
) -> Result<serde_json::Value, String> {
let communication_id: Uuid = job
.payload
.get("communication_id")
.and_then(|v| v.as_str())
.and_then(|s| s.parse().ok())
.ok_or("Mangler gyldig communication_id i payload")?;
let requested_by: Uuid = job
.payload
.get("requested_by")
.and_then(|v| v.as_str())
.and_then(|s| s.parse().ok())
.ok_or("Mangler gyldig requested_by i payload")?;
// Bygg kommando
let bin = summarize_bin();
let mut cmd = tokio::process::Command::new(&bin);
cmd.arg("--communication-id")
.arg(communication_id.to_string())
.arg("--requested-by")
.arg(requested_by.to_string())
.arg("--write");
// Sett miljøvariabler CLI-verktøyet trenger
let db_url = std::env::var("DATABASE_URL")
.map_err(|_| "DATABASE_URL ikke satt".to_string())?;
cmd.env("DATABASE_URL", &db_url);
// Videresend AI-relaterte env-variabler hvis satt
if let Ok(v) = std::env::var("AI_GATEWAY_URL") {
cmd.env("AI_GATEWAY_URL", v);
}
if let Ok(v) = std::env::var("LITELLM_MASTER_KEY") {
cmd.env("LITELLM_MASTER_KEY", v);
}
if let Ok(v) = std::env::var("AI_SUMMARY_MODEL") {
cmd.env("AI_SUMMARY_MODEL", v);
}
cmd.stdout(Stdio::piped())
.stderr(Stdio::piped());
tracing::info!(
communication_id = %communication_id,
requested_by = %requested_by,
bin = %bin,
"Starter synops-summarize"
);
// Spawn og vent
let child = cmd
.spawn()
.map_err(|e| format!("Kunne ikke starte {bin}: {e}"))?;
let output = child
.wait_with_output()
.await
.map_err(|e| format!("Feil ved kjøring av {bin}: {e}"))?;
let stderr = String::from_utf8_lossy(&output.stderr);
if !stderr.is_empty() {
tracing::info!(stderr = %stderr, "synops-summarize stderr");
}
if !output.status.success() {
let code = output.status.code().unwrap_or(-1);
return Err(format!(
"synops-summarize feilet (exit {code}): {stderr}"
));
}
// Parse stdout som JSON — det er resultatet
let stdout = String::from_utf8_lossy(&output.stdout);
let result: serde_json::Value = serde_json::from_str(&stdout)
.map_err(|e| format!("Kunne ikke parse synops-summarize output: {e}"))?;
tracing::info!(
communication_id = %communication_id,
summary_node_id = result["summary_node_id"].as_str().unwrap_or("n/a"),
status = result["status"].as_str().unwrap_or("unknown"),
"synops-summarize fullført"
);
Ok(result)
}