0a9588abb7
- 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
196 lines
5.7 KiB
TypeScript
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");
|
|
}
|
|
};
|