robit_agent/tool/
search_history.rs1use async_trait::async_trait;
4use serde::Deserialize;
5use serde_json::Value;
6
7use crate::storage::{search_messages, MessageSearchFilter};
8use crate::tool::{Tool, ToolContext, ToolResult};
9use crate::error::Result;
10
11#[derive(Debug, Deserialize)]
12struct SearchHistoryArgs {
13 query: String,
14 role: Option<String>,
15 since: Option<String>,
16 until: Option<String>,
17 limit: Option<usize>,
18 #[serde(default)]
19 all_sessions: bool,
20}
21
22pub struct SearchHistoryTool;
23
24impl SearchHistoryTool {
25 pub fn new() -> Self {
26 Self
27 }
28}
29
30impl Default for SearchHistoryTool {
31 fn default() -> Self {
32 Self::new()
33 }
34}
35
36#[async_trait]
37impl Tool for SearchHistoryTool {
38 fn name(&self) -> &str {
39 "search_history"
40 }
41
42 fn description(&self) -> &str {
43 "Search through chat history using full-text search. \
44 By default searches only the current session. \
45 Useful for finding past messages, references, or context from earlier in the conversation."
46 }
47
48 fn parameters_schema(&self) -> Value {
49 serde_json::json!({
50 "type": "object",
51 "properties": {
52 "query": {
53 "type": "string",
54 "description": "Search query. Supports phrase matching (\"quoted text\"), prefix queries (word*), and boolean operators (AND, OR, NOT)"
55 },
56 "role": {
57 "type": "string",
58 "description": "Filter by message role: user, assistant, or tool"
59 },
60 "since": {
61 "type": "string",
62 "description": "Only return messages after this ISO 8601 timestamp"
63 },
64 "until": {
65 "type": "string",
66 "description": "Only return messages before this ISO 8601 timestamp"
67 },
68 "limit": {
69 "type": "integer",
70 "description": "Maximum number of results to return",
71 "default": 10
72 },
73 "all_sessions": {
74 "type": "boolean",
75 "description": "If true, search across all sessions. If false, search only the current session",
76 "default": false
77 }
78 },
79 "required": ["query"]
80 })
81 }
82
83 fn requires_confirmation(&self) -> bool {
84 false
85 }
86
87 async fn execute(&self, args: Value, ctx: &ToolContext) -> Result<ToolResult> {
88 let parsed: SearchHistoryArgs = match serde_json::from_value(args) {
89 Ok(a) => a,
90 Err(e) => return Ok(ToolResult::error(format!("Argument parsing failed: {}", e))),
91 };
92
93 if parsed.query.trim().is_empty() {
94 return Ok(ToolResult::error("Search query cannot be empty".to_string()));
95 }
96
97 let db_path = match crate::storage::resolve_db_path(&ctx.working_dir, false) {
98 Ok(p) => p,
99 Err(e) => return Ok(ToolResult::error(format!("Failed to resolve DB path: {}", e))),
100 };
101
102 let conn = match rusqlite::Connection::open(&db_path) {
103 Ok(c) => c,
104 Err(e) => return Ok(ToolResult::error(format!("Failed to open DB: {}", e))),
105 };
106
107 let session_id = if parsed.all_sessions {
108 None
109 } else {
110 Some(ctx.session_id.as_str())
111 };
112
113 let filter = MessageSearchFilter {
115 session_id,
116 role: parsed.role.as_deref(),
117 since: parsed.since.as_deref(),
118 until: parsed.until.as_deref(),
119 };
120
121 let limit = parsed.limit.unwrap_or(10).min(50); match search_messages(&conn, &parsed.query, &filter, limit) {
124 Ok(results) if results.is_empty() => {
125 Ok(ToolResult::success(
126 "No messages found matching the search criteria.".to_string()
127 ))
128 }
129 Ok(results) => {
130 let mut output = format!(
131 "Found {} message{} matching \"{}\":\n\n",
132 results.len(),
133 if results.len() == 1 { "" } else { "s" },
134 parsed.query
135 );
136
137 let current_session = ctx.session_id.as_str();
138
139 for (i, msg) in results.iter().enumerate() {
140 output.push_str(&format!(
141 "{}. [{}] {}",
142 i + 1,
143 msg.role,
144 msg.created_at
145 ));
146
147 if parsed.all_sessions && msg.session_id != current_session {
149 output.push_str(&format!(
150 " (Session: {})",
151 msg.session_title
152 ));
153 }
154
155 output.push_str("\n");
156
157 for line in msg.content_snippet.lines() {
159 output.push_str(&format!(" {}\n", line));
160 }
161 output.push_str("\n");
162 }
163
164 Ok(ToolResult::success(output))
165 }
166 Err(e) => Ok(ToolResult::error(format!("Search failed: {}", e))),
167 }
168 }
169}