api-v1.controller.ts 10 KB

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