Mirror · technical architecture
A person keeps a journal and speaks out, gets reports on their psychological state, and the system ranks registered therapists by how effective they can be for that specific person. Below — how that works under the hood.
The whole product is a single docker-compose application — one command to deploy. The API is client-agnostic: iOS/Android/web clients plug into the same use-case layer later without duplicating logic.
docker-compose stack · reverse proxy with auto-TLS on a cloud VPS
Latency-sensitive dialog, heavy async analytics and quality control are separated — each contour scales and fails independently.
Real-time, one turn at a time. LangGraph state machine picks the conversational move; RAG grounds persona replies.
Async jobs: daily/weekly reports, psychological portrait updates, multi-agent therapist matching.
Offline experiments with LLM-as-judge and guard metrics; live A/B analytics per prompt version. No prompt reaches production on "looks good".
The dialog is a graph of states, not a single prompt. Every client message runs through the graph; the crisis check is a hard edge — a prompt can be talked around, a graph edge cannot.
LangGraph state machine · gestalt interventions (e.g. empty-chair practice) attach as separate subgraphs behind the router
The graph generates several candidate replies, scores them and picks the move instead of one-shot generation. Currently disabled via config to keep per-turn cost low for clients — the graph path stays and re-enables per persona.
For the Yalom persona a vector lookup runs before the model call — his published books are chunked and indexed, replies are grounded in real case-work examples. The same pipeline extends to registered therapists as their session history accumulates.
Every LLM response that feeds business logic is parsed into a Pydantic schema — never used as raw text. LangChain handles prompt templates and structured-output parsing inside graph nodes.
All model access goes through an LLMPort interface with three implementations: Gemini (production), Ollama (local experiments), stub (fast tests). Swapping providers is a config change, not a refactor.
Long AI analysis never blocks the live dialog. Reports run as LangChain chains; therapist matching is a multi-agent pipeline where each agent has its own prompt and typed output.
multi-agent matching pipeline · daily/weekly reports run as separate LangChain chains over the same history
Every LLM call is metered against a per-user balance ("yaloms") — cost control and a path to monetization from day one.
Voice messages and handwritten notes are recognized via Gemini multimodal and enter the same journal pipeline as text.
The differentiator isn't persona artwork — it's how the therapist talks. That is the hardest thing to verify, so it gets its own pipeline.
one full run = 302 complete 10-turn conversations · judge trusted only after 80%+ agreement with manual grading · a version that breaks a guard threshold is rejected regardless of other numbers
One rule keeps the codebase honest as it grows toward multiple clients: business logic never lives in a transport handler.
app/core/use_cases/) — LLM calls, DB writes, crisis detection, report generation, portrait updates, matching. Anything a future web client would also need.app/bot/) — Telegram transport only: parse the event, call a use case, render the response. No LLM calls, no direct DB writes.Privacy is critical for therapy data — the next steps move inference and sensitive data fully under the product's control.
Deploy a local model (Ollama/vLLM) and plug per-persona LoRA adapters — a distinct communication style for each therapist persona, and client data stops leaving the perimeter. Local provider and experiments already in place.
Personal data stripped/masked before any external API call while a cloud LLM remains the production provider.
Sensitive dialog content encrypted in the database.
Replace the manual "analyze losers → generate new prompt → re-run" cycle with DSPy, using the calibrated LLM-judge as the metric. Plus judge-scores on sampled production traffic pushed to Langfuse.