1use 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 text: String,
33 #[serde(default = "default_style")]
35 style: String,
36 #[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 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}