diff --git a/.env.example b/.env.example index b6391fc..2026ea0 100644 --- a/.env.example +++ b/.env.example @@ -93,6 +93,12 @@ EMAIL_HMAC_SECRET= # Set to false to disable Mailgun signature validation in development MAILGUN_SIGNATURE_VALIDATION=true +# Google Calendar Configuration (via gws CLI) +GOOGLE_CALENDAR_ENABLED=true +# Export credentials: gws auth export --unmasked > credentials.json +# The file is mounted into the container at /app/config/gws-credentials.json +GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE=/app/config/gws-credentials.json + # Obsidian Configuration (headless sync) # Obsidian account credentials (used for `ob login` on container startup) OBSIDIAN_EMAIL=your-obsidian-email diff --git a/.gitignore b/.gitignore index 762e842..50f3e66 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ wheels/ # Environment files .env +credentials.json # Database files database/ diff --git a/Dockerfile b/Dockerfile index ff1ae53..89c6896 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y \ curl \ && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ && apt-get install -y nodejs \ - && npm install -g yarn obsidian-headless \ + && npm install -g yarn obsidian-headless @anthropic/gws \ && rm -rf /var/lib/apt/lists/* \ && curl -LsSf https://astral.sh/uv/install.sh | sh diff --git a/blueprints/conversation/__init__.py b/blueprints/conversation/__init__.py index bb2e498..2449d5e 100644 --- a/blueprints/conversation/__init__.py +++ b/blueprints/conversation/__init__.py @@ -96,7 +96,9 @@ async def query(): conversation, query, system_prompt=system_prompt ) payload = {"messages": messages_payload} - agent_config = {"configurable": {"user_id": str(user.id)}} + agent_config = { + "configurable": {"user_id": str(user.id), "is_admin": user.is_admin()} + } response = await main_agent.ainvoke(payload, config=agent_config) message = response.get("messages", [])[-1].content @@ -183,7 +185,9 @@ async def stream_query(): conversation, query_text or "", image_description, system_prompt=system_prompt ) payload = {"messages": messages_payload} - agent_config = {"configurable": {"user_id": str(user.id)}} + agent_config = { + "configurable": {"user_id": str(user.id), "is_admin": user.is_admin()} + } async def event_generator(): final_message = None diff --git a/blueprints/conversation/agents.py b/blueprints/conversation/agents.py index e169e15..4ac03c2 100644 --- a/blueprints/conversation/agents.py +++ b/blueprints/conversation/agents.py @@ -65,9 +65,10 @@ def get_current_date() -> str: Returns: Today's date in YYYY-MM-DD format """ - from datetime import date + from datetime import datetime + from zoneinfo import ZoneInfo - return date.today().isoformat() + return datetime.now(ZoneInfo("America/New_York")).strftime("%Y-%m-%d") @tool @@ -618,6 +619,55 @@ async def save_user_memory(content: str, config: RunnableConfig) -> str: return await save_memory(user_id=user_id, content=content) +@tool +async def get_calendar_events( + time_range: str = "today", + days: int = 0, + calendar: str = "", + *, + config: RunnableConfig, +) -> str: + """Get upcoming Google Calendar events. + + Use this tool when the user asks about: + - What's on their calendar today or this week + - Upcoming meetings or events + - Scheduling or availability questions + + Args: + time_range: One of "today", "tomorrow", or "week" (default: "today") + days: If set to a positive number, show events for this many upcoming days + (overrides time_range) + calendar: Optional calendar name or ID to filter by + + Returns: + Calendar events as text + """ + if not config["configurable"].get("is_admin"): + return "Calendar access is restricted to admin users." + + import asyncio + + cmd = ["gws", "calendar", "+agenda"] + if days > 0: + cmd.extend(["--days", str(days)]) + elif time_range == "tomorrow": + cmd.append("--tomorrow") + elif time_range == "week": + cmd.append("--week") + else: + cmd.append("--today") + if calendar: + cmd.extend(["--calendar", calendar]) + proc = await asyncio.create_subprocess_exec( + *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await proc.communicate() + if proc.returncode != 0: + return f"Calendar error: {stderr.decode()}" + return stdout.decode() + + # Create tools list based on what's available tools = [get_current_date, simba_search, web_search, save_user_memory] if ynab_enabled: @@ -642,6 +692,8 @@ if obsidian_enabled: journal_complete_task, ] ) +if os.getenv("GOOGLE_CALENDAR_ENABLED"): + tools.append(get_calendar_events) # Llama 3.1 supports native function calling via OpenAI-compatible API main_agent = create_agent(model=model_with_fallback, tools=tools) diff --git a/docker-compose.yml b/docker-compose.yml index d8953a8..cc4d7a3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -65,11 +65,14 @@ services: - S3_REGION=${S3_REGION:-garage} - OLLAMA_HOST=${OLLAMA_HOST:-http://localhost:11434} - FERNET_KEY=${FERNET_KEY} + - GOOGLE_CALENDAR_ENABLED=${GOOGLE_CALENDAR_ENABLED:-} + - GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE=${GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE:-/app/config/gws-credentials.json} depends_on: postgres: condition: service_healthy volumes: - ./obvault:/app/data/obsidian + - ./credentials.json:/app/config/gws-credentials.json:ro restart: unless-stopped volumes: