feat: member portal Sprint 9 — Ask AI (RAG over message archive)

- AskAIQuery model + migration (logs question/answer/citations per member)
- AskAIService: RAG pipeline — retrieve via tenant-isolated SearchService (Meili),
  ground LLM in top-8 snippets with inline [n] citations, degrade gracefully when
  OPENROUTER_API_KEY absent or no context (snippet fallback)
- Shared api/common/llm-client.ts (mirrors worker's OpenRouter client)
- SearchModule now exports SearchService
- Member endpoints: GET/POST /my/ask (history + ask)
- /my/ask page + AskChat client component: question box, answer cards with sources
- Ask AI nav item (top of member nav)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 17:03:01 +05:30
parent 6d89d8a609
commit f61c070428
12 changed files with 378 additions and 1 deletions
+21
View File
@@ -95,6 +95,7 @@ model Tenant {
sevaOpportunities SevaOpportunity[]
gamificationEvents GamificationEvent[]
circles Circle[]
askAIQueries AskAIQuery[]
}
enum AdminRole {
@@ -422,6 +423,7 @@ model TowerUser {
sevaEntries SevaEntry[]
gamificationEvents GamificationEvent[]
circleMemberships CircleMembership[]
askAIQueries AskAIQuery[]
@@unique([tenantId, phoneHash])
@@index([phoneHash])
@@ -730,3 +732,22 @@ model CircleMembership {
@@unique([circleId, userId])
@@index([userId])
}
// ============================================================================
// Ask AI (RAG over the message archive)
// ============================================================================
model AskAIQuery {
id String @id @default(cuid())
tenantId String
tenant Tenant @relation(fields: [tenantId], references: [id])
userId String
user TowerUser @relation(fields: [userId], references: [id])
question String
answer String
citations Json @default("[]")
createdAt DateTime @default(now())
@@index([tenantId, createdAt])
@@index([userId, createdAt])
}