11const https = require ( 'https' ) ;
22
33const 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 ) ;
77const LLM_ENABLED = process . env . LLM_RERANK === '1' ;
88
99function 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
82103function 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