148 lines
8.1 KiB
Markdown
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).
|