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