Files
microdao-daarion/services/calendar-service/main.py
Apple 129e4ea1fc feat(platform): add new services, tools, tests and crews modules
New router intelligence modules (26 files): alert_ingest/store, audit_store,
architecture_pressure, backlog_generator/store, cost_analyzer, data_governance,
dependency_scanner, drift_analyzer, incident_* (5 files), llm_enrichment,
platform_priority_digest, provider_budget, release_check_runner, risk_* (6 files),
signature_state_store, sofiia_auto_router, tool_governance

New services:
- sofiia-console: Dockerfile, adapters/, monitor/nodes/ops/voice modules, launchd, react static
- memory-service: integration_endpoints, integrations, voice_endpoints, static UI
- aurora-service: full app suite (analysis, job_store, orchestrator, reporting, schemas, subagents)
- sofiia-supervisor: new supervisor service
- aistalk-bridge-lite: Telegram bridge lite
- calendar-service: CalDAV calendar service with reminders
- mlx-stt-service / mlx-tts-service: Apple Silicon speech services
- binance-bot-monitor: market monitor service
- node-worker: STT/TTS memory providers

New tools (9): agent_email, browser_tool, contract_tool, observability_tool,
oncall_tool, pr_reviewer_tool, repo_tool, safe_code_executor, secure_vault

New crews: agromatrix_crew (10 modules: depth_classifier, doc_facts, doc_focus,
farm_state, light_reply, llm_factory, memory_manager, proactivity, reflection_engine,
session_context, style_adapter, telemetry)

Tests: 85+ test files for all new modules
Made-with: Cursor
2026-03-03 07:14:14 -08:00

640 lines
18 KiB
Python

"""
Calendar Service - FastAPI application for CalDAV integration
Provides unified API for Sofiia agent to manage calendars
"""
import os
import logging
from contextlib import asynccontextmanager
from typing import Optional, List, Dict, Any
from datetime import datetime
from fastapi import FastAPI, HTTPException, Header, Depends, Request
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base
from calendar_client import CalDAVClient
from storage import CalendarStorage
from reminder_worker import ReminderWorker
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Database
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./calendar.db")
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def get_db():
"""Database session dependency"""
db = SessionLocal()
try:
yield db
finally:
db.close()
# =============================================================================
# MODELS
# =============================================================================
class ConnectRequest(BaseModel):
workspace_id: str
user_id: str
username: str
password: str
provider: str = "radicale"
class CreateEventRequest(BaseModel):
title: str
start: str # ISO 8601
end: str # ISO 8601
timezone: str = "Europe/Kiev"
location: Optional[str] = None
description: Optional[str] = None
attendees: Optional[List[str]] = None
idempotency_key: Optional[str] = None
class UpdateEventRequest(BaseModel):
title: Optional[str] = None
start: Optional[str] = None
end: Optional[str] = None
timezone: str = "Europe/Kiev"
location: Optional[str] = None
description: Optional[str] = None
class SetReminderRequest(BaseModel):
event_uid: str
remind_at: str # ISO 8601
channel: str = "inapp" # inapp, telegram, email
class CalendarToolRequest(BaseModel):
action: str
workspace_id: str
user_id: str
provider: str = "radicale"
account_id: Optional[str] = None
calendar_id: Optional[str] = None
params: Dict[str, Any] = Field(default_factory=dict)
# =============================================================================
# APP SETUP
# =============================================================================
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Startup and shutdown events"""
# Startup
logger.info("Starting calendar service...")
# Create tables
Base.metadata.create_all(bind=engine)
# Initialize storage
storage = CalendarStorage(SessionLocal())
app.state.storage = storage
# Initialize reminder worker
worker = ReminderWorker(storage)
app.state.reminder_worker = worker
worker.start()
yield
# Shutdown
logger.info("Shutting down calendar service...")
worker.stop()
app = FastAPI(
title="Calendar Service",
description="CalDAV integration for DAARION agents",
version="1.0.0",
lifespan=lifespan
)
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# =============================================================================
# AUTH & RBAC
# =============================================================================
async def verify_agent(
x_agent_id: str = Header(default="anonymous"),
x_workspace_id: str = Header(default=None)
) -> Dict[str, str]:
"""Verify agent authorization"""
# In production, verify against RBAC system
return {
"agent_id": x_agent_id,
"workspace_id": x_workspace_id or "default"
}
# =============================================================================
# CONNECTION MANAGEMENT
# =============================================================================
@app.post("/v1/calendar/connect/radicale")
async def connect_radicale(
request: ConnectRequest,
auth: Dict = Depends(verify_agent),
db=Depends(get_db)
):
"""Connect a Radicale CalDAV account"""
storage: CalendarStorage = app.state.storage
try:
# Create CalDAV client and test connection
client = CalDAVClient(
server_url=os.getenv("RADICALE_URL", "https://caldav.daarion.space"),
username=request.username,
password=request.password
)
# Test connection
calendars = client.list_calendars()
# Find default calendar
default_cal = None
for cal in calendars:
if cal.get("display_name") in ["default", "Calendar", "Personal"]:
default_cal = cal.get("id")
break
if not default_cal and calendars:
default_cal = calendars[0].get("id")
# Store account
account = storage.create_account(
workspace_id=request.workspace_id,
user_id=request.user_id,
provider=request.provider,
username=request.username,
password=request.password,
principal_url=client.principal_url,
default_calendar_id=default_cal
)
return {
"status": "connected",
"account_id": account.id,
"calendars": calendars,
"default_calendar": default_cal
}
except Exception as e:
logger.error(f"Failed to connect account: {e}")
raise HTTPException(status_code=400, detail=str(e))
@app.get("/v1/calendar/accounts")
async def list_accounts(
workspace_id: str,
user_id: str,
auth: Dict = Depends(verify_agent),
db=Depends(get_db)
):
"""List connected calendar accounts"""
storage: CalendarStorage = app.state.storage
accounts = storage.list_accounts(workspace_id, user_id)
return {
"accounts": [
{
"id": a.id,
"provider": a.provider,
"username": a.username,
"default_calendar_id": a.default_calendar_id,
"created_at": a.created_at.isoformat()
}
for a in accounts
]
}
# =============================================================================
# CALENDAR OPERATIONS
# =============================================================================
@app.get("/v1/calendar/calendars")
async def list_calendars(
account_id: str,
auth: Dict = Depends(verify_agent),
db=Depends(get_db)
):
"""List calendars for an account"""
storage: CalendarStorage = app.state.storage
account = storage.get_account(account_id)
if not account:
raise HTTPException(status_code=404, detail="Account not found")
client = CalDAVClient(
server_url=os.getenv("RADICALE_URL", "https://caldav.daarion.space"),
username=account.username,
password=account.password
)
calendars = client.list_calendars()
return {"calendars": calendars}
@app.get("/v1/calendar/events")
async def list_events(
account_id: str,
calendar_id: Optional[str] = None,
time_min: Optional[str] = None,
time_max: Optional[str] = None,
q: Optional[str] = None,
auth: Dict = Depends(verify_agent),
db=Depends(get_db)
):
"""List events from calendar"""
storage: CalendarStorage = app.state.storage
account = storage.get_account(account_id)
if not account:
raise HTTPException(status_code=404, detail="Account not found")
calendar_id = calendar_id or account.default_calendar_id
client = CalDAVClient(
server_url=os.getenv("RADICALE_URL", "https://caldav.daarion.space"),
username=account.username,
password=account.password
)
events = client.list_events(
calendar_id=calendar_id,
time_min=time_min,
time_max=time_max
)
# Filter by query if provided
if q:
events = [e for e in events if q.lower() in e.get("title", "").lower()]
return {"events": events}
@app.get("/v1/calendar/events/{uid}")
async def get_event(
uid: str,
account_id: str,
calendar_id: Optional[str] = None,
auth: Dict = Depends(verify_agent),
db=Depends(get_db)
):
"""Get single event"""
storage: CalendarStorage = app.state.storage
account = storage.get_account(account_id)
if not account:
raise HTTPException(status_code=404, detail="Account not found")
calendar_id = calendar_id or account.default_calendar_id
client = CalDAVClient(
server_url=os.getenv("RADICALE_URL", "https://caldav.daarion.space"),
username=account.username,
password=account.password
)
event = client.get_event(uid, calendar_id)
if not event:
raise HTTPException(status_code=404, detail="Event not found")
return {"event": event}
@app.post("/v1/calendar/events")
async def create_event(
request: CreateEventRequest,
account_id: str,
calendar_id: Optional[str] = None,
auth: Dict = Depends(verify_agent),
db=Depends(get_db)
):
"""Create new calendar event"""
storage: CalendarStorage = app.state.storage
# Check idempotency
if request.idempotency_key:
existing = storage.get_by_idempotency_key(request.idempotency_key)
if existing:
return {
"status": "already_exists",
"event_uid": existing.event_uid,
"message": "Event already exists"
}
account = storage.get_account(account_id)
if not account:
raise HTTPException(status_code=404, detail="Account not found")
calendar_id = calendar_id or account.default_calendar_id
client = CalDAVClient(
server_url=os.getenv("RADICALE_URL", "https://caldav.daarion.space"),
username=account.username,
password=account.password
)
event_uid = client.create_event(
calendar_id=calendar_id,
title=request.title,
start=request.start,
end=request.end,
timezone=request.timezone,
location=request.location,
description=request.description,
attendees=request.attendees
)
# Store idempotency key
if request.idempotency_key:
storage.store_idempotency_key(
request.idempotency_key,
account.workspace_id,
account.user_id,
event_uid
)
return {
"status": "created",
"event_uid": event_uid
}
@app.patch("/v1/calendar/events/{uid}")
async def update_event(
uid: str,
request: UpdateEventRequest,
account_id: str,
calendar_id: Optional[str] = None,
auth: Dict = Depends(verify_agent),
db=Depends(get_db)
):
"""Update existing event"""
storage: CalendarStorage = app.state.storage
account = storage.get_account(account_id)
if not account:
raise HTTPException(status_code=404, detail="Account not found")
calendar_id = calendar_id or account.default_calendar_id
client = CalDAVClient(
server_url=os.getenv("RADICALE_URL", "https://caldav.daarion.space"),
username=account.username,
password=account.password
)
client.update_event(
uid=uid,
calendar_id=calendar_id,
title=request.title,
start=request.start,
end=request.end,
timezone=request.timezone,
location=request.location,
description=request.description
)
return {"status": "updated", "event_uid": uid}
@app.delete("/v1/calendar/events/{uid}")
async def delete_event(
uid: str,
account_id: str,
calendar_id: Optional[str] = None,
auth: Dict = Depends(verify_agent),
db=Depends(get_db)
):
"""Delete event"""
storage: CalendarStorage = app.state.storage
account = storage.get_account(account_id)
if not account:
raise HTTPException(status_code=404, detail="Account not found")
calendar_id = calendar_id or account.default_calendar_id
client = CalDAVClient(
server_url=os.getenv("RADICALE_URL", "https://caldav.daarion.space"),
username=account.username,
password=account.password
)
client.delete_event(uid, calendar_id)
return {"status": "deleted", "event_uid": uid}
# =============================================================================
# REMINDERS
# =============================================================================
@app.post("/v1/calendar/reminders")
async def set_reminder(
request: SetReminderRequest,
account_id: str,
auth: Dict = Depends(verify_agent),
db=Depends(get_db)
):
"""Set event reminder"""
storage: CalendarStorage = app.state.storage
reminder = storage.create_reminder(
workspace_id=auth["workspace_id"],
user_id=auth["user_id"],
account_id=account_id,
event_uid=request.event_uid,
remind_at=request.remind_at,
channel=request.channel
)
return {
"status": "created",
"reminder_id": reminder.id
}
# =============================================================================
# CALENDAR TOOL FOR SOFIIA
# =============================================================================
@app.post("/v1/tools/calendar")
async def calendar_tool(
request: CalendarToolRequest,
auth: Dict = Depends(verify_agent),
db=Depends(get_db)
):
"""
Unified calendar tool endpoint for Sofiia agent.
Actions:
- connect: Connect Radicale account
- list_calendars: List available calendars
- list_events: List events in calendar
- get_event: Get single event
- create_event: Create new event
- update_event: Update event
- delete_event: Delete event
- set_reminder: Set reminder
"""
storage: CalendarStorage = app.state.storage
try:
if request.action == "connect":
# Connect account
params = request.params
connect_req = ConnectRequest(
workspace_id=request.workspace_id,
user_id=request.user_id,
username=params.get("username"),
password=params.get("password")
)
# Reuse connect logic
result = await connect_radicale(connect_req, auth, db)
return {"status": "succeeded", "data": result}
elif request.action == "list_calendars":
if not request.account_id:
raise HTTPException(status_code=400, detail="account_id required")
return await list_calendars(request.account_id, auth, db)
elif request.action == "list_events":
if not request.account_id:
raise HTTPException(status_code=400, detail="account_id required")
params = request.params
return await list_events(
request.account_id,
request.calendar_id,
params.get("time_min"),
params.get("time_max"),
params.get("q"),
auth,
db
)
elif request.action == "get_event":
if not request.account_id:
raise HTTPException(status_code=400, detail="account_id required")
params = request.params
return await get_event(
params.get("uid"),
request.account_id,
request.calendar_id,
auth,
db
)
elif request.action == "create_event":
if not request.account_id:
raise HTTPException(status_code=400, detail="account_id required")
params = request.params
create_req = CreateEventRequest(**params)
return await create_event(
create_req,
request.account_id,
request.calendar_id,
auth,
db
)
elif request.action == "update_event":
if not request.account_id:
raise HTTPException(status_code=400, detail="account_id required")
params = request.params
update_req = UpdateEventRequest(**params)
return await update_event(
params.get("uid"),
update_req,
request.account_id,
request.calendar_id,
auth,
db
)
elif request.action == "delete_event":
if not request.account_id:
raise HTTPException(status_code=400, detail="account_id required")
params = request.params
return await delete_event(
params.get("uid"),
request.account_id,
request.calendar_id,
auth,
db
)
elif request.action == "set_reminder":
if not request.account_id:
raise HTTPException(status_code=400, detail="account_id required")
params = request.params
reminder_req = SetReminderRequest(**params)
return await set_reminder(reminder_req, request.account_id, auth, db)
else:
raise HTTPException(status_code=400, detail=f"Unknown action: {request.action}")
except HTTPException:
raise
except Exception as e:
logger.error(f"Calendar tool error: {e}")
return {
"status": "failed",
"error": {
"code": "internal_error",
"message": str(e),
"retryable": False
}
}
# =============================================================================
# HEALTH & METRICS
# =============================================================================
@app.get("/health")
async def health():
"""Health check"""
return {"status": "healthy"}
@app.get("/metrics")
async def metrics():
"""Service metrics"""
storage: CalendarStorage = app.state.storage
worker: ReminderWorker = app.state.reminder_worker
return {
"accounts_count": storage.count_accounts(),
"reminders_pending": storage.count_pending_reminders(),
"worker_status": worker.get_status()
}