mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(composio): normalize bare calendar dates to RFC 3339 before dispatch (#1802)
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
co-authored by
Steven Enamakel
parent
32485b6b8b
commit
7968f3c783
@@ -197,7 +197,8 @@ impl ComposioClient {
|
||||
if tool.is_empty() {
|
||||
anyhow::bail!("composio.execute_tool: tool slug must not be empty");
|
||||
}
|
||||
let arguments = arguments.unwrap_or(serde_json::Value::Object(Default::default()));
|
||||
let mut arguments = arguments.unwrap_or(serde_json::Value::Object(Default::default()));
|
||||
normalize_calendar_query_args(tool, &mut arguments);
|
||||
tracing::debug!(tool = %tool, "[composio] execute_tool");
|
||||
let body = json!({ "tool": tool, "arguments": arguments });
|
||||
self.execute_tool_with_post_oauth_retry(tool, &body, POST_OAUTH_ACTION_RETRY_DELAY)
|
||||
@@ -505,6 +506,58 @@ impl ComposioClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Calendar query slugs whose `timeMin`/`timeMax` values should be
|
||||
/// normalized to RFC 3339 timestamps. LLM-generated arguments sometimes
|
||||
/// emit bare dates like `"2026-05-14"` instead of
|
||||
/// `"2026-05-14T00:00:00Z"`, which Google Calendar rejects.
|
||||
const CALENDAR_QUERY_SLUGS: &[&str] = &["GOOGLECALENDAR_EVENTS_LIST", "GOOGLECALENDAR_FIND_EVENT"];
|
||||
|
||||
/// Normalize `timeMin`/`timeMax` from bare dates to RFC 3339 for
|
||||
/// Google Calendar query slugs. The LLM prompt instructs the model to
|
||||
/// use RFC 3339 format, but some model invocations still produce bare
|
||||
/// `YYYY-MM-DD` strings.
|
||||
fn normalize_calendar_query_args(tool: &str, arguments: &mut serde_json::Value) {
|
||||
if !CALENDAR_QUERY_SLUGS.contains(&tool) {
|
||||
return;
|
||||
}
|
||||
let Some(map) = arguments.as_object_mut() else {
|
||||
return;
|
||||
};
|
||||
for key in &["timeMin", "timeMax"] {
|
||||
if let Some(serde_json::Value::String(val)) = map.get(*key).cloned() {
|
||||
if is_bare_date(&val) {
|
||||
let normalized = format!("{}T00:00:00Z", val);
|
||||
tracing::debug!(
|
||||
tool = %tool,
|
||||
key = %key,
|
||||
normalized = %normalized,
|
||||
"[composio] normalized bare date to RFC 3339 for calendar query"
|
||||
);
|
||||
map.insert((*key).to_string(), serde_json::Value::String(normalized));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` when `s` is a bare date string like `"2026-05-14"`
|
||||
/// with no time component.
|
||||
fn is_bare_date(s: &str) -> bool {
|
||||
if s.len() != 10 {
|
||||
return false;
|
||||
}
|
||||
let bytes = s.as_bytes();
|
||||
bytes[0].is_ascii_digit()
|
||||
&& bytes[1].is_ascii_digit()
|
||||
&& bytes[2].is_ascii_digit()
|
||||
&& bytes[3].is_ascii_digit()
|
||||
&& bytes[4] == b'-'
|
||||
&& bytes[5].is_ascii_digit()
|
||||
&& bytes[6].is_ascii_digit()
|
||||
&& bytes[7] == b'-'
|
||||
&& bytes[8].is_ascii_digit()
|
||||
&& bytes[9].is_ascii_digit()
|
||||
}
|
||||
|
||||
fn is_post_oauth_auth_readiness_error(resp: &ComposioExecuteResponse) -> bool {
|
||||
if resp.successful {
|
||||
return false;
|
||||
|
||||
@@ -854,6 +854,104 @@ async fn execute_tool_sends_tool_slug_in_request_body() {
|
||||
"tool slug must be forwarded in request body"
|
||||
);
|
||||
}
|
||||
// ── Calendar query argument normalization ───────────────────────
|
||||
|
||||
#[test]
|
||||
fn is_bare_date_rejects_non_date_strings() {
|
||||
assert!(!is_bare_date(""));
|
||||
assert!(!is_bare_date("2026-05"));
|
||||
assert!(!is_bare_date("2026-05-14T00:00:00Z"));
|
||||
assert!(!is_bare_date("2026/05/14"));
|
||||
assert!(!is_bare_date("hello-world"));
|
||||
assert!(!is_bare_date("2026-5-14"));
|
||||
assert!(!is_bare_date("2026-05-144"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_bare_date_accepts_valid_date_strings() {
|
||||
assert!(is_bare_date("2026-05-14"));
|
||||
assert!(is_bare_date("2025-01-01"));
|
||||
assert!(is_bare_date("1999-12-31"));
|
||||
assert!(is_bare_date("0001-01-01"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_calendar_query_args_ignores_non_calendar_slugs() {
|
||||
let mut args = serde_json::json!({ "timeMin": "2026-05-14" });
|
||||
normalize_calendar_query_args("GMAIL_SEND_EMAIL", &mut args);
|
||||
assert_eq!(args["timeMin"], "2026-05-14");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_calendar_query_args_converts_bare_date_to_rfc3339() {
|
||||
let mut args = serde_json::json!({
|
||||
"connectionId": "conn-1",
|
||||
"timeMin": "2026-05-14",
|
||||
"timeMax": "2026-05-15",
|
||||
});
|
||||
normalize_calendar_query_args("GOOGLECALENDAR_EVENTS_LIST", &mut args);
|
||||
assert_eq!(args["timeMin"], "2026-05-14T00:00:00Z");
|
||||
assert_eq!(args["timeMax"], "2026-05-15T00:00:00Z");
|
||||
assert_eq!(args["connectionId"], "conn-1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_calendar_query_args_preserves_rfc3339_timestamp() {
|
||||
let mut args = serde_json::json!({
|
||||
"timeMin": "2026-05-14T00:00:00+05:30",
|
||||
"timeMax": "2026-05-14T23:59:59Z",
|
||||
});
|
||||
normalize_calendar_query_args("GOOGLECALENDAR_EVENTS_LIST", &mut args);
|
||||
assert_eq!(args["timeMin"], "2026-05-14T00:00:00+05:30");
|
||||
assert_eq!(args["timeMax"], "2026-05-14T23:59:59Z");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_calendar_query_args_handles_missing_time_fields() {
|
||||
let mut args = serde_json::json!({ "connectionId": "conn-1" });
|
||||
normalize_calendar_query_args("GOOGLECALENDAR_EVENTS_LIST", &mut args);
|
||||
assert_eq!(args["connectionId"], "conn-1");
|
||||
// timeMin/timeMax should not be inserted if absent
|
||||
assert!(args.get("timeMin").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_calendar_query_args_handles_non_object_arguments() {
|
||||
let mut args = serde_json::json!("just a string");
|
||||
normalize_calendar_query_args("GOOGLECALENDAR_EVENTS_LIST", &mut args);
|
||||
assert_eq!(args, "just a string");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_calendar_query_args_handles_calendar_find_event_slug() {
|
||||
let mut args = serde_json::json!({ "timeMin": "2026-06-01" });
|
||||
normalize_calendar_query_args("GOOGLECALENDAR_FIND_EVENT", &mut args);
|
||||
assert_eq!(args["timeMin"], "2026-06-01T00:00:00Z");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_calendar_query_args_normalizes_one_side_when_other_absent() {
|
||||
// Asymmetric: only timeMin present, timeMax missing. The normalizer
|
||||
// must convert timeMin without inserting a synthetic timeMax.
|
||||
let mut args = serde_json::json!({ "timeMin": "2026-05-14" });
|
||||
normalize_calendar_query_args("GOOGLECALENDAR_EVENTS_LIST", &mut args);
|
||||
assert_eq!(args["timeMin"], "2026-05-14T00:00:00Z");
|
||||
assert!(args.get("timeMax").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_calendar_query_args_skips_non_string_values() {
|
||||
// Non-string values (numbers, bools, nulls, objects) must be left
|
||||
// untouched — the normalizer only rewrites string bare-date inputs.
|
||||
let mut args = serde_json::json!({
|
||||
"timeMin": 42,
|
||||
"timeMax": null,
|
||||
});
|
||||
normalize_calendar_query_args("GOOGLECALENDAR_EVENTS_LIST", &mut args);
|
||||
assert_eq!(args["timeMin"], 42);
|
||||
assert!(args["timeMax"].is_null());
|
||||
}
|
||||
|
||||
// ── Factory tests (`create_composio_client`) ────────────────────────
|
||||
//
|
||||
// Mirror the four branches the spec demands:
|
||||
|
||||
Reference in New Issue
Block a user