1use crate::{generate_with, AgentContext, AgentGenerator, AgentResult};
8use car_inference::{GenerateParams, GenerateRequest};
9use std::sync::Arc;
10
11#[derive(Debug, Clone)]
13pub struct SummaryConfig {
14 pub target_tokens: usize,
16 pub temperature: f64,
17 pub model: Option<String>,
18}
19
20impl Default for SummaryConfig {
21 fn default() -> Self {
22 Self {
23 target_tokens: 500,
24 temperature: 0.2,
25 model: None,
26 }
27 }
28}
29
30pub struct Summarizer {
32 ctx: AgentContext,
33 config: SummaryConfig,
34 generator: Option<Arc<AgentGenerator>>,
35}
36
37impl Summarizer {
38 pub fn new(ctx: AgentContext) -> Self {
39 Self {
40 ctx,
41 config: SummaryConfig::default(),
42 generator: None,
43 }
44 }
45
46 pub fn with_config(ctx: AgentContext, config: SummaryConfig) -> Self {
47 Self {
48 ctx,
49 config,
50 generator: None,
51 }
52 }
53
54 pub fn with_generator(
56 ctx: AgentContext,
57 config: SummaryConfig,
58 generator: Arc<AgentGenerator>,
59 ) -> Self {
60 Self {
61 ctx,
62 config,
63 generator: Some(generator),
64 }
65 }
66
67 pub async fn summarize(&self, content: &str, focus: Option<&str>) -> AgentResult {
69 let focus_instruction = focus
70 .map(|f| format!("\nFocus specifically on: {f}"))
71 .unwrap_or_default();
72
73 let prompt = format!(
74 "Summarize the following content in approximately {} tokens. \
75 Preserve all specific facts, numbers, names, and actionable items. \
76 Drop generic preamble and filler.{focus_instruction}\n\n\
77 Content:\n{content}",
78 self.config.target_tokens,
79 );
80
81 let start = std::time::Instant::now();
82 let req = GenerateRequest {
83 prompt,
84 model: self.config.model.clone(),
85 params: GenerateParams {
86 temperature: self.config.temperature,
87 max_tokens: self.config.target_tokens * 2, ..Default::default()
89 },
90 context: None,
91 context_stable_prefix: None,
92 tools: None,
93 images: None,
94 messages: None,
95 cache_control: false,
96 response_format: None,
97 intent: None,
98 client_ref: None,
99 expected_row_digest: None,
100 expected_catalog_revision: None,
101 caller: None,
102 };
103
104 match generate_with(&self.ctx, self.generator.as_ref(), req).await {
105 Ok(result) => {
106 let compression = if !content.is_empty() {
107 1.0 - (result.text.len() as f64 / content.len() as f64)
108 } else {
109 0.0
110 };
111 AgentResult {
112 agent: "summarizer".into(),
113 output: result.text,
114 confidence: if compression > 0.3 { 0.8 } else { 0.5 },
115 model_used: result.model_used,
116 latency_ms: start.elapsed().as_millis() as u64,
117 }
118 }
119 Err(e) => AgentResult {
120 agent: "summarizer".into(),
121 output: format!("Summarization failed: {}", e),
122 confidence: 0.0,
123 model_used: String::new(),
124 latency_ms: start.elapsed().as_millis() as u64,
125 },
126 }
127 }
128
129 pub async fn synthesize_answer(&self, research: &str, goal: &str) -> AgentResult {
138 let g = goal.trim().to_lowercase();
141 let is_broad_review = g.split_whitespace().count() < 8
142 && (g.starts_with("review")
143 || g.starts_with("analy")
144 || g.contains("codebase")
145 || g.starts_with("describe")
146 || g.starts_with("overview")
147 || g == "what is this");
148
149 let structure_instruction = if is_broad_review {
150 "\nBecause this is a broad review ask, structure your answer with ALL of the following sections. Fill each with concrete specifics (file paths, symbol names, numbers). Do not skip any section.\n\
151 ## Overview\n — one paragraph: what the project is and what it does, grounded in real components from the research.\n\
152 ## Main Components\n — the major subsystems/services, each with its directory path and a one-line purpose.\n\
153 ## Key Integrations\n — external systems (databases, auth, APIs, cloud services) and the files that handle them.\n\
154 ## Top Risks or Gaps\n — 3–5 concrete things that look fragile, under-tested, or deserve attention. Cite files.\n\
155 ## Recommended Next Actions\n — 3 high-value things a new engineer could do this week.\n"
156 } else {
157 ""
158 };
159
160 let prompt = format!(
161 "You are writing the FINAL user-facing answer to a question about a codebase. \
162 Another agent has already done the research. Your job is to turn that research \
163 into a clear, direct, genuinely useful answer.\n\n\
164 Rules:\n\
165 1. ANSWER the user's question. Do not outline HOW to answer it. Do NOT return \
166 a list of steps or a workflow unless the user explicitly asked for steps.\n\
167 2. Be specific. Every claim should cite a file path, symbol, or number when \
168 the research supports it. Vague statements (\"well-organized\", \"robust\") \
169 are forbidden unless backed by evidence.\n\
170 3. Use markdown structure (headings, bullets, code spans) to make the answer \
171 scannable.\n\
172 4. If the research is thin on a point, say so — do not invent details.\n\
173 5. Lead with the answer. Minimal preamble.\n{structure_instruction}\n\
174 ## User's question\n{goal}\n\n\
175 ## Research\n{research}\n\n\
176 Now write the final answer:"
177 );
178
179 let start = std::time::Instant::now();
180 let req = GenerateRequest {
181 prompt,
182 model: self.config.model.clone(),
183 params: GenerateParams {
184 temperature: self.config.temperature.max(0.3),
185 max_tokens: 4096,
188 ..Default::default()
189 },
190 context: None,
191 context_stable_prefix: None,
192 tools: None,
193 images: None,
194 messages: None,
195 cache_control: false,
196 response_format: None,
197 intent: None,
198 client_ref: None,
199 expected_row_digest: None,
200 expected_catalog_revision: None,
201 caller: None,
202 };
203
204 match generate_with(&self.ctx, self.generator.as_ref(), req).await {
205 Ok(result) => AgentResult {
206 agent: "summarizer".into(),
207 output: result.text,
208 confidence: 0.85,
209 model_used: result.model_used,
210 latency_ms: start.elapsed().as_millis() as u64,
211 },
212 Err(e) => AgentResult {
213 agent: "summarizer".into(),
214 output: format!("Synthesis failed: {}", e),
215 confidence: 0.0,
216 model_used: String::new(),
217 latency_ms: start.elapsed().as_millis() as u64,
218 },
219 }
220 }
221}