From 6364a195c52703bfe31cc52b85fb194776eaa967 Mon Sep 17 00:00:00 2001 From: tanyar09 Date: Fri, 6 Mar 2026 12:42:45 -0500 Subject: [PATCH] Calendar integration: add timezone config and fix tool call parsing - Add timezone field to CalendarConfig for local timezone support - Update CustomProvider to parse calendar tool calls from JSON in LLM responses - Add pytz dependency to pyproject.toml for timezone handling --- nanobot/config/schema.py | 1 + nanobot/providers/custom_provider.py | 46 +++++++++++++++++++++++----- pyproject.toml | 1 + 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/nanobot/config/schema.py b/nanobot/config/schema.py index 13811c1..a6fd43c 100644 --- a/nanobot/config/schema.py +++ b/nanobot/config/schema.py @@ -252,6 +252,7 @@ class CalendarConfig(Base): token_file: str = "" # Path to store OAuth2 token (default: ~/.nanobot/calendar_token.json) calendar_id: str = "primary" # Calendar ID to use (default: primary calendar) auto_schedule_from_email: bool = True # Automatically schedule meetings from emails + timezone: str = "UTC" # Timezone for parsing times (e.g., "America/New_York", "Europe/London", "UTC") class ExecToolConfig(Base): diff --git a/nanobot/providers/custom_provider.py b/nanobot/providers/custom_provider.py index 1a1f2fb..dadda1e 100644 --- a/nanobot/providers/custom_provider.py +++ b/nanobot/providers/custom_provider.py @@ -62,22 +62,37 @@ class CustomProvider(LLMProvider): # If no structured tool calls, try to parse from content (Ollama sometimes returns JSON in content) # Only parse if content looks like it contains a tool call JSON (to avoid false positives) content = msg.content or "" - if not tool_calls and content and '"name"' in content and '"parameters"' in content: + # Check for standard format: {"name": "...", "parameters": {...}} + has_standard_format = '"name"' in content and '"parameters"' in content + # Check for calendar tool format: {"action": "...", ...} + has_calendar_format = '"action"' in content and ("calendar" in content.lower() or any(action in content for action in ["list_events", "create_event", "update_event", "delete_event"])) + + if not tool_calls and content and (has_standard_format or has_calendar_format): import re - # Look for JSON tool call patterns: {"name": "exec", "parameters": {...}} + # Look for JSON tool call patterns: {"name": "exec", "parameters": {...}} or {"action": "list_events", ...} # Find complete JSON objects by matching braces - pattern = r'\{\s*"name"\s*:\s*"(\w+)"' + # Try "action" pattern first (for calendar tool), then "name" pattern + patterns = [ + (r'\{\s*"action"\s*:\s*"(\w+)"', "action"), # Calendar tool format + (r'\{\s*"name"\s*:\s*"(\w+)"', "name"), # Standard format + ] start_pos = 0 - max_iterations = 5 # Safety limit + max_iterations = 10 # Increased for multiple patterns iteration = 0 while iteration < max_iterations: iteration += 1 - match = re.search(pattern, content[start_pos:]) + match = None + pattern_type = None + for pattern, ptype in patterns: + match = re.search(pattern, content[start_pos:]) + if match: + pattern_type = ptype + break if not match: break json_start = start_pos + match.start() - name = match.group(1) + key_value = match.group(1) # Find the matching closing brace by counting braces brace_count = 0 @@ -98,7 +113,24 @@ class CustomProvider(LLMProvider): try: json_str = content[json_start:json_end] tool_obj = json_repair.loads(json_str) - # Only accept if it has both name and parameters, and name is a valid tool name + + # Handle calendar tool format: {"action": "...", ...} + if isinstance(tool_obj, dict) and "action" in tool_obj: + # This is a calendar tool call in JSON format + action = tool_obj.get("action") + if action and action in ["list_events", "create_event", "update_event", "delete_event", "delete_events", "check_availability"]: + # Convert to calendar tool call format + tool_calls.append(ToolCallRequest( + id=f"call_{len(tool_calls)}", + name="calendar", + arguments=tool_obj # Pass the whole object as arguments + )) + # Remove the tool call from content + content = content[:json_start] + content[json_end:].strip() + start_pos = json_start # Stay at same position since we removed text + continue + + # Handle standard format: {"name": "...", "parameters": {...}} # Note: This list should match tools registered in AgentLoop._register_default_tools() valid_tools = [ # File tools diff --git a/pyproject.toml b/pyproject.toml index 7b7acc6..e168858 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,7 @@ dependencies = [ "google-api-python-client>=2.0.0", "google-auth-httplib2>=0.2.0", "google-auth-oauthlib>=1.0.0", + "pytz>=2024.1", ] [project.optional-dependencies]