diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index bfe9f79..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,105 +0,0 @@ -# LLM Agent Instructions - -This document contains instructions and documentation references for AI assistants working with this codebase. - -> **๐Ÿ“– Important**: Before working on this project, read [docs/philosophy.md](docs/philosophy.md) to understand the system vision, architectural patterns, and design goals. All development should work towards realizing those patterns. -# AGENTS.md - -> **Start every session by reading this file.** -> This file outlines the operational protocols, coding standards, and architectural decisions for this FastAPI project. - -## 1. Agent Operational Protocols - -### ๐Ÿง  Work Patterns (Plan-Act-Reflect) -* **Plan:** Before writing code, briefly outline your plan. Identify which files you will touch and what the side effects might be. -* **Act:** Execute the changes in small, atomic steps. -* **Reflect:** After coding, verify your work. Did you break existing tests? Did you add new tests? - -### ๐Ÿงช Local Development Setup -* **Always test locally first** before committing and deploying. The build-deploy loop is slow. -* **Start the local server** with `make run` - logs are written to `build/logs/server.log` for easy tailing -* **Auto-reload**: `make run` runs uvicorn in reload mode - code changes are picked up automatically without restart (except for dependency changes) -* **Test REST endpoints** against `http://localhost:8777` using curl or similar tools -* **Only deploy** when a phase or feature is complete and tested locally -* **Environment**: Copy `.env.example` to `.env` and configure for your local setup (Ollama, Redis, Qdrant hosts) -* **Running tests**: Always use the venv explicitly to avoid environment mismatches: - ```bash - .venv/bin/python -m pytest tests/ # All tests - .venv/bin/python -m pytest tests/core/ -v # Core tests only - ``` - -### ๐ŸŒ Internal Service Access -* **git.schweitz.net**: Access via `http://localhost:3002` (direct Gitea) to bypass Authentik SSO - * Example: `curl http://localhost:3002/jpmschweitzer/library-desk/raw/branch/main/README.md` - * Public repos are readable without authentication - * Related repos: `library-desk`, `scheduler`, `core-api`, `portainer-core` - -### ๐Ÿณ Deployment & Infrastructure -* **Full stack documentation**: Available in the `portainer-core` repo - * Access: `curl http://localhost:3002/jpmschweitzer/portainer-core/raw/branch/main/CONTAINERS.md` - * Contains: All service ports, URLs, Redis DB allocations, external domains -* **Tatlock deployment**: - * LAN: `http://192.168.86.149:8000` - * External: `tatlock.schweitz.net` (behind Authentik SSO) - * Redis DBs: 1 (memory), 6 (benchmarks) -* **Health check**: `curl http://192.168.86.149:8000/health` - -### ๐Ÿ›ก๏ธ Git Discipline -* **NEVER commit to `main` or `master` directly.** Always create a feature branch: `feature/your-feature-name` or `fix/issue-description`. -* **Commit Messages:** Use the [Conventional Commits](https://www.conventionalcommits.org/) format. - * `feat: add user login endpoint` - * `fix: resolve database connection timeout` - * `refactor: split monolith dependency file` -* **Atomic Commits:** Keep commits small. One logical change = one commit. - -### ๐Ÿ“ Changelog Maintenance -* **Update `CHANGELOG.md`** with every user-facing change. -* Format: `## [Unreleased] - YYYY-MM-DD` followed by `### Added`, `### Changed`, or `### Fixed`. - -### ๐Ÿš€ Release Flow -When changes are ready for deployment: - -1. **Ask user if deploy cycle is desired** - -2. **Update version** in `pyproject.toml`: - - Bug fixes: bump patch version (1.8.3 โ†’ 1.8.4) - - New features: bump minor version (1.8.4 โ†’ 1.9.0) - -3. **Update CHANGELOG.md**: - - Move items from `[Unreleased]` to new version section - - Add release date: `## [1.8.4] - 2025-12-16` - -4. **Commit and tag**: - ```bash - git add -A - git commit -m "fix: description of changes" - git tag v1.8.4 - git push origin main --tags - ``` - -5. **CI/CD triggers automatically**: - - Gitea CI builds Docker image on new tag - - Watchtower pulls and deploys to production - - Verify deployment: `curl http://192.168.86.149:8000/health` - ---- - -## 2. FastAPI Architecture & Best Practices -*Reference: [FastAPI Best Practices](https://github.com/zhanymkanov/fastapi-best-practices)* - -### ๐Ÿ“‚ Project Structure (Directory-based, NOT File-type based) -Do **not** group files by type (e.g., one huge `routers` folder). Group by **domain/module** inside a `src/` directory. - -**Correct Structure:** -```text -src/ -โ”œโ”€โ”€ auth/ -โ”‚ โ”œโ”€โ”€ router.py # Endpoints -โ”‚ โ”œโ”€โ”€ schemas.py # Pydantic models -โ”‚ โ”œโ”€โ”€ service.py # Business logic (CRUD, etc.) -โ”‚ โ”œโ”€โ”€ dependencies.py# Module-specific dependencies -โ”‚ โ””โ”€โ”€ config.py # Module-specific settings -โ”œโ”€โ”€ posts/ -โ”‚ โ”œโ”€โ”€ router.py -โ”‚ โ””โ”€โ”€ ... -โ””โ”€โ”€ main.py # App entry point \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 76a9af8..4d044ff 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,34 +1,196 @@ -# CLAUDE.md +# CLAUDE.md โ€” tatlock -Claude Code-specific notes for this project. For general development instructions, architecture, coding standards, and deployment โ€” see [AGENTS.md](AGENTS.md). +Privacy-first homelab butler. An OpenAI-compatible orchestration API over local models, with +household staff agents built on PydanticAI. Python 3.12 / FastAPI, `version = "2.4.3"`. +Container `tatlock` on `docker-dataplane`, port **8000**. Redis DB **1** (memory), Qdrant for +vectors. -## Setup & Commands +## Ports + +| | Port | How | +|---|---|---| +| Local dev | **8777** | `make run` โ€” uvicorn reload, logs to `build/logs/server.log` | +| Production | **8000** | container; `http://192.168.86.149:8000/health`, external `tatlock.schweitz.net` behind Authentik | + +Test endpoints against `localhost:8777` while developing. `localhost:8000` is the *container*. + +## Live contract + +`http://localhost:8000/openapi.json` โ€” **5 paths**, `title: OpenAI-Compatible API`, `version: +2.4.3` (verified 2026-08-09): `/`, `/health`, `/v1/models`, `/v1/chat/completions`, +`/v1/responses`. `/v1/responses` is primary; `/v1/chat/completions` exists for Open WebUI. + +**The spec is the public surface, not the system.** The household capability registry is internal +and appears nowhere in those 5 paths. Absence from the spec means "not exposed", not "does not +exist". + +## Two traps that make the runtime look like the opposite of what it is + +**1. `src/anthropic` loads at startup; `src/ollama` does not โ€” and Ollama is the primary +backend.** A cold `import src.main` inside the container shows `agents, anthropic, chat, core, +main, models, responses` โ€” no `ollama`. The only import of it is a *function-body* one at +`src/anthropic/model_selector.py:230`. Meanwhile `PREFER_CLOUD_BACKEND=false`, so every request +actually goes to Ollama and the Claude path is off (see **D-11**). Reading the module list +naively gives you exactly the wrong answer: the package that looks live is the disabled fallback, +and the one that looks dead is the hot path. Do not conclude anything about backends from +`sys.modules`; read the config. + +**2. In-process singletons are empty outside the app.** `get_household_registry()` +(`src/core/household_registry.py:334`) in a fresh `docker exec python` returns **0 members**, +while the running app serves 2 models from it โ€” it is populated at startup. Import the +module-level definitions or ask the endpoint; never import a singleton and assume it is +populated. + +## Stack decisions that bind this repo + +Recorded in the workspace vault, not here. Read before assuming anything about the LLM backend: ```bash -make setup # Create venv and install all dependencies -make test # Unit tests (no external services) -make test-integration # Integration tests (needs Claude/Ollama) -make test-contracts # Wire-level contract tests against live service boundaries -make run # Start dev server on port 8777 -make lint # Ruff linter + formatter check -make typecheck # Mypy -make clean # Remove caches and build artifacts +/home/jpmschweitzer/.local/bin/pql --vault /mnt/media/Projects decisions read D-11 ``` -Dependencies are in `pyproject.toml` (`[project.dependencies]` and `[project.optional-dependencies.dev]`). +**D-11 โ€” the Claude migration is abandoned. Tatlock stays on Ollama.** Do not resume it and do +not treat its remnants as unfinished work. What you will find, and why none of it is a TODO: +`ANTHROPIC_MODEL` is set on the container (`claude-sonnet-4-20250514`) and never used because +`PREFER_CLOUD_BACKEND=false`; `ANTHROPIC_API_KEY` is a variable reference whose literal was +revoked 2026-08-09; `docs/claude-integration.md` documents a capability that exists but is +switched off. The cost is deliberate: reasoning stays at `gemma4:e2b` scale because VRAM is +shared with Speaches. -## Critical Gotchas +`REDIS_BENCHMARK_DB=6` is allocated on the container but the benchmarking module was never +implemented โ€” see the gotcha below. Vestigial, like the Anthropic settings. -**ASGITransport does NOT trigger FastAPI lifespan events.** The session-scoped `_initialize_app` fixture in `tests/conftest.py` calls `initialize_application()` explicitly via `asyncio.run()`. Without this, the Ollama/Claude health checks never run: `_ollama_available` stays `None` (treated as available, so requests go to Ollama) and `_claude_available` stays `None` (treated as unavailable, so the Claude fallback never engages). +## Critical gotchas -**AsyncIO scope mismatch.** `asyncio_default_fixture_loop_scope = function` is set in `pyproject.toml`. Session-scoped async fixtures cause `ScopeMismatch` errors. The fix is to use a sync fixture with `asyncio.run()` for session-scoped initialization. +**ASGITransport does NOT trigger FastAPI lifespan events.** The session-scoped `_initialize_app` +fixture in `tests/conftest.py` calls `initialize_application()` explicitly via `asyncio.run()`. +Without it the Ollama/Claude health checks never run: `_ollama_available` stays `None` (treated +as available, so requests go to Ollama) and `_claude_available` stays `None` (treated as +unavailable, so the Claude fallback never engages). -**The butler persona prompt suppresses local-model tool calling.** With `TATLOCK_SYSTEM_PROMPT` attached, gemma4 reasons about calling the calculator, then answers from memory with wrong arithmetic (a different wrong product each run). `orchestrate_tool_calls()` therefore uses the terse `TATLOCK_ORCHESTRATION_PROMPT`; the persona is applied in `synthesize_from_results()`. Do not reattach the persona prompt to a tool-phase agent. `tool_choice: "required"` via extra_body does NOT force Ollama to call tools โ€” it is advisory at best. +**AsyncIO scope mismatch.** `asyncio_default_fixture_loop_scope = function` is set in +`pyproject.toml`. Session-scoped async fixtures raise `ScopeMismatch`. Use a sync fixture with +`asyncio.run()` for session-scoped initialization. -**Claude Sonnet 5+ rejects sampling parameters.** `temperature`/`top_p`/`top_k` return a 400. Use `get_sampling_settings()` from the model selector instead of passing `ModelSettings(temperature=...)` directly to agents that can run on the Claude fallback. The contract test suite pins this (`make test-contracts`). +**The butler persona prompt suppresses local-model tool calling.** With `TATLOCK_SYSTEM_PROMPT` +attached, gemma4 reasons about calling the calculator, then answers from memory with wrong +arithmetic โ€” a different wrong product each run. `orchestrate_tool_calls()` therefore uses the +terse `TATLOCK_ORCHESTRATION_PROMPT`; the persona is applied in `synthesize_from_results()`. Do +not reattach the persona prompt to a tool-phase agent. `tool_choice: "required"` via `extra_body` +does **not** force Ollama to call tools โ€” advisory at best. -**Integration test timeouts.** Set to 120s to match `OLLAMA_TIMEOUT` config (300s for the pure-Ollama fallback test, which cannot be rescued by Claude). Current GPU-resident numbers (measured 2026-08-07, gemma4:e2b at ~95 tok/s): full Steward โ†’ orchestrate โ†’ synthesize flow ~10โ€“13s for simple turns; librarian-routed queries ~20-25s (not re-measured). A single turn costs **3 sequential Ollama calls and ~710 generated tokens** even for "what is 61 plus 12?" โ€” most of it the model's own reasoning, paid three times. Cold model load is ~36s, avoided while the model is pinned with `keep_alive: -1`; the `OLLAMA_KEEP_ALIVE=2h` default otherwise reintroduces it. The old "~35s steward / ~2 min flow" and "11โ€“25s flow" figures are superseded โ€” do not plan against them. `STEWARD_TIMEOUT` defaults to 60s. +**Claude Sonnet 5+ rejects sampling parameters.** `temperature`/`top_p`/`top_k` return 400. Use +`get_sampling_settings()` from the model selector rather than passing `ModelSettings(temperature=โ€ฆ)` +to agents that can run on the Claude fallback. `make test-contracts` pins this. -**`get_benchmark_store` does not exist.** The benchmarking module (`src/core/benchmarks.py`) was never implemented. `scripts/benchmark_analysis.py` also references it and is broken. Do not add mocks for it in tests. +**Integration test timeouts** are 120s to match `OLLAMA_TIMEOUT` (300s for the pure-Ollama +fallback test, which Claude cannot rescue). GPU-resident numbers measured 2026-08-07 with +gemma4:e2b at ~95 tok/s: full Steward โ†’ orchestrate โ†’ synthesize ~10โ€“13s for simple turns; +librarian-routed ~20โ€“25s (not re-measured). **A single turn costs 3 sequential Ollama calls and +~710 generated tokens even for "what is 61 plus 12?"** โ€” mostly the model's own reasoning, paid +three times. Cold model load is ~36s, avoided while pinned with `keep_alive: -1`; the +`OLLAMA_KEEP_ALIVE=2h` default reintroduces it. Older "~35s steward / ~2 min flow" and "11โ€“25s" +figures are superseded โ€” do not plan against them. `STEWARD_TIMEOUT` defaults to 60s. -**Steward tests need household registry.** Use `register_household_members()` (sync) in fixtures, not `initialize_application()` (async). The steward extracts capabilities from the registry. +**`get_benchmark_store` does not exist.** `src/core/benchmarks.py` was never implemented, and +`scripts/benchmark_analysis.py` references it and is broken. Do not add mocks for it in tests. + +**Steward tests need the household registry.** Use `register_household_members()` (sync) in +fixtures, not `initialize_application()` (async). The steward extracts capabilities from the +registry. + +## Commands + +```bash +make setup # venv + all dependencies +make run # dev server on 8777, reload, logs to build/logs/server.log +make test # unit tests, no external services +make test-integration # needs Ollama (and Claude, if enabled) +make test-contracts # wire-level contract tests against live service boundaries +make lint # ruff linter + formatter check +make typecheck # mypy +make clean # remove caches and build artifacts +``` + +Always run pytest through the venv explicitly, to avoid environment mismatch: + +```bash +.venv/bin/python -m pytest tests/ +.venv/bin/python -m pytest tests/core/ -v +``` + +Dependencies live in `pyproject.toml` (`[project.dependencies]`, `[project.optional-dependencies.dev]`). +Copy `.env.example` to `.env` and configure Ollama, Redis and Qdrant hosts. + +**Contract tests before code review.** When the question is "do these two services still agree?", +`make test-contracts` answers it by observing the live boundary; reading both codebases only tells +you what should happen. Semantics: unreachable โ†’ skip, reachable-but-wrong-shape โ†’ fail. + +## Architecture + +Domain-first under `src/`: `agents/` (steward, librarian, biographer, housekeeper, tatlock_core), +`core/`, `chat/`, `responses/`, `models/`, `ollama/`, `anthropic/`. Two tiers โ€” the Steward routes, +Tatlock coordinates. Group new work by domain, not by file type. + +## Internal service access + +`http://localhost:3002` reaches Gitea directly, bypassing Authentik SSO โ€” verified returning +`{"version":"1.27.1"}`. Useful for reading a sibling repo's raw files: + +```bash +curl http://localhost:3002/jpmschweitzer/library-desk/raw/branch/main/README.md +``` + +The old AGENTS.md pointed at **`portainer-core`** for full-stack documentation. That repo is +**deprecated** and must not be used as a source of infra facts; it was merged into +`system-admin-toj/containers/`, where `CONTAINERS.md` is the live inventory. + +## Work tracking + +Work lives in **pql**, not a markdown TODO. **This repo's vault is standalone** โ€” its tickets and +internal decisions live here in `.pql/` and `governance/`, and travel with a clone, because +`.pql/changelog/` is committed and replayed by the git hooks (D-15). The databases are gitignored +and rebuildable with `pql plan rebuild`. + +`pql` is **not** on the non-interactive `PATH` โ€” invoke it as `/home/jpmschweitzer/.local/bin/pql`. +From inside this repo no `--vault` is needed; pql anchors at the nearest `.git/` ancestor. + +```bash +/home/jpmschweitzer/.local/bin/pql ticket list # this repo's open work +/home/jpmschweitzer/.local/bin/pql plan whatsnext # next unblocked item, with context +/home/jpmschweitzer/.local/bin/pql decisions list # this repo's own decisions +``` + +Stack decisions that constrain this service need the flag: + +```bash +/home/jpmschweitzer/.local/bin/pql --vault /mnt/media/Projects decisions list --domain tatlock-api +``` + +The workspace domain is `tatlock-api`, not `tatlock` โ€” pql rejects a domain stem that prefixes +another, and `tatlock` prefixes `tatlock-ui`. A `tatlock-api -> tatlock` symlink at the workspace +root makes the directory answer to both (D-15). + +Note `ticket new --decision D-N` resolves ids within **one** vault, so a ticket here cannot link +to a workspace decision. Cite the id in the ticket body instead. + +## Git + +- **History is linear โ€” no merge commits.** Work on `main`, or a short-lived branch that is + fast-forwarded and deleted. This repo's AGENTS.md mandated a feature branch for every change; + that rule was retired workspace-wide on 2026-08-08 and does not apply. +- **Conventional Commits**: `feat:`, `fix:`, `refactor:`, `docs:`, `chore:`. +- **Stage explicitly. Never `git add -A`** โ€” denied by policy, and it sweeps in whatever else is + dirty, including secrets. +- Update `CHANGELOG.md` for every user-facing change, under `[Unreleased]`. + +## Releasing + +Test locally first โ€” the build-deploy loop is slow. Deploy only when a feature is complete. + +1. Ask whether a deploy is wanted; it is not automatic. +2. Bump `version` in `pyproject.toml` (patch for fixes, minor for features). +3. Move `[Unreleased]` entries into a dated section in `CHANGELOG.md`. +4. Stage the changed files by name, commit, tag `vX.Y.Z`, `git push origin main --tags`. +5. Gitea CI builds and pushes on the tag; Watchtower deploys. +6. Verify: `curl http://192.168.86.149:8000/health`. diff --git a/README.md b/README.md index c47563a..aad93ec 100644 --- a/README.md +++ b/README.md @@ -406,7 +406,7 @@ tatlock/ ## Development -For LLM agent development guidelines and architectural decisions, see [AGENTS.md](AGENTS.md). +For LLM agent development guidelines and architectural decisions, see [CLAUDE.md](CLAUDE.md). ## Contributing @@ -420,7 +420,7 @@ For LLM agent development guidelines and architectural decisions, see [AGENTS.md - **System Philosophy**: [docs/philosophy.md](docs/philosophy.md) - Vision, goals, and architectural patterns - **Development Roadmap**: [docs/roadmap.md](docs/roadmap.md) - Open work and planned phases -- **Developer Guidelines**: [AGENTS.md](AGENTS.md) - LLM agent development patterns +- **Developer Guidelines**: [CLAUDE.md](CLAUDE.md) - LLM agent development patterns - **Version History**: [CHANGELOG.md](CHANGELOG.md) - Changes and releases ### External References diff --git a/docs/philosophy.md b/docs/philosophy.md index 66b2122..aa396c1 100644 --- a/docs/philosophy.md +++ b/docs/philosophy.md @@ -10,7 +10,7 @@ This document establishes the foundational philosophy and architectural patterns - When new architectural insights require rethinking core principles **When NOT to modify this document**: -- During implementation of these patterns (use README.md, AGENTS.md, or code comments for technical details) +- During implementation of these patterns (use README.md, CLAUDE.md, or code comments for technical details) - For adding new household members or capabilities within the existing pattern - For tactical decisions about specific technologies or tools @@ -270,7 +270,7 @@ The user never directly interacts with the Steward or individual expert agents **Related Documents**: - **README.md**: User-facing documentation and usage guide -- **AGENTS.md**: LLM agent development guidelines and technical patterns +- **CLAUDE.md**: LLM agent development guidelines and technical patterns - **CHANGELOG.md**: Version history and implemented features ---