docs: add CLAUDE.md project guide; fix(processor_lmstudio): use json_schema response_format
This commit is contained in:
parent
cf2a6614f9
commit
cf152c55af
|
|
@ -0,0 +1,147 @@
|
|||
# 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).
|
||||
|
|
@ -60,7 +60,14 @@ class LMStudioProcessor:
|
|||
{"role": "system", "content": self.system_prompt},
|
||||
self._user_message(payload),
|
||||
],
|
||||
"response_format": {"type": "json_object"},
|
||||
"response_format": {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
"name": "JournalEntry",
|
||||
"strict": True,
|
||||
"schema": ProcessorOutput.model_json_schema(),
|
||||
},
|
||||
},
|
||||
"temperature": 0.2,
|
||||
}
|
||||
last_error: Optional[Exception] = None
|
||||
|
|
|
|||
Loading…
Reference in New Issue