Architecture
The Two-App Split
The entire codebase is split into two distinct layers. This is a non-negotiable architectural decision.
genai/ — Pure Python agent framework
- Zero Django imports. Ever.
- Testable without Django running.
- Contains:
base.py,llm_client.py,memory.py,registry.py,tools/,agents/ - Verified after every sprint with:
grep -r 'from django' genai/
agent_service/ — Django bridge app
- The only place allowed to import from both Django and
genai/. - All ORM calls, views, URL routing live here.
- Contains:
models.py,views.py,service.py,urls.py,admin.py,apps.py
bookstore/ — Bookstore Django app
- Series, Book, Chapter, ChapterChunk models.
- Four public views: index, series, book, chapter reader.
- Arthur's workspace — commission, write, rewrite, lock.
- Management commands:
export_bookstore,import_bookstore.
The Registry
genai/registry.py — AgentRegistry class.
- Holds agent classes (blueprints), never instances.
- Agents register in
agent_service/apps.pyready()— fires once per Passenger process startup. get_by_type()returns the class; caller instantiates with dependencies.- Module-level singleton:
registry = AgentRegistry()
Singleton types — only one allowed: assistant, librarian
Non-singleton types — many allowed, stored as list: project_agent, publisher
Agent Lifecycle — Per Request
Every agent is instantiated fresh per Django request, destroyed after response. Continuity comes from the database, not a persistent object.
Request → service.py → query DB → instantiate agent → load_memory() → agent.run() → save to DB → Response
Background Task Pattern (Arthur)
Long-running Arthur operations (write chapter, rewrite, lock) run in background threads. The view creates an ArthurTask DB record and returns a task_id immediately. The frontend polls GET /api/arthur/task/<id>/ every 4 seconds until status=done.
This means Tomas can navigate away — the task survives in the DB.
Craft Memory System (Arthur)
Four-layer file-based craft memory. Grows with every locked chapter.
| Layer | File | Scope |
|---|---|---|
| General | media/arthur/craft/general/{language}.md |
All books, all genres |
| Genre | media/arthur/craft/genre/{genre}/{language}.md |
Same genre books |
| Series | media/arthur/series/{slug}/craft.md |
All books in series |
| Book | media/arthur/series/{slug}/books/{slug}/craft.md |
This book only |
Language rules (Czech quotation marks, register, diacritics, sentence rhythm) live in the general craft files — not hardcoded in the system prompt.
ConversationMessage — Unified Model
One model handles all agent conversations.
agent_name— which agent owns this conversation (teo,vega,dash,arthur)scope—user:TomasDfor authenticated users,session:abc123for anonymousrole—useroragentonly- Rolling window: 20 messages per
(agent_name, scope)pair
ChapterFeedback — Separate from Conversation
Tomas's rewrite feedback is stored in ChapterFeedback, not ConversationMessage. Arthur's acknowledgements are UI-only and never saved. At lock time, ChapterFeedback records are read to synthesise craft notes.
Agent Offices — URL Structure
| URL | Agent | Access |
|---|---|---|
/teo/ |
Teo | Authenticated only |
/wiki/ |
Vega | Public + authenticated |
/dashboard/ |
Dash | Authenticated only |
/bookstore/ |
Arthur | Public (read) + authenticated (write) |
Structured Response Format
Every agent returns exactly this dict:
{
"message": "string — the response text",
"emotion": "neutral|happy|thinking|excited|confused|concerned|playful|sarcastic|angry|satisfied",
"agent": "string — agent name",
"actions": []
}
Proactive Agent Pattern
Cron jobs trigger Django management commands. Each proactive agent has an observe() method.
*/10 * * * * manage.py run_dash # market monitoring
0 * * * * manage.py run_arthur # chapter activity check
0 8 * * * manage.py run_vega # visitor pattern review
Proactive agents never contact Tomas directly. They write TeoNotification records. Teo surfaces them as the morning briefing.
Non-Negotiable Rules
- Zero Django imports in
genai/— verify:findstr /r /s "from django" genai\* manage.py check— 0 issues after every sprint- Registry holds classes, never instances
- Tools never write to DB — they return structured data,
service.pywrites it - All wiki writes go through Vega — no exceptions
- Proactive agents notify Teo via TeoNotification — never contact Tomas directly
- Deploy at the end of every chapter
- Every chapter ends with: polish sprint + look-back sprint (Ch II+) + handover sprint
Vector Infrastructure (Chapter VI)
SQLite is extended with sqlite-vec for semantic vector search. No separate database — vectors live in the same db.sqlite3 file alongside all other tables.
Extension Loading
sqlite-vec is loaded on every DB connection via a connection_created Django signal handler defined at module level in agent_service/apps.py. Module-level placement prevents garbage collection. Cross-platform path resolution handles .so (Linux), .dll (Windows), and .dylib (macOS).
vec_chunks Virtual Table
CREATE VIRTUAL TABLE IF NOT EXISTS vec_chunks
USING vec0(
chunk_id INTEGER PRIMARY KEY,
embedding FLOAT[3072]
)
Maps 1:1 with ChapterChunk.id. Created automatically via post_migrate signal on first manage.py migrate. IF NOT EXISTS makes it idempotent on all subsequent runs.
Embeddings
genai/embeddings.py — pure Python, zero Django imports.
generate_embedding(text)→ 3072 floats viagemini-embedding-001serialize_embedding(vector)→ 12288-byte binary blob for sqlite-vec storagedeserialize_embedding(blob)→ list of 3072 floats
Embedding model: gemini-embedding-001 (3072 dimensions). Raw sqlite3 connection used for all vec_chunks operations — Django's cursor wrapper mangles binary blobs.
Embedding Pipeline (Chapter VII)
ChapterChunk records are automatically embedded into vec_chunks via Django signals defined at module level in agent_service/apps.py.
- On create:
on_chunk_savedcallsgenerate_embedding(chunk.content), serializes, inserts intovec_chunks. Failures are logged and swallowed — never crash the lock pipeline. - On delete:
on_chunk_deletedremoves the matching row fromvec_chunksbychunk_id. - Historical backfill:
manage.py embed_chunks— delta mode (skips existing),--force(re-embed all),--dry-run(preview only).
ChapterChunk.id = chunk_id in vec_chunks. They are always in sync.