geminiService.ts 5.8 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: "请始终使用中文回答。",
  109. en: "Please always answer in English.",
  110. ja: "常に日本語で答えてください。"
  111. };
  112. const langInstruction = langInstructionMap[language];
  113. // RAG検索(知識ベースファイルがある場合)
  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. // 検索ステータスがリセットされていることを確認
  138. setTimeout(() => {
  139. onSearchComplete?.([]);
  140. }, 100);
  141. }
  142. }
  143. const systemInstruction = buildSystemInstruction(files, settings, langInstruction);
  144. try {
  145. // APIキーはオプションです - ローカルモデルを許可します
  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. // より詳細なエラー情報を提供
  159. if (error.name === 'TypeError' && error.message.includes('fetch')) {
  160. throw new Error('ネットワーク接続に失敗しました。サーバーの状態を確認してください');
  161. }
  162. throw new Error(error.message || "API_ERROR");
  163. }
  164. };