diff --git a/src/ai/insight_generator.rs b/src/ai/insight_generator.rs index d380b40..1e39f9a 100644 --- a/src/ai/insight_generator.rs +++ b/src/ai/insight_generator.rs @@ -1558,7 +1558,7 @@ Return ONLY the summary, nothing else."#, ) -> String { let result = match tool_name { "search_rag" => self.tool_search_rag(arguments, ollama, cx).await, - "search_messages" => self.tool_search_messages(arguments).await, + "search_messages" => self.tool_search_messages(arguments, cx).await, "get_sms_messages" => self.tool_get_sms_messages(arguments, cx).await, "get_calendar_events" => self.tool_get_calendar_events(arguments, cx).await, "get_location_history" => self.tool_get_location_history(arguments, cx).await, @@ -1807,15 +1807,43 @@ Return ONLY the summary, nothing else."#, /// Tool: search_messages — keyword / semantic / hybrid search over all /// SMS message bodies via the Django FTS5 + embeddings pipeline. Now /// supports optional `contact_id`, `start_ts`, `end_ts` filters. - async fn tool_search_messages(&self, args: &serde_json::Value) -> String { + async fn tool_search_messages(&self, args: &serde_json::Value, cx: &opentelemetry::Context) -> String { let query = match args.get("query").and_then(|v| v.as_str()) { Some(q) if !q.trim().is_empty() => q.trim(), _ => { - let has_date = args.get("date").is_some() - || args.get("start_ts").is_some() - || args.get("end_ts").is_some(); - let has_contact = args.get("contact").is_some() || args.get("contact_id").is_some(); - if has_date || has_contact { + // Common LLM mistake: calling search_messages with + // { date, ... } as if it were date-browsing. The two + // tools share the "messages" word, and search_messages + // sounds like the natural verb. Instead of returning + // an error and burning a turn, transparently route to + // get_sms_messages when there's a `date` (and a + // contact-name string, optional). The LLM gets real + // data on its first try; the result is logged with a + // routing note so a human reading the trace can see + // what happened. + let has_date_str = args + .get("date") + .and_then(|v| v.as_str()) + .map(|s| !s.trim().is_empty()) + .unwrap_or(false); + let has_numeric_contact = args.get("contact_id").is_some(); + let has_ts_window = + args.get("start_ts").is_some() || args.get("end_ts").is_some(); + if has_date_str && !has_numeric_contact && !has_ts_window { + log::info!( + "search_messages with `date` and no `query` — routing to get_sms_messages" + ); + let routed = self.tool_get_sms_messages(args, cx).await; + return format!( + "(Note: routed to get_sms_messages — search_messages requires a \ + `query`; date-only browsing belongs on get_sms_messages. Prefer \ + get_sms_messages directly next time.)\n\n{}", + routed + ); + } + let has_contact_name = + args.get("contact").and_then(|v| v.as_str()).is_some(); + if has_ts_window || has_numeric_contact || has_contact_name { return "Error: search_messages needs a 'query' (keywords/phrase). \ To fetch messages around a date or from a contact without keywords, \ call get_sms_messages with { date, contact? } instead."