7 Commits

31 changed files with 1397 additions and 69 deletions
+6 -4
View File
@@ -1,4 +1,6 @@
# Non-secret process overrides only. # Optionale, nicht geheime Prozesswerte. Diese Datei wird nicht automatisch geladen.
JAVIS_DISPLAY_NAME=Javis JAVIS_DATA_DIR=D:\Javis-Data\runtime
JAVIS_CONFIG_PATH=config/javis.toml JAVIS_PROVIDER=ollama
JAVIS_SECRETS_PROVIDER=none JAVIS_MODEL=qwen3:8b
JAVIS_OLLAMA_URL=http://127.0.0.1:11434
JAVIS_MODEL_TIMEOUT=180
+1
View File
@@ -0,0 +1 @@
3.12
+10
View File
@@ -12,6 +12,12 @@ Alle wesentlichen Projektänderungen werden hier in verständlicher Form dokumen
- Eng begrenzte projektinterne Codex-Agentenrollen. - Eng begrenzte projektinterne Codex-Agentenrollen.
- Abhängigkeitsfreier Strukturprüfer. - Abhängigkeitsfreier Strukturprüfer.
- Phasenbezogene Empfehlung für Entwicklungswerkzeuge, VS-Code-Erweiterungen, Obsidian und lokale Modelllaufzeiten. - Phasenbezogene Empfehlung für Entwicklungswerkzeuge, VS-Code-Erweiterungen, Obsidian und lokale Modelllaufzeiten.
- Dokumentierte, portable Ollama-Laufzeit 0.32.5 und genau ein lokales Modell
`qwen3:8b` außerhalb des Repositories.
- Lokaler Ollama-Provider und ausführbarer `javis chat`-Einstiegspunkt.
- SQLite-Sitzungen mit geordneter Nachrichtenhistorie außerhalb des Repositories.
- CLI-Befehle für neue, gelistete, geladene, geleerte und beendete Sitzungen.
- Unit-Tests für Konfiguration, Speicher, Chatkern, Provider und CLI.
### Changed ### Changed
@@ -19,6 +25,10 @@ Alle wesentlichen Projektänderungen werden hier in verständlicher Form dokumen
- Tokenbewusste Arbeitsweise als dauerhafte Regel ergänzt; Qualität und Sicherheit bleiben vorrangig. - Tokenbewusste Arbeitsweise als dauerhafte Regel ergänzt; Qualität und Sicherheit bleiben vorrangig.
- Laufende Handoff-Aktualisierung nach jedem abgeschlossenen logischen Arbeitspaket festgelegt. - Laufende Handoff-Aktualisierung nach jedem abgeschlossenen logischen Arbeitspaket festgelegt.
- Vollständigen Erst-Handoff einmalig archiviert und die laufende Übergabe auf einen kompakten aktuellen Stand umgestellt. - Vollständigen Erst-Handoff einmalig archiviert und die laufende Übergabe auf einen kompakten aktuellen Stand umgestellt.
- Isolierte Python-3.12-Entwicklungsumgebung mit uv-Lockfile und Ruff vorbereitet.
- Offizielle VS-Code-Erweiterungen für Python, Pylance und Python Environments eingerichtet.
- `qwen3:8b` mit echten deutschen Antworten und GPU-/Zeitmessungen auf der RTX 3060 geprüft.
- Projektstatus, Architektur und Datenmodell auf den funktionierenden Textchat aktualisiert.
### Security ### Security
+28 -9
View File
@@ -4,16 +4,33 @@ Javis ist ein langfristiges, lokal-first ausgerichtetes Projekt für einen pers
## Aktueller Stand ## Aktueller Stand
Dieses Repository enthält ausschließlich das sichere Projektgrundgerüst und die Architektur-Dokumentation der Phase 0. Es gibt noch: Der erste lokale Textchat funktioniert auf Windows mit Ollama und `qwen3:8b`.
Er bietet echte lokale Modellantworten ohne Cloud-Fallback, neue und fortsetzbare
Sitzungen sowie SQLite-Persistenz außerhalb des Repositories.
- keinen KI-Chat, Noch nicht implementiert sind Obsidian-Zugriff, Serverzugriff, Sprache, Mobile
- keine Modellanbindung, Client und Living Mind.
- keinen Obsidian-Schreibzugriff,
- keinen Serverzugriff,
- keine Sprachpipeline,
- keine Living-Mind-Implementierung.
Der nächste Entwicklungsschritt beginnt erst nach Pascals Prüfung auf einem eigenen Feature-Branch. ## Lokaler Start
Ollama läuft in einem eigenen Terminal ausschließlich auf localhost:
```powershell
$env:OLLAMA_MODELS = 'D:\Javis-Data\ollama-models'
$env:OLLAMA_HOST = '127.0.0.1:11434'
& 'D:\Javis-Tools\ollama\ollama.exe' serve
```
Der Chat startet aus dem Repository in einem zweiten Terminal:
```powershell
$env:UV_CACHE_DIR = 'D:\Javis-Tools\uv-cache'
$env:JAVIS_DATA_DIR = 'D:\Javis-Data\runtime'
& 'D:\Javis-Tools\uv\uv.exe' run javis chat
```
Im Chat zeigt `/help` alle Befehle. Ohne `JAVIS_DATA_DIR` nutzt Javis einen
sicheren benutzerspezifischen Plattformstandard.
## Grundregeln ## Grundregeln
@@ -29,9 +46,11 @@ Der nächste Entwicklungsschritt beginnt erst nach Pascals Prüfung auf einem ei
- [Projektstatus](docs/PROJECT_STATUS.md) - [Projektstatus](docs/PROJECT_STATUS.md)
- [Roadmap](docs/ROADMAP.md) - [Roadmap](docs/ROADMAP.md)
- [Architektur](docs/ARCHITECTURE.md) - [Architektur](docs/ARCHITECTURE.md)
- [Lokales Modell](docs/LOCAL_MODEL.md)
- [Entwicklungsumgebung](docs/DEVELOPMENT_SETUP.md)
- [Sicherheit](docs/SECURITY.md) - [Sicherheit](docs/SECURITY.md)
- [Nächste Sitzung](docs/NEXT_SESSION.md) - [Nächste Sitzung](docs/NEXT_SESSION.md)
- [Vollständiger Handoff](docs/CHATGPT_HANDOFF.md) - [Kompakter Handoff](docs/CHATGPT_HANDOFF.md)
## Historischer Verbindungstest ## Historischer Verbindungstest
+8 -2
View File
@@ -1,12 +1,18 @@
# Entwurf für eine spätere TOML-Konfiguration.
# Der erste Chat liest die in .env.example genannten Umgebungsvariablen.
[app] [app]
display_name = "Javis" display_name = "Javis"
language = "de" language = "de"
[paths] [paths]
runtime_dir = "runtime" data_dir = ""
[providers] [providers]
model = "disabled" active = "ollama"
model = "qwen3:8b"
ollama_base_url = "http://127.0.0.1:11434"
timeout_seconds = 180
cloud_fallback = false cloud_fallback = false
[obsidian] [obsidian]
+14 -8
View File
@@ -4,12 +4,12 @@
Javis beginnt als modularer Monolith. Module besitzen klare Zuständigkeiten und kommunizieren über kleine, typisierte Schnittstellen. Erst reale Betriebsgründe rechtfertigen später separate Dienste. Javis beginnt als modularer Monolith. Module besitzen klare Zuständigkeiten und kommunizieren über kleine, typisierte Schnittstellen. Erst reale Betriebsgründe rechtfertigen später separate Dienste.
## Vorgesehene Ebenen ## Ebenen
1. Interface: CLI, später Sprache, Mobilzugriff und Living Mind. 1. Interface: implementierte CLI; später möglicherweise Sprache, Mobilzugriff und Living Mind.
2. Core: Gesprächsablauf, Sitzungszustand und Orchestrierung. 2. Core: implementierter Gesprächsablauf und Sitzungslogik.
3. Providers: austauschbare lokale und optionale Cloud-Modellanbindungen. 3. Providers: kleine Schnittstelle und implementierter lokaler Ollama-Adapter.
4. Memory: strukturierte Laufzeitdaten und regenerierbarer semantischer Index. 4. Memory: implementierte SQLite-Sitzungen; ein semantischer Index ist nur geplant.
5. Obsidian: zunächst lesender Adapter, später eng begrenzte Entwürfe. 5. Obsidian: zunächst lesender Adapter, später eng begrenzte Entwürfe.
6. Tools und Status: feste Schemas, Allowlists, Timeouts und bereinigte Ausgaben. 6. Tools und Status: feste Schemas, Allowlists, Timeouts und bereinigte Ausgaben.
7. Security: Freigaben, Secret-Provider und Berechtigungsprüfung. 7. Security: Freigaben, Secret-Provider und Berechtigungsprüfung.
@@ -17,11 +17,17 @@ Javis beginnt als modularer Monolith. Module besitzen klare Zuständigkeiten und
## Plattformgrenzen ## Plattformgrenzen
- Pfade werden später per Konfiguration und `pathlib` aufgelöst. - Pfade werden zentral in `Settings` und mit `pathlib` aufgelöst.
- Betriebssystemspezifische Funktionen liegen hinter Adaptern. - Betriebssystemspezifische Funktionen liegen hinter Adaptern.
- Produktname, Datenpfade und Provider werden zentral konfiguriert. - Produktname, Datenpfade und Provider werden zentral konfiguriert.
- Laufzeitdaten liegen außerhalb von Git. - Laufzeitdaten liegen außerhalb von Git.
## Noch nicht implementiert ## Implementierter Chatfluss
Es existieren nur Paketgrenzen und Dokumentation. Es gibt keinen Chat, Provider, Speicher, Toolaufruf oder Netzwerkdienst. `interface.cli` nimmt Text an, `core.chat_service` verbindet Sitzungsverlauf und
Provider, `providers.ollama` spricht nur die lokale Ollama-HTTP-API an und
`memory.sqlite_store` speichert erfolgreiche Benutzer-/Assistentenpaare atomar.
Scheitert das Modell, wird keine erfundene Antwort gespeichert.
Es existiert kein Cloudadapter, Toolaufruf, Obsidian-Zugriff oder eigener
öffentlicher Netzwerkdienst. Der Ollama-Endpunkt muss eine Loopback-Adresse sein.
+83 -20
View File
@@ -45,23 +45,37 @@ Rootserver:
## Aktueller technischer Stand ## Aktueller technischer Stand
- Projektgrundgerüst ist auf `main` vorhanden und nach Gitea gepusht. - Der erste lokale Textchat ist auf `main` implementiert, abgenommen und lokal gemergt.
- Paketstruktur unter `src/javis` ist angelegt, enthält aber noch keinen Chatkern. - `uv run javis chat` liefert echte lokale Antworten und speichert Sitzungen in SQLite.
- Konfigurationsbeispiel, Sicherheitsdokumente, ADRs und Strukturtest sind vorhanden. - Konfigurationsbeispiel, Sicherheitsdokumente, ADRs und Strukturtest sind vorhanden.
- Living-Mind-Verzeichnis ist ausschließlich ein inaktiver Vertrag ohne Frontend oder Fantasiedaten. - Living-Mind-Verzeichnis ist ausschließlich ein inaktiver Vertrag ohne Frontend oder Fantasiedaten.
- Projektbezogene Codex-Rollen Scout, Architect, Builder, Verifier und Security Reviewer sind konfiguriert; maximal zwei Unteragenten, nur Builder schreibend. - Projektbezogene Codex-Rollen Scout, Architect, Builder, Verifier und Security Reviewer sind konfiguriert; maximal zwei Unteragenten, nur Builder schreibend.
- Im normalen Windows-`PATH` sind Git und VS Code verfügbar. Eine Projektinstallation von Python, `uv`, Ruff, Ollama oder llama.cpp ist noch nicht bestätigt. - Toolchain eingerichtet: uv 0.11.32 und CPython 3.12.13 isoliert unter `D:\Javis-Tools`; Ruff 0.16.0 in der lokalen `.venv`.
- Offizielle VS-Code-Erweiterungen installiert: Python 2026.4.0, Pylance 2026.3.1, Python Environments 1.36.0 und Debugpy 2026.6.0.
- Windows-PATH und vorhandene Python-Installationen wurden nicht verändert.
- Ollama 0.32.5 ist portabel unter `D:\Javis-Tools\ollama` installiert.
- Genau ein Modell ist installiert: `qwen3:8b`, GGUF `Q4_K_M`, 5,2 GB, Apache-2.0.
- Modelle liegen außerhalb von Git unter `D:\Javis-Data\ollama-models`.
- Der lokale Ollama-Dienst bindet nur an `127.0.0.1:11434`; PATH, Registry,
Autostart und Windows-Dienste blieben unverändert.
- CLI-Befehle: `/new`, `/sessions`, `/load <ID>`, `/clear`, `/help`, `/exit`.
- Sitzungsdaten liegen über `JAVIS_DATA_DIR` außerhalb von Git; der bestätigte
Smoke-Test nutzte `D:\Javis-Data\runtime\core-chat-smoke`.
- Kein Obsidian-, Laptop- oder Rootserverzugriff wurde implementiert. - Kein Obsidian-, Laptop- oder Rootserverzugriff wurde implementiert.
- Keine KI-, Sprach-, Mobil- oder Statusfunktion ist implementiert. - Keine Tool-, Sprach-, Mobil-, Status- oder Living-Mind-Funktion ist implementiert.
## Aktuelle Architektur ## Aktuelle Architektur
- modularer Monolith, bevorzugt Python - modularer Monolith, bevorzugt Python
- plattformunabhängige Pfade und zentrale Konfiguration - plattformunabhängige Pfade und zentrale Konfiguration
- Modell-Provider als kleine austauschbare Schnittstelle - Modell-Provider als kleine austauschbare Schnittstelle
- SQLite für ersten Sitzungsspeicher - Ollama-Provider akzeptiert nur lokale HTTP-Loopback-Adressen
- SQLite-Sitzungsspeicher mit atomaren Benutzer-/Assistentenpaaren
- Laufzeitdaten außerhalb von Git über `JAVIS_DATA_DIR` oder sicheren Plattformstandard - Laufzeitdaten außerhalb von Git über `JAVIS_DATA_DIR` oder sicheren Plattformstandard
- lokale Modelle zuerst; keine kostenpflichtige API und kein stiller Cloud-Fallback - aktuell ausschließlich lokales Modell; als separates Folgepaket ist Gemini als
bevorzugter kostenloser Provider für ausdrücklich cloudgeeignete Inhalte geplant
- strikter Nullkostenmodus ohne Billing-Aktivierung oder kostenpflichtigen Fallback;
lokales Modell bleibt Offline-, Datenschutz-, Ausfall- und Quota-Fallback
- Secret-Werte außerhalb von Code, Git, Prompts und Logs - Secret-Werte außerhalb von Code, Git, Prompts und Logs
- Modell erhält niemals Passwörter, Tokens oder private Schlüssel - Modell erhält niemals Passwörter, Tokens oder private Schlüssel
- keine freie Shell; spätere Tools benötigen feste Schemas, Allowlists, Timeouts und Freigaben - keine freie Shell; spätere Tools benötigen feste Schemas, Allowlists, Timeouts und Freigaben
@@ -77,11 +91,23 @@ Rootserver:
- Tokenbewusster Arbeitsablauf festgelegt: gezielte Reads, kurze Ergebnisse, keine unnötigen Agenten oder Plugins. - Tokenbewusster Arbeitsablauf festgelegt: gezielte Reads, kurze Ergebnisse, keine unnötigen Agenten oder Plugins.
- Werkzeugempfehlung auf `uv`, Microsoft Python/Pylance/Python Environments und Ruff begrenzt. - Werkzeugempfehlung auf `uv`, Microsoft Python/Pylance/Python Environments und Ruff begrenzt.
- Langer Handoff einmalig unverändert archiviert und durch diese kompakte aktuelle Zusammenfassung ersetzt. - Langer Handoff einmalig unverändert archiviert und durch diese kompakte aktuelle Zusammenfassung ersetzt.
- Handoff-Verdichtung als Commit `e6eb995` nach `origin/main` gepusht.
- Branch `feat/core-chat` von diesem Stand erstellt.
- uv, CPython 3.12, lokale `.venv`, Ruff, Lockfile und offizielle VS-Code-Python-Erweiterungen eingerichtet.
- Ollama und llama.cpp anhand offizieller Quellen verglichen; Ollama als einfachere
erste Windows-Laufzeit gewählt.
- Ollama-Archiv gegen die offizielle SHA-256-Prüfsumme verifiziert und portabel installiert.
- `qwen3:8b` außerhalb des Repositories geladen und mit zwei echten deutschen
Modellantworten auf der RTX 3060 geprüft.
- Chatkern, Ollama-Provider, zentrale Konfiguration, SQLite-Speicher und CLI implementiert.
- 13 Unit-Tests für Konfiguration, Speicher, Verlauf, Providervertrag und CLI ergänzt.
- Echten CLI-Chat ausgeführt und dieselbe Sitzung in einem neuen Prozess geladen,
aufgelistet und erfolgreich mit einer weiteren Modellantwort fortgesetzt.
- Obsidian, Ubuntu-Laptop und Rootserver blieben unverändert. - Obsidian, Ubuntu-Laptop und Rootserver blieben unverändert.
## Aktuelle Tests ## Aktuelle Tests
Letzter bestätigter Grundgerüststand: Letzter bestätigter Projektstand:
- Strukturprüfung: bestanden - Strukturprüfung: bestanden
- Unit-Test des Paketgerüsts: bestanden - Unit-Test des Paketgerüsts: bestanden
@@ -89,15 +115,42 @@ Letzter bestätigter Grundgerüststand:
- UTF-8- und interne Markdown-Linkprüfung: bestanden - UTF-8- und interne Markdown-Linkprüfung: bestanden
- Secret-Musterprüfung: keine Treffer - Secret-Musterprüfung: keine Treffer
- Gitignore-Prüfung für lokale Secret-Dateien: bestanden - Gitignore-Prüfung für lokale Secret-Dateien: bestanden
- uv-Lock und `uv sync --dev`: bestanden
- Python in `.venv`: 3.12.13
- Ruff in `.venv`: 0.16.0
- Unit-Tests: 13 bestanden
- Ruff Lint: bestanden
- Ruff Formatprüfung: bestanden
Für den Textchat existieren noch keine Funktions- oder Modell-Smoke-Tests. Lokaler Modell-Smoke-Test:
- Ollama-Version/API: 0.32.5, localhost erreichbar
- Modell: 5.225.388.164 Byte, 8,2B, Q4_K_M, Apache-2.0
- kalter Aufruf: 36,19 s Wandzeit, davon 17,53 s Laden, ungefähr 7,22 Token/s
- warmer kurzer Aufruf: 0,56 s Wandzeit, ungefähr 55,51 Token/s
- GPU-Belegung: 1.418 MiB vorher, 6.842 MiB mit geladenem Modell
- Ollama meldet 100 % GPU und 5,6 GB Modellbelegung
CLI-/Persistenz-Smoke:
- neuer CLI-Prozess erzeugte eine Sitzung und erhielt eine echte Modellantwort
- zweiter CLI-Prozess lud dieselbe UUID und zeigte zwei gespeicherte Nachrichten
- `/sessions` listete Provider, Modell und Nachrichtenanzahl korrekt
- fortgesetzte Frage erhielt die echte lokale Antwort `4`
## Git-Stand ## Git-Stand
- aktueller Branch: `main`
- stabiler Branch: `main` - stabiler Branch: `main`
- Grundgerüstcommit: `6d04171` - Grundgerüstcommit: `6d04171`
- letzter Commit vor der Handoff-Verdichtung: `0e82748` - letzter Commit vor der Handoff-Verdichtung: `0e82748`
- der Commit mit dieser Verdichtung ist über `git log -1 --oneline` zu bestimmen - Handoff-Verdichtung auf `main`: `e6eb995`
- Python-Entwicklungsumgebung auf dem Feature-Branch: `fa90a5c`
- Laufzeit-/Modelldokumentation: `10d7cd7`
- Chatkern und CLI: `ec9b2f9`
- Unit-Tests: `b153daa`
- Feature-Dokumentation: `5d567e5`
- lokaler Merge nach `main`: `f3d6dfb`
- `origin` verwendet HTTPS - `origin` verwendet HTTPS
- kein Force-Push und keine umgeschriebene Historie - kein Force-Push und keine umgeschriebene Historie
@@ -106,15 +159,20 @@ Für den Textchat existieren noch keine Funktions- oder Modell-Smoke-Tests.
- `49a1dd3`: erster Gitea-Verbindungstest - `49a1dd3`: erster Gitea-Verbindungstest
- `6d04171`: sicheres Javis-Grundgerüst - `6d04171`: sicheres Javis-Grundgerüst
- `0e82748`: Werkzeug-, Token- und Handoff-Regeln - `0e82748`: Werkzeug-, Token- und Handoff-Regeln
- aktuell: einmalige Archivierung und Verdichtung des Handoffs - `e6eb995`: einmalige Archivierung und Verdichtung des Handoffs
- aktuell: isolierte Python-Toolchain und reproduzierbare Entwicklungsumgebung
- aktuell: portable Ollama-Laufzeit und `qwen3:8b` außerhalb von Git verifiziert
- aktuell: lokaler CLI-Chat und persistente SQLite-Sitzungen vollständig abgenommen
## Offene Entscheidungen und Fehler ## Offene Entscheidungen und Fehler
- endgültiger Produkt-/Repositoryname bleibt offen - endgültiger Produkt-/Repositoryname bleibt offen
- konkrete Python-Version und Installation sind noch festzulegen - Startmechanismus für Ollama ist noch bewusst manuell und nur pro Prozess konfiguriert
- lokale Modelllaufzeit und erstes Modell sind noch nicht installiert oder benchmarked - Antworten werden noch nicht gestreamt; Sitzungen besitzen noch keine Titel oder Suche
- genauer späterer Obsidian-Schreibbereich ist nicht freigegeben - genauer späterer Obsidian-Schreibbereich ist nicht freigegeben
- endgültiger Secret-Provider ist offen; lokale Klartext-XML wäre nur restriktiv geschützt und Git-ignoriert zulässig - endgültiger Secret-Provider ist offen; lokale Klartext-XML wäre nur restriktiv geschützt und Git-ignoriert zulässig
- aktuelle kostenlose Gemini-Modelle, Modellkennungen und Kontingente müssen vor
der Implementierung anhand offizieller Google-Dokumentation neu geprüft werden
- Gaming-PC zeigt derzeit nur 8 statt 16 logische CPU-Prozessoren; Ursache ungeklärt - Gaming-PC zeigt derzeit nur 8 statt 16 logische CPU-Prozessoren; Ursache ungeklärt
- ASUS-Laptop ist nicht automatisch für dasselbe Modell wie die RTX 3060 geeignet - ASUS-Laptop ist nicht automatisch für dasselbe Modell wie die RTX 3060 geeignet
@@ -123,6 +181,9 @@ Für den Textchat existieren noch keine Funktions- oder Modell-Smoke-Tests.
- lokale Modellqualität und Geschwindigkeit unterscheiden sich stark zwischen RTX 3060 und GTX 1050 - lokale Modellqualität und Geschwindigkeit unterscheiden sich stark zwischen RTX 3060 und GTX 1050
- ATM10 und Javis dürfen den ASUS-Laptop nicht gleichzeitig überlasten - ATM10 und Javis dürfen den ASUS-Laptop nicht gleichzeitig überlasten
- Secrets können über Logs, Prompts, Screenshots oder Git abfließen - Secrets können über Logs, Prompts, Screenshots oder Git abfließen
- Cloudrouting könnte sensible Daten preisgeben; vor jedem Cloudaufruf ist deshalb
lokal `allowed`, `ask` oder `never` zu entscheiden und Kontext zu minimieren
- kostenlose Gemini-Kontingente und Modellnamen können sich ändern
- Prompt Injection aus später eingelesenen Notizen oder Webseiten - Prompt Injection aus später eingelesenen Notizen oder Webseiten
- unkontrollierter Obsidian-Schreibzugriff könnte Wissen beschädigen - unkontrollierter Obsidian-Schreibzugriff könnte Wissen beschädigen
- Self-hosted LiveSync ist kein Ersatz für ein getestetes Backup - Self-hosted LiveSync ist kein Ersatz für ein getestetes Backup
@@ -130,14 +191,16 @@ Für den Textchat existieren noch keine Funktions- oder Modell-Smoke-Tests.
## Nächster sinnvoller Auftrag ## Nächster sinnvoller Auftrag
Der genehmigte nächste Meilenstein ist `feat/core-chat`: Als separates nächstes Featurepaket auf neuem Branch:
1. von sauberem `main` einen Feature-Branch erstellen 1. offizielle aktuelle Gemini-Free-Tier-Modelle und Kontingente neu prüfen
2. minimale Python-Toolchain mit `uv`, lokaler `.venv`, Ruff und offiziellen VS-Code-Erweiterungen einrichten 2. Secret-Provider ohne Schlüsselzugriff des Modells implementieren
3. Ollama und llama.cpp anhand aktueller offizieller Quellen vergleichen und nur eine Laufzeit installieren 3. Datenschutzrouting `allowed`/`ask`/`never` vor jedem Cloudaufruf erzwingen
4. genau ein frei lokal nutzbares Modell unter ungefähr 10 GB herunterladen und auf der RTX 3060 messen 4. Gemini als kostenlosen bevorzugten Provider und Ollama als lokalen
5. echten lokalen CLI-Chat mit Provider-Schnittstelle und SQLite-Sitzungen implementieren Datenschutz-/Offline-/429-Fallback implementieren
6. Unit-, Ruff-, Persistenz-, Secret- und echten Modell-Smoke-Test ausführen 5. `free_only = true`, keine Billing-Aktivierung und alle geforderten Erfolgs-,
7. nur bei vollständig erfüllter Abnahme nach `main` mergen Key-, 429-, Netzwerk-, Fallback- und Datenschutztests nachweisen
Keinen API-Key in Chat, Git, Obsidian, Logs oder Dokumentation übernehmen.
Nicht Teil dieses Meilensteins: Obsidian-Integration, Serverzugriff, Sprache, Mobile Client oder Living-Mind-Code. Nicht Teil dieses Meilensteins: Obsidian-Integration, Serverzugriff, Sprache, Mobile Client oder Living-Mind-Code.
+15 -10
View File
@@ -1,18 +1,23 @@
# Datenmodell Entwurf # Datenmodell
## Laufzeitdaten ## Laufzeitdaten
Eine spätere strukturierte Datenbank kann mindestens enthalten: Der erste Chat verwendet SQLite mit zwei Tabellen:
- `sessions`: Beginn, Ende und aktiver Kontext - `sessions`: UUID, Erstellungs-/Änderungszeit, Provider und Modell
- `messages`: Rolle, Inhalt, Zeit und Providerreferenz - `messages`: Sitzungsbezug, feste Reihenfolge, Rolle, Inhalt und Zeitstempel
- `tasks`: Status, Projekt, Fälligkeit und Quelle
- `approvals`: angefragte Aktion, Entscheidung und Zeitpunkt
- `tool_calls`: Toolname, bereinigte Parameter, Ergebnisstatus und Dauer
- `memories`: Aussage, Quelle, Gültigkeit und Bestätigung
- `source_documents`: Pfad, Änderungszeit und Indexstatus
SQLite ist der bevorzugte erste Kandidat, aber noch nicht implementiert. Nachrichtenrollen sind auf `user` und `assistant` begrenzt. Ein Benutzer-/
Assistentenpaar wird erst nach erfolgreicher Modellantwort in einer Transaktion
gespeichert. Fremdschlüssel und eine eindeutige Nachrichtenposition sichern die
Reihenfolge.
Der Pfad ist über `JAVIS_DATA_DIR` konfigurierbar. Ohne Override wird unter Windows
`%LOCALAPPDATA%\Javis` und unter Linux `$XDG_DATA_HOME/javis` beziehungsweise
`~/.local/share/javis` verwendet. Die Datenbank liegt nie im Repository.
Aufgaben, Freigaben, Toolaufrufe, Erinnerungen und Quelldokumente sind noch nicht
implementiert und werden erst bei einem konkreten Arbeitspaket modelliert.
## Obsidian ## Obsidian
+4 -1
View File
@@ -7,7 +7,10 @@
| ADR-0003 | Obsidian-Links benötigen echte inhaltliche Beziehungen | angenommen | | ADR-0003 | Obsidian-Links benötigen echte inhaltliche Beziehungen | angenommen |
| ADR-0004 | Tokenbewusst arbeiten, ohne Qualität oder Sicherheit zu senken | angenommen | | ADR-0004 | Tokenbewusst arbeiten, ohne Qualität oder Sicherheit zu senken | angenommen |
| ADR-0005 | Laufenden Handoff kompakt halten und Vollstände nur zu Meilensteinen archivieren | angenommen | | ADR-0005 | Laufenden Handoff kompakt halten und Vollstände nur zu Meilensteinen archivieren | angenommen |
| ADR-0006 | Ollama und qwen3:8b für den ersten Windows-Chat verwenden | angenommen |
| ADR-0007 | Gemini kostenlos bevorzugen, sensible Inhalte lokal halten und niemals Billing aktivieren | angenommen |
Offen bleiben insbesondere die konkrete lokale Modelllaufzeit, das endgültige Runtime-Secret-System, der genaue Obsidian-Schreibbereich und der Produktname. Offen bleiben insbesondere das endgültige Runtime-Secret-System, der genaue
Obsidian-Schreibbereich, das spätere Laptop-Modell und der Produktname.
Ausführliche Begründungen stehen unter [`docs/adr`](adr/README.md). Ausführliche Begründungen stehen unter [`docs/adr`](adr/README.md).
+53
View File
@@ -0,0 +1,53 @@
# Entwicklungsumgebung
Stand: 30.07.2026, Branch `feat/core-chat`.
## Installierte Werkzeuge
| Werkzeug | Version | Speicherort |
|---|---:|---|
| uv | 0.11.32 | `D:\Javis-Tools\uv` |
| CPython | 3.12.13 | `D:\Javis-Tools\python` |
| Ruff | 0.16.0 | lokale Projektumgebung `.venv` |
| VS Code Python | 2026.4.0 | Benutzerprofil von VS Code |
| VS Code Pylance | 2026.3.1 | Benutzerprofil von VS Code |
| VS Code Python Environments | 1.36.0 | Benutzerprofil von VS Code |
| VS Code Debugpy | 2026.6.0 | Abhängigkeit der Python-Erweiterung |
`uv` und Python wurden isoliert auf `D:` installiert. Eine vorhandene Python-Version wurde nicht ersetzt und der Windows-Benutzer- oder System-`PATH` wurde nicht verändert.
## Projektumgebung
- gewünschte Python-Linie: `3.12`, festgehalten in `.python-version`
- lokale Umgebung: `.venv`, durch Git ignoriert
- Abhängigkeiten: `pyproject.toml`
- reproduzierbare Auflösung: `uv.lock`
- einzige Entwicklungsabhängigkeit in dieser Phase: Ruff
Typische Befehle aus dem Repository:
```powershell
$env:UV_PYTHON_INSTALL_DIR = 'D:\Javis-Tools\python'
$env:UV_CACHE_DIR = 'D:\Javis-Tools\uv-cache'
& 'D:\Javis-Tools\uv\uv.exe' sync --dev
& 'D:\Javis-Tools\uv\uv.exe' run python -m unittest discover -s tests/unit -v
& 'D:\Javis-Tools\uv\uv.exe' run ruff check .
& 'D:\Javis-Tools\uv\uv.exe' run ruff format --check .
```
Vor dem Chat wird Ollama in einem eigenen Terminal gestartet:
```powershell
$env:OLLAMA_MODELS = 'D:\Javis-Data\ollama-models'
$env:OLLAMA_HOST = '127.0.0.1:11434'
& 'D:\Javis-Tools\ollama\ollama.exe' serve
```
Danach:
```powershell
$env:JAVIS_DATA_DIR = 'D:\Javis-Data\runtime'
& 'D:\Javis-Tools\uv\uv.exe' run javis chat
```
Die absoluten Pfade sind ausschließlich lokale Entwicklungsdokumentation. Der Anwendungscode darf keine festen Windows-Pfade enthalten.
+61
View File
@@ -0,0 +1,61 @@
# Lokales Modell
Stand: 30.07.2026, erster Windows-Test auf dem Gaming-PC.
## Auswahl
- Laufzeit: Ollama `0.32.5`, portable Windows-Version
- Modell: `qwen3:8b`
- Format: GGUF
- Quantisierung: `Q4_K_M`
- Parameter: 8,2 Milliarden
- Kontext laut Modellmetadaten: 40.960 Token; Ollama-Testkontext: 4.096
- Downloadgröße laut lokaler Ollama-API: 5.225.388.164 Byte, ungefähr 5,2 GB
- Lizenz: Apache License 2.0
Ollama wurde gegenüber llama.cpp für diesen ersten Windows-Test gewählt, weil die
offizielle portable Laufzeit ohne Build, Installer, PATH-Änderung oder Systemdienst
eine stabile lokale HTTP-Schnittstelle bereitstellt. llama.cpp bleibt eine mögliche
spätere Alternative, falls auf schwächerer Hardware feinere Kontrolle über Kontext,
Offloading oder Quantisierung benötigt wird.
`qwen3:8b` wurde gewählt, weil die Variante unter 10 GB bleibt, vollständig in den
12-GB-VRAM der RTX 3060 passt, deutschsprachige Antworten liefert und nach dem
Download offline über localhost nutzbar ist.
## Speicherorte
- portable Laufzeit: `D:\Javis-Tools\ollama`
- Modellablage: `D:\Javis-Data\ollama-models`
- Repository: enthält weder Laufzeit noch Modell
Die Ablage wird beim Start des Dienstes über `OLLAMA_MODELS` nur für diesen Prozess
gesetzt. PATH, Registry, Autostart und Windows-Dienste wurden nicht verändert.
## Gemessener Test
Hardware: AMD Ryzen 7 9700X, 64 GB RAM, Nvidia RTX 3060 12 GB, Windows 11.
- Laufzeit-Archiv: 1.457.824.795 Byte
- verifizierte SHA-256:
`7c941ae084569d298062d29f8139163a3187c76dbca0479c70d085e78fd8c7bb`
- kalter deutscher Zweitsatz-Test: 36,19 Sekunden Wandzeit
- davon Modell-Ladezeit: 17,53 Sekunden
- Ausgabe: 49 Token mit ungefähr 7,22 Token/Sekunde
- warmer kurzer Test: 0,56 Sekunden Wandzeit
- Ausgabe: 19 Token mit ungefähr 55,51 Token/Sekunde
- GPU-Grundbelegung vor Laden: 1.418 MiB
- GPU-Belegung bei geladenem Modell/Test: 6.842 MiB
- beobachtete zusätzliche VRAM-Belegung: ungefähr 5.424 MiB
- `ollama ps`: 5,6 GB, 100 % GPU, Kontext 4.096
Die Messwerte sind Momentaufnahmen und hängen von Prompt, Kontext, bereits belegtem
VRAM und Warmzustand ab.
## Grenze
Dieses Modell ist der bestätigte erste Gaming-PC-Test, nicht automatisch das
endgültige Modell für den ASUS-Laptop mit GTX 1050 4 GB. Dort müssen später
kleineres Modell, stärkere Quantisierung, teilweises CPU-Offloading und tatsächliche
Antwortqualität separat gemessen werden. Ein Laptop-Deployment gehört nicht zu
diesem Arbeitspaket.
+13 -8
View File
@@ -2,7 +2,8 @@
## Ziel ## Ziel
Phase 1 auf `feat/core-chat`: minimale Python-Toolchain, genau eine lokale Modelllaufzeit und ein echter lokaler CLI-Textchat mit SQLite-Sitzungen. Nächstes separates Featurepaket: kostenlose Gemini-Anbindung mit strikt lokalem
Datenschutzrouting und Ollama als Ausfall-, Quota-, Offline- und Sensitivitätsfallback.
## Vor Änderungen erneut prüfen ## Vor Änderungen erneut prüfen
@@ -13,13 +14,17 @@ Phase 1 auf `feat/core-chat`: minimale Python-Toolchain, genau eine lokale Model
## Vorgeschlagener Umfang ## Vorgeschlagener Umfang
- `uv`, unterstützte stabile Python-Version und offizielle VS-Code-Erweiterungen einrichten - aktuelle kostenlose Gemini-Modelle, Kennungen und Kontingente ausschließlich in
- Microsoft Python/Pylance/Python Environments sowie Ruff als minimale Entwicklungswerkzeuge prüfen offizieller Google-Dokumentation prüfen; Kandidaten nicht ungeprüft übernehmen
- Ollama und llama.cpp offiziell vergleichen; nur eine Laufzeit und ein Modell unter ungefähr 10 GB installieren - Providerrouter mit `free_only = true` und ohne Billing-Konfiguration entwerfen
- lokale Konfiguration, Provider-Schnittstelle und SQLite-Sitzungen implementieren - lokale Datenschutzentscheidung `allowed`, `ask` oder `never` vor jedem Cloudaufruf
- CLI mit neuen, gelisteten und fortsetzbaren Sitzungen - Gemini-Key nur über lokalen Secret-Provider; Wert niemals an Modell, Git,
- Unit-, Ruff-, Persistenz-, Secret- und echter Modell-Smoke-Test Obsidian, Logs, Dokumentation oder Chat geben
- Gemini-Erfolg, fehlenden/ungültigen Key, 429, Netzwerkausfall, lokalen Fallback,
`never`, `ask` und ausbleibende Billing-Aktivierung testen
## Nicht beginnen ## Nicht beginnen
Keine zweite Modelllaufzeit, kein Obsidian-Schreiben, kein SSH-/Serverzugriff, keine Sprache und kein Living-Mind-Frontend. Keinen echten Schlüssel anfordern oder dokumentieren, kein Billingkonto verbinden,
keine kostenpflichtigen Fallbacks, keinen vollständigen Vault übertragen und
weiterhin kein Obsidian-Schreiben, SSH-/Serverzugriff, Sprache oder Living Mind.
+27 -4
View File
@@ -1,6 +1,6 @@
# Projektstatus # Projektstatus
Stand: Dokumentationsverdichtung vor `feat/core-chat`, 30.07.2026. Stand: lokaler Textchat auf `main`, 30.07.2026.
## Erreicht ## Erreicht
@@ -12,17 +12,40 @@ Stand: Dokumentationsverdichtung vor `feat/core-chat`, 30.07.2026.
- projektinterne Codex-Agentensyntax anhand der aktuellen offiziellen Codex-Dokumentation geprüft - projektinterne Codex-Agentensyntax anhand der aktuellen offiziellen Codex-Dokumentation geprüft
- phasenbezogene Werkzeug- und Plugin-Empfehlungen sowie tokenbewusste Arbeitsregeln dokumentiert - phasenbezogene Werkzeug- und Plugin-Empfehlungen sowie tokenbewusste Arbeitsregeln dokumentiert
- vollständigen Erst-Handoff archiviert und kompakte laufende Übergabe eingeführt - vollständigen Erst-Handoff archiviert und kompakte laufende Übergabe eingeführt
- uv 0.11.32 und CPython 3.12.13 isoliert unter `D:\Javis-Tools` installiert
- lokale `.venv`, reproduzierbares `uv.lock` und Ruff 0.16.0 eingerichtet
- offizielle VS-Code-Erweiterungen Python, Pylance und Python Environments installiert
- Ollama 0.32.5 portabel und ohne Systemänderungen unter `D:\Javis-Tools` installiert
- `qwen3:8b` (Q4_K_M, 5,2 GB, Apache-2.0) unter `D:\Javis-Data` geladen
- zwei echte deutsche Modellantworten erfolgreich vollständig auf der RTX 3060 erzeugt
- kalter Test 36,19 s, warmer Kurztest 0,56 s; geladene GPU-Belegung 6.842 MiB
- lokaler Ollama-Provider, zentrale Konfiguration und SQLite-Sitzungsspeicher implementiert
- CLI mit `/new`, `/sessions`, `/load`, `/clear`, `/help` und `/exit` implementiert
- echte CLI-Modellantwort sowie Laden und Fortsetzen derselben Sitzung in einem
neuen Prozess bestätigt
- 13 Unit-Tests, Ruff und Strukturprüfung erfolgreich
## Nicht implementiert ## Nicht implementiert
KI, Chat, Provider, Gedächtnis, Obsidian-Adapter, Tools, Serverzugriff, Sprache, Mobile Client und Living-Mind-Frontend. Obsidian-Adapter, Tools, Serverzugriff, Sprache, Mobile Client,
Living-Mind-Frontend und automatische Ollama-Prozessverwaltung.
## Git ## Git
- Branch: `main` - Branch: `main`
- Grundgerüstcommit: `6d04171` - Grundgerüstcommit: `6d04171`
- Der aktuelle Dokumentationscommit ist mit `git log -1` zu ermitteln. - Basis des Feature-Branches: `e6eb995`
- Toolchain: `fa90a5c`
- Laufzeit/Modell: `10d7cd7`
- Chatkern: `ec9b2f9`
- Tests: `b153daa`
- Dokumentation: `5d567e5`
- Merge nach `main`: `f3d6dfb`
## Bekannte Einschränkung ## Bekannte Einschränkung
Im normalen Windows-`PATH` sind Python, `uv`, Ruff und eine lokale Modelllaufzeit noch nicht bestätigt. Der Textchat ist noch nicht implementiert. Ollama wird bewusst manuell in einem eigenen Terminal gestartet. Antworten werden
noch nicht gestreamt; Sitzungen haben noch keine Titel, Suche oder Löschfunktion.
`qwen3:8b` ist nicht automatisch für die GTX 1050 des späteren Zielhosts geeignet.
Gemini ist als separates kostenloses Folgepaket mit lokalem Datenschutzrouting
geplant, aber noch nicht implementiert oder mit einem API-Key konfiguriert.
+23
View File
@@ -0,0 +1,23 @@
# ADR-0006: Erste lokale Modelllaufzeit
Status: angenommen am 30.07.2026.
## Entscheidung
Der erste Windows-Textchat verwendet die portable Ollama-Laufzeit 0.32.5 und genau
das Modell `qwen3:8b` in der GGUF-Quantisierung Q4_K_M. Laufzeit und Modell liegen
außerhalb des Repositories. Javis akzeptiert für diesen Provider ausschließlich
eine lokale HTTP-Loopback-Adresse.
## Begründung
Ollama stellt ohne Build, Installer oder Systemdienst eine kleine stabile lokale
API bereit. Das Modell bleibt unter 10 GB, passt beim bestätigten Test vollständig
in die RTX 3060 mit 12 GB und liefert deutsche Antworten.
## Folgen
Die Entscheidung gilt für den ersten Gaming-PC-Meilenstein. Sie legt weder die
endgültige Modelllaufzeit noch das Modell für den ASUS-Laptop mit GTX 1050 4 GB
fest. Ein späterer Vergleich mit llama.cpp bleibt möglich, wenn messbare
Anforderungen an Offloading, Quantisierung oder Ressourcensteuerung entstehen.
+36
View File
@@ -0,0 +1,36 @@
# ADR-0007: Kostenlose hybride Providerstrategie
Status: angenommen am 30.07.2026, Implementierung ausstehend.
## Entscheidung
Javis bleibt in Logik, Gedächtnis, Berechtigungen, Audit und Kontextauswahl lokal.
Für ausdrücklich cloudgeeignete Inhalte soll Gemini später der bevorzugte
kostenlose Provider werden. Ollama bleibt lokaler Datenschutz-, Offline-, Ausfall-
und Quota-Fallback. `free_only = true` verbietet Billing-Aktivierung,
kostenpflichtige Modellwahl und kostenpflichtige Fallbacks.
Vor jedem Cloudaufruf entscheidet eine lokale Richtlinie:
- `allowed`: minimal nötiger Ausschnitt darf übertragen werden
- `ask`: Pascal muss vorher zustimmen
- `never`: ausschließlich lokal verarbeiten
Persönliche und familiäre Informationen sind mindestens `ask`. Secrets sowie
Finanz-, Gesundheits- und besonders sensible Daten sind `never`. Der vollständige
Obsidian-Vault darf niemals an Gemini übertragen werden.
## Voraussetzungen
Vor der Implementierung werden aktuelle kostenlose Modelle, exakte Modellnamen und
Kontingente anhand offizieller Google-Dokumentation geprüft. Ein Gemini-Schlüssel
wird nur über einen lokalen Secret-Provider geladen, auf die Gemini API begrenzt
und niemals an Modellkontext, Git, Obsidian, Logs, Dokumentation oder Chat gegeben.
Javis verbindet oder aktiviert kein Google-Billingkonto.
## Folgen
HTTP 429, Quota- und Netzwerkausfälle müssen verständlich behandelt und sicher zum
lokalen Provider geroutet werden. Gemini ist bevorzugt, aber nie unersetzliche
Grundlage. Die Umsetzung erfolgt als eigenes getestetes Featurepaket; der bereits
abgenommene lokale Chat bleibt bis dahin unverändert.
+2
View File
@@ -5,3 +5,5 @@
- [ADR-0003: Sinnvolle Obsidian-Verknüpfungen](0003-meaningful-obsidian-links.md) - [ADR-0003: Sinnvolle Obsidian-Verknüpfungen](0003-meaningful-obsidian-links.md)
- [ADR-0004: Tokenbewusster Arbeitsablauf](0004-token-efficient-workflow.md) - [ADR-0004: Tokenbewusster Arbeitsablauf](0004-token-efficient-workflow.md)
- [ADR-0005: Kompakter Handoff](0005-compact-handoff.md) - [ADR-0005: Kompakter Handoff](0005-compact-handoff.md)
- [ADR-0006: Erste lokale Modelllaufzeit](0006-first-local-runtime.md)
- [ADR-0007: Kostenlose hybride Providerstrategie](0007-free-hybrid-providers.md)
+15
View File
@@ -10,8 +10,23 @@ readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [] dependencies = []
[project.scripts]
javis = "javis.interface.cli:main"
[dependency-groups]
dev = [
"ruff>=0.12,<1",
]
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
where = ["src"] where = ["src"]
[tool.pytest.ini_options] [tool.pytest.ini_options]
testpaths = ["tests"] testpaths = ["tests"]
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = ["B", "E", "F", "I", "UP"]
+1 -3
View File
@@ -6,7 +6,6 @@ import sys
import tomllib import tomllib
from pathlib import Path from pathlib import Path
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
REQUIRED = ( REQUIRED = (
"README.md", "README.md",
@@ -46,8 +45,7 @@ def main() -> int:
forbidden = [ forbidden = [
path path
for path in ROOT.rglob("*") for path in ROOT.rglob("*")
if path.is_file() if path.is_file() and path.name.lower() in {"key-pw.xml", "secrets.local.xml", ".env"}
and path.name.lower() in {"key-pw.xml", "secrets.local.xml", ".env"}
] ]
if forbidden: if forbidden:
print("Forbidden local secret files are present:") print("Forbidden local secret files are present:")
+5
View File
@@ -0,0 +1,5 @@
"""Allow ``python -m javis`` to invoke the CLI."""
from javis.interface.cli import main
raise SystemExit(main())
+96
View File
@@ -0,0 +1,96 @@
"""Central runtime configuration for the local Javis chat."""
from __future__ import annotations
import os
from collections.abc import Mapping
from dataclasses import dataclass
from pathlib import Path
from urllib.parse import urlparse
class ConfigurationError(ValueError):
"""Raised when runtime configuration is unsafe or invalid."""
def default_data_dir(
environ: Mapping[str, str] | None = None,
*,
platform_name: str | None = None,
home: Path | None = None,
) -> Path:
"""Return a platform-appropriate user data directory."""
values = os.environ if environ is None else environ
platform = os.name if platform_name is None else platform_name
user_home = Path.home() if home is None else home
if platform == "nt":
base = Path(values.get("LOCALAPPDATA", user_home / "AppData" / "Local"))
return base / "Javis"
xdg_data_home = values.get("XDG_DATA_HOME")
base = Path(xdg_data_home) if xdg_data_home else user_home / ".local" / "share"
return base / "javis"
@dataclass(frozen=True, slots=True)
class Settings:
"""Validated settings shared by CLI, provider, and storage."""
data_dir: Path
provider: str = "ollama"
model: str = "qwen3:8b"
ollama_base_url: str = "http://127.0.0.1:11434"
timeout_seconds: float = 180.0
@property
def database_path(self) -> Path:
return self.data_dir / "sessions.sqlite3"
@classmethod
def from_env(cls, environ: Mapping[str, str] | None = None) -> Settings:
values = os.environ if environ is None else environ
configured_dir = values.get("JAVIS_DATA_DIR")
data_dir = Path(configured_dir).expanduser() if configured_dir else default_data_dir(values)
if not data_dir.is_absolute():
raise ConfigurationError("JAVIS_DATA_DIR muss ein absoluter Pfad sein.")
provider = values.get("JAVIS_PROVIDER", "ollama").strip()
model = values.get("JAVIS_MODEL", "qwen3:8b").strip()
base_url = values.get("JAVIS_OLLAMA_URL", "http://127.0.0.1:11434").strip()
timeout_raw = values.get("JAVIS_MODEL_TIMEOUT", "180")
try:
timeout = float(timeout_raw)
except ValueError as exc:
raise ConfigurationError("JAVIS_MODEL_TIMEOUT muss eine Zahl sein.") from exc
settings = cls(
data_dir=data_dir.resolve(),
provider=provider,
model=model,
ollama_base_url=base_url,
timeout_seconds=timeout,
)
settings.validate()
return settings
def validate(self) -> None:
if self.provider != "ollama":
raise ConfigurationError(
"Unbekannter Provider. In dieser Phase wird nur 'ollama' unterstützt."
)
if not self.model:
raise ConfigurationError("JAVIS_MODEL darf nicht leer sein.")
if self.timeout_seconds <= 0:
raise ConfigurationError("JAVIS_MODEL_TIMEOUT muss größer als 0 sein.")
parsed = urlparse(self.ollama_base_url)
if parsed.scheme != "http" or parsed.hostname not in {
"127.0.0.1",
"::1",
"localhost",
}:
raise ConfigurationError("JAVIS_OLLAMA_URL muss eine lokale HTTP-Adresse sein.")
if parsed.username or parsed.password or parsed.query or parsed.fragment:
raise ConfigurationError("JAVIS_OLLAMA_URL enthält unzulässige Bestandteile.")
+57
View File
@@ -0,0 +1,57 @@
"""Chat orchestration without interface or provider-specific details."""
from __future__ import annotations
from dataclasses import dataclass
from javis.memory.sqlite_store import ChatSession, SQLiteSessionStore
from javis.providers.base import ChatMessage, LocalModelProvider
class SessionProviderMismatchError(RuntimeError):
"""The active provider cannot safely continue the stored session."""
@dataclass(frozen=True, slots=True)
class LoadedSession:
session: ChatSession
messages: list[ChatMessage]
class ChatService:
def __init__(
self,
store: SQLiteSessionStore,
provider: LocalModelProvider,
) -> None:
self.store = store
self.provider = provider
def new_session(self) -> ChatSession:
return self.store.create_session(self.provider.name, self.provider.model)
def list_sessions(self) -> list[ChatSession]:
return self.store.list_sessions()
def load_session(self, session_id: str) -> LoadedSession:
session = self.store.get_session(session_id)
if session.provider != self.provider.name or session.model != self.provider.model:
raise SessionProviderMismatchError(
"Die Sitzung verwendet "
f"{session.provider}/{session.model}, aktiv ist "
f"{self.provider.name}/{self.provider.model}."
)
return LoadedSession(session, self.store.get_messages(session_id))
def send(self, session_id: str, text: str) -> str:
normalized = text.strip()
if not normalized:
raise ValueError("Eine leere Nachricht wird nicht gesendet.")
loaded = self.load_session(session_id)
messages = [*loaded.messages, ChatMessage("user", normalized)]
response = self.provider.chat(messages)
self.store.append_exchange(session_id, normalized, response)
return response
def clear_session(self, session_id: str) -> None:
self.store.clear_messages(session_id)
+182
View File
@@ -0,0 +1,182 @@
"""Command-line interface for the first local Javis text chat."""
from __future__ import annotations
import argparse
import sys
from collections.abc import Callable, Sequence
from javis.config.settings import ConfigurationError, Settings
from javis.core.chat_service import ChatService, SessionProviderMismatchError
from javis.memory.sqlite_store import (
ChatSession,
SessionNotFoundError,
SessionStoreError,
SQLiteSessionStore,
)
from javis.providers.base import ProviderError
from javis.providers.ollama import OllamaProvider
InputFunction = Callable[[str], str]
OutputFunction = Callable[[str], None]
HELP_TEXT = """Befehle:
/new neue Sitzung beginnen
/sessions gespeicherte Sitzungen auflisten
/load <ID> frühere Sitzung laden und fortsetzen
/clear Nachrichten der aktiven Sitzung leeren
/help diese Hilfe anzeigen
/exit Chat ordentlich beenden"""
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="javis", description="Lokaler Javis-Textchat")
subparsers = parser.add_subparsers(dest="command", required=True)
chat_parser = subparsers.add_parser("chat", help="lokalen Textchat starten")
chat_parser.add_argument("--session", help="vorhandene Sitzungs-ID laden")
return parser
def _show_history(service: ChatService, session_id: str, output: OutputFunction) -> None:
loaded = service.load_session(session_id)
if not loaded.messages:
output("(Sitzung enthält noch keine Nachrichten.)")
return
for message in loaded.messages:
label = "Du" if message.role == "user" else "Javis"
output(f"{label}: {message.content}")
def _show_sessions(sessions: list[ChatSession], output: OutputFunction) -> None:
if not sessions:
output("Keine gespeicherten Sitzungen vorhanden.")
return
for session in sessions:
output(
f"{session.id} | {session.updated_at} | "
f"{session.provider}/{session.model} | {session.message_count} Nachrichten"
)
def run_chat(
service: ChatService,
*,
session_id: str | None = None,
input_fn: InputFunction = input,
output: OutputFunction = print,
) -> int:
try:
if session_id:
active_id = service.load_session(session_id).session.id
output(f"Sitzung geladen: {active_id}")
_show_history(service, active_id, output)
else:
active_id = service.new_session().id
output(f"Neue Sitzung: {active_id}")
except (
SessionNotFoundError,
SessionProviderMismatchError,
SessionStoreError,
) as exc:
output(f"Fehler: {exc}")
return 2
output("Lokaler Javis-Chat. /help zeigt die Befehle.")
while True:
try:
entered = input_fn("Du> ").strip()
except (EOFError, KeyboardInterrupt):
output("\nChat beendet.")
return 0
if not entered:
continue
if entered == "/exit":
output("Chat beendet.")
return 0
if entered == "/help":
output(HELP_TEXT)
continue
if entered == "/new":
try:
active_id = service.new_session().id
output(f"Neue Sitzung: {active_id}")
except SessionStoreError as exc:
output(f"Fehler: {exc}")
continue
if entered == "/sessions":
try:
_show_sessions(service.list_sessions(), output)
except SessionStoreError as exc:
output(f"Fehler: {exc}")
continue
if entered == "/clear":
try:
service.clear_session(active_id)
output("Aktive Sitzung wurde geleert.")
except (SessionNotFoundError, SessionStoreError) as exc:
output(f"Fehler: {exc}")
continue
if entered.startswith("/load"):
parts = entered.split(maxsplit=1)
if len(parts) != 2 or not parts[1].strip():
output("Verwendung: /load <Sitzungs-ID>")
continue
try:
loaded = service.load_session(parts[1].strip())
active_id = loaded.session.id
output(f"Sitzung geladen: {active_id}")
_show_history(service, active_id, output)
except (
SessionNotFoundError,
SessionProviderMismatchError,
SessionStoreError,
) as exc:
output(f"Fehler: {exc}")
continue
if entered.startswith("/"):
output("Unbekannter Befehl. /help zeigt die verfügbaren Befehle.")
continue
try:
response = service.send(active_id, entered)
output(f"Javis: {response}")
except (ProviderError, SessionStoreError, SessionProviderMismatchError) as exc:
output(f"Fehler: {exc}")
except ValueError as exc:
output(f"Fehler: {exc}")
def main(
argv: Sequence[str] | None = None,
*,
input_fn: InputFunction = input,
output: OutputFunction = print,
) -> int:
args = build_parser().parse_args(argv)
if args.command != "chat":
return 2
try:
settings = Settings.from_env()
store = SQLiteSessionStore(settings.database_path)
provider = OllamaProvider(
settings.model,
settings.ollama_base_url,
settings.timeout_seconds,
)
except (ConfigurationError, SessionStoreError) as exc:
output(f"Konfigurationsfehler: {exc}")
return 2
return run_chat(
ChatService(store, provider),
session_id=args.session,
input_fn=input_fn,
output=output,
)
if __name__ == "__main__":
sys.exit(main())
+236
View File
@@ -0,0 +1,236 @@
"""SQLite persistence for local chat sessions."""
from __future__ import annotations
import sqlite3
import uuid
from collections.abc import Iterator
from contextlib import contextmanager
from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path
from javis.providers.base import ChatMessage
class SessionStoreError(RuntimeError):
"""Base class for expected persistence failures."""
class SessionNotFoundError(SessionStoreError):
"""A requested session ID does not exist."""
@dataclass(frozen=True, slots=True)
class ChatSession:
id: str
created_at: str
updated_at: str
provider: str
model: str
message_count: int = 0
def _utc_now() -> str:
return datetime.now(UTC).isoformat(timespec="seconds")
class SQLiteSessionStore:
def __init__(self, database_path: Path) -> None:
self.database_path = database_path
try:
self.database_path.parent.mkdir(parents=True, exist_ok=True)
self._initialize()
except (OSError, sqlite3.Error) as exc:
raise SessionStoreError(
f"Die Sitzungsdatenbank kann nicht geöffnet werden: {database_path}"
) from exc
def _connect(self) -> sqlite3.Connection:
connection = sqlite3.connect(self.database_path, timeout=10)
connection.row_factory = sqlite3.Row
connection.execute("PRAGMA foreign_keys = ON")
return connection
@contextmanager
def _connection(self) -> Iterator[sqlite3.Connection]:
connection = self._connect()
try:
with connection:
yield connection
finally:
connection.close()
def _initialize(self) -> None:
with self._connection() as connection:
connection.executescript(
"""
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
provider TEXT NOT NULL,
model TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
position INTEGER NOT NULL,
role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
content TEXT NOT NULL,
created_at TEXT NOT NULL,
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE,
UNIQUE (session_id, position)
);
CREATE INDEX IF NOT EXISTS idx_messages_session_position
ON messages(session_id, position);
"""
)
def create_session(self, provider: str, model: str) -> ChatSession:
session_id = str(uuid.uuid4())
now = _utc_now()
try:
with self._connection() as connection:
connection.execute(
"""
INSERT INTO sessions (id, created_at, updated_at, provider, model)
VALUES (?, ?, ?, ?, ?)
""",
(session_id, now, now, provider, model),
)
except sqlite3.Error as exc:
raise SessionStoreError("Die Sitzung konnte nicht gespeichert werden.") from exc
return ChatSession(session_id, now, now, provider, model)
def get_session(self, session_id: str) -> ChatSession:
try:
with self._connection() as connection:
row = connection.execute(
"""
SELECT s.*, COUNT(m.id) AS message_count
FROM sessions AS s
LEFT JOIN messages AS m ON m.session_id = s.id
WHERE s.id = ?
GROUP BY s.id
""",
(session_id,),
).fetchone()
except sqlite3.Error as exc:
raise SessionStoreError("Die Sitzung konnte nicht geladen werden.") from exc
if row is None:
raise SessionNotFoundError(f"Sitzung '{session_id}' wurde nicht gefunden.")
return self._session_from_row(row)
def list_sessions(self) -> list[ChatSession]:
try:
with self._connection() as connection:
rows = connection.execute(
"""
SELECT s.*, COUNT(m.id) AS message_count
FROM sessions AS s
LEFT JOIN messages AS m ON m.session_id = s.id
GROUP BY s.id
ORDER BY s.updated_at DESC, s.created_at DESC
"""
).fetchall()
except sqlite3.Error as exc:
raise SessionStoreError("Die Sitzungen konnten nicht aufgelistet werden.") from exc
return [self._session_from_row(row) for row in rows]
def get_messages(self, session_id: str) -> list[ChatMessage]:
self.get_session(session_id)
try:
with self._connection() as connection:
rows = connection.execute(
"""
SELECT role, content
FROM messages
WHERE session_id = ?
ORDER BY position ASC
""",
(session_id,),
).fetchall()
except sqlite3.Error as exc:
raise SessionStoreError("Die Nachrichten konnten nicht geladen werden.") from exc
return [ChatMessage(row["role"], row["content"]) for row in rows]
def append_exchange(
self,
session_id: str,
user_content: str,
assistant_content: str,
) -> None:
now = _utc_now()
try:
with self._connection() as connection:
exists = connection.execute(
"SELECT 1 FROM sessions WHERE id = ?", (session_id,)
).fetchone()
if exists is None:
raise SessionNotFoundError(f"Sitzung '{session_id}' wurde nicht gefunden.")
row = connection.execute(
"SELECT COALESCE(MAX(position), -1) + 1 FROM messages WHERE session_id = ?",
(session_id,),
).fetchone()
next_position = int(row[0])
connection.executemany(
"""
INSERT INTO messages
(session_id, position, role, content, created_at)
VALUES (?, ?, ?, ?, ?)
""",
(
(session_id, next_position, "user", user_content, now),
(
session_id,
next_position + 1,
"assistant",
assistant_content,
now,
),
),
)
connection.execute(
"UPDATE sessions SET updated_at = ? WHERE id = ?",
(now, session_id),
)
except SessionNotFoundError:
raise
except sqlite3.Error as exc:
raise SessionStoreError("Die Nachrichten konnten nicht gespeichert werden.") from exc
def clear_messages(self, session_id: str) -> None:
now = _utc_now()
try:
with self._connection() as connection:
cursor = connection.execute(
"DELETE FROM messages WHERE session_id = ?", (session_id,)
)
exists = connection.execute(
"SELECT 1 FROM sessions WHERE id = ?", (session_id,)
).fetchone()
if exists is None:
raise SessionNotFoundError(f"Sitzung '{session_id}' wurde nicht gefunden.")
if cursor.rowcount:
connection.execute(
"UPDATE sessions SET updated_at = ? WHERE id = ?",
(now, session_id),
)
except SessionNotFoundError:
raise
except sqlite3.Error as exc:
raise SessionStoreError("Die Sitzung konnte nicht geleert werden.") from exc
@staticmethod
def _session_from_row(row: sqlite3.Row) -> ChatSession:
return ChatSession(
id=row["id"],
created_at=row["created_at"],
updated_at=row["updated_at"],
provider=row["provider"],
model=row["model"],
message_count=row["message_count"],
)
+46
View File
@@ -0,0 +1,46 @@
"""Small provider contract used by the chat core."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Protocol, runtime_checkable
@dataclass(frozen=True, slots=True)
class ChatMessage:
role: str
content: str
class ProviderError(RuntimeError):
"""Base class for expected local provider failures."""
class ProviderUnavailableError(ProviderError):
"""The local model runtime cannot be reached."""
class ModelNotInstalledError(ProviderError):
"""The configured local model is not available."""
class ProviderTimeoutError(ProviderError):
"""The local model did not respond in time."""
class ResponseAbortedError(ProviderError):
"""The model response was interrupted before completion."""
class InvalidProviderResponseError(ProviderError):
"""The local runtime returned an unusable response."""
@runtime_checkable
class LocalModelProvider(Protocol):
name: str
model: str
def chat(self, messages: list[ChatMessage]) -> str:
"""Return one complete assistant response."""
...
+82
View File
@@ -0,0 +1,82 @@
"""Ollama implementation of the local model provider contract."""
from __future__ import annotations
import json
import socket
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
from javis.providers.base import (
ChatMessage,
InvalidProviderResponseError,
ModelNotInstalledError,
ProviderTimeoutError,
ProviderUnavailableError,
ResponseAbortedError,
)
class OllamaProvider:
name = "ollama"
def __init__(self, model: str, base_url: str, timeout_seconds: float) -> None:
self.model = model
self._endpoint = f"{base_url.rstrip('/')}/api/chat"
self._timeout_seconds = timeout_seconds
def chat(self, messages: list[ChatMessage]) -> str:
payload = {
"model": self.model,
"messages": [
{"role": message.role, "content": message.content} for message in messages
],
"stream": False,
"think": False,
}
request = Request(
self._endpoint,
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
try:
with urlopen(request, timeout=self._timeout_seconds) as response:
raw_response = response.read()
except HTTPError as exc:
details = exc.read().decode("utf-8", errors="replace")
if exc.code == 404 or "not found" in details.lower():
raise ModelNotInstalledError(
f"Das lokale Modell '{self.model}' ist nicht installiert."
) from exc
raise ProviderUnavailableError(f"Ollama meldet HTTP-Fehler {exc.code}.") from exc
except TimeoutError as exc:
raise ProviderTimeoutError(
"Die Modellantwort hat das Zeitlimit überschritten."
) from exc
except URLError as exc:
if isinstance(exc.reason, (TimeoutError, socket.timeout)):
raise ProviderTimeoutError(
"Die Modellantwort hat das Zeitlimit überschritten."
) from exc
raise ProviderUnavailableError(
"Ollama ist unter der konfigurierten lokalen Adresse nicht erreichbar."
) from exc
try:
result = json.loads(raw_response)
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
raise InvalidProviderResponseError(
"Ollama hat keine gültige JSON-Antwort geliefert."
) from exc
if result.get("done") is False:
raise ResponseAbortedError("Die Modellantwort wurde vorzeitig abgebrochen.")
content = result.get("message", {}).get("content")
if not isinstance(content, str) or not content.strip():
raise InvalidProviderResponseError(
"Ollama hat keine verwendbare Textantwort geliefert."
)
return content.strip()
+57
View File
@@ -0,0 +1,57 @@
import tempfile
import unittest
from pathlib import Path
from javis.core.chat_service import ChatService
from javis.memory.sqlite_store import SQLiteSessionStore
from javis.providers.base import ChatMessage, LocalModelProvider
class RecordingProvider:
name = "ollama"
model = "test-model"
def __init__(self) -> None:
self.calls: list[list[ChatMessage]] = []
def chat(self, messages: list[ChatMessage]) -> str:
self.calls.append(messages)
return f"Antwort {len(self.calls)}"
class ChatServiceTests(unittest.TestCase):
def setUp(self) -> None:
self.temporary_directory = tempfile.TemporaryDirectory()
database = Path(self.temporary_directory.name) / "sessions.sqlite3"
self.provider = RecordingProvider()
self.service = ChatService(SQLiteSessionStore(database), self.provider)
def tearDown(self) -> None:
self.temporary_directory.cleanup()
def test_test_double_implements_provider_contract(self) -> None:
self.assertIsInstance(self.provider, LocalModelProvider)
def test_send_passes_and_persists_conversation_history(self) -> None:
session = self.service.new_session()
first = self.service.send(session.id, "Hallo")
second = self.service.send(session.id, "Und jetzt?")
self.assertEqual(first, "Antwort 1")
self.assertEqual(second, "Antwort 2")
self.assertEqual(len(self.provider.calls[1]), 3)
loaded = self.service.load_session(session.id)
self.assertEqual(
[message.content for message in loaded.messages],
[
"Hallo",
"Antwort 1",
"Und jetzt?",
"Antwort 2",
],
)
if __name__ == "__main__":
unittest.main()
+40
View File
@@ -0,0 +1,40 @@
import tempfile
import unittest
from pathlib import Path
from javis.core.chat_service import ChatService
from javis.interface.cli import run_chat
from javis.memory.sqlite_store import SQLiteSessionStore
from javis.providers.base import ChatMessage
class _FakeProvider:
name = "ollama"
model = "test-model"
def chat(self, messages: list[ChatMessage]) -> str:
return f"Echo: {messages[-1].content}"
class CliTests(unittest.TestCase):
def test_basic_chat_commands(self) -> None:
with tempfile.TemporaryDirectory() as directory:
store = SQLiteSessionStore(Path(directory) / "sessions.sqlite3")
service = ChatService(store, _FakeProvider())
inputs = iter(["Hallo", "/sessions", "/new", "/clear", "/exit"])
output: list[str] = []
result = run_chat(
service,
input_fn=lambda _prompt: next(inputs),
output=output.append,
)
self.assertEqual(result, 0)
self.assertTrue(any(line == "Javis: Echo: Hallo" for line in output))
self.assertTrue(any("Nachrichten" in line for line in output))
self.assertEqual(output[-1], "Chat beendet.")
if __name__ == "__main__":
unittest.main()
+56
View File
@@ -0,0 +1,56 @@
import json
import threading
import unittest
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import ClassVar
from javis.providers.base import ChatMessage
from javis.providers.ollama import OllamaProvider
class _OllamaHandler(BaseHTTPRequestHandler):
request_payload: ClassVar[dict[str, object]] = {}
def do_POST(self) -> None:
length = int(self.headers["Content-Length"])
type(self).request_payload = json.loads(self.rfile.read(length))
body = json.dumps(
{
"message": {"role": "assistant", "content": "Lokale Antwort"},
"done": True,
}
).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, format: str, *args: object) -> None:
return
class OllamaProviderTests(unittest.TestCase):
def test_provider_uses_local_chat_endpoint(self) -> None:
server = ThreadingHTTPServer(("127.0.0.1", 0), _OllamaHandler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
provider = OllamaProvider(
"test-model",
f"http://127.0.0.1:{server.server_port}",
2,
)
response = provider.chat([ChatMessage("user", "Hallo")])
finally:
server.shutdown()
server.server_close()
thread.join()
self.assertEqual(response, "Lokale Antwort")
self.assertEqual(_OllamaHandler.request_payload["model"], "test-model")
self.assertFalse(_OllamaHandler.request_payload["stream"])
if __name__ == "__main__":
unittest.main()
+39
View File
@@ -0,0 +1,39 @@
import unittest
from pathlib import Path
from javis.config.settings import ConfigurationError, Settings, default_data_dir
class SettingsTests(unittest.TestCase):
def test_windows_default_uses_local_app_data(self) -> None:
result = default_data_dir(
{"LOCALAPPDATA": "C:\\Local"},
platform_name="nt",
home=Path("C:\\Users\\test"),
)
self.assertEqual(result, Path("C:\\Local") / "Javis")
def test_posix_default_uses_xdg_data_home(self) -> None:
result = default_data_dir(
{"XDG_DATA_HOME": "/tmp/data"},
platform_name="posix",
home=Path("/home/test"),
)
self.assertEqual(result, Path("/tmp/data/javis"))
def test_explicit_data_dir_must_be_absolute(self) -> None:
with self.assertRaises(ConfigurationError):
Settings.from_env({"JAVIS_DATA_DIR": "relative"})
def test_provider_url_must_remain_local(self) -> None:
with self.assertRaises(ConfigurationError):
Settings.from_env(
{
"JAVIS_DATA_DIR": str(Path.cwd()),
"JAVIS_OLLAMA_URL": "https://example.com",
}
)
if __name__ == "__main__":
unittest.main()
+58
View File
@@ -0,0 +1,58 @@
import tempfile
import unittest
from pathlib import Path
from javis.memory.sqlite_store import SessionNotFoundError, SQLiteSessionStore
class SQLiteSessionStoreTests(unittest.TestCase):
def setUp(self) -> None:
self.temporary_directory = tempfile.TemporaryDirectory()
database = Path(self.temporary_directory.name) / "sessions.sqlite3"
self.store = SQLiteSessionStore(database)
def tearDown(self) -> None:
self.temporary_directory.cleanup()
def test_create_list_and_load_session(self) -> None:
created = self.store.create_session("ollama", "test-model")
loaded = self.store.get_session(created.id)
self.assertEqual(loaded.id, created.id)
self.assertEqual(loaded.provider, "ollama")
self.assertEqual(loaded.model, "test-model")
self.assertEqual(self.store.list_sessions()[0].id, created.id)
def test_messages_keep_exchange_order(self) -> None:
session = self.store.create_session("ollama", "test-model")
self.store.append_exchange(session.id, "eins", "zwei")
self.store.append_exchange(session.id, "drei", "vier")
messages = self.store.get_messages(session.id)
self.assertEqual(
[(message.role, message.content) for message in messages],
[
("user", "eins"),
("assistant", "zwei"),
("user", "drei"),
("assistant", "vier"),
],
)
def test_unknown_session_raises_clear_error(self) -> None:
with self.assertRaises(SessionNotFoundError):
self.store.get_session("missing")
def test_clear_keeps_session_and_removes_messages(self) -> None:
session = self.store.create_session("ollama", "test-model")
self.store.append_exchange(session.id, "eins", "zwei")
self.store.clear_messages(session.id)
self.assertEqual(self.store.get_messages(session.id), [])
self.assertEqual(self.store.get_session(session.id).message_count, 0)
if __name__ == "__main__":
unittest.main()
Generated
+43
View File
@@ -0,0 +1,43 @@
version = 1
revision = 3
requires-python = ">=3.11"
[[package]]
name = "javis-ai"
version = "0.0.0"
source = { editable = "." }
[package.dev-dependencies]
dev = [
{ name = "ruff" },
]
[package.metadata]
[package.metadata.requires-dev]
dev = [{ name = "ruff", specifier = ">=0.12,<1" }]
[[package]]
name = "ruff"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4d/94/1e5e4967626faf12fa56999cd6222dff6992ceb086ad7945756baf70c7a7/ruff-0.16.0.tar.gz", hash = "sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982", size = 4790557, upload-time = "2026-07-23T19:11:30.981Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4b/81/1c8818fee7ce1a04cd7d1b3172e0a8f8e4f1dc4feb7fc390e16daa8af323/ruff-0.16.0-py3-none-linux_armv6l.whl", hash = "sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e", size = 10754633, upload-time = "2026-07-23T19:10:46.415Z" },
{ url = "https://files.pythonhosted.org/packages/23/df/beaf59c09d68db84304d555f188b276a77132a5d5b0b67a5c762aa143628/ruff-0.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522", size = 10969164, upload-time = "2026-07-23T19:10:50.271Z" },
{ url = "https://files.pythonhosted.org/packages/42/ce/741cd197496a1abbf51352710fd15ed995d2a2be87189c1da26a450d6e83/ruff-0.16.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81", size = 10488846, upload-time = "2026-07-23T19:10:52.639Z" },
{ url = "https://files.pythonhosted.org/packages/52/2a/a2db8e88cade358f5cdcb05674a917751074109315d014eb6352d9a893f7/ruff-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9", size = 10889729, upload-time = "2026-07-23T19:10:54.89Z" },
{ url = "https://files.pythonhosted.org/packages/42/65/62a771694ebd63029dc953e27dbad40e1588bd4860ff9fe881018fddaa49/ruff-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a", size = 10568275, upload-time = "2026-07-23T19:10:56.993Z" },
{ url = "https://files.pythonhosted.org/packages/3f/e2/ced249fe8af5f086c5c58cc21cc3356d50f32f7401c5df87050c999620a7/ruff-0.16.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717", size = 11385112, upload-time = "2026-07-23T19:10:59.615Z" },
{ url = "https://files.pythonhosted.org/packages/87/0b/05154977a8fd69eeb6c103271f55403bfd8711f5c0f8ed07489d95a504e7/ruff-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17", size = 12207008, upload-time = "2026-07-23T19:11:02.154Z" },
{ url = "https://files.pythonhosted.org/packages/fb/29/98225831a3a1eab0e02f4acc6ca6559a98611dcc68b6965ff4b7234627c1/ruff-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4", size = 11650842, upload-time = "2026-07-23T19:11:04.557Z" },
{ url = "https://files.pythonhosted.org/packages/91/66/6bd3cf90500653d55dc0ffc8507aa8300bd49d0214b2e8cb4d3fef2943ba/ruff-0.16.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b", size = 11400718, upload-time = "2026-07-23T19:11:09.233Z" },
{ url = "https://files.pythonhosted.org/packages/8e/a2/a54eb4eae05d66364050a5d3b8a9c5ef88196531b3cbe7109d873f87f819/ruff-0.16.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0", size = 11426177, upload-time = "2026-07-23T19:11:11.994Z" },
{ url = "https://files.pythonhosted.org/packages/1a/be/16e3eea4b2a478a496919f5e36f17c4559e54620bd3bbac5d6affa068006/ruff-0.16.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213", size = 10856126, upload-time = "2026-07-23T19:11:14.221Z" },
{ url = "https://files.pythonhosted.org/packages/a2/84/252eb8b868a16eec7257c14f504f77537e734b2d69c762e639e588e304a3/ruff-0.16.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af", size = 10571208, upload-time = "2026-07-23T19:11:16.378Z" },
{ url = "https://files.pythonhosted.org/packages/21/09/817a482f542f7570cbb4554b26e896610c7114f539b1d9e2d2145bf6bef6/ruff-0.16.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09", size = 11063329, upload-time = "2026-07-23T19:11:19.173Z" },
{ url = "https://files.pythonhosted.org/packages/2e/23/9403c180ca1cb9b1f7335f5c3e5305c09d49ea5b345196682a36028bde4a/ruff-0.16.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed", size = 11489751, upload-time = "2026-07-23T19:11:21.74Z" },
{ url = "https://files.pythonhosted.org/packages/b2/1d/1b2ef7bcde851c78d7f17f1cca13fd6dc695fc4b3d6197941e72cae5b132/ruff-0.16.0-py3-none-win32.whl", hash = "sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb", size = 10785885, upload-time = "2026-07-23T19:11:23.947Z" },
{ url = "https://files.pythonhosted.org/packages/b2/a3/d5e4ef7a56be3f928ffb90b94c25ba7d3cb9c7fe0736aeaaedf361770712/ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472", size = 11923141, upload-time = "2026-07-23T19:11:26.409Z" },
{ url = "https://files.pythonhosted.org/packages/cb/9a/8415f2657cbe200f41a4531ccededf135505a92d4a012229121f885b26f9/ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d", size = 11273407, upload-time = "2026-07-23T19:11:28.705Z" },
]