# 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 build-tools.ps1 # publish .NET tools as self-contained EXEs install-service.ps1 # (Admin) register JournalBotService install-tray.ps1 # autostart tray @logon (HKCU Run key) 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: `\05 Daily Notes\YYYY-MM-DD.md` - Anhänge in: `\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. Service + Tray installieren (supersedes the old `install-task.ps1`): siehe **Service + Tray** weiter unten. 4. Trigger manuell: `scripts/run.ps1 both`. ## Service + Tray (tools/) Two thin .NET 10 shells around the Python CLI (solution: `tools/JournalBot.slnx`): - **JournalBot.Service** — Windows Service (LocalSystem), `PeriodicTimer` runs `python -m journal_bot ingest` every `IntervalMinutes` (default 15). Config in `appsettings.json` next to the EXE. Logs to `runtime/logs/service.log` + Event Log. Only `ingest` — no LM Studio needed. - **JournalBot.Tray** — WPF tray app (user process, autostart @logon). Tray icon + window with status banner (pending/failed), Historie tab (done items, newest first, with target daily note), Log tab, and Ingest/Process/Both buttons. Runs `process` here because LM Studio is a user-session app. Command errors surface in the Log tab (no crash). - **JournalBot.Shared** — `QueueItem` DTO (mirrors the pydantic model, snake_case JSON), `BotRunner` (subprocess wrapper, kills process tree on cancel), `RuntimeReader` (reads done/, counts pending/failed, tails logs). xUnit tests in `JournalBot.Shared.Tests` (5 tests). Build + install (PowerShell): ```powershell scripts\build-tools.ps1 # publish self-contained EXEs to tools\publish scripts\install-service.ps1 # (Admin) register + start JournalBotService scripts\install-tray.ps1 # autostart tray @logon (HKCU Run key) ``` .NET tests: `dotnet test tools/JournalBot.slnx`. .NET SDK 10 required. **Gotcha:** the GUI history shows `target_path`/`written_entry`/`processed_at`, which `process` now records on done items (`queue.py` `complete()` rewrites the JSON instead of a plain rename). Old done items lack these fields (all optional -> render blank). **Note:** SDK 10 generates a `.slnx` solution file (XML), not legacy `.sln`. Both work with the `dotnet` CLI. ## 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/|]]`. 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> - `. - 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).