Files
aurak/web/services/geminiService.ts
Developer 0a9588abb7 feat: implement QuestionBank CRUD with pagination and template query
- Add pagination support to findAll (page, limit query params)
- Add findByTemplateId method to service
- Add GET /by-template/:templateId endpoint to controller
- Service already includes CRUD for QuestionBank and QuestionBankItem
2026-04-23 17:19:11 +08:00

196 lines
5.7 KiB
TypeScript

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<string> => {
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<string> => {
// 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<Language, string> = {
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");
}
};