The 21 the automatic pass could not make on its own. `ruff check` and `ruff format --check` are both clean now; typecheck is still red and is next. `in_reasoning` in chat/service.py was a complete state machine that nothing read: initialised False, set True when a reasoning delta arrived, set False when the summary ended — three assignments, zero reads. Ruff reported one at a time, and removing each revealed the next, so what looked like a single stray variable took three passes to bottom out. The branches themselves do real work and are untouched; only the flag is gone. Four `raise HTTPException` inside `except` blocks now chain with `from e`. Until now a failure while handling an error was indistinguishable from the error, which matters most in exactly the situation where the traceback is all you have. In biographer/tools.py the binding was unused but the call is not: MemoryType() is called for the ValueError it raises on an invalid name. The binding is gone and the call and its comment stay, because dropping the line would have removed the validation. The rest are unused bindings in tests where the assertions are on something else (call_args, mostly), plus three unused loop variables and an isinstance tuple. One correction to my own work: removing a dead comprehension in test_error_handling.py left an `if` block with nothing but comments in it, which is a SyntaxError. Ruff caught it immediately. The block now says what the test actually pins — that the stream parses without crashing, which reaching that line demonstrates — rather than computing a list nobody asserts on. `make test` is intermittent here, and it is not this change. test_tatlock_tool_call_logging_calculator failed in two of five full runs across both HEAD and this branch, and passes in the other three; it also fails in isolation at HEAD while passing in isolation here. Order- or timing-dependent. Recorded rather than chased, since tests are not gated in this repo yet. Co-Authored-By: Claude <noreply@anthropic.com>
Tatlock - Your Homelab Butler
📖 For the complete system vision and architectural philosophy, see docs/philosophy.md
A privacy-first, offline-capable personal assistant system that coordinates specialized AI agents to help with research, development, home automation, and daily organization.
Current Status
- ✅ Production-ready API with OpenAI Responses API format
- ✅ Open WebUI integration with reasoning bubbles (
<think>tags) - ✅ Two-tier architecture - The Steward analyzes requests, Tatlock coordinates execution
- ✅ Multi-agent coordination - Expert household staff for specialized tasks
- ✅ Memory system - User profile, preferences, and semantic recall
- ✅ Comprehensive testing - 399 tests with good coverage
The Household Staff
| Agent | Role | Status |
|---|---|---|
| Tatlock | The Butler - Primary interface with witty personality | ✅ Active |
| The Steward | Request analysis and capability recommendation | ✅ Active |
| The Librarian | Research, wiki management, knowledge synthesis | ✅ Active |
| The Biographer | User memory - profiles, preferences, facts | ✅ Active |
| The Developer | Code assistance, debugging, architecture | 🔜 Planned |
| The Secretary | Scheduling, calendars, reminders | 🔜 Planned |
| The Handyman | System administration, monitoring | 🔜 Planned |
| The Housekeeper | Home automation (Home Assistant) | 🔜 Planned |
Features
API Endpoints
-
Responses API (
/v1/responses) - OpenAI Responses API format with structured output- Reasoning items for displaying thinking process
- Function call items for tool execution
- Message items for assistant responses
- Streaming and non-streaming support
-
Chat Completions (
/v1/chat/completions) - OpenAI Chat Completions compatibility- Automatic reasoning conversion to
<think>tags for Open WebUI - Full OpenAI API compatibility
- Streaming support
- Automatic reasoning conversion to
-
Models (
/v1/models) - List available models
Advanced Capabilities
- Conversation History: Auto-generated IDs, configurable max turns (default: 20)
- Context Management: Token counting, automatic trimming, usage statistics
- Parameter Validation: Temperature (0.0-2.0), reasoning effort levels, max tokens, stop sequences
- Real-time Enforcement: Stop sequence detection and max token limits during streaming
Available Models
-
lorem-tester: Full-featured mock agent with realistic behavior
- Configurable reasoning effort levels
- Random tool/function calls
- Error triggers for testing (rate_limit, context_overflow)
-
Tatlock: Real PydanticAI agent with butler personality
- LLM Backend: Ollama (gemma4:e2b by default, local-first) with optional Claude fallback
- Personality: Witty British butler, research-oriented
- Core Tools:
- Calculator: Safe mathematical expression evaluation
- Date/Time Toolkit: Current time, relative dates, time differences
- Web Search: Privacy-preserving search via SearXNG
- Household Coordination:
- The Steward: Analyzes requests and recommends capabilities
- The Librarian: Research via library-desk HybridRAG + wiki
- The Biographer: User memory and preference management
- Capabilities: Streaming, reasoning, tool calling, multi-agent delegation
Requirements
- Python 3.12+ (Python 3.12.11 recommended)
- External Services (must be running separately):
- Ollama: LLM inference (gemma4:e2b, nomic-embed-text)
- Redis: Caching and session memory
- Qdrant: Vector storage for The Biographer's memory
- SearXNG: Web search (optional)
- library-desk: Research API for The Librarian (optional)
Quick Start
Installation
# Clone the repository
git clone https://git.schweitz.net/jpmschweitzer/tatlock.git
cd tatlock
# Install dependencies
make setup
Run the Server
uvicorn src.main:app --reload
API available at http://localhost:8000
Usage Examples
Responses API
Generate a response with reasoning:
curl http://localhost:8000/v1/responses \
-H "Content-Type: application/json" \
-d '{
"model": "lorem-tester",
"input": [
{"role": "user", "content": "Explain quantum computing"}
],
"reasoning": {
"effort": "medium",
"summary": "auto"
},
"max_output_tokens": 500,
"stream": false
}'
Response Structure:
{
"id": "resp_abc123",
"object": "response",
"created_at": 1733529600,
"model": "lorem-tester",
"status": "completed",
"output": [
{
"type": "reasoning",
"summary": ["Analyzing the request...", "Considering quantum mechanics..."]
},
{
"type": "message",
"role": "assistant",
"content": [{"type": "output_text", "text": "Quantum computing uses..."}]
}
],
"usage": {
"input_tokens": 10,
"output_tokens": 50,
"reasoning_tokens": 20,
"total_tokens": 80
}
}
Chat Completions (OpenAI-compatible)
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "lorem-tester",
"messages": [
{"role": "user", "content": "Hello!"}
],
"temperature": 0.7,
"stream": true
}'
List Models
curl http://localhost:8000/v1/models
Conversation History
Optionally track conversations using metadata:
curl http://localhost:8000/v1/responses \
-H "Content-Type: application/json" \
-d '{
"model": "lorem-tester",
"input": [{"role": "user", "content": "Hello"}],
"metadata": {"conversation_id": "conv_abc123"}
}'
Note: Client must send full conversation history in input array (OpenAI compatible). Server optionally tracks via metadata.conversation_id for future features.
Using Tatlock with Tools
Tatlock automatically uses his permanent tools when appropriate:
# Mathematical calculation
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Tatlock",
"messages": [{"role": "user", "content": "What is sqrt(144) + 25?"}]
}'
# Date/time queries
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Tatlock",
"messages": [{"role": "user", "content": "What was the date 2 weeks ago?"}]
}'
# Web search for current information
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Tatlock",
"messages": [{"role": "user", "content": "Search for recent Python 3.12 features"}]
}'
Tatlock's Tool Usage Philosophy:
- Uses calculator for ALL mathematics (even simple arithmetic)
- Uses date/time tools instead of guessing dates
- Searches for current/volatile information to verify facts
- Maintains a researcher's mindset with tool-assisted verification
Open WebUI Integration
Connection
If running Open WebUI in Docker and API on host:
# Use Docker bridge gateway IP
http://172.17.0.1:8000/v1/chat/completions
Reasoning Display
The Chat Completions endpoint automatically:
- Enables reasoning generation
- Converts reasoning to
<think>tags - Streams thinking before the response
Open WebUI displays this as thought bubbles separate from the main response.
Testing Error Handling
Use special triggers in user messages:
"trigger_rate_limit"- Simulates rate limit error"trigger_context_overflow"- Simulates context length error
API Documentation
Interactive documentation available at:
- Swagger UI:
http://localhost:8000/docs - ReDoc:
http://localhost:8000/redoc
Testing
# Run all tests
pytest
# Run unit tests only (no external services needed)
pytest --ignore=tests/e2e --ignore=tests/integration --ignore=tests/contracts
# Wire-level contract tests against live service boundaries
make test-contracts
# Run with coverage
pytest --cov=src --cov-report=term-missing
# Current: ~400 tests
Test Categories:
- Unit tests: Agent tools, capabilities, schemas, memory service
- Integration tests: Full API stack with real Ollama
- End-to-end tests: Chat completions, responses API
Deployment
Production Server
# Multiple workers for production
uvicorn src.main:app --host 0.0.0.0 --port 8000 --workers 4
Recommendations
- Use reverse proxy (nginx/caddy) for HTTPS
- Enable rate limiting
- Set up monitoring and logging
- Configure resource limits
- Use process manager (systemd/supervisor)
Configuration
Create a .env file for custom configuration:
# API Configuration
API_HOST=0.0.0.0
API_PORT=8000
# Ollama Configuration (primary backend)
OLLAMA_HOST=http://localhost:11434
OLLAMA_DEFAULT_MODEL=gemma4:e2b
OLLAMA_EMBEDDING_MODEL=nomic-embed-text
OLLAMA_TIMEOUT=120
# Claude fallback (optional; used when Ollama is down or PREFER_CLOUD_BACKEND=true)
# ANTHROPIC_API_KEY=sk-ant-api03-your-key-here
ANTHROPIC_MODEL=claude-sonnet-5
PREFER_CLOUD_BACKEND=false
# Redis Configuration
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_MEMORY_DB=2
REDIS_MEMORY_TTL_HOURS=24
# Qdrant Configuration (for memory)
QDRANT_HOST=localhost
QDRANT_PORT=6333
QDRANT_EMBEDDING_DIM=768
# Library-desk Configuration (for The Librarian)
LIBRARY_DESK_HOST=http://localhost:8089
LIBRARY_DESK_TIMEOUT=60
# SearXNG Configuration (for web search)
SEARXNG_HOST=http://localhost:8087
SEARXNG_TIMEOUT=30
# Logging
LOG_LEVEL=INFO
# CORS (default: allow all)
CORS_ORIGINS=["*"]
See .env.example for full configuration options.
Troubleshooting
Streaming not working
- Verify SSE-Starlette is installed
- Check client supports Server-Sent Events
- Test with:
pytest tests/responses/ -k streaming
Open WebUI can't connect
- Use Docker bridge gateway IP:
172.17.0.1:8000 - Check firewall settings
- Verify server is running on
0.0.0.0
Reasoning not showing
- Ensure using Chat Completions endpoint (auto-enables reasoning)
- Or manually enable in Responses API:
"reasoning": {"effort": "medium", "summary": "auto"} - Check Open WebUI version supports
<think>tags
Tatlock agent errors
- Verify Ollama is running:
curl http://localhost:11434/api/tags - Check model is downloaded:
ollama list - Review environment variables:
OLLAMA_HOST,OLLAMA_DEFAULT_MODEL - Check logs:
tail -f logs/server.log
Web search not working
- Verify SearXNG is running:
curl http://localhost:8087/ - Check
SEARXNG_HOSTenvironment variable - SearXNG is optional - Tatlock will note if search is unavailable
Project Structure
tatlock/
├── src/
│ ├── agents/ # Agent implementations
│ │ ├── biographer/ # The Biographer - memory management
│ │ ├── librarian/ # The Librarian - research & wiki
│ │ ├── steward/ # The Steward - request analysis
│ │ ├── tatlock_core/ # Core butler tools
│ │ ├── tatlock.py # Tatlock PydanticAI agent
│ │ ├── delegation.py # Expert delegation wrappers
│ │ └── protocol.py # Agent error protocol
│ ├── responses/ # Responses API (primary endpoint)
│ ├── chat/ # Chat Completions wrapper
│ ├── models/ # Models listing
│ ├── core/ # Shared infrastructure
│ │ ├── config.py # Configuration management
│ │ ├── context.py # Request context (ContextVar)
│ │ ├── memory_service.py # Direct memory access
│ │ ├── memory_cache.py # Redis session cache
│ │ ├── embeddings.py # Ollama embedding client
│ │ ├── qdrant.py # Vector database client
│ │ └── multi_tenancy.py # User isolation utilities
│ └── main.py # Application entry point
├── tests/ # Comprehensive test suite
├── docs/ # Project documentation
├── CHANGELOG.md # Version history
└── README.md # This file
Development
For LLM agent development guidelines and architectural decisions, see CLAUDE.md.
Contributing
- Fork the repository
- Create a feature branch
- Make changes with tests
- Ensure tests pass:
pytest - Submit pull request
Documentation
- System Philosophy: docs/philosophy.md - Vision, goals, and architectural patterns
- Development Roadmap: docs/roadmap.md - Open work and planned phases
- Developer Guidelines: CLAUDE.md - LLM agent development patterns
- Version History: CHANGELOG.md - Changes and releases
External References
- OpenAI Responses API: https://platform.openai.com/docs/api-reference/responses
- FastAPI: https://fastapi.tiangolo.com/
- PydanticAI: https://ai.pydantic.dev/
License
[Add your license here]
Version
Current version: see CHANGELOG.md
Note: Tatlock is a production-ready homelab butler. All household staff use PydanticAI with local Ollama inference (gemma4), with an optional Claude cloud fallback.