@reconcrap/boss-recommend-mcp 1.3.30 → 1.3.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/screening-config.example.json +2 -0
- package/package.json +1 -1
- package/src/adapters.js +22 -0
- package/src/boss-chat.js +14 -1
- package/src/test-adapters-runtime.js +90 -0
- package/src/test-boss-chat.js +719 -58
- package/vendor/boss-chat-cli/src/app.js +411 -175
- package/vendor/boss-chat-cli/src/cli.js +20 -0
- package/vendor/boss-chat-cli/src/services/chrome-client.js +8 -2
- package/vendor/boss-chat-cli/src/services/llm.js +252 -84
- package/vendor/boss-chat-cli/src/services/profile-store.js +6 -0
- package/vendor/boss-chat-cli/src/services/report-store.js +301 -3
- package/vendor/boss-chat-cli/src/services/resume-capture.js +41 -126
- package/vendor/boss-chat-cli/src/services/resume-network.js +727 -0
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +44 -2
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
} from './services/profile-store.js';
|
|
39
39
|
import { ReportStore } from './services/report-store.js';
|
|
40
40
|
import { ResumeCaptureService } from './services/resume-capture.js';
|
|
41
|
+
import { ResumeNetworkTracker } from './services/resume-network.js';
|
|
41
42
|
import { NoopStateStore, StateStore } from './services/state-store.js';
|
|
42
43
|
|
|
43
44
|
const CLI_FILE_PATH = fileURLToPath(import.meta.url);
|
|
@@ -264,6 +265,14 @@ function parseArgs(argv) {
|
|
|
264
265
|
case 'reasoning-effort':
|
|
265
266
|
args.overrides.llm.thinkingLevel = value || '';
|
|
266
267
|
break;
|
|
268
|
+
case 'llm-timeout-ms':
|
|
269
|
+
case 'timeout-ms':
|
|
270
|
+
args.overrides.llm.timeoutMs = Number.parseInt(value, 10);
|
|
271
|
+
break;
|
|
272
|
+
case 'llm-max-retries':
|
|
273
|
+
case 'max-retries':
|
|
274
|
+
args.overrides.llm.maxRetries = Number.parseInt(value, 10);
|
|
275
|
+
break;
|
|
267
276
|
case 'port':
|
|
268
277
|
args.overrides.chrome.port = Number.parseInt(value, 10);
|
|
269
278
|
break;
|
|
@@ -315,6 +324,8 @@ function printUsage() {
|
|
|
315
324
|
console.log(' --apikey <key> Override LLM API key');
|
|
316
325
|
console.log(' --model <name> Override LLM model');
|
|
317
326
|
console.log(' --thinking-level <level> LLM thinking level: off|low|medium|high|current');
|
|
327
|
+
console.log(' --llm-timeout-ms <n> Override per-request LLM timeout (default: 60000)');
|
|
328
|
+
console.log(' --llm-max-retries <n> Override per-request LLM retry count (default: 3)');
|
|
318
329
|
console.log(' --port <n> Override Chrome remote debugging port');
|
|
319
330
|
}
|
|
320
331
|
|
|
@@ -925,6 +936,12 @@ function buildDetachedRunArgs(args, runId) {
|
|
|
925
936
|
if (args.overrides.llm.thinkingLevel) {
|
|
926
937
|
workerArgs.push('--thinking-level', String(args.overrides.llm.thinkingLevel));
|
|
927
938
|
}
|
|
939
|
+
if (Number.isFinite(args.overrides.llm.timeoutMs) && args.overrides.llm.timeoutMs > 0) {
|
|
940
|
+
workerArgs.push('--llm-timeout-ms', String(args.overrides.llm.timeoutMs));
|
|
941
|
+
}
|
|
942
|
+
if (Number.isFinite(args.overrides.llm.maxRetries) && args.overrides.llm.maxRetries > 0) {
|
|
943
|
+
workerArgs.push('--llm-max-retries', String(args.overrides.llm.maxRetries));
|
|
944
|
+
}
|
|
928
945
|
if (Number.isFinite(args.overrides.chrome.port)) {
|
|
929
946
|
workerArgs.push('--port', String(args.overrides.chrome.port));
|
|
930
947
|
}
|
|
@@ -1341,6 +1358,7 @@ async function executeRunCommand(args, dataDir) {
|
|
|
1341
1358
|
});
|
|
1342
1359
|
const llmClient = new LlmClient(runProfile.llm);
|
|
1343
1360
|
const resumeCaptureService = new ResumeCaptureService({ chromeClient, logger });
|
|
1361
|
+
const resumeNetworkTracker = new ResumeNetworkTracker({ chromeClient, logger });
|
|
1344
1362
|
const stateStore = args.noState ? new NoopStateStore() : new StateStore(dataDir, args.profile);
|
|
1345
1363
|
const reportStore = new ReportStore(dataDir);
|
|
1346
1364
|
const app = new BossChatApp({
|
|
@@ -1350,6 +1368,7 @@ async function executeRunCommand(args, dataDir) {
|
|
|
1350
1368
|
resumeCaptureService,
|
|
1351
1369
|
stateStore,
|
|
1352
1370
|
reportStore,
|
|
1371
|
+
resumeNetworkTracker,
|
|
1353
1372
|
runControl,
|
|
1354
1373
|
logger,
|
|
1355
1374
|
dryRun: args.dryRun,
|
|
@@ -1540,6 +1559,7 @@ async function main() {
|
|
|
1540
1559
|
}
|
|
1541
1560
|
|
|
1542
1561
|
export const __testables = {
|
|
1562
|
+
parseArgs,
|
|
1543
1563
|
connectBossChatPage,
|
|
1544
1564
|
hasHydratedChatShell,
|
|
1545
1565
|
promptRunProfile,
|
|
@@ -15,14 +15,20 @@ export class ChromeClient {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
this.client = await CDP({ port: this.port, target });
|
|
18
|
-
const { Runtime, DOM, Page, Input } = this.client;
|
|
18
|
+
const { Runtime, DOM, Page, Input, Network } = this.client;
|
|
19
19
|
|
|
20
|
-
await Promise.all([
|
|
20
|
+
await Promise.all([
|
|
21
|
+
Runtime.enable(),
|
|
22
|
+
DOM.enable(),
|
|
23
|
+
Page.enable(),
|
|
24
|
+
Network && typeof Network.enable === 'function' ? Network.enable() : Promise.resolve(),
|
|
25
|
+
]);
|
|
21
26
|
|
|
22
27
|
this.Runtime = Runtime;
|
|
23
28
|
this.DOM = DOM;
|
|
24
29
|
this.Page = Page;
|
|
25
30
|
this.Input = Input;
|
|
31
|
+
this.Network = Network || null;
|
|
26
32
|
|
|
27
33
|
return target;
|
|
28
34
|
}
|
|
@@ -43,6 +43,22 @@ function getCompletionContent(data) {
|
|
|
43
43
|
return '';
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
function flattenChatMessageContent(content) {
|
|
47
|
+
if (Array.isArray(content)) {
|
|
48
|
+
return content
|
|
49
|
+
.map((item) => {
|
|
50
|
+
if (typeof item === 'string') return item;
|
|
51
|
+
if (item && typeof item === 'object') {
|
|
52
|
+
return item.text || item.content || item.reasoning_content || '';
|
|
53
|
+
}
|
|
54
|
+
return '';
|
|
55
|
+
})
|
|
56
|
+
.filter(Boolean)
|
|
57
|
+
.join('\n');
|
|
58
|
+
}
|
|
59
|
+
return String(content || '');
|
|
60
|
+
}
|
|
61
|
+
|
|
46
62
|
function getResponsesContent(data) {
|
|
47
63
|
if (typeof data?.output_text === 'string' && data.output_text.trim()) {
|
|
48
64
|
return data.output_text;
|
|
@@ -163,6 +179,100 @@ function toStringArray(value, maxItems = 8) {
|
|
|
163
179
|
return normalized;
|
|
164
180
|
}
|
|
165
181
|
|
|
182
|
+
function collectNestedText(value, out = [], depth = 0) {
|
|
183
|
+
if (depth > 6 || value === null || value === undefined) return out;
|
|
184
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
185
|
+
const normalized = normalizeText(String(value));
|
|
186
|
+
if (normalized) out.push(normalized);
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
if (Array.isArray(value)) {
|
|
190
|
+
for (const item of value) {
|
|
191
|
+
collectNestedText(item, out, depth + 1);
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
if (typeof value === 'object') {
|
|
196
|
+
const priorityKeys = ['text', 'reasoning_content', 'summary_text', 'summary', 'content', 'cot', 'reason'];
|
|
197
|
+
const seen = new Set();
|
|
198
|
+
for (const key of priorityKeys) {
|
|
199
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
200
|
+
seen.add(key);
|
|
201
|
+
collectNestedText(value[key], out, depth + 1);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
205
|
+
if (seen.has(key)) continue;
|
|
206
|
+
collectNestedText(nested, out, depth + 1);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function dedupeTextFragments(fragments = []) {
|
|
213
|
+
const deduped = [];
|
|
214
|
+
const seen = new Set();
|
|
215
|
+
for (const item of fragments) {
|
|
216
|
+
const normalized = normalizeText(item);
|
|
217
|
+
if (!normalized) continue;
|
|
218
|
+
if (seen.has(normalized)) continue;
|
|
219
|
+
seen.add(normalized);
|
|
220
|
+
deduped.push(normalized);
|
|
221
|
+
}
|
|
222
|
+
return deduped;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function joinTextFragments(fragments = []) {
|
|
226
|
+
return dedupeTextFragments(fragments).join('\n');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function extractCompletionReasoningText(data) {
|
|
230
|
+
const choice = data?.choices?.[0] || {};
|
|
231
|
+
const fragments = [];
|
|
232
|
+
const content = choice?.message?.content;
|
|
233
|
+
if (Array.isArray(content)) {
|
|
234
|
+
for (const part of content) {
|
|
235
|
+
const partType = normalizeText(part?.type || '').toLowerCase();
|
|
236
|
+
if (partType.includes('reason') || partType.includes('summary')) {
|
|
237
|
+
collectNestedText(part, fragments);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const candidates = [
|
|
242
|
+
choice?.message?.reasoning_content,
|
|
243
|
+
choice?.message?.reasoning,
|
|
244
|
+
choice?.reasoning_content,
|
|
245
|
+
choice?.reasoning,
|
|
246
|
+
];
|
|
247
|
+
for (const candidate of candidates) {
|
|
248
|
+
collectNestedText(candidate, fragments);
|
|
249
|
+
}
|
|
250
|
+
return joinTextFragments(fragments);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function extractResponsesReasoningText(data) {
|
|
254
|
+
const fragments = [];
|
|
255
|
+
collectNestedText(data?.reasoning, fragments);
|
|
256
|
+
collectNestedText(data?.reasoning_content, fragments);
|
|
257
|
+
|
|
258
|
+
const output = Array.isArray(data?.output) ? data.output : [];
|
|
259
|
+
for (const item of output) {
|
|
260
|
+
const itemType = normalizeText(item?.type || '').toLowerCase();
|
|
261
|
+
if (itemType.includes('reason') || itemType.includes('summary')) {
|
|
262
|
+
collectNestedText(item, fragments);
|
|
263
|
+
}
|
|
264
|
+
const content = Array.isArray(item?.content) ? item.content : [];
|
|
265
|
+
for (const chunk of content) {
|
|
266
|
+
const chunkType = normalizeText(chunk?.type || '').toLowerCase();
|
|
267
|
+
if (chunkType.includes('reason') || chunkType.includes('summary')) {
|
|
268
|
+
collectNestedText(chunk, fragments);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return joinTextFragments(fragments);
|
|
274
|
+
}
|
|
275
|
+
|
|
166
276
|
function extractEvidenceTokens(text, maxItems = MAX_EVIDENCE_TOKENS) {
|
|
167
277
|
const normalized = normalizeText(text);
|
|
168
278
|
if (!normalized) return [];
|
|
@@ -300,8 +410,9 @@ function buildImagePrompt({ screeningCriteria, candidate }) {
|
|
|
300
410
|
'若无法在教育经历模块确认学校名称,不要编造学校名;按信息不足处理。',
|
|
301
411
|
'必须完整阅读全部简历截图分段后再判断。',
|
|
302
412
|
'必须且只能返回 JSON,不要输出 Markdown。',
|
|
303
|
-
'返回格式:{"passed":true
|
|
304
|
-
'
|
|
413
|
+
'返回格式:{"passed":true} 或 {"passed":false}。',
|
|
414
|
+
'不要返回理由、总结、证据、思维过程或额外字段。',
|
|
415
|
+
'当信息不足以支持通过时,返回 {"passed":false}。',
|
|
305
416
|
'',
|
|
306
417
|
`筛选标准:${screeningCriteria}`,
|
|
307
418
|
'',
|
|
@@ -316,7 +427,7 @@ function buildTextPrompt({ screeningCriteria, candidate, resumeText, chunkIndex
|
|
|
316
427
|
const profileContext = buildProfileContext(candidate);
|
|
317
428
|
const chunkHint =
|
|
318
429
|
chunkTotal > 1
|
|
319
|
-
? `\n\n当前输入是简历分段 ${chunkIndex}/${chunkTotal}。请严格基于本分段文本判断;如果本分段证据不足,必须返回 passed
|
|
430
|
+
? `\n\n当前输入是简历分段 ${chunkIndex}/${chunkTotal}。请严格基于本分段文本判断;如果本分段证据不足,必须返回 {"passed":false}。`
|
|
320
431
|
: '';
|
|
321
432
|
return [
|
|
322
433
|
'你是招聘筛选助手,请基于简历文本判断候选人是否符合筛选标准。',
|
|
@@ -325,8 +436,9 @@ function buildTextPrompt({ screeningCriteria, candidate, resumeText, chunkIndex
|
|
|
325
436
|
'必须忽略推荐模块与匿名卡片信息(例如“其他名企大厂经历牛人”“相似牛人”“推荐牛人”)。',
|
|
326
437
|
'若无法在教育经历模块确认学校名称,不要编造学校名;按信息不足处理。',
|
|
327
438
|
'必须且只能返回 JSON,不要输出 Markdown。',
|
|
328
|
-
'返回格式:{"passed":true
|
|
329
|
-
'
|
|
439
|
+
'返回格式:{"passed":true} 或 {"passed":false}。',
|
|
440
|
+
'不要返回理由、总结、证据、思维过程或额外字段。',
|
|
441
|
+
'当信息不足以支持通过时,返回 {"passed":false}。',
|
|
330
442
|
'',
|
|
331
443
|
`筛选标准:${screeningCriteria}`,
|
|
332
444
|
'',
|
|
@@ -339,12 +451,52 @@ function buildTextPrompt({ screeningCriteria, candidate, resumeText, chunkIndex
|
|
|
339
451
|
].join('\n');
|
|
340
452
|
}
|
|
341
453
|
|
|
454
|
+
function pickFirstText(...values) {
|
|
455
|
+
for (const value of values) {
|
|
456
|
+
const normalized = normalizeText(value);
|
|
457
|
+
if (normalized) return normalized;
|
|
458
|
+
}
|
|
459
|
+
return '';
|
|
460
|
+
}
|
|
461
|
+
|
|
342
462
|
export function parseLlmJson(content, options = {}) {
|
|
343
463
|
const text = String(content || '').trim();
|
|
344
464
|
if (!text) {
|
|
345
465
|
throw new Error('LLM returned empty content');
|
|
346
466
|
}
|
|
347
467
|
|
|
468
|
+
const normalizedText = normalizeText(text);
|
|
469
|
+
const chunkIndex = Number.isInteger(options.chunkIndex) && options.chunkIndex > 0 ? options.chunkIndex : 1;
|
|
470
|
+
const chunkTotal = Number.isInteger(options.chunkTotal) && options.chunkTotal > 0 ? options.chunkTotal : 1;
|
|
471
|
+
|
|
472
|
+
if (/^(pass|passed|true)$/i.test(normalizedText)) {
|
|
473
|
+
return {
|
|
474
|
+
passed: true,
|
|
475
|
+
rawOutputText: text,
|
|
476
|
+
rawReasoningText: normalizeText(options.reasoningText || ''),
|
|
477
|
+
cot: normalizeText(options.reasoningText || ''),
|
|
478
|
+
reason: '',
|
|
479
|
+
summary: '',
|
|
480
|
+
evidence: [],
|
|
481
|
+
chunkIndex,
|
|
482
|
+
chunkTotal,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (/^(fail|failed|false)$/i.test(normalizedText)) {
|
|
487
|
+
return {
|
|
488
|
+
passed: false,
|
|
489
|
+
rawOutputText: text,
|
|
490
|
+
rawReasoningText: normalizeText(options.reasoningText || ''),
|
|
491
|
+
cot: normalizeText(options.reasoningText || ''),
|
|
492
|
+
reason: '',
|
|
493
|
+
summary: '',
|
|
494
|
+
evidence: [],
|
|
495
|
+
chunkIndex,
|
|
496
|
+
chunkTotal,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
348
500
|
const codeFenceMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
349
501
|
const candidate = codeFenceMatch ? codeFenceMatch[1] : text;
|
|
350
502
|
const jsonMatch = candidate.match(/\{[\s\S]*\}/);
|
|
@@ -353,54 +505,40 @@ export function parseLlmJson(content, options = {}) {
|
|
|
353
505
|
}
|
|
354
506
|
|
|
355
507
|
const parsed = JSON.parse(jsonMatch[0]);
|
|
356
|
-
const parsedPassed =
|
|
508
|
+
const parsedPassed =
|
|
509
|
+
typeof parsed.passed === 'boolean'
|
|
510
|
+
? parsed.passed
|
|
511
|
+
: typeof parsed.matched === 'boolean'
|
|
512
|
+
? parsed.matched
|
|
513
|
+
: /^pass$/i.test(String(parsed.decision || '').trim())
|
|
514
|
+
? true
|
|
515
|
+
: /^fail$/i.test(String(parsed.decision || '').trim())
|
|
516
|
+
? false
|
|
517
|
+
: null;
|
|
357
518
|
if (typeof parsedPassed !== 'boolean') {
|
|
358
519
|
throw new Error('LLM response missing boolean "passed"');
|
|
359
520
|
}
|
|
360
|
-
if (typeof parsed.reason !== 'string' || !parsed.reason.trim()) {
|
|
361
|
-
throw new Error('LLM response missing string "reason"');
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const reason = normalizeText(parsed.reason);
|
|
365
|
-
const summary = normalizeText(parsed.summary || reason);
|
|
366
|
-
const parsedEvidence = toStringArray(parsed.evidence);
|
|
367
521
|
|
|
368
|
-
const
|
|
369
|
-
const
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
const matched = matchEvidenceAgainstResume(item, evidenceCorpus, normalizedCorpus, normalizedCorpusLower);
|
|
380
|
-
if (matched.matched) {
|
|
381
|
-
evidence.push(item);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
evidenceMatchedCount = evidence.length;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const rawPassed = parsedPassed === true;
|
|
388
|
-
const evidenceRawCount = parsedEvidence.length;
|
|
389
|
-
const evidenceGateDemoted = rawPassed && evidenceMatchedCount <= 0;
|
|
390
|
-
const passed = evidenceGateDemoted ? false : rawPassed;
|
|
391
|
-
const finalReason = evidenceGateDemoted
|
|
392
|
-
? `模型未给出可在简历原文中校验的证据,按安全策略判为不通过。${reason ? ` 原始原因: ${reason}` : ''}`
|
|
393
|
-
: reason;
|
|
522
|
+
const parsedReason = pickFirstText(parsed?.reason, parsed?.summary, parsed?.summary_text);
|
|
523
|
+
const parsedSummary = pickFirstText(parsed?.summary, parsed?.summary_text, parsed?.reason);
|
|
524
|
+
const parsedCot = pickFirstText(
|
|
525
|
+
options.reasoningText,
|
|
526
|
+
parsed?.cot,
|
|
527
|
+
parsed?.reasoning_content,
|
|
528
|
+
parsed?.reasoning,
|
|
529
|
+
parsedReason,
|
|
530
|
+
parsedSummary,
|
|
531
|
+
);
|
|
532
|
+
const parsedEvidence = toStringArray(parsed?.evidence);
|
|
394
533
|
|
|
395
534
|
return {
|
|
396
|
-
passed,
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
evidenceGateDemoted,
|
|
535
|
+
passed: parsedPassed,
|
|
536
|
+
rawOutputText: text,
|
|
537
|
+
rawReasoningText: normalizeText(options.reasoningText || ''),
|
|
538
|
+
cot: parsedCot,
|
|
539
|
+
reason: parsedReason || parsedCot,
|
|
540
|
+
summary: parsedSummary || parsedReason || parsedCot,
|
|
541
|
+
evidence: parsedEvidence,
|
|
404
542
|
chunkIndex,
|
|
405
543
|
chunkTotal,
|
|
406
544
|
};
|
|
@@ -485,10 +623,16 @@ export class LlmClient {
|
|
|
485
623
|
throw lastError || new Error(`${label} evaluation failed`);
|
|
486
624
|
}
|
|
487
625
|
|
|
488
|
-
async requestResponses({ prompt, imageDataUrl = null, evidenceCorpus = '', chunkIndex = 1, chunkTotal = 1 }) {
|
|
626
|
+
async requestResponses({ prompt, imageDataUrl = null, imageDataUrls = [], evidenceCorpus = '', chunkIndex = 1, chunkTotal = 1 }) {
|
|
489
627
|
const content = [{ type: 'input_text', text: prompt }];
|
|
628
|
+
const normalizedImageDataUrls = Array.isArray(imageDataUrls)
|
|
629
|
+
? imageDataUrls.map((item) => String(item || '').trim()).filter(Boolean)
|
|
630
|
+
: [];
|
|
490
631
|
if (imageDataUrl) {
|
|
491
|
-
|
|
632
|
+
normalizedImageDataUrls.unshift(String(imageDataUrl));
|
|
633
|
+
}
|
|
634
|
+
for (const item of normalizedImageDataUrls) {
|
|
635
|
+
content.push({ type: 'input_image', image_url: item });
|
|
492
636
|
}
|
|
493
637
|
const payload = {
|
|
494
638
|
model: this.model,
|
|
@@ -526,6 +670,7 @@ export class LlmClient {
|
|
|
526
670
|
}
|
|
527
671
|
|
|
528
672
|
const outputContent = getResponsesContent(data);
|
|
673
|
+
const reasoningText = extractResponsesReasoningText(data);
|
|
529
674
|
if (!outputContent) {
|
|
530
675
|
const incompleteReason = String(data?.incomplete_details?.reason || '').trim();
|
|
531
676
|
const outputTypes = Array.isArray(data?.output)
|
|
@@ -548,6 +693,7 @@ export class LlmClient {
|
|
|
548
693
|
try {
|
|
549
694
|
return parseLlmJson(outputContent, {
|
|
550
695
|
evidenceCorpus,
|
|
696
|
+
reasoningText,
|
|
551
697
|
chunkIndex,
|
|
552
698
|
chunkTotal,
|
|
553
699
|
});
|
|
@@ -560,10 +706,16 @@ export class LlmClient {
|
|
|
560
706
|
}
|
|
561
707
|
}
|
|
562
708
|
|
|
563
|
-
async requestCompletions({ prompt, imageDataUrl = null, evidenceCorpus = '', chunkIndex = 1, chunkTotal = 1 }) {
|
|
709
|
+
async requestCompletions({ prompt, imageDataUrl = null, imageDataUrls = [], evidenceCorpus = '', chunkIndex = 1, chunkTotal = 1 }) {
|
|
564
710
|
const content = [{ type: 'text', text: prompt }];
|
|
711
|
+
const normalizedImageDataUrls = Array.isArray(imageDataUrls)
|
|
712
|
+
? imageDataUrls.map((item) => String(item || '').trim()).filter(Boolean)
|
|
713
|
+
: [];
|
|
565
714
|
if (imageDataUrl) {
|
|
566
|
-
|
|
715
|
+
normalizedImageDataUrls.unshift(String(imageDataUrl));
|
|
716
|
+
}
|
|
717
|
+
for (const item of normalizedImageDataUrls) {
|
|
718
|
+
content.push({ type: 'image_url', image_url: { url: item } });
|
|
567
719
|
}
|
|
568
720
|
const payload = {
|
|
569
721
|
model: this.model,
|
|
@@ -605,6 +757,7 @@ export class LlmClient {
|
|
|
605
757
|
}
|
|
606
758
|
|
|
607
759
|
const outputContent = getCompletionContent(data);
|
|
760
|
+
const reasoningText = extractCompletionReasoningText(data);
|
|
608
761
|
if (!String(outputContent || '').trim()) {
|
|
609
762
|
const emptyError = new Error('Completions API empty textual content');
|
|
610
763
|
emptyError.code = 'COMPLETIONS_EMPTY_CONTENT';
|
|
@@ -614,6 +767,7 @@ export class LlmClient {
|
|
|
614
767
|
try {
|
|
615
768
|
return parseLlmJson(outputContent, {
|
|
616
769
|
evidenceCorpus,
|
|
770
|
+
reasoningText,
|
|
617
771
|
chunkIndex,
|
|
618
772
|
chunkTotal,
|
|
619
773
|
});
|
|
@@ -648,20 +802,33 @@ export class LlmClient {
|
|
|
648
802
|
}
|
|
649
803
|
}
|
|
650
804
|
|
|
651
|
-
async evaluateImageResume({ screeningCriteria, candidate, imagePath }) {
|
|
805
|
+
async evaluateImageResume({ screeningCriteria, candidate, imagePath, imagePaths = [] }) {
|
|
652
806
|
const prompt = buildImagePrompt({ screeningCriteria, candidate });
|
|
653
|
-
const
|
|
807
|
+
const normalizedImagePaths = Array.isArray(imagePaths)
|
|
808
|
+
? imagePaths.map((item) => String(item || '').trim()).filter(Boolean)
|
|
809
|
+
: [];
|
|
810
|
+
if (imagePath) {
|
|
811
|
+
normalizedImagePaths.unshift(String(imagePath));
|
|
812
|
+
}
|
|
813
|
+
const uniqueImagePaths = [...new Set(normalizedImagePaths)];
|
|
814
|
+
if (uniqueImagePaths.length <= 0) {
|
|
815
|
+
throw new Error('IMAGE_MODEL_FAILED: missing image paths');
|
|
816
|
+
}
|
|
817
|
+
const imageDataUrls = await Promise.all(
|
|
818
|
+
uniqueImagePaths.map((item) => this.readImageAsDataUrl(item)),
|
|
819
|
+
);
|
|
654
820
|
const evidenceCorpus = normalizeText(candidate?.evidenceCorpus || candidate?.resumeText || '');
|
|
655
821
|
const result = await this.requestByPreference({
|
|
656
822
|
prompt,
|
|
657
|
-
|
|
823
|
+
imageDataUrls,
|
|
658
824
|
evidenceCorpus,
|
|
659
825
|
chunkIndex: 1,
|
|
660
826
|
chunkTotal: 1,
|
|
661
827
|
});
|
|
662
828
|
return {
|
|
663
829
|
...result,
|
|
664
|
-
evaluationMode: 'image',
|
|
830
|
+
evaluationMode: uniqueImagePaths.length > 1 ? 'image-multi-chunk' : 'image',
|
|
831
|
+
imageCount: uniqueImagePaths.length,
|
|
665
832
|
};
|
|
666
833
|
}
|
|
667
834
|
|
|
@@ -736,51 +903,52 @@ export class LlmClient {
|
|
|
736
903
|
};
|
|
737
904
|
}
|
|
738
905
|
|
|
739
|
-
const firstReason = chunkResults.map((item) => normalizeText(item?.reason)).find(Boolean);
|
|
740
906
|
return {
|
|
741
907
|
passed: false,
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
908
|
+
rawOutputText:
|
|
909
|
+
chunkResults.map((item) => normalizeText(item?.rawOutputText)).find(Boolean) ||
|
|
910
|
+
`{"passed":false,"mode":"text-chunk-fallback","chunks":${chunks.length}}`,
|
|
911
|
+
rawReasoningText: chunkResults.map((item) => normalizeText(item?.rawReasoningText)).find(Boolean) || '',
|
|
912
|
+
cot: chunkResults.map((item) => normalizeText(item?.cot)).find(Boolean) || '',
|
|
913
|
+
reason: chunkResults.map((item) => normalizeText(item?.reason)).find(Boolean) || '',
|
|
914
|
+
summary: chunkResults.map((item) => normalizeText(item?.summary)).find(Boolean) || '',
|
|
745
915
|
evidence: [],
|
|
746
|
-
evidenceRawCount: chunkResults.reduce(
|
|
747
|
-
(acc, item) =>
|
|
748
|
-
acc + (Number.isFinite(Number(item?.evidenceRawCount)) ? Number(item.evidenceRawCount) : 0),
|
|
749
|
-
0,
|
|
750
|
-
),
|
|
751
|
-
evidenceMatchedCount: chunkResults.reduce(
|
|
752
|
-
(acc, item) =>
|
|
753
|
-
acc + (Number.isFinite(Number(item?.evidenceMatchedCount)) ? Number(item.evidenceMatchedCount) : 0),
|
|
754
|
-
0,
|
|
755
|
-
),
|
|
756
|
-
evidenceGateDemoted: chunkResults.some((item) => item?.evidenceGateDemoted === true),
|
|
757
916
|
chunkIndex: null,
|
|
758
917
|
chunkTotal: chunks.length,
|
|
759
918
|
evaluationMode: 'text',
|
|
760
919
|
};
|
|
761
920
|
}
|
|
762
921
|
|
|
763
|
-
async evaluateResume({ screeningCriteria, candidate, imagePath }) {
|
|
922
|
+
async evaluateResume({ screeningCriteria, candidate, imagePath, imagePaths = [] }) {
|
|
923
|
+
const normalizedImagePaths = Array.isArray(imagePaths)
|
|
924
|
+
? imagePaths.map((item) => String(item || '').trim()).filter(Boolean)
|
|
925
|
+
: [];
|
|
926
|
+
if (imagePath) {
|
|
927
|
+
normalizedImagePaths.unshift(String(imagePath));
|
|
928
|
+
}
|
|
929
|
+
const uniqueImagePaths = [...new Set(normalizedImagePaths)];
|
|
930
|
+
if (uniqueImagePaths.length > 0) {
|
|
931
|
+
return this.evaluateImageResume({
|
|
932
|
+
screeningCriteria,
|
|
933
|
+
candidate,
|
|
934
|
+
imagePaths: uniqueImagePaths,
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
|
|
764
938
|
const hasResumeText = Boolean(normalizeText(candidate?.resumeText || ''));
|
|
765
939
|
if (hasResumeText) {
|
|
766
|
-
|
|
767
|
-
return await this.evaluateTextResume({ screeningCriteria, candidate });
|
|
768
|
-
} catch (textError) {
|
|
769
|
-
if (!imagePath) {
|
|
770
|
-
throw textError;
|
|
771
|
-
}
|
|
772
|
-
const imageResult = await this.evaluateImageResume({ screeningCriteria, candidate, imagePath });
|
|
773
|
-
return {
|
|
774
|
-
...imageResult,
|
|
775
|
-
textFallbackError: normalizeText(textError?.message || textError),
|
|
776
|
-
};
|
|
777
|
-
}
|
|
940
|
+
return this.evaluateTextResume({ screeningCriteria, candidate });
|
|
778
941
|
}
|
|
779
|
-
|
|
942
|
+
|
|
943
|
+
throw new Error('LLM evaluation requires at least one resume image or non-empty resume text');
|
|
780
944
|
}
|
|
781
945
|
}
|
|
782
946
|
|
|
783
947
|
export const __testables = {
|
|
948
|
+
flattenChatMessageContent,
|
|
949
|
+
collectNestedText,
|
|
950
|
+
extractCompletionReasoningText,
|
|
951
|
+
extractResponsesReasoningText,
|
|
784
952
|
extractEvidenceTokens,
|
|
785
953
|
matchEvidenceAgainstResume,
|
|
786
954
|
splitTextByChunks,
|
|
@@ -11,6 +11,8 @@ const DEFAULT_PROFILE = {
|
|
|
11
11
|
apiKey: '',
|
|
12
12
|
model: '',
|
|
13
13
|
thinkingLevel: '',
|
|
14
|
+
timeoutMs: 60000,
|
|
15
|
+
maxRetries: 3,
|
|
14
16
|
},
|
|
15
17
|
chrome: {
|
|
16
18
|
port: 9222,
|
|
@@ -80,6 +82,8 @@ export function toPersistentProfile(profile = {}) {
|
|
|
80
82
|
apiKey: normalized.llm.apiKey,
|
|
81
83
|
model: normalized.llm.model,
|
|
82
84
|
thinkingLevel: normalized.llm.thinkingLevel,
|
|
85
|
+
timeoutMs: normalized.llm.timeoutMs,
|
|
86
|
+
maxRetries: normalized.llm.maxRetries,
|
|
83
87
|
},
|
|
84
88
|
chrome: {
|
|
85
89
|
port: normalized.chrome.port,
|
|
@@ -104,6 +108,8 @@ export function normalizeProfile(profile = {}) {
|
|
|
104
108
|
merged.llm.thinkingLevel = String(
|
|
105
109
|
merged.llm.thinkingLevel || merged.llm.llmThinkingLevel || merged.llm.reasoningEffort || merged.llm.reasoning_effort || '',
|
|
106
110
|
).trim();
|
|
111
|
+
merged.llm.timeoutMs = normalizeNumber(merged.llm.timeoutMs, DEFAULT_PROFILE.llm.timeoutMs);
|
|
112
|
+
merged.llm.maxRetries = normalizeNumber(merged.llm.maxRetries, DEFAULT_PROFILE.llm.maxRetries);
|
|
107
113
|
merged.runtime.batchRestEnabled = merged.runtime.batchRestEnabled !== false;
|
|
108
114
|
merged.runtime.safePacing = merged.runtime.safePacing !== false;
|
|
109
115
|
return merged;
|