Fixes age validation errors caused by the LLM not knowing the current date.
Changes:
- prompts.py: inject date.today() at the top of both system prompts so the
LLM can accurately calculate a child's age from their date of birth
- llm.py: add optional thinking_budget parameter to complete(); when set,
passes thinking={"type": "enabled", "budget_tokens": N} to litellm and
raises max_tokens to thinking_budget + 4096 (Anthropic models only)
- config.py: add thinking_budget field, read from THINKING_BUDGET env var
- .env.example: document the THINKING_BUDGET option
- core.py: pass thinking_budget through to llm.complete()
- main.py: pass thinking_budget when constructing EmailAgent
- chat_app.py: switch from stream_complete to asyncio.to_thread(complete)
so extended thinking works and so only the reply field is shown to
the parent (not the raw JSON wrapper)
To enable extended thinking set THINKING_BUDGET=8000 in .env.
https://claude.ai/code/session_01SUWzMzFvSfWiHXA2p6rPg9
- Change user message author label from "Du / You" to "Du" (German-first)
- Add CHAINLIT_HOST env var to .env.example so the server listens on all
interfaces and is reachable from outside localhost
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous implementation used asyncio.to_thread(llm.complete) to avoid
blocking the event loop, but Chainlit's contextvars context is not reliably
propagated across thread boundaries, causing the session to reset and clear
the message history on each user submission.
Changes:
- Add llm.acomplete() using litellm.acompletion() (native coroutine)
- Replace asyncio.to_thread() in on_message with await llm.acomplete()
- Store the welcome message in state.messages so it is replayed on reconnect
- Persist state to cl.user_session immediately after appending the user's
message (before the LLM call) so reconnect detection has the latest history
- Add pytest-asyncio dev dependency and asyncio_mode = "auto" config
- Add 6 async tests for acomplete() in tests/test_llm.py
https://claude.ai/code/session_01SUWzMzFvSfWiHXA2p6rPg9
Two related issues caused the screen to clear after each answer:
1. Blocking event loop: the synchronous llm.stream_complete() for-loop
was running directly in the async on_message handler, blocking the
event loop for the full LLM response duration. This caused the
WebSocket to time out and Chainlit to reconnect after each message.
Fix: replace stream_complete() with asyncio.to_thread(llm.complete)
so the network-bound LLM call runs in a thread pool and the event
loop (and WebSocket) stay alive throughout.
2. Reconnect resets history: on_chat_start always created a fresh empty
state and sent the welcome message, even on WebSocket reconnections
where cl.user_session still held the existing conversation.
Fix: if cl.user_session["state"] is already present, replay the
stored message history into the new thread instead of starting fresh.
https://claude.ai/code/session_01SUWzMzFvSfWiHXA2p6rPg9
The LLM returns a structured JSON object. Previously, raw tokens were
streamed directly to the user via msg.stream_token(), causing the full
JSON blob to appear in the chat.
Fix: collect all chunks silently, parse the JSON, then send only the
reply field with cl.Message(content=reply_text).send(). The JSON fields
(updates, next_step, registration_complete, language, intent) are still
processed in the background — parents never see them.
https://claude.ai/code/session_01SUWzMzFvSfWiHXA2p6rPg9
Add dedicated "Web chat" section under Running with:
- chainlit run command and port/host flags
- Minimum required env vars for chat-only deployments
(no IMAP needed; only AI model + SMTP + admin emails)
- "Running both channels together" example with two terminals
- Note that both channels share DATA_DIR and notification config
https://claude.ai/code/session_01SUWzMzFvSfWiHXA2p6rPg9
Key corrections from codebase analysis:
- Use `uv add chainlit` (not requirements.txt) — project already uses uv+pyproject.toml
- Remove .env.example task — file already exists with full config
- Add task to extend src/llm.py with stream_complete() using litellm streaming
instead of calling Anthropic SDK directly
- Reuse existing src/ modules directly in chat_app.py:
Config, KnowledgeBase, ConversationStore, AdminNotifier, prompts.py
- chat_app.py lives at project root (parallel to main.py), not in a new app/ dir
- Parse LLM response with existing _parse_llm_response logic from core.py
- Session state stored in cl.user_session, keyed by Chainlit session ID
https://claude.ai/code/session_01SUWzMzFvSfWiHXA2p6rPg9
Selects Chainlit as the chat UI library (AI-native, Python, handles
WebSocket/streaming/session out of the box). Establishes Python as the
project language. Documents accessibility gap mitigations (ARIA live
regions, focus management, reduced-motion, contrast overrides). Defines
project layout and Chainlit configuration.
https://claude.ai/code/session_01SUWzMzFvSfWiHXA2p6rPg9
Defines why the web chat is being built now, what changes (chat-interface
capability moving from spec to implementation), and adds accessibility as
a first-class requirement (WCAG 2.1 AA, keyboard nav, ARIA live regions).
https://claude.ai/code/session_01SUWzMzFvSfWiHXA2p6rPg9
- Remove ANTHROPIC_API_KEY and OPENAI_API_KEY from the Required
variables table; they are provider-specific, not universally
required. Add a note pointing readers to the Switching AI providers
section instead.
- Replace "Supports German and English; defaults to German" with
"Responds in any language the parent uses; defaults to German" to
accurately reflect that the agent is fully language-agnostic.
https://claude.ai/code/session_01F9RoUQYKktPrmsvemSYrPk
fetch_unread_messages now returns both `body` (stripped, for the LLM)
and `raw_body` (full with nested quotes, for the outgoing reply).
main.py passes raw_body as quoted_text so each reply carries the
complete conversation thread, not just the single last message.
https://claude.ai/code/session_01HaUFs7SaLD5SoiuGCY27Tw
Previously routing was hardcoded (Andrea for indoor, Barbara for outdoor).
Then it was replaced with a flat ADMIN_EMAILS list which lost the routing.
This commit restores routing via three separate env vars:
ADMIN_EMAIL_INDOOR — indoor leader, To when indoor days are booked
ADMIN_EMAIL_OUTDOOR — outdoor leader, To when outdoor days are booked
ADMIN_EMAIL_CC — always Cc'd (comma-separated for multiple)
For testing, set all three to your own address so no real leader gets mail.
Changes:
- Config: replaced admin_emails with admin_email_indoor/outdoor/cc fields
- AdminNotifier: replaced admin_emails param with indoor_email/outdoor_email/
cc_emails; _recipients_for() restored as an instance method using these
- main.py: wires the three new config fields into AdminNotifier
- .env.example: documents the three new variables with production defaults
- Tests: fixture updated to use new params
https://claude.ai/code/session_01HaUFs7SaLD5SoiuGCY27Tw
Previously the To/Cc addresses were hardcoded in notifier.py (Andrea,
Barbara, Markus). This caused accidental emails to production contacts
during testing.
Changes:
- New ADMIN_EMAILS env var: comma-separated list of addresses.
First address → To; remaining addresses → Cc.
- AdminNotifier now accepts admin_emails list; warns and skips if empty.
- Removed hardcoded _INDOOR_EMAIL / _OUTDOOR_EMAIL / _ADMIN_CC_EMAIL
constants and the _recipients_for() routing method.
- Config.from_env() parses ADMIN_EMAILS into a list.
- main.py passes config.admin_emails to AdminNotifier.
- .env.example documents the new variable with production example.
- Tests: fixture updated; TestRecipientsFor removed (routing gone).
For testing: ADMIN_EMAILS=you@example.com
For production: ADMIN_EMAILS=andrea.sigrist@gmx.net,baba.laeubli@gmail.com,spielgruppen@familien-verein.chhttps://claude.ai/code/session_01HaUFs7SaLD5SoiuGCY27Tw
notifier.py:
- All email body text translated to German (section headers, labels,
day names, playgroup type names, age format, change diff labels)
- Subject lines changed to German: "Neue Anmeldung:" / "Anmeldung aktualisiert:"
- Channel label localised: "E-Mail" / "Chat"
- Fallback special needs label changed to "Keine"
prompts.py:
- New rule: always store free-text field values (especially specialNeeds)
in German in `updates`, translating from the parent's language if needed;
use "Keine" for no special needs
https://claude.ai/code/session_01HaUFs7SaLD5SoiuGCY27Tw
Two prompt changes:
1. Greeting step: explicitly tell parents they can write in any language
(German, English, French, Italian, Spanish, …) and the agent will reply
in the same language. Also kick off info collection immediately by asking
for child name + DOB in the greeting reply.
2. Personality: replace the "1–2 questions at a time" rule with a strategy
that gathers all relevant questions per step in one message (woven into
natural sentences, not a form), and explicitly re-asks any unanswered
questions before advancing — no open question is silently skipped.
https://claude.ai/code/session_01HaUFs7SaLD5SoiuGCY27Tw
The LLM previously only received the stripped email body, giving it no
way to extract the sender's email address for the parentGuardian.email
field. Prepend Von:/Betreff: headers to every message so the LLM can
read the From address and subject without asking the parent for them.
The quoted_text sent back in the reply still uses only msg["body"] so
the quote block stays clean.
https://claude.ai/code/session_01HaUFs7SaLD5SoiuGCY27Tw
Tell the LLM explicitly that the reply field must be plain text with no
markdown (no bold, italic, headers, bullet points, or backticks). Email
clients display raw text so markdown syntax would appear as literal
characters rather than formatting.
https://claude.ai/code/session_01HaUFs7SaLD5SoiuGCY27Tw
Add standard > -prefixed quote block to outbound replies so parents can
see what they wrote in the previous message, matching natural email client
behaviour. The quote header uses the German "Am <date> schrieb <addr>:"
convention (matching Outlook/Thunderbird).
- email_channel.py: add _build_quoted_block() helper; extend send_reply()
with optional quoted_text/quoted_from params
- main.py: pass msg["body"] and msg["from"] as quoted_text/quoted_from
https://claude.ai/code/session_01HaUFs7SaLD5SoiuGCY27Tw
- prompts.py: correct age restrictions (indoor ≥2 yrs, outdoor ≥2.5 yrs)
Previously had indoor ≥2.5 and outdoor ≥3, which was too restrictive
- README.md: add full setup and configuration guide covering prerequisites,
installation, env var reference, provider switching, cron scheduling,
running tests, and knowledge base editing
https://claude.ai/code/session_01HaUFs7SaLD5SoiuGCY27Tw
Drops the src/providers/ package (base class, AnthropicProvider,
OpenAIProvider, factory) in favour of a single src/llm.py that calls
litellm.completion() directly. litellm handles provider routing,
authentication, and SDK differences for 100+ providers without any
code we need to maintain.
Changes:
- Delete src/providers/ entirely
- Add src/llm.py — one complete() function wrapping litellm
- src/agent/core.py: EmailAgent takes model: str instead of LLMProvider
- src/config.py: ai_provider + api key fields → single ai_model string
in litellm format (e.g. "anthropic/claude-opus-4-6")
- main.py: remove provider factory wiring; pass config.ai_model to agent
- .env.example: simplify AI section, show litellm model string examples
- pyproject.toml: replace anthropic + openai deps with litellm>=1.0.0
- uv.lock: regenerated
https://claude.ai/code/session_01HaUFs7SaLD5SoiuGCY27Tw
Closes the gap where parents sending a new email (instead of replying)
would lose their registration progress. All changes follow the
email-based-conversation-matching OpenSpec change.
Key changes
-----------
storage/json_store.py
- normalize_email() helper (lowercase + trim)
- Conversations now keyed by sender email address, not thread ID
- Versioned registration storage: data/registrations/<email>/v<N>_<ts>.json
- current.json always reflects the latest version
- save_registration() returns (email_key, version) tuple
- save_registration_version() for updates with change_summary
- get_registration_history() returns all versions in order
models/conversation.py
- Added last_inbound_message_id field for reply threading (not matching)
channels/email_channel.py
- fetch_unread_messages() no longer exposes thread_id
- Conversation matching removed from channel layer (now in agent)
- Removed _resolve_thread_id() — threading headers kept for SMTP only
agent/core.py
- process_message() takes parent_email + inbound_message_id (no thread ID)
- Looks up conversation by normalized email address
- Post-completion handler: detects intent (question / update / new_child)
- Registration updates: diffs old vs new, versions storage, notifies admin
agent/prompts.py
- build_system_prompt() dispatches to registration or post-completion prompt
- Post-completion prompt guides LLM to return intent field
- Reminder language updated: no expiration threats
notifications/notifier.py
- notify_admin() accepts version parameter
- notify_registration_update() sends "Registration Updated" emails with diff
- _build_update_body() includes field-level old→new change summary
main.py
- Poll loop passes parent_email + inbound_message_id to agent (no thread_id)
https://claude.ai/code/session_01HaUFs7SaLD5SoiuGCY27Tw
Replace thread-ID-based conversation matching with email-address-based
matching for more reliable conversation continuity. Key changes:
- One conversation per email address (simpler model)
- No data expiration (conversations persist indefinitely)
- Post-completion support (questions and registration updates)
- Versioned storage for registration updates (audit trail)
- Admin notifications for registration changes
This addresses the gap where parents sending new emails (instead of
replying) would lose their registration progress.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Documents project overview, repository structure, OpenSpec workflow,
system architecture decisions, domain knowledge (fees, schedule,
registration schema, conversation flow), and conventions for AI
assistants working in this repo.
https://claude.ai/code/session_01A6JAs2Pbg1hF1aLs7bcKhx
Define complete non-technical scope for replacing Google Forms registration
with an AI-powered conversational agent. Includes:
- Registration data requirements (13 fields with validation rules)
- Knowledge base content (fees, regulations, schedule, FAQ from official PDFs)
- Conversation flow design with bilingual support (German/English)
- Agent personality guidelines (warm & friendly tone)
- Admin notification routing by playgroup type
- Email reminder system for incomplete registrations
- Data export formats (CSV/JSON)
- Channel configuration (email + chat)
- Release priorities (MVP scope defined)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>