forked from hangshuo652/aurak
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
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
import { ChatOpenAI } from '@langchain/openai';
|
||||
import { SystemMessage, HumanMessage } from '@langchain/core/messages';
|
||||
import { RunnableConfig } from '@langchain/core/runnables';
|
||||
import { EvaluationState } from '../state';
|
||||
import { safeParseJson } from '../../../common/json-utils';
|
||||
|
||||
/**
|
||||
* Node responsible for generating assessment questions based on the knowledge base content.
|
||||
*/
|
||||
export const questionGeneratorNode = async (
|
||||
state: EvaluationState,
|
||||
config?: RunnableConfig,
|
||||
): Promise<Partial<EvaluationState>> => {
|
||||
const { model, knowledgeBaseContent, targetCount } = (config?.configurable as any) || {};
|
||||
const limitCount = targetCount || 5;
|
||||
|
||||
console.log('[GeneratorNode] Starting generation...', {
|
||||
language: state.language,
|
||||
hasModel: !!model,
|
||||
contentLength: knowledgeBaseContent?.length,
|
||||
keywords: state.keywords || [],
|
||||
targetCount: limitCount,
|
||||
});
|
||||
|
||||
if (!model || !knowledgeBaseContent) {
|
||||
console.error('[GeneratorNode] Missing model or knowledgeBaseContent');
|
||||
throw new Error(
|
||||
'Missing model or knowledgeBaseContent in node configuration',
|
||||
);
|
||||
}
|
||||
|
||||
const isZh = state.language === 'zh';
|
||||
const isJa = state.language === 'ja';
|
||||
|
||||
const style = state.style || 'technical';
|
||||
const difficultyText = state.difficultyDistribution
|
||||
? JSON.stringify(state.difficultyDistribution)
|
||||
: isZh
|
||||
? '随机分布'
|
||||
: isJa
|
||||
? 'ランダム分布'
|
||||
: 'Random distribution';
|
||||
const keywords = state.keywords || [];
|
||||
const hasKeywords = keywords.length > 0;
|
||||
const keywordText = hasKeywords ? keywords.join(', ') : '';
|
||||
|
||||
const rulesZh = [
|
||||
`**禁止重复**:绝对禁止生成与下方“禁止重复列表”中相似的题目。`,
|
||||
`**深度挖掘**:如果之前的题目考查了核心定义,新题目必须考查具体的应用案例、对比分析或隐藏的细节。`,
|
||||
hasKeywords
|
||||
? `**关键词权重**:必须围绕关键词 (${keywordText}) 展开,但要从关键词的不同侧面(如流程、限制、优缺点、具体参数等)进行挖掘。`
|
||||
: null,
|
||||
`**随机扰动**:即使对于相同的主题或关键词,也要尝试从不同的逻辑链条(如“因为...所以...” vs “如果没有...会怎样”)出发。`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.map((r, i) => `${i + 1}. ${r}`)
|
||||
.join('\n');
|
||||
|
||||
const rulesJa = [
|
||||
`**重複禁止**:下記の「作成済み問題リスト」と類似した内容は絶対に避けてください。`,
|
||||
`**多角的アプローチ**:前回が定義だった場合は、今回は応用方法、制限事項、具体的な数値などに焦点を当ててください。`,
|
||||
hasKeywords
|
||||
? `**キーワードの深掘り**:キーワード (${keywordText}) の異なる側面から出題してください。`
|
||||
: null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.map((r, i) => `${i + 1}. ${r}`)
|
||||
.join('\n');
|
||||
|
||||
const rulesEn = [
|
||||
`**NO REPETITION**: Strictly avoid any conceptual overlap with the "Previous Questions" list below.`,
|
||||
`**New Facets**: If previous questions were about definitions, focus on applications, edge cases, or specific details.`,
|
||||
hasKeywords
|
||||
? `**Keyword Variety**: Center on (${keywordText}), but explore different aspects (process, pros/cons, requirements).`
|
||||
: null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.map((r, i) => `${i + 1}. ${r}`)
|
||||
.join('\n');
|
||||
|
||||
const existingQuestions = state.questions || [];
|
||||
const existingQuestionsText = existingQuestions
|
||||
.map((q, i) => `Q${i + 1}: ${q.questionText}`)
|
||||
.join('\n');
|
||||
|
||||
const systemPromptZh = `你是一位专业的知识评估专家。请根据提供的知识库片段生成 1 个唯一的测试题目。
|
||||
|
||||
### 强制性语言规则:
|
||||
**必须使用中文 (Simplified Chinese) 进行回复**。即使知识库内容是英文或其他语言,问题(question_text)和关键点(key_points)也必须使用中文。
|
||||
|
||||
### 强制性多样性规则:
|
||||
${rulesZh}
|
||||
|
||||
### 禁止重复列表(已出过):
|
||||
${existingQuestionsText || '无'}
|
||||
|
||||
### 任务:
|
||||
${hasKeywords ? `目标关键词:${keywordText}\n` : ''}出题风格:${style}
|
||||
难度:${difficultyText}
|
||||
|
||||
请以 JSON 数组格式返回 1 个问题:
|
||||
[
|
||||
{
|
||||
"question_text": "...",
|
||||
"key_points": ["点1", "点2"],
|
||||
"difficulty": "...",
|
||||
"dimension": "prompt/llm/ide/devPattern/workCapability",
|
||||
"basis": "[n] 引用原文..."
|
||||
}
|
||||
]`;
|
||||
// dimension取值:prompt=提示词, llm=LLM原理, ide=IDE协作, devPattern=开发范式, workCapability=工作能力
|
||||
|
||||
const systemPromptJa = `あなたは専門的なアセスメントエキスパートです。提供されたナレッジベースに基づいて、ユニークな問題を 1 つ作成してください。
|
||||
|
||||
### 言語ルール(最重要):
|
||||
**必ず日本語で作成してください**。提供されたナレッジベースが英語や中国語、その他の言語であっても、質問文(question_text)およびキーポイント(key_points)は必ず日本語で回答してください。中国語が混ざらないように厳格に注意してください。
|
||||
|
||||
### 多様性ルール:
|
||||
${rulesJa}
|
||||
|
||||
### 作成済み問題リスト:
|
||||
${existingQuestionsText || 'なし'}
|
||||
|
||||
### 任務:
|
||||
${hasKeywords ? `目標キーワード:${keywordText}\n` : ''}出題スタイル:${style}
|
||||
難易度:${difficultyText}
|
||||
|
||||
以下のJSON配列形式で問題を1つ返してください:
|
||||
[
|
||||
{
|
||||
"question_text": "...",
|
||||
"key_points": ["ポイント1", "ポイント2"],
|
||||
"difficulty": "...",
|
||||
"dimension": "prompt/llm/ide/devPattern/workCapability",
|
||||
"basis": "[n] 引用箇所..."
|
||||
}
|
||||
]`;
|
||||
|
||||
const systemPromptEn = `You are an expert examiner. Generate 1 UNIQUE question based on the provided context.
|
||||
|
||||
### Language Rule:
|
||||
**You MUST generate the question and key points in English.**
|
||||
|
||||
### Diversity Rules:
|
||||
${rulesEn}
|
||||
|
||||
### Previous Questions (DO NOT REPEAT):
|
||||
${existingQuestionsText || 'None'}
|
||||
|
||||
Return 1 question as a JSON array with format:
|
||||
[
|
||||
{
|
||||
"question_text": "...",
|
||||
"key_points": ["point1", "point2"],
|
||||
"difficulty": "...",
|
||||
"dimension": "prompt/llm/ide/devPattern/workCapability",
|
||||
"basis": "[n] citation..."
|
||||
}
|
||||
]`;
|
||||
|
||||
// dimension values: prompt=prompt engineering, llm=LLM principles, ide=IDE collaboration, devPattern=development paradigm, workCapability=work capability
|
||||
|
||||
const systemPrompt = isZh
|
||||
? systemPromptZh
|
||||
: isJa
|
||||
? systemPromptJa
|
||||
: systemPromptEn;
|
||||
const humanMsg = isZh
|
||||
? `请使用中文基于以下内容生成题目:\n\n${knowledgeBaseContent}`
|
||||
: isJa
|
||||
? `以下の内容に基づいて、必ず日本語でアセスメント問題を作成してください:\n\n${knowledgeBaseContent}`
|
||||
: `Generate evaluation question in English based on:\n\n${knowledgeBaseContent}`;
|
||||
|
||||
try {
|
||||
const response = await model.invoke([
|
||||
new SystemMessage(systemPrompt),
|
||||
new HumanMessage(humanMsg),
|
||||
]);
|
||||
|
||||
try {
|
||||
let newQuestions = safeParseJson<any>(response.content as string);
|
||||
|
||||
if (!newQuestions) {
|
||||
console.error('[GeneratorNode] Failed to parse JSON. Raw content:', response.content);
|
||||
throw new Error('Invalid JSON format from AI');
|
||||
}
|
||||
|
||||
// Handle both array and single object
|
||||
if (!Array.isArray(newQuestions)) {
|
||||
newQuestions = [newQuestions];
|
||||
}
|
||||
|
||||
const dimensionMap: Record<string, string> = {
|
||||
// 中文
|
||||
'技术能力-提示词': 'prompt',
|
||||
'提示词': 'prompt',
|
||||
'技术能力-LLM': 'llm',
|
||||
'LLM': 'llm',
|
||||
'IDE协作能力': 'ide',
|
||||
'IDE': 'ide',
|
||||
'AI开发范式': 'devPattern',
|
||||
'开发范式': 'devPattern',
|
||||
'工作能力-安全': 'workCapability',
|
||||
'工作能力': 'workCapability',
|
||||
// 英文直接映射
|
||||
'prompt': 'prompt',
|
||||
'llm': 'llm',
|
||||
'ide': 'ide',
|
||||
'devPattern': 'devPattern',
|
||||
'workCapability': 'workCapability',
|
||||
};
|
||||
|
||||
const mappedNewQuestions = newQuestions.map((q: any) => {
|
||||
let inferredDimension = 'workCapability';
|
||||
const dimValue = q.dimension?.toString().toLowerCase().trim();
|
||||
if (dimValue) {
|
||||
inferredDimension = dimensionMap[dimValue] || 'workCapability';
|
||||
console.log('[GeneratorNode] Dimension mapping:', { original: q.dimension, mapped: inferredDimension });
|
||||
}
|
||||
return {
|
||||
id: (existingQuestions.length + 1).toString(),
|
||||
questionText: q.question_text,
|
||||
keyPoints: q.key_points,
|
||||
difficulty: q.difficulty,
|
||||
basis: q.basis,
|
||||
dimension: inferredDimension,
|
||||
};
|
||||
});
|
||||
|
||||
const questionsToGenerate = Math.max(1, limitCount - existingQuestions.length);
|
||||
const limitedNewQuestions = mappedNewQuestions.slice(0, questionsToGenerate);
|
||||
|
||||
console.log('[GeneratorNode] Generated questions:', mappedNewQuestions.length, 'Limit:', questionsToGenerate);
|
||||
|
||||
return {
|
||||
questions: [...existingQuestions, ...limitedNewQuestions],
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[GeneratorNode] Parse error:', error);
|
||||
return { questions: existingQuestions };
|
||||
}
|
||||
} catch (invokeError) {
|
||||
console.error('[GeneratorNode] Invoke error:', invokeError);
|
||||
throw invokeError;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user