CODEX // oaoisme wiki

§ unswayed-backend · Chat & messaging (Phase 9)

Lexi AI assistant

updated 2026-06-10

Lexi AI assistant

Lexi is the public, throttled AI assistant (§9) — the second half of Phase 9, fully independent of the chat domain. Two routes, no database models, one big prompt. It lives in src/chat-ai/.

The wire

POST /api/ai/assistant takes question (≤ 1500 chars) and an optional session_id (UUID). The response is always { answer, session_id } — the session id is generated server-side when omitted and always echoed back, so the first response seeds the thread id the client persists. POST /api/ai/assistant/reset forgets a session ("New Chat"). Both routes are public (OptionalJwtAuthGuard — a bearer token personalizes, guests get the generic greeting) and hard-throttled at 30 requests/minute/IP (the legacy throttle:30,1, enforced route-level regardless of the global limit).

The prompt is a versioned artifact

The legacy buried a ~43 KB "Lexi v2.0" system prompt — the entire platform knowledge base, tone rules, and guardrails — in a PHP heredoc inside the service. The rebuild ports it verbatim (byte-verified: same length, same 799 lines, the \$ escapes unescaped) into lexi-prompt.v2.ts, a standalone versioned file that can be reviewed and revised independently of the service code. The per-user session-context and identity lines are ported with their exact strings; since the rebuild's users table has no name columns, personalization resolves names through the same shared applicant/employer name resolution every resource uses.

Sessions: ephemeral by design, Redis when available

History lives under lexi_session_<uuid>10 turns (20 messages), 2-hour TTL refreshed on every save — exactly the legacy cache semantics. The store is a small SessionStorePort with two adapters chosen at boot, mirroring the queue's optional-Redis posture (ADR-0020): when the queue's REDIS_URL detection finds a reachable Redis, sessions go there (SETEX); otherwise a bounded, clock-injected in-memory map serves dev/test. Ephemeral is a feature, not a bug — a cache flush loses the thread, which is the documented legacy behavior; persistence/analytics is flagged future work.

The completion port and the 503 taxonomy

The OpenAI call sits behind AiCompletionPort — gpt-4o (from OPENAI_MODEL) with the verbatim sampling params (max_tokens 800, temperature 0.6, top_p 0.95, frequency_penalty 0.1, presence_penalty 0.0) so answers stay on-brand. Every failure class maps to a user-safe 503 with the exact legacy strings: a missing OPENAI_API_KEYThe AI assistant is not configured. Please contact support. (the app boots and the whole test pyramid runs without a key — that 503 is the contract, like JSearch's); transport failures → Could not reach the AI assistant. Please check your connection and try again.; an empty/odd response → Received an unexpected response from the AI. Please try again.; provider/quota errors per the legacy switch. Nothing provider-internal ever leaks; anything genuinely unexpected is the logged 500 fallback.

Testing it

The unit suite (131 tests) pins session resolution, the history cap/TTL via an injected clock, the personalized-vs-guest prompt branch, both stores, every 503 string, and the throttle metadata. The e2e suite proves history actually flows without poking internals: it overrides AiCompletionPort with a scripted fake whose answer encodes the received messages array — so turn two demonstrably sees turn one, and a reset demonstrably forgets it. The unconfigured-503 and the 31st-request-429 each get their own app instance.