Alkiviades — Architecture¶
Overview¶
Alkiviades is an agentic AI chat system for strategic communication, political campaigns, and marketing. It combines retrieval-augmented generation (RAG) over a curated knowledge base with agentic web search, and modulates every response along three configurable political axes.
System Components¶
Browser (Next.js static SPA)
│
├── POST /auth/login ───────────────────► FastAPI (Python)
├── POST /chat/stream (SSE) ────────────► ├── Auth (JWT + httpOnly refresh cookies)
├── GET /sessions ──────────────────────► ├── PostgreSQL (users, sessions, messages)
└── POST /rebuild ──────────────────────► └── Agent Loop
│
├── RAG (TF-IDF retrieval)
├── LLM (streaming + tool calls)
├── Tools (web search, scraping, file ops)
└── Three-axis modulation
The frontend is a static single-page application. The backend exposes a REST + Server-Sent Events (SSE) API. All chat interactions are streamed token-by-token over SSE.
Data Flow — One Question¶
- The client sends the question with a JWT bearer token.
- The agent retrieves the most relevant passages from the knowledge base using TF-IDF cosine similarity.
- The messages are assembled: system prompt (with the current axis settings) + conversation history + the question with retrieved context.
- The LLM streams its answer; the stream yields
tokenevents to the client. - If the LLM signals a tool call, the tool executes (web search, scraping, file operation) and the result is appended to the context; the loop repeats (up to a fixed iteration limit).
- The finished turn is persisted to the database.
Retrieval¶
- Vectorization — TF-IDF over the knowledge base vocabulary, computed locally with numpy. Language-agnostic: Greek and English are handled identically.
- Storage — vectors are cached to disk in a compact float16 format and memory-mapped for low RAM usage.
- Query — the question is vectorized the same way, then ranked against all chunks by cosine similarity; the top results become the context for the answer.
The Three Axes¶
Every response is generated under three axis values, each in [-1, 1]:
| Axis | -1 |
1 |
|---|---|---|
| Ethical | No ethical restraint | Absolute ethical adherence |
| Alignment (economic) | Right / capitalist | Left / socialist |
| Social freedom | Authoritarian | Libertarian |
The system prompt is assembled at runtime from these values (quantized to 7 positions per axis), so changing a slider changes the next answer — not the model, not the knowledge base.
Context Management¶
Conversation history stores raw user questions — RAG chunk context applies only to the current turn, so earlier exchanges stay visible to the model. The context window is 120K tokens (env-configurable MAX_CONTEXT_TOKENS). When the accumulated context approaches the budget, older messages are automatically summarized into a progressive structured recap (topics, facts/names/dates, decisions, open questions) that merges the previous summary, keeping the most recent 6 messages intact. Replies cut by the token limit are auto-continued (up to 3 rounds) and stitched, so truncated answers never enter history. This keeps long sessions responsive without losing continuity.
Sessions and Accounts¶
- Users — email + password; passwords stored as bcrypt hashes.
- Sessions — each conversation is a session with a title, axis settings, and a message log with sources, tool calls, and retrieval metadata.
- Auth — short-lived JWT access tokens plus long-lived httpOnly refresh cookies with rotation. Admin-only endpoints are role-gated.
Security Model¶
- Passwords bcrypt-hashed; never logged.
- JWT + rotating httpOnly refresh cookies; cookies use SameSite/Secure policies in production.
- CORS restricted to an explicit origin allowlist.
- The agent's file tools are restricted to a dedicated workspace directory.
- All database access uses parameterized queries.
- Web search results are cached locally and cited in responses.
Diagrams¶
Request flow (SSE chat)¶
sequenceDiagram
participant B as Browser (SPA)
participant A as FastAPI
participant D as Supabase
participant L as DeepSeek
participant T as Tools (Tavily/Diavgeia/ScrapingAnt)
B->>A: POST /chat/stream (Bearer + X-DEK)
A->>D: get_session (decrypt history w/ DEK, lazy re-key)
A->>A: RAG retrieve + skill routing
A->>L: stream (system prompt + history + question)
L-->>A: tokens (SSE)
A-->>B: event: token
loop tool calls (max 5)
L-->>A: tool_calls
A->>T: execute_tool
T-->>A: result
A->>L: tool result
end
A->>D: add_turn (AES-GCM ciphertext)
A-->>B: event: done (server session id)
Encryption key hierarchy¶
graph TD
PW[User password] -->|PBKDF2-SHA256 600k, browser only| KEK[KEK 256-bit]
KEK -->|wrap| WDEK[wrapped_dek stored in users table]
KEK -->|unwrap at login| DEK[DEK 256-bit]
DEK -->|X-DEK header per request| SRV[Server RAM, per request]
SRV -->|AES-GCM encrypt| DB[(Supabase ciphertext)]
SALT[dek_salt public] --> KEK
Skills routing¶
flowchart LR
Q[User question] --> R[route_skill]
S[skills/*.md] --> L[load_skills cached]
L --> R
R -->|phrase 3pts / word 1pt, Greek-stemmed + acronyms| M{score > 0?}
M -->|yes, top by priority| I[inject active_skill into system prompt]
M -->|no| N[no skill]
I --> E[StatusEvent: Skill name]
Frontend data flow¶
flowchart TD
LOGIN[LoginModal] -->|api.login| BOOT[bootstrapCrypto]
BOOT -->|PBKDF2| DEK[(sessionStorage DEK)]
SEND[ChatInput send] --> UC[useChat.send]
UC --> SSE[createSSEStream + X-DEK]
SSE -->|token| STORE[store.appendToLastMessage]
STORE --> BUBBLE[MessageBubble]
SSE -->|done| SID[adopt server session id]
SID --> LIST[fetchSessions to sidebar]