# email-classifier FastAPI service that classifies email using a configurable LLM backend, enriches the output for human review, and can upsert Todoist tasks without creating duplicates. ## Environment configuration LLM defaults: ```bash export LLM_PROVIDER=openai export LLM_BASE_URL=http://ollama.internal.henryhosted.com:9292/v1 export LLM_API_KEY=none export LLM_MODEL=qwen2.5-7b-instruct.q4_k_m export LLM_TEMPERATURE=0.1 export LLM_TIMEOUT_SECONDS=60 export LLM_MAX_RETRIES=3 ``` MiniMax via Anthropic-compatible API: ```bash export LLM_PROVIDER=anthropic export LLM_BASE_URL=https://api.minimax.io/anthropic export LLM_API_KEY=your_minimax_key export LLM_MODEL=MiniMax-M2.7 ``` Optional Todoist sync: ```bash export TODOIST_API_KEY=your_todoist_token export TODOIST_PROJECT_ID=optional_project_id export EMAIL_CLASSIFIER_DB_PATH=.data/email_classifier.db ``` ## API ### POST /classify Backward-compatible top-level response fields are preserved. Optional request metadata for dedupe and richer sync: ```json { "email_data": { "subject": "Can you review this by Friday?", "body": "Hi Daniel, please review the attached budget proposal." }, "message_id": "", "thread_id": "thread-789", "from_address": "sender@example.com", "received_at": "2026-04-09T12:55:00Z", "provider": "anthropic", "base_url": "https://api.minimax.io/anthropic", "model": "MiniMax-M2.7" } ``` Response now includes optional enrichment and Todoist sync info: ```json { "needs_action": true, "category": "question", "priority": "high", "task_description": "Review the budget proposal and respond by Friday", "reasoning": "Direct request with a deadline requires follow-up", "confidence": 0.91, "details": { "summary": "Budget proposal review requested with Friday deadline.", "suggested_title": "Review budget proposal and respond by Friday", "suggested_notes": "Requester asked for feedback on attached budget proposal before Friday.", "deadline": "Friday", "people": ["Daniel"], "organizations": [], "attachments_referenced": ["budget proposal"], "next_steps": ["Review attachment", "Reply with feedback"], "key_points": ["Deadline is Friday"], "source_signals": ["request", "deadline"], "dedupe_key": "..." }, "todoist": { "status": "created", "task_id": "1234567890", "comment_added": false, "dedupe_match": "none", "message": null } } ``` ## Dedupe behavior When Todoist sync is enabled and `needs_action=true`: - first match by `message_id` - then by `thread_id` - then by normalized content fingerprint fallback Behavior: - no existing task: create Todoist task - existing task, same classification: do not duplicate, mark `unchanged` - existing task, changed classification/context: update task in place - add a Todoist comment only when material context changed ## Architecture - `app/classifier.py`: classification orchestration and Todoist sync handoff - `app/prompts.py`: richer extraction prompt - `app/sync.py`: dedupe, task rendering, Todoist upsert logic - `app/dedupe_store.py`: SQLite-backed mapping store - `app/todoist.py`: Todoist REST client - `app/llm_adapters.py`: provider adapters - `app/config.py`: LLM settings ## Notes - `/classify` remains backward compatible at the top level. - New request metadata fields are optional. - Todoist sync safely no-ops when `TODOIST_API_KEY` is not configured. - SQLite is used for lightweight production-safe dedupe tracking.