Skip to content

craigk5n/scheduling-agent

Repository files navigation

scheduling-agent

A Python/LangGraph agent that turns natural-language scheduling requests ("set up a recurring standup with the team, avoiding Fridays") into validated, RFC 5545-compliant calendar events written to a live WebCalendar instance via MCP — with human approval before every write, an eval suite for RRULE/DST correctness, and full tracing.

Docs

The short version

flowchart LR
    U[NL request] --> P[parse_intent]
    P --> G[gather_context]
    G --> PR[propose]
    PR -->|query / error| R[respond]
    PR -->|create/update/delete| H{{human_approval\ninterrupt}}
    H -->|reject + feedback| P
    H -->|approve| E[execute]
    E --> V[verify]
    V --> R
    G -. MCP .-> WC[(WebCalendar mcp.php)]
    E -. MCP .-> WC
    V -. MCP .-> WC
Loading
  • Orchestration: LangGraph — stateful graph, SQLite checkpointing, a human-in-the-loop interrupt before every write, and a reject→replan loop.
  • Models: pluggable via MODEL_PROVIDER — Anthropic API key, OpenRouter, an Anthropic Pro/Max plan via the claude CLI, or a local model through ollama / LM Studio (OpenAI-compatible, no API key). Structured output uses native json_schema where the provider supports it and falls back to a provider-agnostic validate-and-repair loop otherwise, so no model needs tool-calling or MCP support — the graph does orchestration in code and the model only maps natural language to a validated ScheduleProposal.
  • Correctness: every recurrence is built and validated against the exact subset WebCalendar can store/expand (a Python twin of the PHP validator), with DST-correct expansion previews via dateutil.
  • Backend: WebCalendar's MCP server (mcp.php), extended with availability, conflict-detection, and recurrence tools.
  • Related repos: webcalendar, k5n-mcp-hub, php-icalendar-core

Usage

uv sync
cp .env.example .env        # then fill in MODEL_PROVIDER + its key, MCP_URL, MCP_TOKEN
uv run scheduling-agent     # or: uv run python -m scheduling_agent

You describe what to schedule; the agent plans, shows you a proposal (with the recurrence expanded and any conflicts flagged), and waits for your approval before writing anything. Type quit to exit.

Web UI (same graph, POST /schedule + POST /approve + a chat page):

uv run scheduling-agent-web         # http://localhost:8000

Or the whole stack in Docker (agent + WebCalendar; add --profile chaos for k5n-mcp-hub fault injection):

docker compose up --build

Everything runs offline in tests via an in-memory calendar and a fake model, so no API key or live instance is needed to develop.

Observability

Structured JSON logs carry a per-conversation correlation_id. Set LANGSMITH_TRACING=true + LANGSMITH_API_KEY to trace every graph run in LangSmith (the startup line reports whether tracing is on).

Evals

A golden dataset (src/scheduling_agent/evals/cases.yaml, 20 cases — recurrence, avoid-Fridays constraints, BYSETPOS, DST spring-forward/ fall-back, update/delete/query) is scored by deterministic checks (RRULE validity, occurrence expansion, weekday constraints, DST local-hour, conflict awareness).

uv run python -m scheduling_agent.evals --mode reference   # no LLM; runs in CI
uv run python -m scheduling_agent.evals --mode agent        # measures a real provider

Reference mode gates CI (any regression fails) and uploads a JSON + markdown report artifact. Agent mode runs the real graph against your configured provider to measure model quality.

Configuration

All credentials come from the environment (.env, gitignored); see .env.example for the variable names (no values). Live-calendar URLs, tokens, and API keys are never committed.

Variable Purpose
MODEL_PROVIDER anthropic | openrouter | claude-subscription | ollama | lmstudio
ANTHROPIC_API_KEY / OPENROUTER_API_KEY / CLAUDE_CODE_OAUTH_TOKEN credential for the selected provider (local providers need none)
MODEL_NAME model tag/id (e.g. qwen2.5:32b for ollama); optional
OLLAMA_BASE_URL / LMSTUDIO_BASE_URL override the local endpoint (defaults :11434/v1 / :1234/v1)
MCP_URL / MCP_TOKEN WebCalendar mcp.php endpoint + API token

Local models & structured output: ollama, lmstudio, and openrouter use native json_schema structured output when the model supports it — the model is constrained to emit a valid ScheduleProposal, which is far more reliable than free-text. If the model/server doesn't support it, the agent automatically falls back to the validate-and-repair loop (the subscription CLI always uses the loop). Support is per model: use a structured-output-capable one — e.g. Qwen2.5, Llama-3.1/3.2, Mistral-Nemo, Command-R, Hermes (in LM Studio, look for the "tool use" badge). Small/base models may struggle with the schema and RRULE reasoning regardless — measure any model with python -m scheduling_agent.evals --mode agent.

Design decisions

  • Validate recurrence before sending. rrule.py is a Python twin of WebCalendar's server-side validator, derived from a schema audit of webcal_entry_repeats. The agent never proposes a rule the backend can't store/expand.
  • HITL is a real graph interrupt. Writes pause at a LangGraph interrupt; state is checkpointed to SQLite, so an approval survives a process restart (proven in tests). Rejections loop back and replan.
  • Native structured output, with a universal fallback. Providers that support it are constrained via native json_schema; the rest (e.g. the Pro/Max subscription CLI) use a provider-agnostic validate-and-repair loop, so every backend works on equal footing — and provider differences become a measured eval number, not a bug class.
  • GMT at the tool boundary. The scheduling MCP tools operate in the storage frame; the agent owns local↔GMT (with DST-correct expansion), keeping the PHP side simple.
  • Direct JSON-RPC client. mcp.php's HTTP transport is custom JSON-RPC (not standard-MCP), so a small httpx client fits better than langchain-mcp-adapters. Every transport/protocol fault is wrapped into one McpError (chaos-tested).
  • Evals split for a keyless CI. Deterministic scorers gate CI over a golden dataset (20/20); a real provider is measured opt-in via --mode agent.

Known v1 limitations (documented): conflict checks don't expand recurring occurrences past the base date, so a proposed slot is only checked against the first instance of an existing series. Slated for a backend follow-up and quantified by live-instance eval runs. (The earlier untimed one-off create limitation is resolved — add_event now takes a time, WebCalendar #670.)

Development

uv sync                    # create the venv (Python 3.12) and install deps
scripts/install-hooks.sh   # one-time: run the gate automatically on git push

Run CI locally

scripts/ci.sh is the single source of truth — GitHub Actions runs the exact same script, so local checks and CI can't drift.

scripts/ci.sh              # full gate: ruff, format, mypy, bandit, pytest+coverage
scripts/ci.sh test         # a single stage: lint | format | type | security | test

Once hooks are installed, the full gate also runs on every git push and blocks it on failure (bypass in a pinch with git push --no-verify).

License

MIT © 2026 Craig Knudsen.

About

LangGraph scheduling agent: natural language to RFC 5545 calendar events on WebCalendar via MCP, with human-in-the-loop approval and an RRULE/DST eval suite

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors