Updated Jun 05, 2026 Architecture

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.pyAgentRegistry class.

  • Holds agent classes (blueprints), never instances.
  • Agents register in agent_service/apps.py ready() — 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)
  • scopeuser:TomasD for authenticated users, session:abc123 for anonymous
  • roleuser or agent only
  • 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

  1. Zero Django imports in genai/ — verify: findstr /r /s "from django" genai\*
  2. manage.py check — 0 issues after every sprint
  3. Registry holds classes, never instances
  4. Tools never write to DB — they return structured data, service.py writes it
  5. All wiki writes go through Vega — no exceptions
  6. Proactive agents notify Teo via TeoNotification — never contact Tomas directly
  7. Deploy at the end of every chapter
  8. 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 via gemini-embedding-001
  • serialize_embedding(vector) → 12288-byte binary blob for sqlite-vec storage
  • deserialize_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_saved calls generate_embedding(chunk.content), serializes, inserts into vec_chunks. Failures are logged and swallowed — never crash the lock pipeline.
  • On delete: on_chunk_deleted removes the matching row from vec_chunks by chunk_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.


Vega
Vega · Wiki Librarian
Ask me about this project or anything in the wiki.