/** * T-172…T-175 (A-010) — AI Knowledge Assistant routes. * * All five capabilities + the metadata apply are plain JSON (non-streamed) — * deterministic, testable, and the writes go through the normal domain * services. Identity via `requireActor` (user or API key; key-driven writes * stay bounded by the key's scope inside the in-service `assertCan`). * Pre-stream throws become the canonical JSON envelope via the global filter. */ import { Body, Controller, HttpCode, Inject, Param, Post, Req } from "@nestjs/common"; import type { Request } from "express"; import { requireActor } from "../../../common/types.js"; import { ZodValidate } from "../../../common/pipes/zod-validation.pipe.js"; import { AssistantService } from "./assistant.service.js"; import { AiApplyMetadataDto, AiExtractActionsDto, AiFindDuplicatesDto, AiFindStaleDto, AiGenerateFaqsDto, AiSuggestMetadataDto, } from "./assistant.dto.js"; import type { AiApplyMetadataInput, AiExtractActionsInput, AiFindDuplicatesInput, AiFindStaleInput, AiGenerateFaqsInput, AiSuggestMetadataInput, } from "./assistant.dto.js"; @Controller() export class AssistantController { constructor(@Inject(AssistantService) private readonly assistant: AssistantService) {} /** Meeting notes → tasks (created as the requester). */ @HttpCode(200) @Post("workspaces/:workspaceId/ai/assistant/extract-actions") extractActions( @Param("workspaceId") workspaceId: string, @Body(ZodValidate(AiExtractActionsDto)) body: AiExtractActionsInput, @Req() req: Request, ) { return this.assistant.extractActions(workspaceId, requireActor(req), body, req); } /** Visible pages → FAQ draft page. */ @HttpCode(200) @Post("workspaces/:workspaceId/ai/assistant/generate-faqs") generateFaqs( @Param("workspaceId") workspaceId: string, @Body(ZodValidate(AiGenerateFaqsDto)) body: AiGenerateFaqsInput, @Req() req: Request, ) { return this.assistant.generateFaqs(workspaceId, requireActor(req), body, req); } /** Candidate pool → duplicate groups (read-only). */ @HttpCode(200) @Post("workspaces/:workspaceId/ai/assistant/find-duplicates") findDuplicates( @Param("workspaceId") workspaceId: string, @Body(ZodValidate(AiFindDuplicatesDto)) body: AiFindDuplicatesInput, @Req() req: Request, ) { return this.assistant.findDuplicates(workspaceId, requireActor(req), body, req); } /** Recent-update pool → stale pages (read-only). */ @HttpCode(200) @Post("workspaces/:workspaceId/ai/assistant/find-stale") findStale( @Param("workspaceId") workspaceId: string, @Body(ZodValidate(AiFindStaleDto)) body: AiFindStaleInput, @Req() req: Request, ) { return this.assistant.findStale(workspaceId, requireActor(req), body, req); } /** One page → suggested title/description (suggestion only). */ @HttpCode(200) @Post("workspaces/:workspaceId/ai/assistant/suggest-metadata") suggestMetadata( @Param("workspaceId") workspaceId: string, @Body(ZodValidate(AiSuggestMetadataDto)) body: AiSuggestMetadataInput, @Req() req: Request, ) { return this.assistant.suggestMetadata(workspaceId, requireActor(req), body, req); } /** Apply a suggested field subset (normal page PATCH, EDIT re-checked). */ @HttpCode(200) @Post("workspaces/:workspaceId/ai/assistant/apply-metadata") applyMetadata( @Param("workspaceId") workspaceId: string, @Body(ZodValidate(AiApplyMetadataDto)) body: AiApplyMetadataInput, @Req() req: Request, ) { return this.assistant.applyMetadata(workspaceId, requireActor(req), body, req); } }