Current State Architecture Map — Psychology Book Assistant / Partner API
Implemented backend state, Book DB decision, LLM/retrieval wiring, coach profile model, frontend prototype status, and next implementation plan.
Generated: 2026-06-04
Scope: /home/legion/Projects/psychology-book-assistant backend plus /home/legion/Projects/soulmates React prototype.
Rule: this document separates implemented state from gaps/recommendations and cites source files with line anchors.
Executive summary
The backend is a Python/FastAPI partner API wrapped around a source-grounded psychology book assistant. It has three separate persistence concerns:
- Book/corpus DB:
data/assistant.sqlite— canonical book index and search source. - Partner API state DB:
data/partner_api.sqlite— couples, consents, conversations, messages, reviews, webhooks, idempotency. - Local assistant session DB:
data/sessions.sqlite— Telegram/local chat memory only.
Use data/assistant.sqlite as the Book DB. Do not merge corpus tables into partner_api.sqlite; partner state should reference answer outputs and audit metadata, not own the book corpus.
High-level architecture
flowchart LR Partner[Partner client] -->|Bearer token + scope| API[FastAPI Partner API /api/v1] API --> Auth[security.py token + scope checks] API --> Store[(data/partner_api.sqlite)] API -->|safe_answer| Agent[answer_with_tools] Agent --> Search[hybrid_search] Search --> Corpus[(data/assistant.sqlite FTS)] Search -. optional .-> Pinecone[Pinecone semantic sidecar] Agent --> LLM[OpenRouter or OmniRoute chat completions] API --> Messages[Persist user + assistant messages] Prototype[soulmates React prototype] -. not connected .-> API
Source anchors: FastAPI defaults and app setup are in src/psych_assistant/api/app.py:44-67; partner request headers/logging are in app.py:69-107; LLM provider constants are in src/psych_assistant/llm.py:11-12; hybrid search fallback is in src/psych_assistant/hybrid_search.py:15-47.
Implemented backend modules
| Area | Implemented now | Source |
|---|---|---|
| Package/runtime | Python >=3.11, FastAPI, Pinecone, Uvicorn, script entrypoints psych-assistant and psych-partner-api. |
pyproject.toml:1-17 |
| API app | FastAPI app title/version, OpenAPI at /api/v1/openapi.json, store defaulting to data/partner_api.sqlite, answerer defaulting to default_answerer. |
src/psych_assistant/api/app.py:52-67 |
| Request envelope | X-Request-Id, X-API-Version, X-Build-Id, no-store cache headers, placeholder rate-limit headers, structured request logs. |
src/psych_assistant/api/app.py:69-107 |
| Auth | Bearer-token auth from PARTNER_API_TOKENS, default token dev-partner-token, scopes from PARTNER_API_SCOPES. |
src/psych_assistant/api/security.py:39-74 |
| Error contract | Problem Details responses with sanitized validation errors and safe 500s. | src/psych_assistant/api/errors.py:36-95 |
| Partner store | SQLite tables for couples, consents, intakes, conversations, messages, exercises, feedback, quality reviews, webhooks, idempotency. | src/psych_assistant/api/store.py:27-112 |
| Book index | SQLite books, chunks, chunks_fts with FTS5 tokenizer; rebuild reads manifest EPUBs and chunks text. |
src/psych_assistant/index.py:24-87 |
| Lexical search | FTS query + author/role filters + fallback LIKE search. |
src/psych_assistant/index.py:90-207 |
| Hybrid search | SQLite FTS first, optional Pinecone semantic search, reciprocal rank fusion, FTS fallback on vector error. | src/psych_assistant/hybrid_search.py:15-78 |
| LLM | Provider switch via LLM_PROVIDER; OpenRouter default; OmniRoute supported with its own URL, model, and secret name. |
src/psych_assistant/llm.py:86-140, llm.py:186-269 |
| Agent | Tool-calling path with deterministic direct-retrieval fallback when tool routing is off or provider fails. | src/psych_assistant/agent.py:146-179, agent.py:242-383 |
Partner API endpoint map
| Method | Path | Scope | Implemented behavior | Source |
|---|---|---|---|---|
| GET | /api/v1/health |
Public | Health, version, timestamp, build metadata. | app.py:109-111 |
| GET | /api/v1/version |
Public | Build metadata from env/defaults. | app.py:113-115, app.py:768-776 |
| GET | /api/v1/capabilities |
Authenticated | Languages, frameworks, safety rules, limits, partner id. | app.py:117-129 |
| GET | /api/v1/coaches |
coaches:read |
Static coach catalog: balanced, soft, analytic. |
app.py:131-155 |
| POST | /api/v1/couples |
couples:write |
Creates partner-scoped couple; idempotency required; strips private fields. | app.py:157-189 |
| GET | /api/v1/couples/{couple_id} |
couples:read |
Fetches only if object belongs to partner. | app.py:191-198, app.py:625-630 |
| PATCH | /api/v1/couples/{couple_id} |
couples:write |
Updates public couple fields and summary. | app.py:200-216 |
| POST | /api/v1/couples/{couple_id}/consents |
consents:write |
Records consent, updates couple consent status, idempotent. | app.py:218-257 |
| POST | /api/v1/couples/{couple_id}/intakes |
intakes:write |
Stores intake summary/themes for couple memory. | app.py:259-291 |
| POST | /api/v1/conversations |
conversations:write |
Creates active conversation for a couple and optional coach id. | app.py:293-330 |
| GET | /api/v1/conversations/{conversation_id}/messages |
messages:read |
Lists paginated messages; requires active consent. | app.py:332-349, app.py:673-679 |
| POST | /api/v1/conversations/{conversation_id}/messages |
messages:write |
Saves user message, safety-checks, calls answerer, saves assistant message. | app.py:351-416 |
| POST | /api/v1/safety/checks |
safety:write |
Returns keyword-based risk flags/escalation policy. | app.py:418-438, app.py:681-700 |
| PATCH | /api/v1/exercises/{exercise_id} |
exercises:write |
Upserts exercise status/notes/reflection. | app.py:440-450 |
| POST | /api/v1/messages/{message_id}/feedback |
feedback:write |
Stores feedback against an existing partner-owned message. | app.py:452-469 |
| GET | /api/v1/quality/cases |
quality:read |
Lists partner-safe redacted quality cases. | app.py:471-483, app.py:641-655 |
| POST | /api/v1/quality/reviews |
quality:write |
Stores review verdict/scores/learning status. | app.py:485-494 |
| GET | /api/v1/webhook-endpoints |
webhooks:read |
Lists partner webhook endpoints. | app.py:496-502 |
| POST | /api/v1/webhook-endpoints |
webhooks:write |
Creates endpoint; idempotency required. | app.py:504-535 |
Notes:
- The OpenAPI contract is checked in at
docs/api/partner-api-v1.openapi.yaml; tests assert app paths cover the contract intests/test_partner_api.py:250-257. conversations:readis defined insecurity.py:18, but the currently implemented message-list endpoint usesmessages:read.- Idempotent create-like operations call
replay_response/remember_response; conflict behavior is inapp.py:559-599.
Which DB should be used for Book?
Use data/assistant.sqlite.
| DB | Current role | Current tables/counts | Decision |
|---|---|---|---|
data/assistant.sqlite |
Canonical corpus/search DB. | books=40, chunks=17964, chunks_fts=17964. Schema is books, chunks, chunks_fts. |
Book DB. Keep as source of truth. |
data/partner_api.sqlite |
Partner API operational state. | couples, consents, conversations, messages, feedback, quality_reviews, webhook_endpoints, idempotency. |
Do not store book chunks here. Store conversation/output/audit state only. |
data/sessions.sqlite |
Local assistant/Telegram memory. | sessions, messages, answer_feedback. |
Do not use for Partner API or Book corpus. |
| Pinecone index | Optional semantic sidecar. | Index psychology-book-assistant, namespace books-v1, source ids book_id:chunk_index. |
Not a source of truth; rebuild from assistant.sqlite when needed. |
Why: README explicitly states SQLite FTS is the local search index and canonical source of truth, while Pinecone is only an optional semantic sidecar (README.md:7-12, README.md:61-63). The physical Book DB schema in index.py contains only books and chunks (src/psych_assistant/index.py:24-50).
LLM and retrieval integration
Provider selection
OPENROUTER_URLandOMNIROUTE_URLare constants insrc/psych_assistant/llm.py:11-12.chat_completionandchat_messageschooseomnirouteonly when.envhasLLM_PROVIDER=omniroute; otherwise they use OpenRouter (llm.py:86-96,llm.py:130-142).- OpenRouter keys are discovered from
OPENROUTER_API_KEY,FREE_OPENROUTER_LLM,FREE OPENROUTER LLM, or normalized env keys containingOPENROUTER(llm.py:48-63). - OmniRoute chat messages default to model
codex/gpt-5.3-codex-spark, chat URLhttp://158.69.1.230:20128/api/v1/chat/completions, and secret nameSPLOX APIunless env overrides are set (llm.py:231-242).
Retrieval path
- Partner message hits
POST /api/v1/conversations/{conversation_id}/messages. - API requires active consent, saves the user message, runs
assess_safety, then callssafe_answer(app.py:351-383). safe_answerblocks crisis/diagnosis cases, otherwise calls the app answerer (app.py:703-711).- Default answerer calls
answer_with_tools(question, DEFAULT_DATABASE, DEFAULT_PROFILES, DEFAULT_ENV, mode=PARTNER_API_DEFAULT_MODE or soft, conversation_context=...)(app.py:714-722). - Agent uses direct retrieval unless
AGENT_TOOL_ROUTER_ENABLED=1; direct retrieval uses hybrid search, source chunk fetch, response planning, and final answer generation with fallback behavior (agent.py:146-179,agent.py:242-383). - Hybrid search uses SQLite FTS first, calls Pinecone only when
HYBRID_SEARCH_ENABLEDis true, then fuses results; vector errors fall back to FTS (hybrid_search.py:15-47,hybrid_search.py:76-78). - Vector config requires
VECTOR_SEARCH_ENABLED,VECTOR_PROVIDER=pinecone, a Pinecone key, index/name/namespace/model settings; semantic matches are mapped back to SQLite source chunks (vector_search.py:46-69,vector_search.py:103-124).
Active configuration from existing docs
The current ops map says the active setup is:
LLM_PROVIDER=omniroute- model
codex/gpt-5.3-codex-spark - chat endpoint
http://158.69.1.230:20128/api/v1/chat/completions - API secret name
SPLOX API - Pinecone index
psychology-book-assistant, namespacebooks-v1 - Pinecone secret should be
PINECIDE API; oldPINECODEvalidation returned 401
Source: skills/psychology-book-assistant-ops/references/system-map.md.
Psychologist/coach profile model
There are two different profile concepts today.
1. Author profiles for book-grounded answer style
data/author_profiles.json stores author-oriented guidance: voice, core_themes, best_for, avoid_for, and weight. The loader builds immutable AuthorProfile records and resolves author names by exact match, substring match, or surname (src/psych_assistant/profiles.py:8-52). Example profiles include Freud, Jung, Fromm, Winnicott, Klein, Berne, Le Bon, Kohut, Csikszentmihalyi, and Cialdini (data/author_profiles.json:1-75).
These profiles are not partner-facing coach identities. They are style/relevance metadata for grounded answers from source authors.
2. Partner API coaches
GET /api/v1/coaches currently returns a static catalog with three coach ids:
balanced— Balanced source-grounded advisor.soft— Gentle relationship support advisor.analytic— Analytic reflection advisor.
All three use persona_policy = source_grounded_style_not_person_impersonation (src/psych_assistant/api/app.py:131-155). This means the API intentionally exposes coach modes, not real psychologist impersonations.
Recommendation
Keep this split:
- Author profiles stay internal to retrieval/answer style.
- Partner API coaches should become a first-class catalog only if product needs coach UX metadata such as display name, specialty, safety policy, language, and allowed modes.
- Do not expose author profiles directly as psychologist personas without a product/legal review.
React prototype status: /home/legion/Projects/soulmates
The soulmates repo is a Vite + React + TypeScript prototype (package.json:1-21). It is not wired to the Partner API.
Implemented UI pieces:
App()renders static product sections:Header,Hero,ProblemSection,ShiftSection,ModesSection,CoupleSetupSection,IdeasWorkspace,ArchiveSection,JournalCoachSection,DashboardSnapshot(src/App.tsx:299-315).- Couple setup uses local React state for solo/couple mode, connected state, warmth, and feed text (
src/App.tsx:405-460). - Permission/ritual setup uses local state arrays (
src/App.tsx:532-615). - Ideas workspace uses local filters, saved idea ids, planned idea, and reaction state (
src/App.tsx:616-729). - Journal/coach area uses local text/tags/share/analyzed/booked state (
src/App.tsx:749-824). - Dashboard snapshot uses local week toggle and static metrics (
src/App.tsx:825-910).
Network status: a grep for fetch, axios, XMLHttpRequest, WebSocket, EventSource, /api/, http://, and https:// across source/docs only found the local Vite preview URL in docs/VERIFICATION_CHECKLIST.md. So the prototype is product/UI only.
Current test coverage
Implemented tests cover:
- Health/version headers and public endpoints (
tests/test_partner_api.py:34-45,tests/test_partner_api.py:260-271). - CORS not wildcard-enabled (
tests/test_partner_api.py:48-56). - Auth, missing scopes, partner object isolation (
tests/test_partner_api.py:59-92). - Idempotent couple creation and location headers (
tests/test_partner_api.py:95-121). - Private-note stripping/redaction (
tests/test_partner_api.py:124-144,tests/test_partner_api.py:309-345). - Consent-gated message flow and grounded reply shape (
tests/test_partner_api.py:147-231). - Safety high-risk flags (
tests/test_partner_api.py:234-247). - OpenAPI path coverage (
tests/test_partner_api.py:250-257). - Session isolation/reset for local assistant memory (
tests/test_session_memory.py:4-33).
Gaps and risks
| Gap/risk | Why it matters | Evidence |
|---|---|---|
| Partner API uses dev token defaults. | Production needs real token provisioning/rotation, per-partner scopes, rate limits, audit controls. | security.py:64-74 |
| Rate-limit headers are placeholders. | Headers say limit/remaining/reset but no enforcement exists in middleware. | app.py:92-96 |
| Safety check is keyword-based. | Crisis/violence/diagnosis detection is simple string matching, not robust clinical safety. | app.py:681-700 |
| Grounding score in Partner API response is hardcoded by safety level. | Message response does not expose actual retrieval source ids yet. | app.py:384-391 |
| Coaches are static modes, not persisted profiles. | Product may expect configurable psychologist/coach profiles, specialties, languages, schedules. | app.py:131-155 |
| Webhooks are stored but no dispatcher is implemented. | Partners can register endpoints, but events are not sent. | app.py:496-535, store.py:92-99 |
partner_api.sqlite is SQLite. |
Fine for prototype; production needs migrations, backups, connection strategy, and possibly Postgres. | app.py:48, store.py:27-112 |
| React prototype is disconnected. | UX cannot exercise real consent/conversation/message flows yet. | soulmates/src/App.tsx:299-315, grep result described above |
| Book corpus paths point to local downloads. | Rebuild portability depends on local files staying present. | data/books_manifest.json:5-120 |
| Pinecone secret naming drift. | Existing code defaults to PINECODE; ops notes say working secret is PINECIDE API. |
vector_search.py:52-59, ops map |
Recommended next implementation
- Make Partner API message grounding real in responses. Return selected
source_ids, titles, authors, and groundedness/coverage fromanswer_with_toolsinstead of hardcodedsources: []and score1.0. - Add production auth/rate-limit layer. Replace dev token defaults with partner-token records or external auth; enforce the rate limit that headers currently advertise.
- Create a persisted coach catalog. Keep
balanced/soft/analyticas modes, but store partner-facing coach metadata separately from author profiles. - Wire
soulmatesto the Partner API. Start with: create couple → record consent → create conversation → send message → render assistant answer and audit/source metadata. - Add webhook dispatch/outbox. Store outbound events in a durable outbox table and deliver to registered endpoints with retries/signatures.
- Normalize environment and secret names. Set
PINECONE_API_KEY_SECRET_NAME=PINECIDE APIexplicitly; document OmniRoute/OpenRouter switch in deployment runbook. - Plan DB migration path. Keep
assistant.sqlitefor Book corpus, but move partner operational state to Postgres when concurrency/multi-instance deployment starts. - Add integration tests for real retrieval payloads. Mock LLM/provider but assert the API carries actual source metadata and consent/safety behavior.
Source index
- Backend README:
README.md:1-63 - Package metadata/scripts:
pyproject.toml:1-24 - FastAPI app/defaults/routes:
src/psych_assistant/api/app.py:44-780 - Partner auth/scopes:
src/psych_assistant/api/security.py:11-74 - API errors:
src/psych_assistant/api/errors.py:36-95 - Request models:
src/psych_assistant/api/models.py:8-142 - Partner store schema:
src/psych_assistant/api/store.py:27-112 - Book index/search:
src/psych_assistant/index.py:24-207 - Hybrid retrieval:
src/psych_assistant/hybrid_search.py:15-95 - Vector/Pinecone:
src/psych_assistant/vector_search.py:46-124,vector_search.py:139-189 - LLM providers:
src/psych_assistant/llm.py:11-269 - Author profiles:
src/psych_assistant/profiles.py:8-52,data/author_profiles.json:1-75 - Book manifest sample:
data/books_manifest.json:1-120 - Partner API tests:
tests/test_partner_api.py:34-345 - Session tests:
tests/test_session_memory.py:4-33 - React prototype package/UI:
/home/legion/Projects/soulmates/package.json:1-21,/home/legion/Projects/soulmates/src/App.tsx:299-910