/** * TASK-0125 — RAG workspace search endpoint (SSE). * * POST /workspaces/:workspaceId/ai/search (body `{ query, context? }`) * * Route style mirrors `ai-writing.controller.ts` (explicit `@Inject`, * `ZodValidate` body, `requireUser`, `@Res()` passthrough — the service * owns the socket; pre-stream failures throw → global filter JSON envelope). * Frames: `sources` (the server-side citations, first), `delta` (the answer * stream), `usage`, `error`, `done` (`assistantMessageId` = the answer row). */ import { Body, Controller, Inject, Param, Post, Req, Res } from "@nestjs/common"; import type { Request, Response } from "express"; import { RagService } from "./rag.service.js"; import { ZodValidate } from "../../common/pipes/zod-validation.pipe.js"; import { requireActor } from "../../common/types.js"; import { RagSearchDto, type RagSearchInput } from "./rag.dto.js"; @Controller() export class RagController { constructor(@Inject(RagService) private readonly rag: RagService) {} /** * The RAG answer (SSE). Pre-stream: 404 (non-member workspace, the search * viewer's fail-closed rule), 403 (no VIEW), 402 (budget), 429 (shared * ai.calls cap), 400/422 (validation). First frame after the headers is * `sources` (the citations); then the answer `delta`s. */ @Post("workspaces/:workspaceId/ai/search") search( @Param("workspaceId") workspaceId: string, @Body(ZodValidate(RagSearchDto)) body: RagSearchInput, @Req() req: Request, @Res() res: Response, ) { return this.rag.ask(workspaceId, requireActor(req), body, req, res); } }