Skip to content

Commit 593dc3d

Browse files
update 2.0
1 parent eface3d commit 593dc3d

5 files changed

Lines changed: 188 additions & 15 deletions

File tree

backend/.env

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# NVIDIA Nim Configuration
2+
OPENAI_API_KEY=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
3+
OPENAI_API_URL=https://integrate.api.nvidia.com/v1/chat/completions
4+
OPENAI_MODEL=mistralai/mistral-nemo-12b-instruct
5+
LLM_RERANK=1
6+
LLM_TIMEOUT_MS=15000

backend/.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# NVIDIA Nim Configuration
2+
OPENAI_API_KEY=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
3+
OPENAI_API_URL=https://integrate.api.nvidia.com/v1/chat/completions
4+
OPENAI_MODEL=mistralai/mistral-nemo-12b-instruct
5+
LLM_RERANK=1
6+
LLM_TIMEOUT_MS=15000

backend/database/career_advisor.db

0 Bytes
Binary file not shown.

backend/routes/chatbot.js

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ const {
1010
getRefinementProgress
1111
} = require('../services/questionEngine');
1212
const { matchCareerAsync } = require('../services/matcher');
13+
const llmScorer = require('../services/llmScorer');
1314

1415
/**
1516
* Start a new conversation
1617
*/
17-
router.post('/start', (req, res) => {
18+
router.post('/start', async (req, res) => {
1819
try {
1920
const { conversationId, userType = 'high_school' } = req.body;
2021

@@ -23,14 +24,35 @@ router.post('/start', (req, res) => {
2324
}
2425

2526
const state = getConversationState(conversationId, userType);
26-
const firstQuestion = getNextQuestion(conversationId, userType);
27+
28+
let firstQuestion = null;
29+
if (llmScorer.isEnabled()) {
30+
const result = await llmScorer.generateAgentQuestion({
31+
profile: { user_type: userType },
32+
history: []
33+
});
34+
if (result && result.question) {
35+
firstQuestion = {
36+
id: 'ai_1',
37+
text: result.question,
38+
type: 'multiple_choice',
39+
options: result.options || ["Có", "Không", "Khác..."],
40+
is_ai: true
41+
};
42+
}
43+
}
44+
45+
if (!firstQuestion) {
46+
firstQuestion = getNextQuestion(conversationId, userType);
47+
}
2748

2849
res.json({
2950
success: true,
3051
conversationId,
3152
mode: 'initial',
3253
question: firstQuestion,
33-
canRefine: false
54+
canRefine: false,
55+
isAiAgent: llmScorer.isEnabled()
3456
});
3557
} catch (error) {
3658
console.error('Error starting conversation:', error);
@@ -41,7 +63,7 @@ router.post('/start', (req, res) => {
4163
/**
4264
* Answer a question
4365
*/
44-
router.post('/answer', (req, res) => {
66+
router.post('/answer', async (req, res) => {
4567
try {
4668
const { conversationId, questionId, answer } = req.body;
4769

@@ -56,10 +78,28 @@ router.post('/answer', (req, res) => {
5678
const state = getConversationState(conversationId);
5779
let nextQuestion = null;
5880

59-
if (state.mode === 'initial') {
81+
if (llmScorer.isEnabled()) {
82+
const history = state.answers.map(a => ({
83+
question: a.question,
84+
answer: a.answer
85+
}));
86+
const result = await llmScorer.generateAgentQuestion({
87+
profile: { user_type: state.userType },
88+
history
89+
});
90+
91+
if (result && result.question && result.question !== "DONE") {
92+
nextQuestion = {
93+
id: `ai_${state.answers.length + 1}`,
94+
text: result.question,
95+
type: 'multiple_choice',
96+
options: result.options || ["Có", "Không", "Khác..."],
97+
is_ai: true
98+
};
99+
}
100+
} else if (state.mode === 'initial') {
60101
nextQuestion = getNextQuestion(conversationId, state.userType);
61102
}
62-
// In refinement mode, frontend manages question flow
63103

64104
const progress = getRefinementProgress(conversationId);
65105

@@ -68,8 +108,8 @@ router.post('/answer', (req, res) => {
68108
recorded: true,
69109
nextQuestion,
70110
progress,
71-
canRefine: canStartRefinement(conversationId),
72-
done: !nextQuestion && state.mode === 'initial'
111+
canRefine: !llmScorer.isEnabled() && canStartRefinement(conversationId),
112+
done: !nextQuestion
73113
});
74114
} catch (error) {
75115
console.error('Error recording answer:', error);
@@ -88,7 +128,32 @@ router.post('/recommendations', async (req, res) => {
88128
return res.status(400).json({ error: 'conversationId is required' });
89129
}
90130

91-
const result = getCareerRecommendations(conversationId);
131+
const state = getConversationState(conversationId);
132+
let result = null;
133+
134+
if (llmScorer.isEnabled()) {
135+
const history = state.answers.map(a => ({
136+
question: a.question,
137+
answer: a.answer
138+
}));
139+
const aiResult = await llmScorer.generateAgentRecommendations({
140+
profile: { user_type: state.userType },
141+
history
142+
});
143+
144+
if (aiResult?.recommendations) {
145+
result = {
146+
recommendations: aiResult.recommendations,
147+
totalAnswers: state.answers.length,
148+
isAiAgent: true
149+
};
150+
}
151+
}
152+
153+
if (!result) {
154+
// Fallback to database-driven logic if AI fails
155+
result = getCareerRecommendations(conversationId);
156+
}
92157

93158
res.json({
94159
success: true,

backend/services/llmScorer.js

Lines changed: 102 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
const https = require('https');
22

33
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
4-
const OPENAI_MODEL = process.env.OPENAI_MODEL || 'gpt-4.1-mini';
5-
const OPENAI_API_URL = process.env.OPENAI_API_URL || 'https://api.openai.com/v1/chat/completions';
6-
const LLM_TIMEOUT_MS = Number(process.env.LLM_TIMEOUT_MS || 8000);
4+
const OPENAI_MODEL = process.env.OPENAI_MODEL || 'mistralai/mistral-nemo-12b-instruct';
5+
const OPENAI_API_URL = process.env.OPENAI_API_URL || 'https://integrate.api.nvidia.com/v1/chat/completions';
6+
const LLM_TIMEOUT_MS = Number(process.env.LLM_TIMEOUT_MS || 15000);
77
const LLM_ENABLED = process.env.LLM_RERANK === '1';
88

99
function isEnabled() {
@@ -65,6 +65,7 @@ function mergeScores(base, llmScores) {
6565
if (!item.career_name) continue;
6666
map.set(item.career_name, clampScore(item.match_score));
6767
}
68+
6869
let merged = base.map((item) => {
6970
const score = map.has(item.career_name)
7071
? map.get(item.career_name)
@@ -75,8 +76,28 @@ function mergeScores(base, llmScores) {
7576
confidence: score >= 70 ? 'high' : score >= 50 ? 'medium' : 'low'
7677
};
7778
});
78-
merged = merged.sort((a, b) => b.match_score - a.match_score).slice(0, 10);
79-
return ensureUniqueScores(merged);
79+
80+
// Sort by new score
81+
merged = merged.sort((a, b) => b.match_score - a.match_score);
82+
83+
// Recalculate probabilities (Softmax-like or simple normalization)
84+
const topK = merged.slice(0, 10);
85+
const maxScore = topK[0]?.match_score || 0;
86+
const expScores = topK.map(item => ({
87+
...item,
88+
exp: Math.exp((item.match_score - maxScore) / 10) // Temperature = 10 for AI scores
89+
}));
90+
const sumExp = expScores.reduce((sum, item) => sum + item.exp, 0);
91+
92+
const final = expScores.map(item => {
93+
const { exp, ...rest } = item;
94+
return {
95+
...rest,
96+
probability: sumExp > 0 ? exp / sumExp : 1 / topK.length
97+
};
98+
});
99+
100+
return ensureUniqueScores(final);
80101
}
81102

82103
function safeProfile(profile) {
@@ -132,4 +153,79 @@ function postJson(url, payload, timeoutMs) {
132153
});
133154
}
134155

135-
module.exports = { scoreCareersWithLLM, mergeScores, isEnabled };
156+
async function generateAgentQuestion({ profile, history }) {
157+
if (!isEnabled()) return null;
158+
159+
const system = [
160+
'Bạn là chuyên gia hướng nghiệp thông minh.',
161+
'Dựa trên lịch sử hội thoại và hồ sơ, hãy đặt MỘT câu hỏi tiếp theo để hiểu rõ hơn về người dùng.',
162+
'Câu hỏi phải giúp thu hẹp các lựa chọn nghề nghiệp.',
163+
'Câu hỏi nên ngắn gọn, tự nhiên, thân thiện.',
164+
'Nếu đã đủ thông tin, hãy trả về chuỗi "DONE".',
165+
'Trả về JSON: {"question": "...", "options": ["Có", "Không", "Khác..."], "reasoning": "Tại sao đặt câu hỏi này"}'
166+
].join(' ');
167+
168+
const payload = {
169+
model: OPENAI_MODEL,
170+
temperature: 0.7,
171+
response_format: { type: 'json_object' },
172+
messages: [
173+
{ role: 'system', content: system },
174+
{
175+
role: 'user',
176+
content: JSON.stringify({ profile: safeProfile(profile), history })
177+
}
178+
]
179+
};
180+
181+
const data = await postJson(OPENAI_API_URL, payload, LLM_TIMEOUT_MS);
182+
const content = data?.choices?.[0]?.message?.content;
183+
if (!content) return null;
184+
try {
185+
return JSON.parse(content);
186+
} catch {
187+
return null;
188+
}
189+
}
190+
191+
async function generateAgentRecommendations({ profile, history }) {
192+
if (!isEnabled()) return null;
193+
194+
const system = [
195+
'Bạn là chuyên gia hướng nghiệp cấp cao.',
196+
'Hãy đề xuất top 5-10 nghề nghiệp phù hợp nhất dựa trên lịch sử hội thoại.',
197+
'KHÔNG giới hạn trong bất kỳ danh sách nào, hãy dùng kiến thức rộng lớn của bạn.',
198+
'Với mỗi nghề, cung cấp: tên nghề, match_score (0-100), xác suất (0-1), lý do cụ thể.',
199+
'Trả về JSON: {"recommendations": [{"career_name": "...", "match_score": 85, "probability": 0.4, "reasons": ["..."]}]}'
200+
].join(' ');
201+
202+
const payload = {
203+
model: OPENAI_MODEL,
204+
temperature: 0.5,
205+
response_format: { type: 'json_object' },
206+
messages: [
207+
{ role: 'system', content: system },
208+
{
209+
role: 'user',
210+
content: JSON.stringify({ profile: safeProfile(profile), history })
211+
}
212+
]
213+
};
214+
215+
const data = await postJson(OPENAI_API_URL, payload, LLM_TIMEOUT_MS * 2);
216+
const content = data?.choices?.[0]?.message?.content;
217+
if (!content) return null;
218+
try {
219+
return JSON.parse(content);
220+
} catch {
221+
return null;
222+
}
223+
}
224+
225+
module.exports = {
226+
scoreCareersWithLLM,
227+
mergeScores,
228+
isEnabled,
229+
generateAgentQuestion,
230+
generateAgentRecommendations
231+
};

0 commit comments

Comments
 (0)