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
+12
View File
@@ -2,6 +2,7 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestj
import { MyService } from './my.service';
import { SevaService } from '../seva/seva.service';
import { CirclesService } from '../circles/circles.service';
import { AskAIService } from '../ask-ai/ask-ai.service';
import { MemberAuth } from '../auth/member-auth.decorator';
import { CurrentMember } from '../auth/current-member.decorator';
import type { MemberJwtPayload } from '@tower/types';
@@ -28,8 +29,19 @@ export class MyController {
private readonly service: MyService,
private readonly seva: SevaService,
private readonly circles: CirclesService,
private readonly askAI: AskAIService,
) {}
@Get('ask')
askHistory(@CurrentMember() member: MemberJwtPayload) {
return this.askAI.history(member.sub, member.tenantId);
}
@Post('ask')
ask(@CurrentMember() member: MemberJwtPayload, @Body() body: { question: string }) {
return this.askAI.ask(member.sub, member.tenantId, body.question);
}
@Get('seva')
sevaList(@CurrentMember() member: MemberJwtPayload) {
return this.seva.listForMember(member.sub, member.tenantId);
+2 -1
View File
@@ -5,9 +5,10 @@ import { AuthModule } from '../auth/auth.module';
import { SevaModule } from '../seva/seva.module';
import { GamificationModule } from '../gamification/gamification.module';
import { CirclesModule } from '../circles/circles.module';
import { AskAIModule } from '../ask-ai/ask-ai.module';
@Module({
imports: [AuthModule, SevaModule, GamificationModule, CirclesModule],
imports: [AuthModule, SevaModule, GamificationModule, CirclesModule, AskAIModule],
controllers: [MyController],
providers: [MyService],
})