Files
Meister-Eder/main.py
Claude 1bd11e6acd feat: inject today's date into system prompt and add extended thinking support
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
2026-02-22 19:57:32 +00:00

153 lines
5.1 KiB
Python

#!/usr/bin/env python3
"""Meister-Eder — Email Registration Agent for Spielgruppe Pumuckl.
Usage
-----
Copy `.env.example` to `.env`, fill in your credentials, then run:
python main.py
The agent polls the configured IMAP inbox every POLL_INTERVAL seconds,
processes new messages, and replies via SMTP.
Environment variables (see .env.example for full list):
AI_MODEL litellm model string (default: anthropic/claude-opus-4-6)
ANTHROPIC_API_KEY Required for Anthropic models
OPENAI_API_KEY Required for OpenAI models
IMAP_HOST IMAP server hostname
IMAP_PORT IMAP port (default: 993)
IMAP_USERNAME Email account username
IMAP_PASSWORD Email account password
SMTP_HOST SMTP server hostname
SMTP_PORT SMTP port (default: 587)
REGISTRATION_EMAIL Sender address shown to parents
DATA_DIR Directory for JSON storage (default: data/)
POLL_INTERVAL Seconds between inbox polls (default: 60)
"""
import logging
import sys
import time
from src.agent.core import EmailAgent
from src.channels.email_channel import EmailChannel
from src.config import Config
from src.knowledge_base.loader import KnowledgeBase
from src.notifications.notifier import AdminNotifier
from src.storage.json_store import ConversationStore
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
datefmt="%Y-%m-%dT%H:%M:%S",
)
logger = logging.getLogger(__name__)
def build_components(config: Config):
"""Instantiate and wire together all agent components."""
logger.info("AI model: %s", config.ai_model)
kb = KnowledgeBase(config.knowledge_base_dir)
store = ConversationStore(config.data_dir)
notifier = AdminNotifier(
smtp_host=config.smtp_host,
smtp_port=config.smtp_port,
username=config.imap_username,
password=config.imap_password,
use_tls=config.smtp_use_tls,
from_email=config.registration_email,
indoor_email=config.admin_email_indoor,
outdoor_email=config.admin_email_outdoor,
cc_emails=[e.strip() for e in config.admin_email_cc.split(",") if e.strip()],
)
agent = EmailAgent(
model=config.ai_model,
kb=kb,
store=store,
notifier=notifier,
thinking_budget=config.thinking_budget,
)
channel = EmailChannel(
imap_host=config.imap_host,
imap_port=config.imap_port,
smtp_host=config.smtp_host,
smtp_port=config.smtp_port,
username=config.imap_username,
password=config.imap_password,
use_ssl=config.imap_use_ssl,
use_tls=config.smtp_use_tls,
registration_email=config.registration_email,
)
return agent, channel
def run_poll_loop(agent: EmailAgent, channel: EmailChannel, poll_interval: int) -> None:
"""Main polling loop — never returns unless interrupted."""
logger.info("Agent started. Polling every %ds for new messages.", poll_interval)
while True:
try:
messages = channel.fetch_unread_messages()
for msg in messages:
logger.info("Processing message from %s", msg["from"])
try:
# Prepend email headers so the LLM can extract the
# sender's address and subject (e.g. to fill in
# parentGuardian.email automatically).
message_text = (
f"Von: {msg['from']}\n"
f"Betreff: {msg['subject']}\n\n"
f"{msg['body']}"
)
reply = agent.process_message(
parent_email=msg["from"],
message_text=message_text,
inbound_message_id=msg["message_id"],
)
if reply:
channel.send_reply(
to=msg["from"],
subject=msg["subject"],
body=reply,
in_reply_to=msg["message_id"],
references=msg["references"],
quoted_text=msg["raw_body"],
quoted_from=msg["from"],
)
except Exception:
logger.exception(
"Unhandled error processing message from %s", msg["from"]
)
except KeyboardInterrupt:
logger.info("Shutdown requested — stopping.")
break
except Exception:
logger.exception("Unexpected error in poll loop")
time.sleep(poll_interval)
def main() -> None:
config = Config.from_env()
if not config.imap_host:
logger.error(
"IMAP_HOST is not set. "
"Copy .env.example to .env and fill in your email credentials."
)
sys.exit(1)
agent, channel = build_components(config)
run_poll_loop(agent, channel, config.poll_interval)
if __name__ == "__main__":
main()