import { KnowledgeFile, Message, Role, Language, AppSettings, ModelConfig } from "../types"; import { ragService } from './ragService'; const buildSystemInstruction = (files: KnowledgeFile[], settings: AppSettings, langInstruction: string) => { const fileNames = files.map(f => f.name).join(", "); return ` You are an intelligent knowledge base assistant operating as a RAG system. System Configuration: - Retrieval: Top ${settings.topK} chunks, Rerank: ${settings.enableRerank ? 'Enabled' : 'Disabled'} - Knowledge Base Files: ${fileNames} You have access to the content of these files in your context. Your goal is to answer user questions primarily based on the content of these files. Strict Citation Rules: 1. Whenever you use information from a file, you MUST cite the source filename at the end of the sentence or paragraph. 2. Citation format: [filename.extension]. 3. If the answer is not found in the files, state so clearly. 4. ${langInstruction} `; }; // --- OpenAI Compatible Implementation --- const callOpenAICompatible = async ( currentPrompt: string, files: KnowledgeFile[], history: Message[], modelConfig: ModelConfig, settings: AppSettings, systemInstruction: string, apiKey: string ): Promise => { if (!modelConfig.baseUrl) throw new Error("Base URL is required"); // Construct OpenAI format messages const messages: any[] = [ { role: "system", content: systemInstruction } ]; // Add history history.forEach(msg => { messages.push({ role: msg.role === Role.USER ? "user" : "assistant", content: msg.text }); }); // Current User Message Construction (Supports Vision) const contentParts: any[] = []; // 1. Add Text Prompt contentParts.push({ type: "text", text: currentPrompt }); // 2. Add Images/Files files.forEach((file) => { if (file.type.startsWith("image/") && modelConfig.type === "vision") { contentParts.push({ type: "image_url", image_url: { url: `data:${file.type};base64,${file.content}` } }); } else { // For non-image files or if vision is not supported, append as text try { const decodedText = atob(file.content); contentParts.push({ type: "text", text: `\n--- Context from file: ${file.name} ---\n${decodedText.substring(0, 10000)}... (truncated)\n` }); } catch (e) { contentParts.push({ type: "text", text: `\n[File attached: ${file.name} (${file.type}) - Content processing skipped]\n` }); } } }); messages.push({ role: "user", content: contentParts }); const response = await fetch(`${modelConfig.baseUrl}/chat/completions`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` }, body: JSON.stringify({ model: modelConfig.modelId, messages: messages, temperature: settings.temperature, max_tokens: settings.maxTokens, stream: false }) }); if (!response.ok) { const err = await response.text(); throw new Error(`API Error: ${response.status} - ${err}`); } const data = await response.json(); return data.choices?.[0]?.message?.content || "NO_RESPONSE_TEXT"; }; export const generateResponse = async ( currentPrompt: string, files: KnowledgeFile[], history: Message[], language: Language, modelConfig: ModelConfig, settings: AppSettings, authToken?: string, onSearchStart?: () => void, onSearchComplete?: (results: any) => void ): Promise => { // 1. Resolve API Key: Use model specific key let apiKey = modelConfig.apiKey; console.log('Model config:', modelConfig); console.log('API Key present:', !!apiKey); const langInstructionMap: Record = { zh: "请始终使用Chinese回答。", en: "Please always answer in English.", ja: "常にJapaneseで答えてください。" }; const langInstruction = langInstructionMap[language]; // RAG search (when knowledge base files exist) let ragPrompt = currentPrompt; let ragSources: string[] = []; if (files.length > 0 && authToken) { try { onSearchStart?.(); console.log('Starting RAG search with prompt:', currentPrompt); const ragResponse = await ragService.search(currentPrompt, { ...settings, language }, authToken); console.log('RAG search response:', ragResponse); if (ragResponse && ragResponse.hasRelevantContent) { ragPrompt = ragResponse.ragPrompt; ragSources = ragResponse.sources; console.log('Using RAG enhanced prompt'); } else { console.log('No relevant content found, using original prompt'); } onSearchComplete?.(ragResponse?.searchResults || []); } catch (error) { console.warn('RAG search failed, using original prompt:', error); onSearchComplete?.([]); } finally { // Ensure search status is reset setTimeout(() => { onSearchComplete?.([]); }, 100); } } const systemInstruction = buildSystemInstruction(files, settings, langInstruction); try { // API key is optional - allow local models // --- OpenAI Compatible API Logic --- return await callOpenAICompatible( ragPrompt, files, history, modelConfig, settings, systemInstruction, apiKey || "ollama", ); } catch (error: any) { console.error("AI Service Error:", error); // Provide more detailed error information if (error.name === 'TypeError' && error.message.includes('fetch')) { throw new Error('Network connection failed. Please check server status'); } throw new Error(error.message || "API_ERROR"); } };