Add read-only Google Calendar integration via gws CLI

Adds a get_calendar_events agent tool that shells out to `gws calendar +agenda`
for admin users. Controlled by GOOGLE_CALENDAR_ENABLED env var, with OAuth
credentials mounted from credentials.json.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 19:01:33 -04:00
parent 9a149cdaa6
commit 98c47d5507
6 changed files with 71 additions and 5 deletions
+6
View File
@@ -93,6 +93,12 @@ EMAIL_HMAC_SECRET=
# Set to false to disable Mailgun signature validation in development # Set to false to disable Mailgun signature validation in development
MAILGUN_SIGNATURE_VALIDATION=true 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 Configuration (headless sync)
# Obsidian account credentials (used for `ob login` on container startup) # Obsidian account credentials (used for `ob login` on container startup)
OBSIDIAN_EMAIL=your-obsidian-email OBSIDIAN_EMAIL=your-obsidian-email
+1
View File
@@ -11,6 +11,7 @@ wheels/
# Environment files # Environment files
.env .env
credentials.json
# Database files # Database files
database/ database/
+1 -1
View File
@@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y \
curl \ curl \
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y nodejs \ && 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/* \ && rm -rf /var/lib/apt/lists/* \
&& curl -LsSf https://astral.sh/uv/install.sh | sh && curl -LsSf https://astral.sh/uv/install.sh | sh
+6 -2
View File
@@ -96,7 +96,9 @@ async def query():
conversation, query, system_prompt=system_prompt conversation, query, system_prompt=system_prompt
) )
payload = {"messages": messages_payload} 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) response = await main_agent.ainvoke(payload, config=agent_config)
message = response.get("messages", [])[-1].content message = response.get("messages", [])[-1].content
@@ -183,7 +185,9 @@ async def stream_query():
conversation, query_text or "", image_description, system_prompt=system_prompt conversation, query_text or "", image_description, system_prompt=system_prompt
) )
payload = {"messages": messages_payload} 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(): async def event_generator():
final_message = None final_message = None
+54 -2
View File
@@ -65,9 +65,10 @@ def get_current_date() -> str:
Returns: Returns:
Today's date in YYYY-MM-DD format 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 @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) 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 # Create tools list based on what's available
tools = [get_current_date, simba_search, web_search, save_user_memory] tools = [get_current_date, simba_search, web_search, save_user_memory]
if ynab_enabled: if ynab_enabled:
@@ -642,6 +692,8 @@ if obsidian_enabled:
journal_complete_task, 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 # Llama 3.1 supports native function calling via OpenAI-compatible API
main_agent = create_agent(model=model_with_fallback, tools=tools) main_agent = create_agent(model=model_with_fallback, tools=tools)
+3
View File
@@ -65,11 +65,14 @@ services:
- S3_REGION=${S3_REGION:-garage} - S3_REGION=${S3_REGION:-garage}
- OLLAMA_HOST=${OLLAMA_HOST:-http://localhost:11434} - OLLAMA_HOST=${OLLAMA_HOST:-http://localhost:11434}
- FERNET_KEY=${FERNET_KEY} - 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: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
volumes: volumes:
- ./obvault:/app/data/obsidian - ./obvault:/app/data/obsidian
- ./credentials.json:/app/config/gws-credentials.json:ro
restart: unless-stopped restart: unless-stopped
volumes: volumes: