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:
@@ -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."
|
||||||
|
|||||||
Reference in New Issue
Block a user