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};
13
14pub struct BriefTool {
15    provider: Arc<dyn Provider>,
16    model: String,
17}
18
19impl BriefTool {
20    pub fn new(provider: Arc<dyn Provider>, model: impl Into<String>) -> Self {
21        Self {
22            provider,
23            model: model.into(),
24        }
25    }
26}
27
28#[derive(Deserialize)]
29struct BriefArgs {
30    /// Content to summarize
31    text: String,
32    /// Summary style: "bullets", "paragraph", "tldr" (default: bullets)
33    #[serde(default = "default_style")]
34    style: String,
35    /// Max length hint in words (default: 150)
36    #[serde(default = "default_max_words")]
37    max_words: usize,
38}
39
40fn default_style() -> String {
41    "bullets".to_string()
42}
43fn default_max_words() -> usize {
44    150
45}
46
47#[async_trait]
48impl Tool for BriefTool {
49    fn name(&self) -> &str {
50        "brief"
51    }
52
53    fn spec(&self) -> ToolSpec {
54        ToolSpec {
55            name: "brief".to_string(),
56            description: "Summarize long text into a concise brief using AI. \
57                Use to condense search results, file contents, or verbose output \
58                before including it in a response."
59                .to_string(),
60            parameters: serde_json::json!({
61                "type": "object",
62                "properties": {
63                    "text": {
64                        "type": "string",
65                        "description": "Content to summarize"
66                    },
67                    "style": {
68                        "type": "string",
69                        "enum": ["bullets", "paragraph", "tldr"],
70                        "description": "Output style (default: bullets)"
71                    },
72                    "max_words": {
73                        "type": "integer",
74                        "description": "Approximate word limit for the summary (default: 150)",
75                        "minimum": 20,
76                        "maximum": 500
77                    }
78                },
79                "required": ["text"]
80            }),
81        }
82    }
83
84    async fn execute(&self, arguments: &str) -> anyhow::Result<ToolResult> {
85        let args: BriefArgs = serde_json::from_str(arguments)?;
86
87        if args.text.trim().is_empty() {
88            return Ok(ToolResult::error("text is empty"));
89        }
90
91        let style_instruction = match args.style.as_str() {
92            "paragraph" => "Write a concise paragraph summary.",
93            "tldr" => "Write a one-sentence TL;DR.",
94            _ => "Write a concise bullet-point summary (use - for each bullet).",
95        };
96
97        let prompt = format!(
98            "{} Keep it under ~{} words. Do not add commentary.\n\n---\n{}",
99            style_instruction,
100            args.max_words,
101            // Truncate input if very long
102            if args.text.len() > 40_000 {
103                format!("{}...[truncated]", &args.text[..40_000])
104            } else {
105                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}