Skip to main content

apollo/tools/
brief.rs

1//! BriefTool — AI-powered summarization using the fast model.
2//!
3//! Summarizes long text, files, or search results into a concise brief.
4//! Uses the configured fast_model (Haiku) to keep cost low.
5
6use std::sync::Arc;
7
8use async_trait::async_trait;
9use serde::Deserialize;
10
11use super::traits::*;
12use crate::providers::{ChatMessage, ChatRequest, Provider};
13use crate::text::truncate_chars_counted;
14
15pub struct BriefTool {
16    provider: Arc<dyn Provider>,
17    model: String,
18}
19
20impl BriefTool {
21    pub fn new(provider: Arc<dyn Provider>, model: impl Into<String>) -> Self {
22        Self {
23            provider,
24            model: model.into(),
25        }
26    }
27}
28
29#[derive(Deserialize)]
30struct BriefArgs {
31    /// Content to summarize
32    text: String,
33    /// Summary style: "bullets", "paragraph", "tldr" (default: bullets)
34    #[serde(default = "default_style")]
35    style: String,
36    /// Max length hint in words (default: 150)
37    #[serde(default = "default_max_words")]
38    max_words: usize,
39}
40
41fn default_style() -> String {
42    "bullets".to_string()
43}
44fn default_max_words() -> usize {
45    150
46}
47
48#[async_trait]
49impl Tool for BriefTool {
50    fn name(&self) -> &str {
51        "brief"
52    }
53
54    fn spec(&self) -> ToolSpec {
55        ToolSpec {
56            name: "brief".to_string(),
57            description: "Summarize long text into a concise brief using AI. \
58                Use to condense search results, file contents, or verbose output \
59                before including it in a response."
60                .to_string(),
61            parameters: serde_json::json!({
62                "type": "object",
63                "properties": {
64                    "text": {
65                        "type": "string",
66                        "description": "Content to summarize"
67                    },
68                    "style": {
69                        "type": "string",
70                        "enum": ["bullets", "paragraph", "tldr"],
71                        "description": "Output style (default: bullets)"
72                    },
73                    "max_words": {
74                        "type": "integer",
75                        "description": "Approximate word limit for the summary (default: 150)",
76                        "minimum": 20,
77                        "maximum": 500
78                    }
79                },
80                "required": ["text"]
81            }),
82        }
83    }
84
85    async fn execute(&self, arguments: &str) -> anyhow::Result<ToolResult> {
86        let args: BriefArgs = serde_json::from_str(arguments)?;
87
88        if args.text.trim().is_empty() {
89            return Ok(ToolResult::error("text is empty"));
90        }
91
92        let style_instruction = match args.style.as_str() {
93            "paragraph" => "Write a concise paragraph summary.",
94            "tldr" => "Write a one-sentence TL;DR.",
95            _ => "Write a concise bullet-point summary (use - for each bullet).",
96        };
97
98        let prompt = format!(
99            "{} Keep it under ~{} words. Do not add commentary.\n\n---\n{}",
100            style_instruction,
101            args.max_words,
102            // Truncate input if very long
103            match truncate_chars_counted(&args.text, 40_000) {
104                Some((head, dropped)) => format!("{head}...[truncated {dropped} chars]"),
105                None => args.text.clone(),
106            }
107        );
108
109        let messages = [ChatMessage::user(&prompt)];
110        let request = ChatRequest {
111            messages: &messages,
112            tools: None,
113            model: &self.model,
114            temperature: 0.3,
115            max_tokens: Some(600),
116        };
117
118        match self.provider.chat(&request).await {
119            Ok(resp) => {
120                let summary = resp.text.unwrap_or_else(|| "(empty response)".to_string());
121                Ok(ToolResult::success(summary))
122            }
123            Err(e) => Ok(ToolResult::error(format!("Brief failed: {}", e))),
124        }
125    }
126}