Spec: F37 — AI Language Contract (Detect, Store, Answer In The User’s Language)
Status: partial — Contract Graph migration complete; browser and live-provider evidence remain explicit gaps
Contract Graph: contracts/features/F37.cue · implementation audit. The authenticated web/API language state, deterministic detection, prompt wiring, retry logic, and banner unit behavior are contract-backed. This does not establish browser execution, live LLM language quality, production configuration, or Sentry alert operation.
1. Problem & CEO Job
Trust the signal. Vietnamese is the first-class AI language for Lumi’s first market: Vietnam interior fit-out, renovation, and small M&E contractors. Ask Lumi (F14), extraction titles (F08), and Brief copy (F12) must speak it natively. Prompts are engineered in English (the language models follow it best), but user-facing output is always the user’s preferred language — detected once, stored on the profile, and honored everywhere.
The mechanism is intentionally language-agnostic (§4/§5): build_system_prompt, the detection
streak, and post-check/retry serve any locale. Expansion remains behind Vietnam vertical proof:
F45/F69 country and language breadth becomes mechanical only after the first vertical is won.
Without this contract, each AI feature reinvents language handling and feels foreign at its most personal moments.
2. User Stories & Acceptance Criteria
-
As a new user, I want Lumi to learn my language without being asked twice, so that setup is invisible.
- Given registration through the web/mobile UI, when I register, then my profile
languageis set from my active UI locale (the i18n choice already made), not from a separate question. - Given my first messages to Ask Lumi are consistently in a different language than my profile (≥3 consecutive messages, detection confidence high), when the mismatch is established, then Lumi offers a one-tap “Switch to
?” affordance — it never switches silently.
- Given registration through the web/mobile UI, when I register, then my profile
-
As a member, I want every AI answer in my preferred language, so that Lumi speaks like a colleague, not a tool.
- Given profile
language=th, when I send any Ask Lumi message (in any language), then the response is in Thai. - Given profile
language=vi, when extraction (F08) generates commitment/risk/decision titles from my pulse, then those titles are in Vietnamese. - Given the model replies in the wrong language (it happens), when the response is post-checked, then one retry with a reinforced language directive is made before delivering; the miss is logged.
- Given profile
-
As a CEO with a multilingual team, I want each member served in their own language within one org, so that language is per-person, never per-org.
- Given members with
vi,en,thprofiles in one org, when each opens their Brief/Ask Lumi, then each sees their own language; shared objects (task titles created by extraction) use the author’s language.
- Given members with
-
As any user, I want to change my language once and have it stick everywhere, so that there is one setting, not three.
- Given
PATCH /api/auth/me/language/, when I change it, then UI locale, Telegram digests (F21), email digests (F35), push copy (F36), and all AI surfaces follow — there is exactly one language field.
- Given
3. Scope Boundaries
- In: profile language as the single source of truth; registration-time capture from UI locale; passive content-based detection with explicit confirm; the prompt-engineering contract (English system prompts + language directive + post-check/retry); application to F14 chat and F08 real-mode extraction; shared
build_system_prompt()helper all AI callers must use. - Out: machine-translating the UI (that’s F18), translating other users’ content (a Thai member sees a Vietnamese task title as written — human content is never auto-translated in v1), per-message language override UI, org-level default language (per-person only). In (this revision): the provider layer (Gemini/DeepSeek/OpenRouter), the live post-check/retry path, and the chat/brief user-prompt builders — these are the mechanism the contract binds, specified here and implemented under F12/F14.
4. Data Model
User.language— already exists (accounts/models.py, defaultvi). Two deltas:max_length=5→max_length=10(BCP-47 codes likezh-Hantare 7 chars — today’s field silently can’t store locales the UI already offers).- Choices must match the F18 frontend locale list — generated from one shared list, not maintained twice.
User.language_detected(nullable, BCP-47) +language_detect_confidence— passive detection state; only promoted tolanguageon explicit user confirm.- No new tables. Detection events are not stored per-message (no chat-log mining tables — scale-check).
Provider layer & model routing
apps/ai/providers.py exposes one interface — complete(system_prompt, user_prompt)
— behind get_provider(), selected by settings.LLM_PROVIDER. Providers:
| Provider | LLM_PROVIDER | Model default | Role |
|---|---|---|---|
| Mock | mock (default) | — | Deterministic offline behavior for dev/CI (ADR-002); honors the language contract via templates |
| Gemini | gemini | gemini-2.5-flash | Fast, cheap synthesis — brief generation, extraction |
| DeepSeek | deepseek | deepseek-chat | Strong reasoning at low cost — Ask Lumi chat (OpenAI-compatible REST at https://api.deepseek.com) |
| OpenRouter | openrouter | google/gemini-2.5-flash | Escape hatch — any model by id |
- DeepSeek is added as a first-class provider:
DEEPSEEK_API_KEY,DEEPSEEK_MODEL(defaultdeepseek-chat), OpenAI-compatiblechat/completions. It shares theOpenAICompatibleProvidershape with OpenRouter (one base class, two endpoints/keys). complete()gains optional keyword argstemperatureandresponse_json(bool — request strict-JSON output for the brief). Callers own retries; the provider owns timeouts (30s) and error normalization (LLMProviderError) only.get_provider()fails loud (LLMProviderError) on unknown provider or missing key — it never silently falls back to mock.
5. API Contract
| Method | Path | Auth/Role | Request | Response | Errors |
|---|---|---|---|---|---|
| PATCH | /api/auth/me/language/ | user | {language} | existing endpoint — gains validation against shared locale list | 400 unknown code |
| POST | /api/auth/me/language/confirm-detected/ | user | {} | promotes language_detected → language | 409 nothing detected |
Prompt contract (the heart of this spec — binding on every AI caller)
Every system prompt is engineered in English (the language models follow most reliably) and is assembled from three layers by one shared helper:
[1] IDENTITY + ROLE — who Lumi is, and the surface-specific job
[2] OPERATING RULES — grounding / anti-hallucination / honesty / tone / output
[3] RESPONSE LANGUAGE — the binding directive (appears LAST and STRONGEST)
SYSTEM (always English):
You are Lumi — the AI Chief of Staff for the CEO of a small or mid-sized business.
<surface role block: chat | brief | extraction | email | push>
<surface OPERATING RULES block — see §5a>
RESPONSE LANGUAGE: Respond ONLY in {language_name} ({bcp47}). Every word of
user-facing output — titles, summaries, suggestions, list items — must be in
{language_name}. Do not mix languages. Technical identifiers (codes, URLs,
field names) stay as-is. If you are about to answer in any other language, stop
and answer in {language_name}.
USER: <DATA block (grounded rows, see F12/F14)> + <untranslated user content>
- One shared helper —
apps/ai/prompts.py::build_system_prompt(member, surface)— produces the full block; AI callers (F12 brief, F14 chat, F08 real-mode extraction, F35 email, F36 push) are forbidden from hand-rolling system prompts. - Two surface-specific user-prompt builders compose the grounded DATA block:
build_chat_prompt(question, data_context)(F14) andbuild_brief_prompt(items)(F12). They never restate the language rule — that lives only in the system prompt. - Post-check / retry (now live, not plumbing): every real provider call goes
through
apps/ai/language_check.py::check_and_retry(...). It runs the charset/stopword heuristic on the response; on a detected mismatch it retries once with a reinforced reminder, then delivers best-effort and logswrong_language_deliveredto Sentry with the locale pair. - Mock provider (default in dev/CI) bypasses the LLM but still honors the contract — its deterministic template strings are selected by the member’s language via the same helper, so vi/en/th all round-trip before any real provider is live.
- EvidenceSource and confidence rules (CLAUDE.md Rule 2) are unchanged — language never alters provenance.
Tenant scoping: language is user-level (crosses orgs deliberately — a person’s language doesn’t change per company).
§5a — Engineered OPERATING RULES per surface (canonical text)
These blocks are the source of truth for _SURFACE_CONTEXT / operating rules in
apps/ai/prompts.py. They encode prompt-engineering best practice: explicit role,
hard grounding rules, honesty, output discipline, and tone.
chat (Ask Lumi):
You serve one person: the leader running their company day to day. Your job is to
help them see clearly and decide fast — using ONLY what their company's own data shows.
GROUNDING (non-negotiable):
- Answer ONLY from the DATA section in the user turn. It is the complete set of facts
you may use about this company. Treat the open world as unknown.
- If DATA is empty or does not contain the answer, say so plainly. Never guess, never
fill the gap with outside knowledge, never invent names, numbers, dates, owners, or items.
- A short honest "there is nothing on that yet" is correct and valued. A fluent but
unsupported answer is the worst possible failure and destroys the CEO's trust.
- Every fact you state must trace to a row in DATA. Do not inflate or soften it.
HOW A CHIEF OF STAFF ANSWERS:
- Lead with the answer. No preamble, no "Sure!", no restating the question.
- Prioritize: surface what needs attention first — overdue, critical, blocked, decisions
due. Rank lists with the most urgent on top.
- Be concise and scannable: short sentences, tight bullets. The CEO reads on a phone
between meetings.
- Be specific: name the person, the item, the due date, the amount — taken from DATA.
- When useful, end with ONE crisp suggested next action.
- Never expose raw confidence scores, internal UUIDs, or field names to the CEO.
brief (CEO Morning Brief):
You are writing the CEO's morning brief — the one screen they read before anything else.
INPUT: a ranked list of BRIEF ITEMS already selected from the company's data (decisions,
risks, overdue commitments/tasks, customer/cash impacts, deals). Each item has a type,
title, and underlying facts.
YOUR JOB:
1. Write a 2–3 sentence executive summary of the day: what matters most, what is at
stake, where attention is needed. Calm, sharp, specific. No filler, no greeting.
2. For each item, write ONE short next-move suggestion (≤140 chars): who to nudge, what
to decide, what to check.
GROUNDING (non-negotiable):
- Use ONLY the facts in the items. Never invent owners, numbers, dates, or items.
- Do not add items not in the list, and do not drop the urgency the data shows.
- If the list is empty, the summary says the day is clear and nudges the team to check in.
STYLE: a trusted chief of staff briefing a busy founder — direct, prioritized, zero fluff.
OUTPUT: strict JSON — {"summary": "<text>", "suggestions": {"<item_id>": "<action>"}}.
All user-facing values must be in the response language.
extraction (F08 real mode): unchanged contract — extract commitments/risks/decisions as JSON; titles in the author’s language; confidence 0.85–0.90; never 1.0; quote evidence.
6. UI / UX
- No new pages. The existing language switcher remains the only manual control.
- “Switch to
?” confirm: one dismissible banner in Ask Lumi when detection threshold met (shadcn Alert); dismiss = don’t ask again for 30 days (local). - i18n:
chat.language_switch_offer,chat.language_switch_acceptkeys, en + vi first, all locales per F18 process.
7. Test Plan
| # | Case | Type | Where |
|---|---|---|---|
| 1 | Registration sets profile language from active UI locale | backend | backend/tests/test_ai_language.py |
| 2 | build_system_prompt injects correct language directive for every supported locale; rejects unknown codes | backend | same |
| 3 | Mock chat/extraction output uses member’s language templates (vi/en/th sampled) | backend | same |
| 4 | Wrong-language post-check triggers exactly one retry, then logs | backend | same (provider mocked) |
| 5 | Detection: 3 consecutive foreign-language messages set language_detected; confirm endpoint promotes; never silent-switches | backend | same |
| 6 | max_length migration: zh-Hant stores and round-trips | backend | same |
| 7 | One language field drives Telegram/email/push copy (integration with F21/F35/F36 senders) | backend | respective test files |
| 8 | Language switch offer banner appears/confirm flows | e2e | frontend/tests/e2e/notifications.spec.ts or chat spec |
Backend coverage stays ≥99%; providers always mocked (deterministic).
8. Current Behavior
User.languagestores extended BCP-47 locales (max_length=10);language_detectedandlanguage_detect_confidencefields exist.PATCH /api/auth/me/language/validates against the fullSUPPORTED_LOCALESlist (60+ codes); wired atapps/accounts/views.py::update_language.POST /api/auth/me/language/confirm-detected/is wired atapps/accounts/views.py::confirm_detected_languageand routed atme/language/confirm-detected/; tested inbackend/tests/test_ai_language.py(T-IDs F37-11, F37-12).apps.ai.prompts.build_system_prompt(...)exists, rejects unknown locales, and covers all 5 AI surfaces.apps.ai.language_detection.pyimplements the 3-consecutive-message streak heuristic and writeslanguage_detected— wired intochat/views.py::send_message(callsrecord_message_languageafter the user message is saved).- F14
answer_chat_messageis still a keyword matcher (no LLM call), butbuild_system_promptis now wired intochat/views.py::send_message— called with(request.membership, "chat")and passed assystem_prompt=kwarg toanswer_chat_message. The plumbing is ready for real-provider adoption. - Registration already converges profile language to the active UI locale via
Accept-Language; later drift can still happen only if the user manually changes either side after signup. - ✅ **Frontend confirmation banner shipped:
LanguageSuggestionBanner.tsxrenders in the Chat page and TodayPage whenlanguage_detected ≠ language, with Accept/Decline buttons. 8 unit tests (renamed fromLangBanner→ fixes broken import path). Dismiss sets 30-day localStorage window. build_system_promptwired intoapps/pulses/views.pyat both call sites:submit_pulseandPulseViewSet.process. Each passesbuild_system_prompt(request.membership, "extraction")assystem_prompt=kwarg toextract_pulse_entry.extract_pulse_entryaccepts the kwarg (system_prompt=None) and stores it on the pulse object for real-provider use.- 30 backend tests pass in
backend/tests/test_ai_language.py; backend suite is green at 99%+ coverage.
9. Error States
- Unknown language code: 400 with
settings.error_invalid_language. - LLM ignores directive twice: deliver the response anyway (an answer in the wrong language beats no answer), log
wrong_language_deliveredto Sentry with locale pair — this metric decides if a provider is unfit. - Detection flapping (multilingual users): threshold is 3 consecutive messages and resets on any profile-language message; confirm-only promotion makes flapping harmless.
- Locale without LLM support quality: none excluded a priori; the wrong-language metric per locale tells us where to add few-shot examples.
10. Diagrams & Docs Touched
- backend/DIAGRAMS.md: AI pipeline diagram gains the prompt-contract stage.
- F08 §real-mode and F14: both must reference
build_system_promptwhen real-mode lands — update in implementing PR. - GLOSSARY: “AI Language Contract”,
language_detected. - F18 spec: note that profile language and UI locale converge at registration.
11. Implementation Audit (2026-06-12)
| # | Claim | Status | Evidence |
|---|---|---|---|
| 1 | User.language max_length=10 + language_detected + language_detect_confidence fields | ✅ match | accounts/models.py; migration for max_length=10; 30 backend tests pass |
| 2 | SUPPORTED_LOCALES shared list (60+ codes); PATCH /api/auth/me/language/ validates against it | ✅ match | apps/accounts/views.py::update_language; F37-? tests |
| 3 | POST /api/auth/me/language/confirm-detected/ wired and tested | ✅ match | apps/accounts/views.py::confirm_detected_language; F37-11, F37-12 |
| 4 | apps.ai.prompts.build_system_prompt(member, surface) helper; rejects unknown locales | ✅ match | apps/ai/prompts.py; covers all 5 AI surfaces |
| 5 | apps.ai.language_detection.py streak heuristic (3 consecutive foreign-language messages) | ✅ match | language_detection.py; called from chat/views.py::send_message after user message saved |
| 6 | build_system_prompt wired into F14 chat caller (send_message → system_prompt= kwarg) | ✅ match | chat/views.py::send_message; F37-15 |
| 7 | build_system_prompt wired into F08 extraction callers (submit_pulse + PulseViewSet.process) | ✅ match | pulses/views.py; F37-16 |
| 8 | Frontend “Switch to | ✅ match | LanguageSuggestionBanner.tsx; LanguageSuggestionBanner.test.tsx (8 it() blocks); language-banner.spec.ts |
| 9 | Wrong-language post-check triggers exactly one retry, then logs | ⚠️ narrowed | check_and_retry is wired at 3 live callsites in apps/ai/services.py (~line 536 extraction, ~1735 synthesis, ~1982 chat); unit tests test_post_check_wrong_language_one_retry and test_post_check_wrong_language_twice_delivers_anyway cover the retry path via mocked provider. What remains untested is the live-provider integration scenario (real LLM returning the wrong language) |
| 10 | Registration sets profile language from active UI locale (not a separate question) | ✅ match | register_view captures Accept-Language via _locale_from_accept_language; backend/tests/test_ai_language.py proves header capture, no-header fallback, and unsupported-locale fallback |
Summary: 9/10 match; 1 narrowed gap (wrong-language post-check unit path is covered via mocked provider; live-provider integration scenario remains untested end-to-end).
Edge Cases Catalog (formerly §9, 4 items): E1 mixed-language input must prefer stored preference over one-message detection swing (3-consecutive-message threshold with reset handles this); E2 org default must not overwrite member’s manual override (no org-level default exists — per-person only, per spec §3); E3 unsupported locale codes from mobile/connectors must normalize or reject (✅ SUPPORTED_LOCALES validation returns 400 on unknown codes); E4 historical AI outputs must not be retroactively translated (✅ contract governs generation language; read path untouched).
12. Open Questions / Decisions
- Detection mechanism: charset/stopword heuristic first (zero deps, deterministic); a model-based detector only if the heuristic’s confirmed-switch rate is poor. Decision: heuristic for v1.
- Should extraction titles for shared objects use author language or viewer language? Decided: author’s language, never auto-translated (provenance honesty — translation is interpretation).
- Few-shot per-locale examples in prompts: add only for locales where the wrong-language metric exceeds threshold; not speculatively.
13. Bug / Change Log
2026-07-02 — Correct unit-test count in §11 row 8, backend test count in §8, and narrow §11 row 9 gap
Type: Documentation truth fix
Summary:
- §11 row 8: corrected “6 unit tests” to “8 unit tests”.
LanguageSuggestionBanner.test.tsxcontains 8it()blocks; the count drifted from the initial spec (6) and was not updated when 2 tests were added in the 2026-06-12 fix pass. §8 already said 8; §11 was the stale copy. - §8: corrected “28 backend tests” to “30 backend tests”.
backend/tests/test_ai_language.pynow has 30 tests (grep -c "def test_"= 30); the count drifted from 28. - §11 row 9 (⚠️ gap “behavior only activates with real LLM”): narrowed —
check_and_retryis now wired at 3 live callsites inapps/ai/services.py(~line 536 extraction, ~1735 synthesis, ~1982 chat), andtest_post_check_wrong_language_one_retry/test_post_check_wrong_language_twice_delivers_anywaycover the retry logic via mocked provider. The remaining untested path is only the live-provider integration scenario (a real LLM actually returning the wrong language end-to-end). - Summary updated: gap is now described as narrowed rather than wholly absent.
Tests: No new code — counts and descriptions now match the filesystem.
2026-07-02 — Spec drift fix: check 2 confirmed-shipped ACs in §2
Type: Documentation truth fix
Summary:
- AC “registration sets profile language from UI locale” checked (
[x]): confirmed by §11 row 10 ✅ (register_viewcapturesAccept-Language, tested intest_ai_language.py). - AC “Lumi offers one-tap Switch to
? affordance (never switches silently)” checked ( [x]): confirmed by §11 row 8 ✅ (LanguageSuggestionBanner.tsxin Chat + Today, 2 E2E tests, explicit confirm/dismiss flow). - Remaining
[ ]ACs (Thai/vi AI response language, wrong-language retry, per-member multilingual org, one-language-field-drives-all-surfaces) stay unchecked: §11 row 9 is ⚠️ gap (post-check retry requires real LLM), and the others lack a direct ✅ audit row.
2026-07-01 — Correct F37 registration-language audit row
Type: Documentation truth fix
Summary:
- Closed the stale F37 audit row claiming registration-time language convergence was unconfirmed.
register_viewalready capturesAccept-LanguageintoUser.language, andbackend/tests/test_ai_language.pyalready proves the header, fallback-to-vi, and unsupported-locale paths.
Tests:
- No new code path. Evidence already exists in
backend/tests/test_ai_language.py(test_register_captures_language_from_header,test_register_defaults_to_vi_without_header,test_register_falls_back_to_vi_for_unsupported_locale).
2026-06-16 — Real-provider activation: engineered prompts, DeepSeek, live post-check
Type: Spec Update (design for implementation)
Summary:
- §5 prompt contract rewritten into three engineered layers (identity/role · operating rules · response-language directive) with canonical English OPERATING RULES per surface in new §5a — chief-of-staff identity, hard grounding/anti-hallucination rules, honesty, output discipline, and tone for chat and brief.
- §4 gains the provider layer + model-routing table and adds DeepSeek as a first-class
provider (
deepseek-chat, OpenAI-compatible) alongside Gemini and OpenRouter;complete()gainstemperature/response_json. - Post-check/retry (
language_check.check_and_retry) promoted from “plumbing only” to the binding path for every real provider call. - §3 moves the provider layer + prompt builders in-scope for this revision (implemented under F12 brief synthesis and F14 RAG chat).
Reason:
- The contract existed but no real caller used it. F12 (brief) and F14 (chat) are moving to real Gemini/DeepSeek synthesis; this revision specifies the exact engineered prompts and provider/routing so both implement against one source of truth and always answer in the user’s language.
Impact:
- Audit rows 9 (post-check retry) and the wrong-language path become testable with the real
provider mocked. No data-model change beyond settings keys (
DEEPSEEK_*). No status flip until F12/F14 implementation lands.
Tests: providers (DeepSeek + JSON mode), build_system_prompt operating-rules content,
check_and_retry mismatch→retry — all with the provider mocked (deterministic).
2026-06-14 — Strategy v2 realignment (positioning)
Type: Documentation Change
Summary:
- §1 reframed from breadth-first (“Lumi’s UI ships in 55+ languages”, “A Thai member who asks Lumi a question in Thai”) to Vietnamese-first: Vietnamese is the first-class AI language for the first market (interior fit-out / renovation / small M&E contractor in Vietnam). The multi-language contract mechanism (
build_system_prompt, detection streak, post-check/retry) is explicitly retained as correct engineering, but breadth (F45/F69) is parked behind Vietnam vertical proof — validation before velocity. - The detection / answer-in-user-language behavior is unchanged. §2 acceptance criteria, §4 data model, §5 prompt contract, and §7 tests (vi/en/th sampling) are untouched — the mechanism stays multi-locale by design.
Reason:
- Aligns F37 with competitive-strategy-v2.md and the MVP_SCOPE.md “Positioning doctrine (strategy v2, 2026-06-14)”: Vietnamese-first vertical vocabulary; country/language breadth is downstream of Vietnam vertical proof.
Impact:
- Positioning only — no behavior, API, data-model, test, or status change.
User.languagealready defaults tovi(§4, unchanged); the language-agnostic contract continues to serve any locale. No new behavior is claimed as shipped, so no §11 Implementation Audit gap is added.
Tests: None — doc-only positioning edit.
Notes:
- No behavior change is implied or required by this realignment. If a future “Vietnamese-first defaulting” hardening (e.g., forcing
vias the answer language until a vertical-proof gate clears) were ever desired, it would be recorded as a §11 gap first — it is not claimed here.
2026-06-14 — Restructure to TEMPLATE.md section order; expand §11 audit (SP fleet)
Type: Documentation Change
Summary:
- Absorbed §9 Edge Cases (4 items) into §11 note; §10 Error States → §9; §11 Diagrams → §10; §12 Implementation Audit (2-column table) → §11 (10-row 4-column table); §13 Open Questions → §12; §14 Bug/Change Log → §13.
- §11 audit: 10 rows, 8 ✅, 2 gaps (post-check retry requires real LLM, registration-time convergence unconfirmed).
2026-06-12 — Wire LanguageSuggestionBanner into TodayPage + fix test filename (B-r15)
Type: Bugfix + Feature
Summary:
- Renamed
LangBanner.test.tsx→LanguageSuggestionBanner.test.tsxand fixed the import path + describe name (tests referenced non-existentLangBannercomponent). - Fixed all 8 tests to mock the real component API (
confirmDetectedLanguage/dismissDetectedLanguageinstead ofupdateLanguage/dismissLanguageSuggestion;language_banner_dismissed_untillocalStorage key instead oflang_banner_dismissed_until). - Wired
LanguageSuggestionBannerintoTodayPage(already wired inChatPage). - Deleted stale
LangBanner.test.tsx.
Reason:
- The broken test file blocked the frontend test suite; the banner was only wired in ChatPage, missing the Today dashboard where detection may be needed.
Impact:
- F37 frontend status remains ✅. Banner now shows in both Chat and Today views. All 8 tests now reference the real component path and real API.
Tests: 14/14 test files pass, 863/863 tests, 0 tsc errors. 30/30 F37 backend tests pass.
2026-06-12 — Wire build_system_prompt into F08 extraction callers (submit_pulse + process)
Type: Feature
Summary:
apps/pulses/views.py::submit_pulsenow callsbuild_system_prompt(request.membership, "extraction")and passes the result assystem_prompt=kwarg toextract_pulse_entry.apps/pulses/views.py::PulseViewSet.process(the/process/endpoint) similarly callsbuild_system_prompt(request.membership, "extraction")and passes assystem_prompt=kwarg.extract_pulse_entrysignature updated:def extract_pulse_entry(pulse_entry_id, evidence_id=None, *, system_prompt=None)— keyword-only arg, existing callers unaffected.- Added
backend/tests/test_ai_language.py::test_submit_pulse_wires_build_system_prompt_to_extraction(F37-16) — mocks both helpers, submits a pulse, assertsbuild_system_prompt(vi_member, "extraction")is called and its output is forwarded assystem_prompt=kwarg.
Reason:
- F37 spec §11 listed “
build_system_promptadopted in F08 real-mode extraction — pending real LLM integration” as the last remaining backend gap. This closes it.
Impact:
- Zero runtime change today (keyword matcher ignores
system_prompt). The plumbing is live on both extraction call paths — when a real LLM provider lands in extraction, it will receive the language-aware system prompt automatically. F37 backend is now fully wired.
Tests: 1225 backend tests, 99.11% coverage. 824 frontend tests, 0 tsc errors.
2026-06-12 — Wire build_system_prompt into F14 chat send_message
Type: Feature
Summary:
chat/views.py::send_messagenow callsbuild_system_prompt(request.membership, "chat")and passes the result assystem_prompt=kwarg toanswer_chat_message. The keyword matcher still serves responses, but the prompt plumbing is live — when a real LLM provider is connected, it will receive the language-aware system prompt automatically.answer_chat_messagesignature updated:def answer_chat_message(thread_id, user_message, organization_id, *, system_prompt=None). Existing callers (3 positional args) are unaffected.- Added
backend/tests/test_ai_language.py::test_send_message_uses_build_system_prompt(F37-15) — mocks both helpers, sends a message via the API, assertsbuild_system_prompt(vi_member, "chat")is called and its output is forwarded assystem_prompt=kwarg.
Reason:
build_system_promptexisted but had no live caller — the F37 audit flagged this as the remaining backend wiring gap. This closes it for F14 chat.
Impact:
- Zero runtime change today (keyword matcher ignores the system_prompt). One extra function call per chat message (negligible). Ready for real-provider adoption without further wiring.
Tests: 1191 backend tests, 99.12% coverage.
2026-06-12 — Wire language detection into chat endpoint
Type: Feature
Summary:
chat/views.py::send_messagenow callsrecord_message_language(request.user, content)after saving the user message. After 3 consecutive non-profile-language messages,User.language_detectedis set.- Added
backend/tests/test_ai_language.py::test_chat_send_message_updates_language_detection— sends 3 English messages as a Vietnamese user and assertslanguage_detected = "en"at threshold.
Reason:
- The detection module existed but was never called from a live endpoint — this closes the wiring gap.
Impact:
- Detection now runs on every Ask Lumi message. Performance: pure Python, no I/O on the happy path (no detection), one
UPDATEwhen detection state changes. Does not slow the response path.
Tests: 1188 backend tests, 99.12% coverage.
2026-06-12 — Second audit pass: two “missing” items were already shipped
Type: Doc Fix
Summary:
- §8 Current Behavior and §11 Implementation Audit corrected:
PATCH /api/auth/me/language/already validates against the fullSUPPORTED_LOCALESlist (not justen/vi), andPOST /api/auth/me/language/confirm-detected/is already wired and tested. The 2026-06-12 first-pass audit overstated the remaining gaps because it read the code state before those commits landed.
Reason:
- These two items were listed as “still missing” but
backend/apps/accounts/views.pyshows both are present. Runningbackend/tests/test_ai_language.pyconfirms (26/26 pass).
Impact:
- F37 backend gap is smaller than previously recorded. Remaining work: live-caller wiring (pending real LLM provider), detection-in-chat, and frontend confirmation UI.
Tests: None — doc-only correction.
2026-06-12 — Partial-backend audit correction
Type: Doc Fix
Summary:
- Reclassified F37 from draft/not-built to in-progress, and replaced stale
Current Behavior/Implementation Audittext with the actual partial backend foundation now in the repo.
Reason:
- The spec still described a
max_length=5model and no detection/prompt helper, which is false on the current filesystem.
Impact:
- The spec now distinguishes clearly between what is already implemented and what remains missing to satisfy the full contract.
Tests:
- No code changes. Evidence comes from
backend/apps/accounts/models.py,backend/apps/ai/prompts.py, andbackend/tests/test_ai_language.py.
Notes:
- This does not mark the feature shipped; it marks the current state honestly as partial.
2026-06-12 — Audit verdict corrected to shipped (D-r17)
Type: Doc Fix
Summary:
docs/implementation-audit/2026-06-12-full-repo-audit.md§4 F37 and §7 Final Verdict saidpartialbecause “no frontend confirmation banner or E2E test” existed at the time that audit ran. Both have since shipped (commit31b735d32) — verifiedfrontend/src/components/chat/LanguageSuggestionBanner.tsx,frontend/src/test/LanguageSuggestionBanner.test.tsx, and thedescribe("Language Detection Banner")block infrontend/tests/e2e/lumi.spec.tsplusfrontend/tests/e2e/language-banner.spec.tsall exist and are wired to/api/auth/me/language/confirm-detected/.
Reason:
- The audit doc and
docs/MVP_SCOPE.md(already correctly says “fully shipped”) disagreed. Audit was the stale one.
Impact:
- Audit doc §4 F37 verdict updated
partial→shipped; §7 Final Verdict moves F37 into the shipped group. F36 left asnot built(Fleet-A’s work not yet landed onmainat audit-update time).
Tests:
- No code changes — doc-only correction.
2026-06-12 — Frontend confirmation banner + E2E shipped
Type: Feature
Summary:
LanguageSuggestionBanner.tsx(shadcnAlertvariant=“info” withLanguagesicon) shows “Switch to?” when language_detecteddiffers from currentlanguageon the user object. CallsconfirmDetectedLanguage(POST/api/auth/me/language/confirm-detected/) on accept → toast + i18n switch. Dismiss sets a 30-day localStorage window, clears from store.- 6 unit tests in
LanguageSuggestionBanner.test.tsx: no-banner on null/same, show on mismatch, confirm calls API, dismiss hides, dismissal window. - 2 E2E tests in
lumi.spec.tsdescribe("Language Detection Banner"): full confirm flow (thread + 3 en messages → detection → banner → click → verify language updated) and dismiss persistence. - Fixed
vitestsetup.ts to polyfilllocalStorage(Node v26 jsdom regression). - Chat locale keys (
language_suggestion_text,language_suggestion_confirm,language_suggestion_dismiss,language_suggestion_confirmed,language_suggestion_error) exist in bothen/chat.jsonandvi/chat.json.
Reason:
- F37 audit verdict was
partial— backend detection + auth endpoints shipped but frontend confirmation UI missing. This closes that gap.
Impact:
- F37 frontend column moves from 🔲 to ✅. E2E column same. Backend column was already ✅.
build_system_promptlive-caller wiring remains pending (requires real LLM provider).
Tests: 820 frontend unit tests pass (12 test files), 0 TypeScript errors. 1190 backend tests at 99.12% coverage.
2026-06-12 — Initial spec
Type: Feature
Summary:
- Fixed the AI language contract: profile language as single source of truth (captured from UI locale at registration, passively detected with explicit confirm), English system prompts with a binding response-language directive via one shared
build_system_prompthelper, post-check + single retry, mock provider included. - Flagged the existing
User.language max_length=5defect (cannot storezh-Hant).
Reason:
- 55+ UI languages shipped with no contract for what language AI surfaces answer in; every AI feature was about to improvise.
Impact:
- New registry entry F37 (🔲). Binds F08/F14 implementations and F21/F35/F36 copy generation to one language field.
Tests:
- Specified in §7; none written yet.
Notes:
- The provider switch (Gemini/OpenRouter) is a separate change under F08/F14 — this contract is provider-agnostic by design.