journal-bot/CLAUDE.md

148 lines
8.1 KiB
Markdown

# journal-bot — Telegram → Obsidian Daily Journal
Pipeline: Telegram-Bot empfängt Text/Sprache/Foto → semantische Verarbeitung → Eintrag in Christians Obsidian Daily Note.
## Zweck
Christian pflegt sein Daily Journal in Obsidian. Dieses Projekt erlaubt es ihm, Einträge bequem per Handy (auch als Sprachnachricht) abzusetzen. Beispiel: „Gestern haben wir eine Party gefeiert" landet in der Daily Note **von gestern**, nicht von heute. Meta-Phrasen („Schreib ins Journal, dass …") werden vor dem Schreiben gefiltert.
## Architektur
Zwei-Stage-Pipeline mit File-Queue dazwischen:
```
Telegram → [Ingest] → queue/pending/ → [Process] → Obsidian Daily Note
↘ [Claude-Skill /journal-sync] (Premium-Fallback)
```
- **Ingest** (`ingest.py`): Long-Polling Telegram `getUpdates`, Sender-Filter, Voice-Transcription via faster-whisper (Fallback: Telegram-Premium `transcribed_text`), Foto-Download, schreibt `QueueItem` als JSON in `runtime/queue/pending/`.
- **Process** (`process.py`): claimt nächstes Item, baut `ProcessorInput`, ruft Processor, schreibt Eintrag via `VaultWriter` in die Daily Note, verschiebt Item nach `done/`.
- **Zwei Processor-Backends** (Protocol in `processor_protocol.py`):
1. **LMStudioProcessor** (default, lokal, hands-off) — spricht LM Studio per OpenAI-kompatibler API.
2. **Claude-Code-Skill `/journal-sync`** im Vault (`D:\projects\chrka\brain\.claude\skills\journal-sync.md`) — manueller Premium-Pfad, wenn die LLM-Qualität von LM Studio nicht reicht oder LM Studio aus ist.
Crash-Safety: atomic `os.rename` zwischen pending/working/done/failed, atomic `.tmp + replace` für State-File, `processed_ids`-Deque verhindert Doppelverarbeitung.
## Projekt-Layout
```
src/journal_bot/
__main__.py # CLI: ingest | process | both | write
config.py # pydantic-settings (env)
state.py # last_update_id + processed_ids deque
queue.py # QueueItem + atomic file queue
vault_writer.py # append to daily note, frontmatter, Klärungs-Callout
telegram_client.py # httpx.AsyncClient, getUpdates, download, react
transcribe.py # faster-whisper + premium fallback
context.py # Personen + aktive Projekte aus Vault
processor_protocol.py # Protocol + Pydantic ProcessorInput/Output
processor_lmstudio.py # OpenAI-kompatibler LM-Studio-Client
ingest.py # poll → queue
process.py # queue → vault
prompts/
journal_system.md # System-Prompt, gilt für beide Backends
scripts/
run.ps1 # venv-Wrapper für CLI
install-task.ps1 # Windows Task Scheduler @logon
runtime/ # generiert, NICHT committen
queue/{pending,working,done,failed}/
state/state.json
attachments/
logs/
tests/
test_*.py # 38 Tests, alle grün
fixtures/prompt_regression/inputs.yaml
```
## Pfade
- Projekt-Root: `D:\projects\chrka\journal-bot`
- Vault: `D:\projects\chrka\brain` (separates Repo, eigener Git)
- Daily Notes landen in: `<vault>\05 Daily Notes\YYYY-MM-DD.md`
- Anhänge in: `<vault>\07 Anhänge\` (Default in `attachments_dir`)
## CLI
```bash
.venv/Scripts/python.exe -m journal_bot ingest # Telegram → queue
.venv/Scripts/python.exe -m journal_bot process # queue → vault (via LM Studio)
.venv/Scripts/python.exe -m journal_bot both # ingest dann process
.venv/Scripts/python.exe -m journal_bot write --target-path "05 Daily Notes/2026-06-15.md" --entry-file tmp.md
```
`write` ist Helper für den Claude-Skill: schreibt einen vorbereiteten Eintrag ohne LLM-Aufruf.
## Konfiguration (`.env`)
Aus `.env.example`. Pflichtfelder ohne Defaults:
- `TELEGRAM_TOKEN` — von @BotFather
- `ALLOWED_USER_ID` — eigene User-ID (via @userinfobot). **Filter im TelegramClient verwirft alle anderen Sender.**
- `VAULT_PATH``D:\projects\chrka\brain`
- `JOURNAL_BOT_HOME``D:\projects\chrka\journal-bot\runtime` (queue/state/logs)
Optional:
- `LMSTUDIO_URL` — Default `http://localhost:1234/v1`
- `LMSTUDIO_MODEL` — z.B. `qwen/qwen3-vl-8b` (vision-capable)
- `WHISPER_MODEL` — Default `large-v3`
- `WHISPER_DEVICE``cpu` oder `cuda`
## Tests
Python 3.12+, uv-managed venv unter `.venv/`. **`uv` ist nicht auf PATH** — venv-Python direkt verwenden:
```bash
.venv/Scripts/python.exe -m pytest -v
```
- 38 Tests, alle Mocks (kein echter Telegram/LM-Studio-Call).
- pytest-asyncio im Auto-Mode (`pyproject.toml` → `asyncio_mode = "auto"`).
- `respx` mockt httpx; `pytest-mock` für Sonstiges.
- Prompt-Regression in `tests/test_prompt_regression.py` mit YAML-Fixtures.
## Deployment
1. `.env` füllen.
2. LM Studio starten, Modell laden, Server auf Port 1234.
3. `powershell -ExecutionPolicy Bypass -File scripts/install-task.ps1` registriert Windows-Task „JournalBot" @logon.
4. Trigger manuell: `scripts/run.ps1 both`.
## Known Gotchas (bei Änderungen beachten)
- **UTF-8 ist Pflicht.** Windows-Default ist cp1252 → zerschießt Umlaute. Immer `encoding="utf-8"` beim Lesen/Schreiben. **NIE PowerShell `Set-Content`/`Get-Content` ohne `-Encoding UTF8`** — sie zerstören UTF-8-Sequenzen (Vorfall in dieser Codebase: „Klärung" → „Klärung", BOM eingefügt). Im Code stattdessen `Path.write_text(..., encoding="utf-8")` oder über `Write`-Tool schreiben.
- **`uv` nicht auf PATH.** Immer `.venv/Scripts/python.exe` direkt aufrufen.
- **LM Studio: `response_format` braucht `json_schema`, nicht `json_object`.** LM Studio lehnt `json_object` mit 400 ab. Wir schicken das Pydantic-JSON-Schema von `ProcessorOutput` mit. Siehe `processor_lmstudio.py:62-69`.
- **Telegram `getUpdates`-Semantik.** Updates bleiben auf Telegram-Server bis `offset > last_update_id` im nächsten Call gesendet wird. Wenn `ALLOWED_USER_ID` falsch ist, filtert der Client lokal alles raus, der State bleibt bei 0, aber pollt weiter — Updates sind dann seltsam „verschluckt" je nach Reihenfolge. Bei Verdacht: `getUpdates?offset=-1` zeigt das letzte Update auch nach Confirmation.
- **`pending_update_count` aus `getWebhookInfo` ist ohne Webhook immer 0.** Heißt nicht „keine Updates" — heißt nur „kein Webhook angedockt".
- **`process_once` bricht nach erstem Fail ab** (`process.py`). Sonst würde `queue.fail()` das Item zurück nach `pending/` legen und die `while True`-Schleife würde es sofort wieder claimen → Endlosschleife. Test `test_process_fails_item_on_error` erwartet `attempts==1`.
- **httpx.Client in LMStudioProcessor wird nie geschlossen.** OK für Single-Run-CLI, bei Long-running-Mode später `close()` ergänzen.
- **Image-MIME hartcodiert `image/jpeg`.** Telegram-Fotos kommen meist als JPEG, PNG-Screenshots werden mislabeled. Vision-Modelle tolerieren das meist.
- **Frontmatter-Parser ist minimal** (kein PyYAML), liest nur flache `key: value`-Zeilen. Reicht für `00 Kontext/Personen/*.md`.
## Vault-Konventionen (relevant für Prompt)
- Daily Note: `05 Daily Notes/YYYY-MM-DD.md` mit Frontmatter `tags: [daily]\ndate: YYYY-MM-DD`.
- Person verlinken: `[[00 Kontext/Personen/<Display>|<Display>]]`. Vorname/Nachname/Spitzname werden aus Frontmatter der Person gematched.
- Mehrdeutige Namen (z.B. zwei „Steffen") → Klärungsfrage in `clarifications` + Placeholder `[[?]]` im Text.
- Klärung wird als Obsidian-Callout angehängt: `> [!warning] Klärung\n> - <text>`.
- Sprache: Deutsch, knapp, faktisch, keine Floskeln.
## Beziehung zum Vault
Das Vault-Repo (`D:\projects\chrka\brain`) ist getrennt. Vom journal-bot wird darauf **lesend zugegriffen** (Personen, Projekte) und **schreibend nur** in `05 Daily Notes/` und `07 Anhänge/`. Der Claude-Code-Skill `/journal-sync` liegt im Vault, weil er pro Vault-Session verfügbar sein muss.
## Commits & Branch-Strategie
- Feature-Branches von `main`, Squash-Merge.
- Conventional Commits (`feat`/`fix`/`refactor`/`docs`/`test`/`chore`).
- Englisch, imperativ.
- TDD: failing test → impl → green → commit pro logischer Einheit.
## Roadmap / V2-Ideen (nicht jetzt)
- Webhook-Mode statt Long-Polling.
- Routing nach Inbox/Projekt statt nur Daily Note.
- Multi-User-Support.
- Long-running-Daemon mit lifecycle-managed httpx-Client.
- Prompt-Tuning nach echten Inputs (Regressions-Suite erweitern).