From fa90a5c5de598e9db5e2b51fa51aab06a062c514 Mon Sep 17 00:00:00 2001 From: Dystroyer8 Date: Thu, 30 Jul 2026 16:45:36 +0200 Subject: [PATCH 1/5] build: configure Python development environment --- .python-version | 1 + CHANGELOG.md | 2 ++ docs/CHATGPT_HANDOFF.md | 31 ++++++++++++++++---------- docs/DEVELOPMENT_SETUP.md | 38 ++++++++++++++++++++++++++++++++ docs/NEXT_SESSION.md | 4 +--- docs/PROJECT_STATUS.md | 12 +++++++---- pyproject.toml | 15 +++++++++++++ scripts/verify_structure.py | 4 +--- uv.lock | 43 +++++++++++++++++++++++++++++++++++++ 9 files changed, 129 insertions(+), 21 deletions(-) create mode 100644 .python-version create mode 100644 docs/DEVELOPMENT_SETUP.md create mode 100644 uv.lock diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/CHANGELOG.md b/CHANGELOG.md index 31d839d..04ec557 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ Alle wesentlichen Projektänderungen werden hier in verständlicher Form dokumen - Tokenbewusste Arbeitsweise als dauerhafte Regel ergänzt; Qualität und Sicherheit bleiben vorrangig. - 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. +- Isolierte Python-3.12-Entwicklungsumgebung mit uv-Lockfile und Ruff vorbereitet. +- Offizielle VS-Code-Erweiterungen für Python, Pylance und Python Environments eingerichtet. ### Security diff --git a/docs/CHATGPT_HANDOFF.md b/docs/CHATGPT_HANDOFF.md index 54e70a3..a91652e 100644 --- a/docs/CHATGPT_HANDOFF.md +++ b/docs/CHATGPT_HANDOFF.md @@ -50,7 +50,10 @@ Rootserver: - Konfigurationsbeispiel, Sicherheitsdokumente, ADRs und Strukturtest sind vorhanden. - 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. -- 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 oder llama.cpp sind noch nicht installiert. - Kein Obsidian-, Laptop- oder Rootserverzugriff wurde implementiert. - Keine KI-, Sprach-, Mobil- oder Statusfunktion ist implementiert. @@ -77,6 +80,9 @@ Rootserver: - Tokenbewusster Arbeitsablauf festgelegt: gezielte Reads, kurze Ergebnisse, keine unnötigen Agenten oder Plugins. - Werkzeugempfehlung auf `uv`, Microsoft Python/Pylance/Python Environments und Ruff begrenzt. - 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. - Obsidian, Ubuntu-Laptop und Rootserver blieben unverändert. ## Aktuelle Tests @@ -89,15 +95,20 @@ Letzter bestätigter Grundgerüststand: - UTF-8- und interne Markdown-Linkprüfung: bestanden - Secret-Musterprüfung: keine Treffer - 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 Für den Textchat existieren noch keine Funktions- oder Modell-Smoke-Tests. ## Git-Stand +- aktueller Branch: `feat/core-chat` - stabiler Branch: `main` - Grundgerüstcommit: `6d04171` - 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` +- aktueller Feature-Commit ist über `git log -1 --oneline` zu bestimmen - `origin` verwendet HTTPS - kein Force-Push und keine umgeschriebene Historie @@ -106,12 +117,12 @@ Für den Textchat existieren noch keine Funktions- oder Modell-Smoke-Tests. - `49a1dd3`: erster Gitea-Verbindungstest - `6d04171`: sicheres Javis-Grundgerüst - `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 ## Offene Entscheidungen und Fehler - endgültiger Produkt-/Repositoryname bleibt offen -- konkrete Python-Version und Installation sind noch festzulegen - lokale Modelllaufzeit und erstes Modell sind noch nicht installiert oder benchmarked - 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 @@ -132,12 +143,10 @@ Für den Textchat existieren noch keine Funktions- oder Modell-Smoke-Tests. Der genehmigte nächste Meilenstein ist `feat/core-chat`: -1. von sauberem `main` einen Feature-Branch erstellen -2. minimale Python-Toolchain mit `uv`, lokaler `.venv`, Ruff und offiziellen VS-Code-Erweiterungen einrichten -3. Ollama und llama.cpp anhand aktueller offizieller Quellen vergleichen und nur eine Laufzeit installieren -4. genau ein frei lokal nutzbares Modell unter ungefähr 10 GB herunterladen und auf der RTX 3060 messen -5. echten lokalen CLI-Chat mit Provider-Schnittstelle und SQLite-Sitzungen implementieren -6. Unit-, Ruff-, Persistenz-, Secret- und echten Modell-Smoke-Test ausführen -7. nur bei vollständig erfüllter Abnahme nach `main` mergen +1. Ollama als einfachere Windows-Laufzeit installieren; llama.cpp bleibt dokumentierte Alternative +2. genau `qwen3:8b` (`Q4_K_M`, 5,2 GB, Apache-2.0) herunterladen und auf der RTX 3060 messen +3. echten lokalen CLI-Chat mit Provider-Schnittstelle und SQLite-Sitzungen implementieren +4. Unit-, Ruff-, Persistenz-, Secret- und echten Modell-Smoke-Test ausführen +5. nur bei vollständig erfüllter Abnahme nach `main` mergen Nicht Teil dieses Meilensteins: Obsidian-Integration, Serverzugriff, Sprache, Mobile Client oder Living-Mind-Code. diff --git a/docs/DEVELOPMENT_SETUP.md b/docs/DEVELOPMENT_SETUP.md new file mode 100644 index 0000000..c1daf2e --- /dev/null +++ b/docs/DEVELOPMENT_SETUP.md @@ -0,0 +1,38 @@ +# 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 javis chat +& 'D:\Javis-Tools\uv\uv.exe' run ruff check . +& 'D:\Javis-Tools\uv\uv.exe' run ruff format --check . +``` + +Die absoluten Pfade sind ausschließlich lokale Entwicklungsdokumentation. Der Anwendungscode darf keine festen Windows-Pfade enthalten. diff --git a/docs/NEXT_SESSION.md b/docs/NEXT_SESSION.md index 04cb964..6a538a6 100644 --- a/docs/NEXT_SESSION.md +++ b/docs/NEXT_SESSION.md @@ -2,7 +2,7 @@ ## 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 Arbeitspaket auf `feat/core-chat`: genau eine lokale Modelllaufzeit und ein echter lokaler CLI-Textchat mit SQLite-Sitzungen. ## Vor Änderungen erneut prüfen @@ -13,8 +13,6 @@ Phase 1 auf `feat/core-chat`: minimale Python-Toolchain, genau eine lokale Model ## Vorgeschlagener Umfang -- `uv`, unterstützte stabile Python-Version und offizielle VS-Code-Erweiterungen einrichten -- Microsoft Python/Pylance/Python Environments sowie Ruff als minimale Entwicklungswerkzeuge prüfen - Ollama und llama.cpp offiziell vergleichen; nur eine Laufzeit und ein Modell unter ungefähr 10 GB installieren - lokale Konfiguration, Provider-Schnittstelle und SQLite-Sitzungen implementieren - CLI mit neuen, gelisteten und fortsetzbaren Sitzungen diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index f21ddce..073b832 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -1,6 +1,6 @@ # Projektstatus -Stand: Dokumentationsverdichtung vor `feat/core-chat`, 30.07.2026. +Stand: Toolchain-Arbeitspaket auf `feat/core-chat`, 30.07.2026. ## Erreicht @@ -12,6 +12,9 @@ Stand: Dokumentationsverdichtung vor `feat/core-chat`, 30.07.2026. - projektinterne Codex-Agentensyntax anhand der aktuellen offiziellen Codex-Dokumentation geprüft - phasenbezogene Werkzeug- und Plugin-Empfehlungen sowie tokenbewusste Arbeitsregeln dokumentiert - 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 ## Nicht implementiert @@ -19,10 +22,11 @@ KI, Chat, Provider, Gedächtnis, Obsidian-Adapter, Tools, Serverzugriff, Sprache ## Git -- Branch: `main` +- Branch: `feat/core-chat` - Grundgerüstcommit: `6d04171` -- Der aktuelle Dokumentationscommit ist mit `git log -1` zu ermitteln. +- Basis des Feature-Branches: `e6eb995` +- Der aktuelle Feature-Commit ist mit `git log -1` zu ermitteln. ## 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. +Die Toolchain ist nutzbar, ohne den Windows-`PATH` zu verändern. Eine lokale Modelllaufzeit und der Textchat sind noch nicht implementiert. diff --git a/pyproject.toml b/pyproject.toml index 8a7ccd6..6ef1069 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,8 +10,23 @@ readme = "README.md" requires-python = ">=3.11" dependencies = [] +[project.scripts] +javis = "javis.interface.cli:main" + +[dependency-groups] +dev = [ + "ruff>=0.12,<1", +] + [tool.setuptools.packages.find] where = ["src"] [tool.pytest.ini_options] testpaths = ["tests"] + +[tool.ruff] +line-length = 100 +target-version = "py312" + +[tool.ruff.lint] +select = ["B", "E", "F", "I", "UP"] diff --git a/scripts/verify_structure.py b/scripts/verify_structure.py index 9565853..36e52ec 100644 --- a/scripts/verify_structure.py +++ b/scripts/verify_structure.py @@ -6,7 +6,6 @@ import sys import tomllib from pathlib import Path - ROOT = Path(__file__).resolve().parents[1] REQUIRED = ( "README.md", @@ -46,8 +45,7 @@ def main() -> int: forbidden = [ path for path in ROOT.rglob("*") - if path.is_file() - and path.name.lower() in {"key-pw.xml", "secrets.local.xml", ".env"} + if path.is_file() and path.name.lower() in {"key-pw.xml", "secrets.local.xml", ".env"} ] if forbidden: print("Forbidden local secret files are present:") diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..9935951 --- /dev/null +++ b/uv.lock @@ -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" }, +] From 10d7cd76621aead6561e104772a388673aa6cf11 Mon Sep 17 00:00:00 2001 From: Dystroyer8 Date: Thu, 30 Jul 2026 16:59:25 +0200 Subject: [PATCH 2/5] build: document local model runtime --- CHANGELOG.md | 3 ++ docs/CHATGPT_HANDOFF.md | 39 ++++++++++++++++++++------ docs/LOCAL_MODEL.md | 61 +++++++++++++++++++++++++++++++++++++++++ docs/NEXT_SESSION.md | 7 +++-- docs/PROJECT_STATUS.md | 13 +++++++-- 5 files changed, 108 insertions(+), 15 deletions(-) create mode 100644 docs/LOCAL_MODEL.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 04ec557..d449a76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ Alle wesentlichen Projektänderungen werden hier in verständlicher Form dokumen - Eng begrenzte projektinterne Codex-Agentenrollen. - Abhängigkeitsfreier Strukturprüfer. - 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. ### Changed @@ -21,6 +23,7 @@ Alle wesentlichen Projektänderungen werden hier in verständlicher Form dokumen - 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. ### Security diff --git a/docs/CHATGPT_HANDOFF.md b/docs/CHATGPT_HANDOFF.md index a91652e..06db9db 100644 --- a/docs/CHATGPT_HANDOFF.md +++ b/docs/CHATGPT_HANDOFF.md @@ -53,7 +53,11 @@ Rootserver: - 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 oder llama.cpp sind noch nicht installiert. +- 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. - Kein Obsidian-, Laptop- oder Rootserverzugriff wurde implementiert. - Keine KI-, Sprach-, Mobil- oder Statusfunktion ist implementiert. @@ -83,6 +87,11 @@ Rootserver: - 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. - Obsidian, Ubuntu-Laptop und Rootserver blieben unverändert. ## Aktuelle Tests @@ -99,7 +108,18 @@ Letzter bestätigter Grundgerüststand: - Python in `.venv`: 3.12.13 - Ruff in `.venv`: 0.16.0 -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 + +Zwölf vorbereitete Unit-Tests für Konfiguration, SQLite, Kern, Provider und CLI +laufen in der Arbeitskopie bereits erfolgreich; sie sind noch nicht in den +Feature-Branch übernommen. ## Git-Stand @@ -108,7 +128,7 @@ Für den Textchat existieren noch keine Funktions- oder Modell-Smoke-Tests. - Grundgerüstcommit: `6d04171` - letzter Commit vor der Handoff-Verdichtung: `0e82748` - Handoff-Verdichtung auf `main`: `e6eb995` -- aktueller Feature-Commit ist über `git log -1 --oneline` zu bestimmen +- Python-Entwicklungsumgebung auf dem Feature-Branch: `fa90a5c` - `origin` verwendet HTTPS - kein Force-Push und keine umgeschriebene Historie @@ -119,11 +139,12 @@ Für den Textchat existieren noch keine Funktions- oder Modell-Smoke-Tests. - `0e82748`: Werkzeug-, Token- und Handoff-Regeln - `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 ## Offene Entscheidungen und Fehler - endgültiger Produkt-/Repositoryname bleibt offen -- lokale Modelllaufzeit und erstes Modell sind noch nicht installiert oder benchmarked +- Startmechanismus für Ollama ist noch bewusst manuell und nur pro Prozess konfiguriert - 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 - Gaming-PC zeigt derzeit nur 8 statt 16 logische CPU-Prozessoren; Ursache ungeklärt @@ -143,10 +164,10 @@ Für den Textchat existieren noch keine Funktions- oder Modell-Smoke-Tests. Der genehmigte nächste Meilenstein ist `feat/core-chat`: -1. Ollama als einfachere Windows-Laufzeit installieren; llama.cpp bleibt dokumentierte Alternative -2. genau `qwen3:8b` (`Q4_K_M`, 5,2 GB, Apache-2.0) herunterladen und auf der RTX 3060 messen -3. echten lokalen CLI-Chat mit Provider-Schnittstelle und SQLite-Sitzungen implementieren -4. Unit-, Ruff-, Persistenz-, Secret- und echten Modell-Smoke-Test ausführen -5. nur bei vollständig erfüllter Abnahme nach `main` mergen +1. vorbereiteten lokalen CLI-Chat mit Provider-Schnittstelle und SQLite-Sitzungen + in den Feature-Branch übernehmen +2. Unit-, Ruff-, Persistenz-, Secret- und echten CLI-Modell-Smoke-Test ausführen +3. Dokumentation und Handoff auf den tatsächlichen Endstand bringen +4. Feature-Branch pushen und nur bei vollständig erfüllter Abnahme nach `main` mergen Nicht Teil dieses Meilensteins: Obsidian-Integration, Serverzugriff, Sprache, Mobile Client oder Living-Mind-Code. diff --git a/docs/LOCAL_MODEL.md b/docs/LOCAL_MODEL.md new file mode 100644 index 0000000..0dc563c --- /dev/null +++ b/docs/LOCAL_MODEL.md @@ -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. diff --git a/docs/NEXT_SESSION.md b/docs/NEXT_SESSION.md index 6a538a6..50e885c 100644 --- a/docs/NEXT_SESSION.md +++ b/docs/NEXT_SESSION.md @@ -2,7 +2,8 @@ ## Ziel -Nächstes Arbeitspaket auf `feat/core-chat`: genau eine lokale Modelllaufzeit und ein echter lokaler CLI-Textchat mit SQLite-Sitzungen. +Nächstes Arbeitspaket auf `feat/core-chat`: den vorbereiteten lokalen CLI-Textchat +mit SQLite-Sitzungen übernehmen, vollständig testen und dokumentieren. ## Vor Änderungen erneut prüfen @@ -13,10 +14,10 @@ Nächstes Arbeitspaket auf `feat/core-chat`: genau eine lokale Modelllaufzeit un ## Vorgeschlagener Umfang -- Ollama und llama.cpp offiziell vergleichen; nur eine Laufzeit und ein Modell unter ungefähr 10 GB installieren - lokale Konfiguration, Provider-Schnittstelle und SQLite-Sitzungen implementieren - CLI mit neuen, gelisteten und fortsetzbaren Sitzungen -- Unit-, Ruff-, Persistenz-, Secret- und echter Modell-Smoke-Test +- Unit-, Ruff-, Persistenz-, Secret- und echter CLI-Modell-Smoke-Test +- Feature-Branch pushen und nur bei erfüllter Abnahme nach `main` mergen ## Nicht beginnen diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index 073b832..c34b899 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -1,6 +1,6 @@ # Projektstatus -Stand: Toolchain-Arbeitspaket auf `feat/core-chat`, 30.07.2026. +Stand: lokale Modelllaufzeit auf `feat/core-chat`, 30.07.2026. ## Erreicht @@ -15,10 +15,15 @@ Stand: Toolchain-Arbeitspaket auf `feat/core-chat`, 30.07.2026. - 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 ## Nicht implementiert -KI, Chat, Provider, Gedächtnis, Obsidian-Adapter, Tools, Serverzugriff, Sprache, Mobile Client und Living-Mind-Frontend. +CLI-Chat, Provider im Repository, Sitzungsspeicher, Obsidian-Adapter, Tools, +Serverzugriff, Sprache, Mobile Client und Living-Mind-Frontend. ## Git @@ -29,4 +34,6 @@ KI, Chat, Provider, Gedächtnis, Obsidian-Adapter, Tools, Serverzugriff, Sprache ## Bekannte Einschränkung -Die Toolchain ist nutzbar, ohne den Windows-`PATH` zu verändern. Eine lokale Modelllaufzeit und der Textchat sind noch nicht implementiert. +Toolchain und lokale Modelllaufzeit sind nutzbar, ohne den Windows-`PATH`, Registry, +Autostart oder Windows-Dienste zu verändern. Der Textchat ist als Nächstes in den +Feature-Branch zu übernehmen und vollständig gegen das reale Modell zu prüfen. From ec9b2f94ad1ab246fd9e339db9ce293dccd61f96 Mon Sep 17 00:00:00 2001 From: Dystroyer8 Date: Thu, 30 Jul 2026 17:00:37 +0200 Subject: [PATCH 3/5] feat: add local chat provider and CLI --- src/javis/__main__.py | 5 + src/javis/config/settings.py | 96 +++++++++++++ src/javis/core/chat_service.py | 57 ++++++++ src/javis/interface/cli.py | 182 ++++++++++++++++++++++++ src/javis/memory/sqlite_store.py | 236 +++++++++++++++++++++++++++++++ src/javis/providers/base.py | 46 ++++++ src/javis/providers/ollama.py | 82 +++++++++++ 7 files changed, 704 insertions(+) create mode 100644 src/javis/__main__.py create mode 100644 src/javis/config/settings.py create mode 100644 src/javis/core/chat_service.py create mode 100644 src/javis/interface/cli.py create mode 100644 src/javis/memory/sqlite_store.py create mode 100644 src/javis/providers/base.py create mode 100644 src/javis/providers/ollama.py diff --git a/src/javis/__main__.py b/src/javis/__main__.py new file mode 100644 index 0000000..cd79e2b --- /dev/null +++ b/src/javis/__main__.py @@ -0,0 +1,5 @@ +"""Allow ``python -m javis`` to invoke the CLI.""" + +from javis.interface.cli import main + +raise SystemExit(main()) diff --git a/src/javis/config/settings.py b/src/javis/config/settings.py new file mode 100644 index 0000000..cf31186 --- /dev/null +++ b/src/javis/config/settings.py @@ -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.") diff --git a/src/javis/core/chat_service.py b/src/javis/core/chat_service.py new file mode 100644 index 0000000..094db1b --- /dev/null +++ b/src/javis/core/chat_service.py @@ -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) diff --git a/src/javis/interface/cli.py b/src/javis/interface/cli.py new file mode 100644 index 0000000..8debf4f --- /dev/null +++ b/src/javis/interface/cli.py @@ -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 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 ") + 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()) diff --git a/src/javis/memory/sqlite_store.py b/src/javis/memory/sqlite_store.py new file mode 100644 index 0000000..c0bc173 --- /dev/null +++ b/src/javis/memory/sqlite_store.py @@ -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"], + ) diff --git a/src/javis/providers/base.py b/src/javis/providers/base.py new file mode 100644 index 0000000..aa3d7fe --- /dev/null +++ b/src/javis/providers/base.py @@ -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.""" + ... diff --git a/src/javis/providers/ollama.py b/src/javis/providers/ollama.py new file mode 100644 index 0000000..358b46f --- /dev/null +++ b/src/javis/providers/ollama.py @@ -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() From b153daa989ea696846bec86a363e68eb1d324b48 Mon Sep 17 00:00:00 2001 From: Dystroyer8 Date: Thu, 30 Jul 2026 17:00:45 +0200 Subject: [PATCH 4/5] test: cover chat core and session storage --- tests/unit/test_chat_service.py | 57 +++++++++++++++++++++++++++++ tests/unit/test_cli.py | 40 +++++++++++++++++++++ tests/unit/test_ollama_provider.py | 56 +++++++++++++++++++++++++++++ tests/unit/test_settings.py | 39 ++++++++++++++++++++ tests/unit/test_sqlite_store.py | 58 ++++++++++++++++++++++++++++++ 5 files changed, 250 insertions(+) create mode 100644 tests/unit/test_chat_service.py create mode 100644 tests/unit/test_cli.py create mode 100644 tests/unit/test_ollama_provider.py create mode 100644 tests/unit/test_settings.py create mode 100644 tests/unit/test_sqlite_store.py diff --git a/tests/unit/test_chat_service.py b/tests/unit/test_chat_service.py new file mode 100644 index 0000000..015ed1a --- /dev/null +++ b/tests/unit/test_chat_service.py @@ -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() diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py new file mode 100644 index 0000000..dc65f97 --- /dev/null +++ b/tests/unit/test_cli.py @@ -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() diff --git a/tests/unit/test_ollama_provider.py b/tests/unit/test_ollama_provider.py new file mode 100644 index 0000000..71c6170 --- /dev/null +++ b/tests/unit/test_ollama_provider.py @@ -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() diff --git a/tests/unit/test_settings.py b/tests/unit/test_settings.py new file mode 100644 index 0000000..f98a4e1 --- /dev/null +++ b/tests/unit/test_settings.py @@ -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() diff --git a/tests/unit/test_sqlite_store.py b/tests/unit/test_sqlite_store.py new file mode 100644 index 0000000..5f312ce --- /dev/null +++ b/tests/unit/test_sqlite_store.py @@ -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() From 5d567e53013ee4c007aec80eeb0a939e5f0c61f6 Mon Sep 17 00:00:00 2001 From: Dystroyer8 Date: Thu, 30 Jul 2026 17:04:46 +0200 Subject: [PATCH 5/5] docs: update core chat status --- .env.example | 10 +++--- CHANGELOG.md | 5 +++ README.md | 37 ++++++++++++++++------ config/javis.example.toml | 10 ++++-- docs/ARCHITECTURE.md | 22 ++++++++----- docs/CHATGPT_HANDOFF.md | 47 +++++++++++++++++++--------- docs/DATA_MODEL.md | 25 +++++++++------ docs/DECISIONS.md | 4 ++- docs/DEVELOPMENT_SETUP.md | 17 +++++++++- docs/NEXT_SESSION.md | 13 ++++---- docs/PROJECT_STATUS.md | 22 ++++++++----- docs/adr/0006-first-local-runtime.md | 23 ++++++++++++++ docs/adr/README.md | 1 + 13 files changed, 173 insertions(+), 63 deletions(-) create mode 100644 docs/adr/0006-first-local-runtime.md diff --git a/.env.example b/.env.example index 6b1c274..be14de6 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,6 @@ -# Non-secret process overrides only. -JAVIS_DISPLAY_NAME=Javis -JAVIS_CONFIG_PATH=config/javis.toml -JAVIS_SECRETS_PROVIDER=none +# Optionale, nicht geheime Prozesswerte. Diese Datei wird nicht automatisch geladen. +JAVIS_DATA_DIR=D:\Javis-Data\runtime +JAVIS_PROVIDER=ollama +JAVIS_MODEL=qwen3:8b +JAVIS_OLLAMA_URL=http://127.0.0.1:11434 +JAVIS_MODEL_TIMEOUT=180 diff --git a/CHANGELOG.md b/CHANGELOG.md index d449a76..c979892 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ Alle wesentlichen Projektänderungen werden hier in verständlicher Form dokumen - 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 @@ -24,6 +28,7 @@ Alle wesentlichen Projektänderungen werden hier in verständlicher Form dokumen - 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 diff --git a/README.md b/README.md index 880e749..8985e83 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,33 @@ Javis ist ein langfristiges, lokal-first ausgerichtetes Projekt für einen pers ## 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, -- keine Modellanbindung, -- keinen Obsidian-Schreibzugriff, -- keinen Serverzugriff, -- keine Sprachpipeline, -- keine Living-Mind-Implementierung. +Noch nicht implementiert sind Obsidian-Zugriff, Serverzugriff, Sprache, Mobile +Client und Living Mind. -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 @@ -29,9 +46,11 @@ Der nächste Entwicklungsschritt beginnt erst nach Pascals Prüfung auf einem ei - [Projektstatus](docs/PROJECT_STATUS.md) - [Roadmap](docs/ROADMAP.md) - [Architektur](docs/ARCHITECTURE.md) +- [Lokales Modell](docs/LOCAL_MODEL.md) +- [Entwicklungsumgebung](docs/DEVELOPMENT_SETUP.md) - [Sicherheit](docs/SECURITY.md) - [Nächste Sitzung](docs/NEXT_SESSION.md) -- [Vollständiger Handoff](docs/CHATGPT_HANDOFF.md) +- [Kompakter Handoff](docs/CHATGPT_HANDOFF.md) ## Historischer Verbindungstest diff --git a/config/javis.example.toml b/config/javis.example.toml index c6d8538..bd88dd8 100644 --- a/config/javis.example.toml +++ b/config/javis.example.toml @@ -1,12 +1,18 @@ +# Entwurf für eine spätere TOML-Konfiguration. +# Der erste Chat liest die in .env.example genannten Umgebungsvariablen. + [app] display_name = "Javis" language = "de" [paths] -runtime_dir = "runtime" +data_dir = "" [providers] -model = "disabled" +active = "ollama" +model = "qwen3:8b" +ollama_base_url = "http://127.0.0.1:11434" +timeout_seconds = 180 cloud_fallback = false [obsidian] diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e6a6937..db8157b 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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. -## Vorgesehene Ebenen +## Ebenen -1. Interface: CLI, später Sprache, Mobilzugriff und Living Mind. -2. Core: Gesprächsablauf, Sitzungszustand und Orchestrierung. -3. Providers: austauschbare lokale und optionale Cloud-Modellanbindungen. -4. Memory: strukturierte Laufzeitdaten und regenerierbarer semantischer Index. +1. Interface: implementierte CLI; später möglicherweise Sprache, Mobilzugriff und Living Mind. +2. Core: implementierter Gesprächsablauf und Sitzungslogik. +3. Providers: kleine Schnittstelle und implementierter lokaler Ollama-Adapter. +4. Memory: implementierte SQLite-Sitzungen; ein semantischer Index ist nur geplant. 5. Obsidian: zunächst lesender Adapter, später eng begrenzte Entwürfe. 6. Tools und Status: feste Schemas, Allowlists, Timeouts und bereinigte Ausgaben. 7. Security: Freigaben, Secret-Provider und Berechtigungsprüfung. @@ -17,11 +17,17 @@ Javis beginnt als modularer Monolith. Module besitzen klare Zuständigkeiten und ## 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. - Produktname, Datenpfade und Provider werden zentral konfiguriert. - 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. diff --git a/docs/CHATGPT_HANDOFF.md b/docs/CHATGPT_HANDOFF.md index 06db9db..5b080d2 100644 --- a/docs/CHATGPT_HANDOFF.md +++ b/docs/CHATGPT_HANDOFF.md @@ -45,8 +45,8 @@ Rootserver: ## Aktueller technischer Stand -- Projektgrundgerüst ist auf `main` vorhanden und nach Gitea gepusht. -- Paketstruktur unter `src/javis` ist angelegt, enthält aber noch keinen Chatkern. +- Der erste lokale Textchat ist auf `feat/core-chat` implementiert und abgenommen. +- `uv run javis chat` liefert echte lokale Antworten und speichert Sitzungen in SQLite. - Konfigurationsbeispiel, Sicherheitsdokumente, ADRs und Strukturtest sind vorhanden. - 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. @@ -58,15 +58,19 @@ Rootserver: - 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 `, `/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. -- Keine KI-, Sprach-, Mobil- oder Statusfunktion ist implementiert. +- Keine Tool-, Sprach-, Mobil-, Status- oder Living-Mind-Funktion ist implementiert. ## Aktuelle Architektur - modularer Monolith, bevorzugt Python - plattformunabhängige Pfade und zentrale Konfiguration - 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 - lokale Modelle zuerst; keine kostenpflichtige API und kein stiller Cloud-Fallback - Secret-Werte außerhalb von Code, Git, Prompts und Logs @@ -92,11 +96,15 @@ Rootserver: - 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. ## Aktuelle Tests -Letzter bestätigter Grundgerüststand: +Letzter bestätigter Projektstand: - Strukturprüfung: bestanden - Unit-Test des Paketgerüsts: bestanden @@ -107,6 +115,9 @@ Letzter bestätigter Grundgerüststand: - 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 Lokaler Modell-Smoke-Test: @@ -117,9 +128,12 @@ Lokaler Modell-Smoke-Test: - GPU-Belegung: 1.418 MiB vorher, 6.842 MiB mit geladenem Modell - Ollama meldet 100 % GPU und 5,6 GB Modellbelegung -Zwölf vorbereitete Unit-Tests für Konfiguration, SQLite, Kern, Provider und CLI -laufen in der Arbeitskopie bereits erfolgreich; sie sind noch nicht in den -Feature-Branch übernommen. +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 @@ -129,6 +143,9 @@ Feature-Branch übernommen. - letzter Commit vor der Handoff-Verdichtung: `0e82748` - Handoff-Verdichtung auf `main`: `e6eb995` - Python-Entwicklungsumgebung auf dem Feature-Branch: `fa90a5c` +- Laufzeit-/Modelldokumentation: `10d7cd7` +- Chatkern und CLI: `ec9b2f9` +- Unit-Tests: `b153daa` - `origin` verwendet HTTPS - kein Force-Push und keine umgeschriebene Historie @@ -140,11 +157,13 @@ Feature-Branch übernommen. - `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 - endgültiger Produkt-/Repositoryname bleibt offen - Startmechanismus für Ollama ist noch bewusst manuell und nur pro Prozess konfiguriert +- Antworten werden noch nicht gestreamt; Sitzungen besitzen noch keine Titel oder Suche - 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 - Gaming-PC zeigt derzeit nur 8 statt 16 logische CPU-Prozessoren; Ursache ungeklärt @@ -162,12 +181,10 @@ Feature-Branch übernommen. ## Nächster sinnvoller Auftrag -Der genehmigte nächste Meilenstein ist `feat/core-chat`: - -1. vorbereiteten lokalen CLI-Chat mit Provider-Schnittstelle und SQLite-Sitzungen - in den Feature-Branch übernehmen -2. Unit-, Ruff-, Persistenz-, Secret- und echten CLI-Modell-Smoke-Test ausführen -3. Dokumentation und Handoff auf den tatsächlichen Endstand bringen -4. Feature-Branch pushen und nur bei vollständig erfüllter Abnahme nach `main` mergen +Nach Merge von `feat/core-chat` den Chat gemeinsam im Alltag benutzen und zuerst +konkrete Bedienungs- oder Stabilitätsprobleme sammeln. Mögliche kleine Folgepakete +sind ein klarer Start-/Stoppablauf, Streaming, Sitzungstitel oder eine explizite +lokale TOML-Konfiguration. Das spätere Laptop-Modell wird erst auf dessen Hardware +gemessen und separat entschieden. Nicht Teil dieses Meilensteins: Obsidian-Integration, Serverzugriff, Sprache, Mobile Client oder Living-Mind-Code. diff --git a/docs/DATA_MODEL.md b/docs/DATA_MODEL.md index 25e83db..a9f09b2 100644 --- a/docs/DATA_MODEL.md +++ b/docs/DATA_MODEL.md @@ -1,18 +1,23 @@ -# Datenmodell – Entwurf +# Datenmodell ## Laufzeitdaten -Eine spätere strukturierte Datenbank kann mindestens enthalten: +Der erste Chat verwendet SQLite mit zwei Tabellen: -- `sessions`: Beginn, Ende und aktiver Kontext -- `messages`: Rolle, Inhalt, Zeit und Providerreferenz -- `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 +- `sessions`: UUID, Erstellungs-/Änderungszeit, Provider und Modell +- `messages`: Sitzungsbezug, feste Reihenfolge, Rolle, Inhalt und Zeitstempel -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 diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 9fd6ff2..565de21 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -7,7 +7,9 @@ | ADR-0003 | Obsidian-Links benötigen echte inhaltliche Beziehungen | 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-0006 | Ollama und qwen3:8b für den ersten Windows-Chat verwenden | 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). diff --git a/docs/DEVELOPMENT_SETUP.md b/docs/DEVELOPMENT_SETUP.md index c1daf2e..9406c26 100644 --- a/docs/DEVELOPMENT_SETUP.md +++ b/docs/DEVELOPMENT_SETUP.md @@ -30,9 +30,24 @@ Typische Befehle aus dem Repository: $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 javis chat +& '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. diff --git a/docs/NEXT_SESSION.md b/docs/NEXT_SESSION.md index 50e885c..eec91e2 100644 --- a/docs/NEXT_SESSION.md +++ b/docs/NEXT_SESSION.md @@ -2,8 +2,8 @@ ## Ziel -Nächstes Arbeitspaket auf `feat/core-chat`: den vorbereiteten lokalen CLI-Textchat -mit SQLite-Sitzungen übernehmen, vollständig testen und dokumentieren. +Nach Abschluss und Merge von `feat/core-chat`: den ersten Textchat gemeinsam +benutzen und nur konkrete Bedienungs- oder Stabilitätsprobleme priorisieren. ## Vor Änderungen erneut prüfen @@ -14,10 +14,11 @@ mit SQLite-Sitzungen übernehmen, vollständig testen und dokumentieren. ## Vorgeschlagener Umfang -- lokale Konfiguration, Provider-Schnittstelle und SQLite-Sitzungen implementieren -- CLI mit neuen, gelisteten und fortsetzbaren Sitzungen -- Unit-, Ruff-, Persistenz-, Secret- und echter CLI-Modell-Smoke-Test -- Feature-Branch pushen und nur bei erfüllter Abnahme nach `main` mergen +- Start-/Stoppablauf von Ollama auf Alltagstauglichkeit prüfen +- reale mehrteilige Gespräche und verständliche Fehlermeldungen beobachten +- bei Bedarf Streaming, Sitzungstitel oder eine explizite lokale TOML-Konfiguration + als eigenes kleines Feature planen +- erst nach Messung auf der Zielhardware ein kleineres Laptop-Modell auswählen ## Nicht beginnen diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index c34b899..e79c8d7 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -1,6 +1,6 @@ # Projektstatus -Stand: lokale Modelllaufzeit auf `feat/core-chat`, 30.07.2026. +Stand: lokaler Textchat auf `feat/core-chat`, 30.07.2026. ## Erreicht @@ -19,21 +19,29 @@ Stand: lokale Modelllaufzeit auf `feat/core-chat`, 30.07.2026. - `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 -CLI-Chat, Provider im Repository, Sitzungsspeicher, 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 - Branch: `feat/core-chat` - Grundgerüstcommit: `6d04171` - Basis des Feature-Branches: `e6eb995` -- Der aktuelle Feature-Commit ist mit `git log -1` zu ermitteln. +- Toolchain: `fa90a5c` +- Laufzeit/Modell: `10d7cd7` +- Chatkern: `ec9b2f9` +- Tests: `b153daa` ## Bekannte Einschränkung -Toolchain und lokale Modelllaufzeit sind nutzbar, ohne den Windows-`PATH`, Registry, -Autostart oder Windows-Dienste zu verändern. Der Textchat ist als Nächstes in den -Feature-Branch zu übernehmen und vollständig gegen das reale Modell zu prüfen. +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. diff --git a/docs/adr/0006-first-local-runtime.md b/docs/adr/0006-first-local-runtime.md new file mode 100644 index 0000000..ba4fa8a --- /dev/null +++ b/docs/adr/0006-first-local-runtime.md @@ -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. diff --git a/docs/adr/README.md b/docs/adr/README.md index b88b94a..11c5389 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -5,3 +5,4 @@ - [ADR-0003: Sinnvolle Obsidian-Verknüpfungen](0003-meaningful-obsidian-links.md) - [ADR-0004: Tokenbewusster Arbeitsablauf](0004-token-efficient-workflow.md) - [ADR-0005: Kompakter Handoff](0005-compact-handoff.md) +- [ADR-0006: Erste lokale Modelllaufzeit](0006-first-local-runtime.md)