lean_ctx/core/value_gate/
report.rs1use super::{ValueAssessment, cpao};
2use crate::core::savings_ledger::roi_report;
3use serde::Serialize;
4
5#[derive(Debug, Clone, Default, Serialize)]
6pub struct ValueReport {
7 pub total_tasks: usize,
8 pub accepted_rate: f64,
9 pub cpao_micros: Option<u64>,
10 pub etpao_tokens: Option<u64>,
11 pub total_cost_micros: u64,
12 pub savings_usd: f64,
13 pub tasks: Vec<ValueAssessment>,
14}
15
16pub fn build(tasks: Vec<ValueAssessment>) -> ValueReport {
17 let total_cost_micros = tasks.iter().map(|task| task.cost_micros).sum();
18 let total_tokens = tasks.iter().map(|task| task.total_tokens).sum();
19 let costs = tasks
20 .iter()
21 .map(|task| task.cost_micros)
22 .collect::<Vec<_>>();
23 let accepted = tasks
24 .iter()
25 .map(|task| task.outcome_accepted)
26 .collect::<Vec<_>>();
27 let accepted_count = accepted.iter().filter(|&&ok| ok).count() as u64;
28 ValueReport {
29 total_tasks: tasks.len(),
30 accepted_rate: if tasks.is_empty() {
31 0.0
32 } else {
33 accepted_count as f64 / tasks.len() as f64
34 },
35 cpao_micros: cpao::cost_per_accepted_outcome(&costs, &accepted),
36 etpao_tokens: cpao::etpao(total_tokens, accepted_count),
37 total_cost_micros,
38 savings_usd: roi_report(crate::core::agent_identity::current_agent_id()).saved_usd,
39 tasks,
40 }
41}
42
43pub fn table(report: &ValueReport) -> String {
44 let mut output = format!(
45 "Value Report\nTasks: {} Accepted: {:.1}% CPAO: {} ETPAO: {} Cost: ${:.4} Savings: ${:.2}\n\n{:<12} {:>10} {:<9} {:>10} {:<18}\n{}\n",
46 report.total_tasks,
47 report.accepted_rate * 100.0,
48 money(report.cpao_micros),
49 tokens(report.etpao_tokens),
50 report.total_cost_micros as f64 / 1_000_000.0,
51 report.savings_usd,
52 "Task",
53 "Cost",
54 "Outcome",
55 "CPAO",
56 "Model",
57 "-".repeat(66)
58 );
59 for task in &report.tasks {
60 output.push_str(&format!(
61 "{:<12} {:>10} {:<9} {:>10} {:<18}\n",
62 short(&task.task_id),
63 money(Some(task.cost_micros)),
64 outcome(task),
65 money(task.cpao_micros),
66 task.model
67 ));
68 }
69 output
70}
71
72pub fn markdown(report: &ValueReport) -> String {
73 let mut output = format!(
74 "# Value Report\n\n- **Total tasks:** {}\n- **Accepted rate:** {:.1}%\n- **CPAO:** {}\n- **ETPAO:** {}\n- **Total cost:** ${:.4}\n- **Savings:** ${:.2}\n\n| Task | Cost | Outcome | CPAO | Model |\n|---|---:|---|---:|---|\n",
75 report.total_tasks,
76 report.accepted_rate * 100.0,
77 money(report.cpao_micros),
78 tokens(report.etpao_tokens),
79 report.total_cost_micros as f64 / 1_000_000.0,
80 report.savings_usd
81 );
82 for task in &report.tasks {
83 output.push_str(&format!(
84 "| {} | {} | {} | {} | {} |\n",
85 short(&task.task_id),
86 money(Some(task.cost_micros)),
87 outcome(task),
88 money(task.cpao_micros),
89 task.model
90 ));
91 }
92 output
93}
94
95pub fn json(report: &ValueReport) -> String {
96 serde_json::to_string_pretty(report).unwrap_or_else(|_| "{}".to_string())
97}
98fn outcome(task: &ValueAssessment) -> &str {
99 if task.outcome_accepted {
100 "accepted"
101 } else {
102 "rejected"
103 }
104}
105fn short(id: &str) -> &str {
106 id.get(..12).unwrap_or(id)
107}
108fn money(value: Option<u64>) -> String {
109 value.map_or_else(
110 || "n/a".to_string(),
111 |value| format!("${:.4}", value as f64 / 1_000_000.0),
112 )
113}
114fn tokens(value: Option<u64>) -> String {
115 value.map_or_else(|| "n/a".to_string(), |value| value.to_string())
116}