api-v1.controller.ts 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. import {
  2. Body,
  3. Controller,
  4. Delete,
  5. Get,
  6. Param,
  7. Post,
  8. Request,
  9. Res,
  10. UploadedFile,
  11. UseGuards,
  12. UseInterceptors,
  13. } from '@nestjs/common';
  14. import { FileInterceptor } from '@nestjs/platform-express';
  15. import { Response } from 'express';
  16. import { ApiKeyGuard } from '../auth/api-key.guard';
  17. import { RagService } from '../rag/rag.service';
  18. import { ChatService } from '../chat/chat.service';
  19. import { KnowledgeBaseService } from '../knowledge-base/knowledge-base.service';
  20. import { ModelConfigService } from '../model-config/model-config.service';
  21. import { UserSettingService } from '../user-setting/user-setting.service';
  22. @Controller('v1')
  23. @UseGuards(ApiKeyGuard)
  24. export class ApiV1Controller {
  25. constructor(
  26. private readonly ragService: RagService,
  27. private readonly chatService: ChatService,
  28. private readonly knowledgeBaseService: KnowledgeBaseService,
  29. private readonly modelConfigService: ModelConfigService,
  30. private readonly userSettingService: UserSettingService,
  31. ) { }
  32. // ========== Chat / RAG ==========
  33. /**
  34. * POST /api/v1/chat
  35. * Tenant-scoped RAG chat. Supports both streaming (SSE) and standard JSON responses.
  36. * Body: { message, stream?, selectedGroups?, selectedFiles? }
  37. */
  38. @Post('chat')
  39. async chat(
  40. @Request() req,
  41. @Body()
  42. body: {
  43. message: string;
  44. stream?: boolean;
  45. selectedGroups?: string[];
  46. selectedFiles?: string[];
  47. },
  48. @Res() res: Response,
  49. ) {
  50. const { message, stream = false, selectedGroups, selectedFiles } = body;
  51. const user = req.user;
  52. if (!message) {
  53. return res.status(400).json({ error: 'message is required' });
  54. }
  55. // Get user settings and model configuration
  56. const userSetting = await this.userSettingService.findOrCreate(user.id);
  57. const models = await this.modelConfigService.findAll(user.id, user.tenantId);
  58. const llmModel = models.find((m) => m.id === userSetting?.selectedLLMId) ?? models.find((m) => m.type === 'llm' && m.isDefault);
  59. if (!llmModel) {
  60. return res.status(400).json({ error: 'No LLM model configured for this user' });
  61. }
  62. const modelConfig = llmModel as any;
  63. if (stream) {
  64. res.setHeader('Content-Type', 'text/event-stream');
  65. res.setHeader('Cache-Control', 'no-cache');
  66. res.setHeader('Connection', 'keep-alive');
  67. try {
  68. const stream = this.chatService.streamChat(
  69. message,
  70. [], // history
  71. user.id,
  72. modelConfig,
  73. userSetting?.language ?? 'zh', // userLanguage
  74. userSetting?.selectedEmbeddingId, // selectedEmbeddingId
  75. selectedGroups, // selectedGroups
  76. selectedFiles, // selectedFiles
  77. undefined, // historyId
  78. userSetting?.enableRerank ?? false, // enableRerank
  79. userSetting?.selectedRerankId, // selectedRerankId
  80. userSetting?.temperature, // temperature
  81. userSetting?.maxTokens, // maxTokens
  82. userSetting?.topK ?? 5, // topK
  83. userSetting?.similarityThreshold ?? 0.3, // similarityThreshold
  84. userSetting?.rerankSimilarityThreshold ?? 0.5, // rerankSimilarityThreshold
  85. userSetting?.enableQueryExpansion ?? false, // enableQueryExpansion
  86. userSetting?.enableHyDE ?? false, // enableHyDE
  87. user.tenantId, // Passing tenantId correctly
  88. );
  89. for await (const chunk of stream) {
  90. res.write(`data: ${JSON.stringify(chunk)}\n\n`);
  91. }
  92. res.write('data: [DONE]\n\n');
  93. res.end();
  94. } catch (error) {
  95. res.write(`data: ${JSON.stringify({ type: 'error', data: error.message })}\n\n`);
  96. res.end();
  97. }
  98. } else {
  99. // Non-streaming: collect all chunks and return as JSON
  100. try {
  101. let fullContent = '';
  102. const sources: any[] = [];
  103. let historyId: string | undefined;
  104. const chatStream = this.chatService.streamChat(
  105. message,
  106. [],
  107. user.id,
  108. modelConfig,
  109. userSetting?.language ?? 'zh',
  110. userSetting?.selectedEmbeddingId,
  111. selectedGroups,
  112. selectedFiles,
  113. undefined, // historyId
  114. userSetting?.enableRerank ?? false,
  115. userSetting?.selectedRerankId,
  116. userSetting?.temperature,
  117. userSetting?.maxTokens,
  118. userSetting?.topK ?? 5,
  119. userSetting?.similarityThreshold ?? 0.3,
  120. userSetting?.rerankSimilarityThreshold ?? 0.5,
  121. userSetting?.enableQueryExpansion ?? false,
  122. userSetting?.enableHyDE ?? false,
  123. user.tenantId, // Passing tenantId correctly
  124. );
  125. for await (const chunk of chatStream) {
  126. if (chunk.type === 'content') fullContent += chunk.data;
  127. else if (chunk.type === 'sources') sources.push(...chunk.data);
  128. else if (chunk.type === 'historyId') historyId = chunk.data;
  129. }
  130. return res.json({ content: fullContent, sources, historyId });
  131. } catch (error) {
  132. return res.status(500).json({ error: error.message });
  133. }
  134. }
  135. }
  136. // ========== Search ==========
  137. /**
  138. * POST /api/v1/search
  139. * Tenant-scoped hybrid search across knowledge base.
  140. * Body: { query, topK?, threshold?, selectedGroups?, selectedFiles? }
  141. */
  142. @Post('search')
  143. async search(
  144. @Request() req,
  145. @Body()
  146. body: {
  147. query: string;
  148. topK?: number;
  149. threshold?: number;
  150. selectedGroups?: string[];
  151. selectedFiles?: string[];
  152. },
  153. ) {
  154. const { query, topK = 5, threshold = 0.3, selectedGroups, selectedFiles } = body;
  155. const user = req.user;
  156. if (!query) return { error: 'query is required' };
  157. const userSetting = await this.userSettingService.findOrCreate(user.id);
  158. const results = await this.ragService.searchKnowledge(
  159. query,
  160. user.id,
  161. topK,
  162. threshold,
  163. userSetting?.selectedEmbeddingId,
  164. userSetting?.enableFullTextSearch ?? false,
  165. userSetting?.enableRerank ?? false,
  166. userSetting?.selectedRerankId,
  167. selectedGroups,
  168. selectedFiles,
  169. userSetting?.rerankSimilarityThreshold ?? 0.5,
  170. user.tenantId,
  171. userSetting?.enableQueryExpansion ?? false,
  172. userSetting?.enableHyDE ?? false,
  173. );
  174. return { results, total: results.length };
  175. }
  176. // ========== Knowledge Base ==========
  177. /**
  178. * GET /api/v1/knowledge-bases
  179. * List all files belonging to the caller's tenant.
  180. */
  181. @Get('knowledge-bases')
  182. async listFiles(@Request() req) {
  183. const user = req.user;
  184. const files = await this.knowledgeBaseService.findAll(user.id, user.tenantId);
  185. return {
  186. files: files.map((f) => ({
  187. id: f.id,
  188. name: f.originalName,
  189. title: f.title,
  190. status: f.status,
  191. size: f.size,
  192. mimetype: f.mimetype,
  193. createdAt: f.createdAt,
  194. })),
  195. total: files.length,
  196. };
  197. }
  198. /**
  199. * POST /api/v1/knowledge-bases/upload
  200. * Upload and index a file into the caller's tenant knowledge base.
  201. */
  202. @Post('knowledge-bases/upload')
  203. @UseInterceptors(FileInterceptor('file'))
  204. async uploadFile(
  205. @Request() req,
  206. @UploadedFile() file: Express.Multer.File,
  207. @Body() body: { mode?: 'fast' | 'precise'; chunkSize?: number; chunkOverlap?: number },
  208. ) {
  209. if (!file) return { error: 'file is required' };
  210. const user = req.user;
  211. const kb = await this.knowledgeBaseService.createAndIndex(
  212. file,
  213. user.id,
  214. user.tenantId,
  215. {
  216. mode: body.mode ?? 'fast',
  217. chunkSize: body.chunkSize ? Number(body.chunkSize) : 1000,
  218. chunkOverlap: body.chunkOverlap ? Number(body.chunkOverlap) : 200,
  219. },
  220. );
  221. return {
  222. id: kb.id,
  223. name: kb.originalName,
  224. status: kb.status,
  225. message: 'File uploaded and indexing started',
  226. };
  227. }
  228. /**
  229. * DELETE /api/v1/knowledge-bases/:id
  230. * Delete a specific file from the knowledge base.
  231. */
  232. @Delete('knowledge-bases/:id')
  233. async deleteFile(@Request() req, @Param('id') id: string) {
  234. const user = req.user;
  235. await this.knowledgeBaseService.deleteFile(id, user.id, user.tenantId);
  236. return { message: 'File deleted successfully' };
  237. }
  238. @Get('knowledge-bases/:id')
  239. async getFile(@Request() req, @Param('id') id: string) {
  240. const user = req.user;
  241. const files = await this.knowledgeBaseService.findAll(user.id, user.tenantId);
  242. const file = files.find((f) => f.id === id);
  243. if (!file) return { error: 'File not found' };
  244. return file;
  245. }
  246. }