use anyhow::Result;
use serde::Deserialize;
use tokio::sync::mpsc::UnboundedSender;
use crate::action::Action;
use crate::components::transcript::{LineKind, MessageLine};
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct TranscriptResponse {
pub entries: Vec<TranscriptEntry>,
}
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct TranscriptEntry {
pub role: String,
pub text: String,
}
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct UsageResponse {
pub input_tokens: u64,
pub output_tokens: u64,
#[serde(default)]
pub per_turn: Vec<PerTurnUsage>,
}
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct PerTurnUsage {}
pub(crate) async fn load_history(
forensics_base_url: &str,
conversation_id: &str,
) -> Result<Vec<MessageLine>> {
let base = forensics_base_url.trim_end_matches('/');
let client = crate::data::http_client();
let url = format!("{base}/api/conversations/{conversation_id}/transcript");
let resp: TranscriptResponse = client
.get(&url)
.send()
.await?
.error_for_status()?
.json()
.await?;
let mut lines: Vec<MessageLine> = resp.entries.iter().map(line_from_entry).collect();
if let Some(line) = load_usage_line(base, conversation_id).await {
lines.push(line);
}
Ok(lines)
}
async fn load_usage_line(base: &str, conversation_id: &str) -> Option<MessageLine> {
let url = format!("{base}/api/conversations/{conversation_id}/usage");
let usage: UsageResponse = crate::data::http_client()
.get(&url)
.send()
.await
.ok()?
.error_for_status()
.ok()?
.json()
.await
.ok()?;
Some(usage_ledger_line(&usage))
}
#[must_use]
pub(crate) fn usage_ledger_line(usage: &UsageResponse) -> MessageLine {
let turns = usage.per_turn.len();
MessageLine {
kind: LineKind::System,
text: format!(
"usage ledger: input={} output={} total={} over {} turn(s)",
usage.input_tokens,
usage.output_tokens,
usage.input_tokens + usage.output_tokens,
turns,
),
}
}
pub(crate) fn spawn_history(
forensics_base_url: String,
conversation_id: String,
tx: UnboundedSender<Action>,
) {
tokio::spawn(async move {
match load_history(&forensics_base_url, &conversation_id).await {
Ok(lines) => {
let _ = tx.send(Action::TranscriptHistory {
conversation_id,
lines,
});
}
Err(err) => {
let _ = tx.send(Action::Error(format!("transcript history: {err}")));
}
}
});
}
#[must_use]
pub(crate) fn line_from_entry(entry: &TranscriptEntry) -> MessageLine {
let kind = match entry.role.as_str() {
"user" => LineKind::User,
"tool" => LineKind::ToolResult {
call_id: String::new(),
},
"thought" => LineKind::Thought,
"system" => LineKind::System,
_ => LineKind::Assistant,
};
MessageLine {
kind,
text: entry.text.clone(),
}
}