geminiService.ts 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. import { KnowledgeFile, Message, Role, Language, AppSettings, ModelConfig } from "../types";
  2. import { ragService } from './ragService';
  3. const buildSystemInstruction = (files: KnowledgeFile[], settings: AppSettings, langInstruction: string) => {
  4. const fileNames = files.map(f => f.name).join(", ");
  5. return `
  6. You are an intelligent knowledge base assistant operating as a RAG system.
  7. System Configuration:
  8. - Retrieval: Top ${settings.topK} chunks, Rerank: ${settings.enableRerank ? 'Enabled' : 'Disabled'}
  9. - Knowledge Base Files: ${fileNames}
  10. You have access to the content of these files in your context.
  11. Your goal is to answer user questions primarily based on the content of these files.
  12. Strict Citation Rules:
  13. 1. Whenever you use information from a file, you MUST cite the source filename at the end of the sentence or paragraph.
  14. 2. Citation format: [filename.extension].
  15. 3. If the answer is not found in the files, state so clearly.
  16. 4. ${langInstruction}
  17. `;
  18. };
  19. // --- OpenAI Compatible Implementation ---
  20. const callOpenAICompatible = async (
  21. currentPrompt: string,
  22. files: KnowledgeFile[],
  23. history: Message[],
  24. modelConfig: ModelConfig,
  25. settings: AppSettings,
  26. systemInstruction: string,
  27. apiKey: string
  28. ): Promise<string> => {
  29. if (!modelConfig.baseUrl) throw new Error("Base URL is required");
  30. // Construct OpenAI format messages
  31. const messages: any[] = [
  32. { role: "system", content: systemInstruction }
  33. ];
  34. // Add history
  35. history.forEach(msg => {
  36. messages.push({
  37. role: msg.role === Role.USER ? "user" : "assistant",
  38. content: msg.text
  39. });
  40. });
  41. // Current User Message Construction (Supports Vision)
  42. const contentParts: any[] = [];
  43. // 1. Add Text Prompt
  44. contentParts.push({ type: "text", text: currentPrompt });
  45. // 2. Add Images/Files
  46. files.forEach((file) => {
  47. if (file.type.startsWith("image/") && modelConfig.type === "vision") {
  48. contentParts.push({
  49. type: "image_url",
  50. image_url: {
  51. url: `data:${file.type};base64,${file.content}`
  52. }
  53. });
  54. } else {
  55. // For non-image files or if vision is not supported, append as text
  56. try {
  57. const decodedText = atob(file.content);
  58. contentParts.push({
  59. type: "text",
  60. text: `\n--- Context from file: ${file.name} ---\n${decodedText.substring(0, 10000)}... (truncated)\n`
  61. });
  62. } catch (e) {
  63. contentParts.push({
  64. type: "text",
  65. text: `\n[File attached: ${file.name} (${file.type}) - Content processing skipped]\n`
  66. });
  67. }
  68. }
  69. });
  70. messages.push({ role: "user", content: contentParts });
  71. const response = await fetch(`${modelConfig.baseUrl}/chat/completions`, {
  72. method: "POST",
  73. headers: {
  74. "Content-Type": "application/json",
  75. "Authorization": `Bearer ${apiKey}`
  76. },
  77. body: JSON.stringify({
  78. model: modelConfig.modelId,
  79. messages: messages,
  80. temperature: settings.temperature,
  81. max_tokens: settings.maxTokens,
  82. stream: false
  83. })
  84. });
  85. if (!response.ok) {
  86. const err = await response.text();
  87. throw new Error(`API Error: ${response.status} - ${err}`);
  88. }
  89. const data = await response.json();
  90. return data.choices?.[0]?.message?.content || "NO_RESPONSE_TEXT";
  91. };
  92. export const generateResponse = async (
  93. currentPrompt: string,
  94. files: KnowledgeFile[],
  95. history: Message[],
  96. language: Language,
  97. modelConfig: ModelConfig,
  98. settings: AppSettings,
  99. authToken?: string,
  100. onSearchStart?: () => void,
  101. onSearchComplete?: (results: any) => void
  102. ): Promise<string> => {
  103. // 1. Resolve API Key: Use model specific key
  104. let apiKey = modelConfig.apiKey;
  105. console.log('Model config:', modelConfig);
  106. console.log('API Key present:', !!apiKey);
  107. const langInstructionMap: Record<Language, string> = {
  108. zh: "请始终使用Chinese回答。",
  109. en: "Please always answer in English.",
  110. ja: "常にJapaneseで答えてください。"
  111. };
  112. const langInstruction = langInstructionMap[language];
  113. // RAG search (when knowledge base files exist)
  114. let ragPrompt = currentPrompt;
  115. let ragSources: string[] = [];
  116. if (files.length > 0 && authToken) {
  117. try {
  118. onSearchStart?.();
  119. console.log('Starting RAG search with prompt:', currentPrompt);
  120. const ragResponse = await ragService.search(currentPrompt, {
  121. ...settings,
  122. language
  123. }, authToken);
  124. console.log('RAG search response:', ragResponse);
  125. if (ragResponse && ragResponse.hasRelevantContent) {
  126. ragPrompt = ragResponse.ragPrompt;
  127. ragSources = ragResponse.sources;
  128. console.log('Using RAG enhanced prompt');
  129. } else {
  130. console.log('No relevant content found, using original prompt');
  131. }
  132. onSearchComplete?.(ragResponse?.searchResults || []);
  133. } catch (error) {
  134. console.warn('RAG search failed, using original prompt:', error);
  135. onSearchComplete?.([]);
  136. } finally {
  137. // Ensure search status is reset
  138. setTimeout(() => {
  139. onSearchComplete?.([]);
  140. }, 100);
  141. }
  142. }
  143. const systemInstruction = buildSystemInstruction(files, settings, langInstruction);
  144. try {
  145. // API key is optional - allow local models
  146. // --- OpenAI Compatible API Logic ---
  147. return await callOpenAICompatible(
  148. ragPrompt,
  149. files,
  150. history,
  151. modelConfig,
  152. settings,
  153. systemInstruction,
  154. apiKey || "ollama",
  155. );
  156. } catch (error: any) {
  157. console.error("AI Service Error:", error);
  158. // Provide more detailed error information
  159. if (error.name === 'TypeError' && error.message.includes('fetch')) {
  160. throw new Error('Network connection failed. Please check server status');
  161. }
  162. throw new Error(error.message || "API_ERROR");
  163. }
  164. };