1/** 2 * TASK-0125 — RAG workspace search endpoint (SSE). 3 * 4 * POST /workspaces/:workspaceId/ai/search (body `{ query, context? }`) 5 * 6 * Route style mirrors `ai-writing.controller.ts` (explicit `@Inject`, 7 * `ZodValidate` body, `requireUser`, `@Res()` passthrough — the service 8 * owns the socket; pre-stream failures throw → global filter JSON envelope). 9 * Frames: `sources` (the server-side citations, first), `delta` (the answer 10 * stream), `usage`, `error`, `done` (`assistantMessageId` = the answer row). 11 */ 12import { Body, Controller, Inject, Param, Post, Req, Res } from "@nestjs/common"; 13import type { Request, Response } from "express"; 14 15import { RagService } from "./rag.service.js"; 16import { ZodValidate } from "../../common/pipes/zod-validation.pipe.js"; 17import { requireActor } from "../../common/types.js"; 18import { RagSearchDto, type RagSearchInput } from "./rag.dto.js"; 19 20@Controller() 21export class RagController { 22 constructor(@Inject(RagService) private readonly rag: RagService) {} 23 24 /** 25 * The RAG answer (SSE). Pre-stream: 404 (non-member workspace, the search 26 * viewer's fail-closed rule), 403 (no VIEW), 402 (budget), 429 (shared 27 * ai.calls cap), 400/422 (validation). First frame after the headers is 28 * `sources` (the citations); then the answer `delta`s. 29 */ 30 @Post("workspaces/:workspaceId/ai/search") 31 search( 32 @Param("workspaceId") workspaceId: string, 33 @Body(ZodValidate(RagSearchDto)) body: RagSearchInput, 34 @Req() req: Request, 35 @Res() res: Response, 36 ) { 37 return this.rag.ask(workspaceId, requireActor(req), body, req, res); 38 } 39} 40