chat: route search_messages({date}) to get_sms_messages

When the LLM calls search_messages with { date, limit } and no
query, it's making the predictable mistake of conflating the two
"messages"-shaped tools. The previous behaviour returned an error
that pointed it at get_sms_messages — correct, but burning a turn
on the misroute. Long photo-chat threads where the user asks
"what was happening that weekend?" hit this on small models
roughly half the time.

Now the date-string-without-query case transparently dispatches
to get_sms_messages with the same args (date / limit / days_radius
/ contact name all pass through unchanged) and prepends a short
"(Note: routed to get_sms_messages — prefer it directly next time)"
to the result. The model sees real data on its first try while
still learning the right tool for next time. Cases that don't have
a get_sms_messages equivalent (numeric contact_id, or start_ts /
end_ts windows) keep the original error so the model knows to
either supply a query or restructure its call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cameron Cordes
2026-05-10 13:48:13 -04:00
parent fbd769e475
commit b9d9ba0320

View File

@@ -1558,7 +1558,7 @@ Return ONLY the summary, nothing else."#,
) -> String { ) -> String {
let result = match tool_name { let result = match tool_name {
"search_rag" => self.tool_search_rag(arguments, ollama, cx).await, "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_sms_messages" => self.tool_get_sms_messages(arguments, cx).await,
"get_calendar_events" => self.tool_get_calendar_events(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, "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 /// Tool: search_messages — keyword / semantic / hybrid search over all
/// SMS message bodies via the Django FTS5 + embeddings pipeline. Now /// SMS message bodies via the Django FTS5 + embeddings pipeline. Now
/// supports optional `contact_id`, `start_ts`, `end_ts` filters. /// 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()) { let query = match args.get("query").and_then(|v| v.as_str()) {
Some(q) if !q.trim().is_empty() => q.trim(), Some(q) if !q.trim().is_empty() => q.trim(),
_ => { _ => {
let has_date = args.get("date").is_some() // Common LLM mistake: calling search_messages with
|| args.get("start_ts").is_some() // { date, ... } as if it were date-browsing. The two
|| args.get("end_ts").is_some(); // tools share the "messages" word, and search_messages
let has_contact = args.get("contact").is_some() || args.get("contact_id").is_some(); // sounds like the natural verb. Instead of returning
if has_date || has_contact { // 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). \ return "Error: search_messages needs a 'query' (keywords/phrase). \
To fetch messages around a date or from a contact without keywords, \ To fetch messages around a date or from a contact without keywords, \
call get_sms_messages with { date, contact? } instead." call get_sms_messages with { date, contact? } instead."