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:
@@ -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
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ wheels/
|
|||||||
|
|
||||||
# Environment files
|
# Environment files
|
||||||
.env
|
.env
|
||||||
|
credentials.json
|
||||||
|
|
||||||
# Database files
|
# Database files
|
||||||
database/
|
database/
|
||||||
|
|||||||
+1
-1
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user