KnowledgeVault AI module-spec M-08
M-08 enables company-authenticated field technicians to query a named AI expert persona and receive a GPT-4o synthesised first-person answer attributed to that expert. The module adds GET /api/virtual-experts (listing all published experts) and POST /api/virtual-experts/[id]/query (scoped pgvector similarity search over the expert published knowledge chunks, GPT-4o synthesis with strict no-fabrication instruction, field_verified_badge logic, and field_queries audit logging). Two React screens co
No items captured.
M-08 enables company-authenticated field technicians to query a named AI expert persona and receive a GPT-4o synthesised first-person answer attributed to that expert. The module adds GET /api/virtual-experts (listing all published experts) and POST /api/virtual-experts/[id]/query (scoped pgvector similarity search over the expert published knowledge chunks, GPT-4o synthesis with strict no-fabrication instruction, field_verified_badge logic, and field_queries audit logging). Two React screens co
No explicit evidence field yet. Require tests, screenshots, linked PRs, or reviewed outputs before marking complete.
No section body captured.
Machine-readable source fields
M-08 enables company-authenticated field technicians to query a named AI expert persona and receive a GPT-4o synthesised first-person answer attributed to that expert. The module adds GET /api/virtual-experts (listing all published experts) and POST /api/virtual-experts/[id]/query (scoped pgvector similarity search over the expert published knowledge chunks, GPT-4o synthesis with strict no-fabrication instruction, field_verified_badge logic, and field_queries audit logging). Two React screens complete the consumer loop: S-07-a (Virtual Expert Selection) and S-07-b (Virtual Expert Chat). This is the primary emotional hook of KnowledgeVault.
M-08
| auth | path | errors | method | request | response |
|---|---|---|---|---|---|
| Company JWT required — Supabase Auth JWT with role=company claim via lib/auth/company-jwt.ts | /api/virtual-experts | - No/invalid JWT -> 401 {error: Unauthorized, code: auth_required, status: 401} - role != company -> 401 - DB error -> 500 {error: Internal server error, code: server_error, status: 500} | GET | No body. No query parameters. | {experts: [{id, name, role_type, description, session_count, avg_accuracy_score, knowledge_chunk_count}]} ordered by session_count DESC |
| Company JWT required — Supabase Auth JWT with role=company and company_id claims via lib/auth/company-jwt.ts | /api/virtual-experts/[id]/query | - No/invalid JWT -> 401 - role != company -> 401 - Empty query -> 400 {error: query is required, code: validation_error, status: 400} - Expert missing/unpublished -> 404 {error: Expert not found, code: expert_not_found, status: 404} - OpenAI error -> 503 {error: <first_name> is unavailable right now, code: synthesis_failed, status: 503} | POST | {query: string} — required, 1–500 chars | {answer: string, expert_name: string, role_type: string, sources: [{chunk_id: uuid, content: string, chunk_type: string, sku: string, captured_at: ISO8601}], field_verified_badge: boolean} |
virtual_experts and knowledge_chunks owned by M-05 — M-08 reads only, no ALTER TABLE. companies owned by M-07 — FK reference only. No triggers or materialised views added by M-08.
| rls | name | indexes | purpose | key columns |
|---|---|---|---|---|
| RLS enabled. Policy companies_read_own: SELECT for authenticated users where company_id matches their company via companies table. INSERT restricted to service role only. | field_queries | - idx_field_queries_virtual_expert_id ON field_queries(virtual_expert_id) - idx_field_queries_company_id ON field_queries(company_id) - idx_field_queries_queried_at ON field_queries(queried_at DESC) | Audit log of every company-submitted query to a virtual expert, including synthesised answer, source chunk references, and field_verified_badge. | - id uuid primary key default gen_random_uuid() - virtual_expert_id uuid not null references virtual_experts(id) on delete cascade - company_id uuid not null references companies(id) on delete cascade - query_text text not null - answer text - sources jsonb not null default '[]' — array of {chunk_id, content, chunk_type, sku, captured_at} - field_verified_badge boolean not null default false - queried_at timestamptz not null default now() - created_at timestamptz not null default now() |
Virtual Expert Query
| id | name | type | inputs | outputs | description |
|---|---|---|---|---|---|
| D-08-01 | 002_field_queries.sql | migration | - Postgres instance with 001_kv_phase1_foundation.sql and M-05/M-07 migrations applied | - field_queries table with all columns, FK constraints, and indexes - RLS enabled with companies_read_own (SELECT) and service_insert (INSERT) policies | Creates field_queries table in supabase/migrations/002_field_queries.sql. Audit log for every company query to a virtual expert — stores query text, synthesised answer, source chunks as JSONB, field_verified_badge boolean, queried_at, created_at. FKs to virtual_experts and companies. RLS: companies SELECT their own rows; INSERT service-role only. Three indexes: virtual_expert_id, company_id, queried_at DESC. |
| D-08-02 | GET /api/virtual-experts — app/api/virtual-experts/route.ts | api_route | - Authorization: Bearer <company-jwt> header (Supabase Auth JWT with role=company claim) - No query parameters or request body | - 200: {experts: [{id: uuid, name: string, role_type: string, description: string|null, session_count: integer, avg_accuracy_score: float|null, knowledge_chunk_count: integer}]} - 401: {error: Unauthorized, code: auth_required, status: 401} - 500: {error: Internal server error, code: server_error, status: 500} | Returns all virtual experts where is_published=true, ordered by session_count DESC. Company JWT required via lib/auth/company-jwt.ts (validates role=company claim). No company-level filtering in v1 — all published experts accessible to any authenticated company. Reads from virtual_experts table. No pagination. |
| D-08-03 | POST /api/virtual-experts/[id]/query — app/api/virtual-experts/[id]/query/route.ts | api_route | - Authorization: Bearer <company-jwt> header - URL path param :id — virtual expert uuid - Request body: {query: string} — required, 1–500 characters | - 200: {answer: string, expert_name: string, role_type: string, sources: [{chunk_id: uuid, content: string, chunk_type: string, sku: string, captured_at: string ISO8601}], field_verified_badge: boolean} - 400: {error: query is required, code: validation_error, status: 400} - 401: {error: Unauthorized, code: auth_required, status: 401} - 404: {error: Expert not found, code: expert_not_found, status: 404} - 503: {error: <first_name> is unavailable right now, try again in a moment, code: synthesis_failed, status: 503} | Accepts natural-language query. Steps: (1) Validate company JWT, extract company_id. (2) Fetch virtual expert WHERE id=:id AND is_published=true — 404 if absent. (3) Join expert_profiles WHERE user_id = virtual_experts.expert_id for years_experience (ASSUMPTION: virtual_experts.expert_id references users.id; Quinn must verify against M-05 schema — if references expert_profiles.id, change join key accordingly). (4) Embed query via text-embedding-3-small (1536 dims). (5) pgvector search: SELECT id, content, chunk_type, sku, captured_at FROM knowledge_chunks WHERE expert_id = AND is_published = |
| D-08-04 | VirtualExpertSelection screen — app/(company)/experts/page.tsx | component | - Company auth session cookie (middleware enforces company role) - GET /api/virtual-experts response | - Rendered /experts page with expert card grid - Navigation to /experts/:id on card tap | React page at /experts. Fetches GET /api/virtual-experts on mount. Renders ExpertCard components (components/experts/ExpertCard.tsx) in 2-column CSS grid at >=768px, 1-column on mobile. Each ExpertCard: name, role_type, knowledge_chunk_count labelled chunks, avg_accuracy_score formatted as N% accuracy (N/A when null), session_count formatted as N sessions. role=button, aria-label=Ask <name>, <role_type>, <years> years, navigates to /experts/:id on click. ExpertCardSkeleton (components/experts/ExpertCardSkeleton.tsx) matching card dimensions — grid of 4 skeletons renders immediately. Empty stat |
| D-08-05 | VirtualExpertChat screen — app/(company)/experts/[id]/page.tsx | component | - URL param :id — virtual expert uuid - Company auth session cookie - POST /api/virtual-experts/:id/query response | - Rendered /experts/:id with identity header, answer area, fixed input - First-person answer with expandable sources | React Client Component at /experts/:id. On mount fetches expert detail — skeleton immediate. ExpertIdentityHeader (components/experts/ExpertIdentityHeader.tsx): name as h1, subtitle role_type + years + accuracy, back link to /experts. QueryInput (components/experts/QueryInput.tsx): fixed bottom, placeholder Ask <first_name> anything (first space-delimited token of name), aria-label=Ask <expert_name>, disabled during loading. On submit: loading state, POST /api/virtual-experts/:id/query, AnswerCard (components/experts/AnswerCard.tsx) in skeleton state. On 200: answer text, Based on N captured a |
| calls | purpose | service |
|---|---|---|
| - openai.embeddings.create({model: text-embedding-3-small, input: query}) — 1536-dim vector - openai.chat.completions.create({model: gpt-4o, messages: [{role: system, content: You are NAME, a ROLE_TYPE with YEARS years experience. Answer in first person using only the knowledge chunks provided. Do not fabricate. If the chunks do not contain an answer, say so.}, {role: user, content: Knowledge chunks: CHUNK1---CHUNK2---CHUNK3 Question: QUERY}]}) — called only when >=1 chunk matched | Embed queries for pgvector similarity search; synthesise first-person attributed answers with no-fabrication guardrail | OpenAI |
| - supabase.from(virtual_experts).select(id,name,role_type,expert_id).eq(id,:id).eq(is_published,true).single() - supabase.from(expert_profiles).select(years_experience).eq(user_id, virtual_expert.expert_id).single() - SELECT id,content,chunk_type,sku,captured_at FROM knowledge_chunks WHERE expert_id=$1 AND is_published=true ORDER BY embedding<=>$2 LIMIT 3 - supabase_service.from(field_queries).insert({virtual_expert_id, company_id, query_text, answer, sources, field_verified_badge}) — service-role, after response composed | pgvector similarity search over knowledge_chunks; read virtual_experts and expert_profiles; insert field_queries | Supabase |
- virtual_experts table exists with columns: id uuid pk, expert_id uuid not null, name text not null, role_type text not null, description text, is_published boolean not null default false, session_count integer not null default 0, avg_accuracy_score float, knowledge_chunk_count integer not null default 0
- knowledge_chunks table exists with columns: id uuid pk, expert_id uuid not null, content text not null, embedding vector(1536) not null, chunk_type text not null (enum: failure_mode|symptom|fix|psi_range|safety_warning|field_tip|tool_required|part_number_reference), sku text, product_id uuid, session_id uuid, is_published boolean not null default false, novelty_score float, ground_truth_verified boolean not null default false, captured_at timestamptz not null default now()
- expert_profiles table exists with user_id uuid not null unique references users(id) and years_experience int — confirmed in 001_kv_phase1_foundation.sql
- companies table exists with id uuid pk (from M-07 migration)
- lib/auth/company-jwt.ts validates Supabase Auth JWT with role=company claim and extracts company_id claim
- lib/openai.ts exports OpenAI client with text-embedding-3-small accessible via openai.embeddings.create()
No items captured.
1.0