KnowledgeVault AI module-spec M-08
internal prototype · canonical JSON + Dreamborn Forge HTML
internal generated
module-spec · supabase_json

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

Planning Surface

Use this to decide what happens next.

Status

approved

Phase

M-08

open questions

No items captured.

Agent Handoff
Start Here

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

Completion Evidence

No explicit evidence field yet. Require tests, screenshots, linked PRs, or reviewed outputs before marking complete.

Open Questions

No section body captured.

Structured Payload

Machine-readable source fields

summary

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.

module id

M-08

api routes
authpatherrorsmethodrequestresponse
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}GETNo 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}
data model
notes

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.

tables
rlsnameindexespurposekey 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()
module name

Virtual Expert Query

deliverables
idnametypeinputsoutputsdescription
D-08-01002_field_queries.sqlmigration- 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) policiesCreates 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-02GET /api/virtual-experts — app/api/virtual-experts/route.tsapi_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-03POST /api/virtual-experts/[id]/query — app/api/virtual-experts/[id]/query/route.tsapi_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-04VirtualExpertSelection screen — app/(company)/experts/page.tsxcomponent- 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 tapReact 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-05VirtualExpertChat screen — app/(company)/experts/[id]/page.tsxcomponent- 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 sourcesReact 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
integrations
callspurposeservice
- 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 matchedEmbed queries for pgvector similarity search; synthesise first-person attributed answers with no-fabrication guardrailOpenAI
- 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 composedpgvector similarity search over knowledge_chunks; read virtual_experts and expert_profiles; insert field_queriesSupabase
prerequisites
  • 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()
open questions

No items captured.

schema version

1.0