@reconcrap/boss-recommend-mcp 2.1.13 → 2.1.14
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/README.md +12 -1
- package/config/screening-config.example.json +5 -0
- package/package.json +1 -1
- package/src/chat-runtime-config.js +26 -0
- package/src/core/reporting/legacy-csv.js +9 -1
- package/src/core/screening/index.js +351 -20
- package/src/domains/chat/run-service.js +55 -26
- package/src/domains/recommend/run-service.js +11 -3
- package/src/domains/recruit/run-service.js +8 -0
package/README.md
CHANGED
|
@@ -354,7 +354,12 @@ config/screening-config.example.json
|
|
|
354
354
|
}
|
|
355
355
|
],
|
|
356
356
|
"greetingMessage": "Hi同学,能麻烦发下简历吗?",
|
|
357
|
+
"llmScreeningStrategy": "single_pass",
|
|
357
358
|
"llmThinkingLevel": "low",
|
|
359
|
+
"llmFastThinkingLevel": "current",
|
|
360
|
+
"llmVerifyThinkingLevel": "low",
|
|
361
|
+
"llmFastMaxTokens": 384,
|
|
362
|
+
"llmVerifyMaxTokens": null,
|
|
358
363
|
"llmMaxRetries": 1
|
|
359
364
|
}
|
|
360
365
|
```
|
|
@@ -366,9 +371,15 @@ config/screening-config.example.json
|
|
|
366
371
|
- `greetingMessage`:chat 求简历流程发送的招呼语。兼容 `greetingText` / `greeting_text`;本次 run 显式传入的 `greeting_text` 优先级最高。
|
|
367
372
|
- `debugPort`:未显式传 `port` 时,recommend / search / chat CDP-only MCP run 和健康检查默认连接这个 Chrome 调试端口。
|
|
368
373
|
- `outputDir`:recommend / search / chat 完成后的最终 CSV 与 report JSON 会写入这里;run state / checkpoint 仍保留在各自状态目录,方便 pause/resume/cancel。
|
|
369
|
-
- `
|
|
374
|
+
- `llmScreeningStrategy`:默认 `single_pass`,保持旧行为并使用 `llmThinkingLevel`。设为 `fast_first_verified` 时,recommend / search / chat 会先用 `llmFastThinkingLevel` 快速判断,再只对快速通过、快速判断要求复核、或快速输出无效/缺字段的候选人用 `llmVerifyThinkingLevel` 复核;明确 `passed=false` 且 `review_required=false` 的快速淘汰不会复核。复核发生时,复核结果作为最终结果。
|
|
375
|
+
- `llmFastThinkingLevel` / `llmVerifyThinkingLevel`:仅在 `llmScreeningStrategy="fast_first_verified"` 时生效;默认分别为 `current` 和 `low`,可设为 `off/minimal/low/medium/high/auto/current`。此策略会忽略 `llmThinkingLevel` 的通过选择。
|
|
376
|
+
- `llmFastMaxTokens` / `llmVerifyMaxTokens`:仅在 `llmScreeningStrategy="fast_first_verified"` 时生效。快速首轮默认最多输出 `384` tokens,即使全局 `llmMaxTokens` 更大,也会使用这个快速轮上限,避免 `current` 模式输出冗长推理;可用 `llmFastMaxTokens` 覆盖。复核轮默认沿用全局 `llmMaxTokens` / `llmMaxCompletionTokens`,也可用 `llmVerifyMaxTokens` 单独覆盖。
|
|
377
|
+
- `fast_first_verified` 的快速轮只有在硬性不通过证据直接、明确、且没有可见反向证据时才应返回 `review_required=false`。如果简历里存在任一可能合格的学校/学历、产品或行业、职责或指标、海外/语言证据、或需要精确计算的任期/年限证据,即使快速轮倾向 `passed=false`,也应返回 `review_required=true` 交给复核轮。
|
|
378
|
+
- LLM provider 返回预算、额度、账单或鉴权类 fatal 错误(例如 `budget_exceeded`、quota exceeded、401/403)时,单次 LLM 调用最多重试 2 次;若仍失败,recommend / search / chat 会直接让本次 run 失败,不再把后续候选人记录为筛选不通过。
|
|
379
|
+
- `llmThinkingLevel`:默认 `low`。可设为 `off/minimal/low/medium/high/auto/current`,用于控制 OpenAI-compatible LLM 的 thinking/reasoning 强度。recommend / search / chat 共用同一套 LLM 筛选提示词:模型会按筛选标准顺序检查硬性淘汰项,若某项已可确定不满足(包括标准要求“证据不足即不通过”的缺失证据),会直接返回不通过;只有当前淘汰项存在截图不清、信息冲突或可能被后续简历内容澄清时才继续核实。
|
|
370
380
|
- `current`:不显式发送 thinking/reasoning 参数,并要求 LLM 返回 `{"passed": true/false, "summary": "少于100个中文词的筛选总结"}`;CSV 的 `判断依据(CoT)` 写入该 summary。
|
|
371
381
|
- 其他值:只要求 LLM 返回 `{"passed": true/false}`;若 provider 返回 reasoning / CoT 字段,CSV 的 `判断依据(CoT)` 写入该内容。
|
|
382
|
+
- 在 `fast_first_verified` 下,快速首轮始终要求 `{"passed": true/false, "summary": "少于100个中文词的筛选总结", "review_required": true/false}`。若最终来自快速轮,CSV 的 `判断依据(CoT)` 写入快速 summary;若最终来自复核轮,CSV 优先写入复核 CoT/reasoning,复核轮为 `current` 时写入复核 summary。
|
|
372
383
|
- `humanBehavior`:默认 `{ "enabled": true, "profile": "paced_with_rests", "restLevel": "low" }`。用于 recommend / search / chat 的可靠性实验,支持:
|
|
373
384
|
- `profile: "baseline"`:关闭人类节奏,保持确定性行为。
|
|
374
385
|
- `profile: "paced"`:启用 CDP-only Bezier 鼠标移动、较大按钮的安全 inset 点击点、分块 `Input.insertText`、列表 wheel/settle jitter,以及小的动作前后读秒。
|
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
"model": "gpt-4.1-mini",
|
|
5
5
|
"llmModels": [],
|
|
6
6
|
"greetingMessage": "Hi同学,能麻烦发下简历吗?",
|
|
7
|
+
"llmScreeningStrategy": "single_pass",
|
|
7
8
|
"llmThinkingLevel": "low",
|
|
9
|
+
"llmFastThinkingLevel": "current",
|
|
10
|
+
"llmVerifyThinkingLevel": "low",
|
|
11
|
+
"llmFastMaxTokens": 384,
|
|
12
|
+
"llmVerifyMaxTokens": null,
|
|
8
13
|
"llmTimeoutMs": 60000,
|
|
9
14
|
"llmMaxTokens": 512,
|
|
10
15
|
"llmMaxRetries": 3,
|
package/package.json
CHANGED
|
@@ -12,6 +12,7 @@ const SCREEN_CONFIG_TEMPLATE_DEFAULTS = Object.freeze({
|
|
|
12
12
|
model: "gpt-4.1-mini"
|
|
13
13
|
});
|
|
14
14
|
const LLM_THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "auto", "current"]);
|
|
15
|
+
const LLM_SCREENING_STRATEGIES = new Set(["single_pass", "fast_first_verified"]);
|
|
15
16
|
|
|
16
17
|
export const TARGET_COUNT_CANONICAL_ALL = "all";
|
|
17
18
|
export const TARGET_COUNT_ACCEPTED_EXAMPLES = [TARGET_COUNT_CANONICAL_ALL, -1, 20, "全部候选人"];
|
|
@@ -322,6 +323,11 @@ function normalizeLlmThinkingLevel(raw, fallback = "low") {
|
|
|
322
323
|
return LLM_THINKING_LEVELS.has(normalized) ? normalized : fallback;
|
|
323
324
|
}
|
|
324
325
|
|
|
326
|
+
function normalizeLlmScreeningStrategy(raw, fallback = "single_pass") {
|
|
327
|
+
const normalized = normalizeText(raw).toLowerCase();
|
|
328
|
+
return LLM_SCREENING_STRATEGIES.has(normalized) ? normalized : fallback;
|
|
329
|
+
}
|
|
330
|
+
|
|
325
331
|
function firstConfiguredValue(...values) {
|
|
326
332
|
for (const value of values) {
|
|
327
333
|
if (value === undefined || value === null) continue;
|
|
@@ -352,6 +358,26 @@ function normalizeScreeningLlmModel(config = {}, rawEntry = {}, index = 0) {
|
|
|
352
358
|
firstConfiguredValue(entry.llmThinkingLevel, entry.thinkingLevel, entry.reasoningEffort, config.llmThinkingLevel, config.thinkingLevel, config.reasoningEffort),
|
|
353
359
|
"low"
|
|
354
360
|
),
|
|
361
|
+
llmScreeningStrategy: normalizeLlmScreeningStrategy(
|
|
362
|
+
firstConfiguredValue(entry.llmScreeningStrategy, entry.screeningStrategy, entry.screening_strategy, config.llmScreeningStrategy, config.screeningStrategy, config.screening_strategy),
|
|
363
|
+
"single_pass"
|
|
364
|
+
),
|
|
365
|
+
llmFastThinkingLevel: normalizeLlmThinkingLevel(
|
|
366
|
+
firstConfiguredValue(entry.llmFastThinkingLevel, entry.fastThinkingLevel, entry.fast_thinking_level, config.llmFastThinkingLevel, config.fastThinkingLevel, config.fast_thinking_level),
|
|
367
|
+
"current"
|
|
368
|
+
),
|
|
369
|
+
llmVerifyThinkingLevel: normalizeLlmThinkingLevel(
|
|
370
|
+
firstConfiguredValue(entry.llmVerifyThinkingLevel, entry.verifyThinkingLevel, entry.verify_thinking_level, config.llmVerifyThinkingLevel, config.verifyThinkingLevel, config.verify_thinking_level),
|
|
371
|
+
"low"
|
|
372
|
+
),
|
|
373
|
+
llmFastMaxTokens: parsePositiveInteger(
|
|
374
|
+
firstConfiguredValue(entry.llmFastMaxTokens, entry.fastMaxTokens, entry.fast_max_tokens, config.llmFastMaxTokens, config.fastMaxTokens, config.fast_max_tokens),
|
|
375
|
+
null
|
|
376
|
+
),
|
|
377
|
+
llmVerifyMaxTokens: parsePositiveInteger(
|
|
378
|
+
firstConfiguredValue(entry.llmVerifyMaxTokens, entry.verifyMaxTokens, entry.verify_max_tokens, config.llmVerifyMaxTokens, config.verifyMaxTokens, config.verify_max_tokens),
|
|
379
|
+
null
|
|
380
|
+
),
|
|
355
381
|
llmTimeoutMs: parsePositiveInteger(firstConfiguredValue(entry.llmTimeoutMs, entry.timeoutMs, config.llmTimeoutMs, config.timeoutMs), null),
|
|
356
382
|
llmMaxRetries: parsePositiveInteger(firstConfiguredValue(entry.llmMaxRetries, entry.maxRetries, config.llmMaxRetries, config.maxRetries), null),
|
|
357
383
|
llmMaxTokens: parsePositiveInteger(firstConfiguredValue(entry.llmMaxTokens, entry.maxTokens, config.llmMaxTokens, config.maxTokens), null),
|
|
@@ -17,6 +17,10 @@ export const LEGACY_RESULT_HEADER = [
|
|
|
17
17
|
"原始判定通过",
|
|
18
18
|
"最终判定通过",
|
|
19
19
|
"LLM thinking_level",
|
|
20
|
+
"LLM screening_strategy",
|
|
21
|
+
"LLM decision_source",
|
|
22
|
+
"LLM verified",
|
|
23
|
+
"LLM verification_reason",
|
|
20
24
|
"错误码",
|
|
21
25
|
"错误信息",
|
|
22
26
|
"候选人ID",
|
|
@@ -282,7 +286,11 @@ export function legacyScreenResultRow(row = {}) {
|
|
|
282
286
|
rawPassed,
|
|
283
287
|
finalPassed,
|
|
284
288
|
firstText(llm.provider?.thinking_level),
|
|
285
|
-
|
|
289
|
+
firstText(llm.screening_strategy),
|
|
290
|
+
firstText(llm.decision_source),
|
|
291
|
+
typeof llm.verified === "boolean" ? llm.verified : "",
|
|
292
|
+
firstText(llm.verification_reason),
|
|
293
|
+
row.error_code || error.code || error.name || llm.error_code || (llm.error ? "LLM_SCREENING_ERROR" : ""),
|
|
286
294
|
row.error_message || error.message || llm.error || "",
|
|
287
295
|
candidate.id || row.candidate_id || "",
|
|
288
296
|
timingValue(row, "total_ms"),
|
|
@@ -38,6 +38,9 @@ const GENDER_CODE_MAP = {
|
|
|
38
38
|
};
|
|
39
39
|
|
|
40
40
|
const LLM_THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "auto", "current"]);
|
|
41
|
+
const LLM_SCREENING_STRATEGIES = new Set(["single_pass", "fast_first_verified"]);
|
|
42
|
+
const FAST_FIRST_DEFAULT_FAST_MAX_TOKENS = 384;
|
|
43
|
+
const FATAL_LLM_PROVIDER_MAX_RETRIES = 2;
|
|
41
44
|
|
|
42
45
|
function nowIso() {
|
|
43
46
|
return new Date().toISOString();
|
|
@@ -48,6 +51,11 @@ function normalizeLlmThinkingLevel(value) {
|
|
|
48
51
|
return LLM_THINKING_LEVELS.has(normalized) ? normalized : "";
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
function normalizeLlmScreeningStrategy(value) {
|
|
55
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
56
|
+
return LLM_SCREENING_STRATEGIES.has(normalized) ? normalized : "single_pass";
|
|
57
|
+
}
|
|
58
|
+
|
|
51
59
|
function normalizeBaseUrl(baseUrl) {
|
|
52
60
|
return String(baseUrl || "").replace(/\/+$/, "");
|
|
53
61
|
}
|
|
@@ -126,6 +134,65 @@ function compactLlmProviderFailure(error, providerConfig = {}, providerIndex = 0
|
|
|
126
134
|
};
|
|
127
135
|
}
|
|
128
136
|
|
|
137
|
+
export function classifyFatalLlmProviderError(error) {
|
|
138
|
+
if (!error) return null;
|
|
139
|
+
if (error.llm_fatal_provider_error) {
|
|
140
|
+
return {
|
|
141
|
+
code: error.code || "LLM_FATAL_PROVIDER_ERROR",
|
|
142
|
+
reason: error.llm_fatal_reason || "fatal_provider_error"
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const status = Number(error?.status);
|
|
146
|
+
const message = String(error?.message || error || "");
|
|
147
|
+
const providerCode = normalizeText(error?.provider_error_code).toLowerCase();
|
|
148
|
+
const providerType = normalizeText(error?.provider_error_type).toLowerCase();
|
|
149
|
+
const searchable = `${providerCode} ${providerType} ${message}`.toLowerCase();
|
|
150
|
+
if (/(?:budget[_\s-]*exceeded|budget has been exceeded|max budget|spend cap|hard limit|credit limit)/i.test(searchable)) {
|
|
151
|
+
return { code: "LLM_BUDGET_EXCEEDED", reason: "budget_exceeded" };
|
|
152
|
+
}
|
|
153
|
+
if (/(?:insufficient[_\s-]*quota|quota[_\s-]*(?:exceeded|exhausted)|(?:exceeded|exhausted).*quota|out of quota|usage quota|token quota|monthly quota|rate limit quota)/i.test(searchable)) {
|
|
154
|
+
return { code: "LLM_QUOTA_EXCEEDED", reason: "quota_exceeded" };
|
|
155
|
+
}
|
|
156
|
+
if (/(?:insufficient[_\s-]*(?:balance|credit|credits)|no credits|credit balance|billing|payment required|unpaid|account balance)/i.test(searchable)) {
|
|
157
|
+
return { code: "LLM_BILLING_REQUIRED", reason: "billing_required" };
|
|
158
|
+
}
|
|
159
|
+
if (status === 401 || /(?:unauthorized|unauthorised|invalid api key|incorrect api key|authentication failed|invalid authentication|api key invalid|no api key)/i.test(searchable)) {
|
|
160
|
+
return { code: "LLM_AUTH_FAILED", reason: "auth_failed" };
|
|
161
|
+
}
|
|
162
|
+
if (status === 403 || /(?:forbidden|permission denied|access denied|not authorized|not authorised|model access denied)/i.test(searchable)) {
|
|
163
|
+
return { code: "LLM_PERMISSION_DENIED", reason: "permission_denied" };
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function isFatalLlmProviderError(error) {
|
|
169
|
+
return Boolean(classifyFatalLlmProviderError(error));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function createFatalLlmRunError(error, { domain = "", candidate = null } = {}) {
|
|
173
|
+
const classification = classifyFatalLlmProviderError(error) || {
|
|
174
|
+
code: "LLM_FATAL_PROVIDER_ERROR",
|
|
175
|
+
reason: "fatal_provider_error"
|
|
176
|
+
};
|
|
177
|
+
const attempts = Number(error?.llm_attempt_count) || 0;
|
|
178
|
+
const suffix = attempts ? ` after ${attempts} attempts` : "";
|
|
179
|
+
const fatal = new Error(`Fatal LLM provider error${suffix}: ${error?.message || String(error || "unknown")}`);
|
|
180
|
+
fatal.name = "FatalLlmProviderError";
|
|
181
|
+
fatal.code = classification.code;
|
|
182
|
+
fatal.llm_fatal_provider_error = true;
|
|
183
|
+
fatal.llm_fatal_reason = classification.reason;
|
|
184
|
+
fatal.llm_attempt_count = attempts;
|
|
185
|
+
fatal.status = Number.isFinite(Number(error?.status)) ? Number(error.status) : null;
|
|
186
|
+
fatal.provider_error_code = error?.provider_error_code || null;
|
|
187
|
+
fatal.provider_error_type = error?.provider_error_type || null;
|
|
188
|
+
fatal.provider_error_message = error?.provider_error_message || null;
|
|
189
|
+
fatal.domain = domain || null;
|
|
190
|
+
fatal.candidate_id = candidate?.id || null;
|
|
191
|
+
fatal.candidate_name = candidate?.identity?.name || null;
|
|
192
|
+
fatal.cause = error || null;
|
|
193
|
+
return fatal;
|
|
194
|
+
}
|
|
195
|
+
|
|
129
196
|
function isVolcengineModel(baseUrl, model) {
|
|
130
197
|
return /volces|volcengine|ark\.cn|doubao|seed/i.test(`${baseUrl || ""} ${model || ""}`);
|
|
131
198
|
}
|
|
@@ -168,7 +235,7 @@ function parseFiniteNumber(value, fallback = null) {
|
|
|
168
235
|
return Number.isFinite(parsed) ? parsed : fallback;
|
|
169
236
|
}
|
|
170
237
|
|
|
171
|
-
function resolveLlmOutputTokenBudget(config = {}, thinkingLevel = "") {
|
|
238
|
+
function resolveLlmOutputTokenBudget(config = {}, thinkingLevel = "", options = {}) {
|
|
172
239
|
const explicit = parsePositiveNumber(
|
|
173
240
|
config.llmMaxCompletionTokens
|
|
174
241
|
?? config.maxCompletionTokens
|
|
@@ -177,10 +244,16 @@ function resolveLlmOutputTokenBudget(config = {}, thinkingLevel = "") {
|
|
|
177
244
|
null
|
|
178
245
|
);
|
|
179
246
|
if (explicit) return Math.max(1, Math.floor(explicit));
|
|
247
|
+
if (options.requireReviewDecision) return 384;
|
|
180
248
|
const normalizedThinking = normalizeLlmThinkingLevel(thinkingLevel || "low") || "low";
|
|
181
249
|
return normalizedThinking === "off" || normalizedThinking === "minimal" ? 64 : 512;
|
|
182
250
|
}
|
|
183
251
|
|
|
252
|
+
function parsePositiveInteger(value, fallback = null) {
|
|
253
|
+
const parsed = parsePositiveNumber(value, fallback);
|
|
254
|
+
return parsed ? Math.max(1, Math.floor(parsed)) : fallback;
|
|
255
|
+
}
|
|
256
|
+
|
|
184
257
|
export function normalizeText(input) {
|
|
185
258
|
return String(input || "").replace(/\s+/g, " ").trim();
|
|
186
259
|
}
|
|
@@ -1451,6 +1524,7 @@ export function compactScreeningLlmResult(llmResult) {
|
|
|
1451
1524
|
ok: Boolean(llmResult.ok),
|
|
1452
1525
|
provider: llmResult.provider || null,
|
|
1453
1526
|
passed: llmResult.passed,
|
|
1527
|
+
review_required: typeof llmResult.review_required === "boolean" ? llmResult.review_required : null,
|
|
1454
1528
|
cot: llmResult.cot || llmResult.decision_cot || "",
|
|
1455
1529
|
reasoning_content: llmResult.reasoning_content || "",
|
|
1456
1530
|
raw_model_output: llmResult.raw_model_output || "",
|
|
@@ -1461,6 +1535,17 @@ export function compactScreeningLlmResult(llmResult) {
|
|
|
1461
1535
|
attempt_count: llmResult.attempt_count || 0,
|
|
1462
1536
|
fallback_count: llmResult.fallback_count || 0,
|
|
1463
1537
|
llm_model_failures: Array.isArray(llmResult.llm_model_failures) ? llmResult.llm_model_failures : [],
|
|
1538
|
+
screening_strategy: llmResult.screening_strategy || "",
|
|
1539
|
+
fast_thinking_level: llmResult.fast_thinking_level || "",
|
|
1540
|
+
verify_thinking_level: llmResult.verify_thinking_level || "",
|
|
1541
|
+
verified: typeof llmResult.verified === "boolean" ? llmResult.verified : null,
|
|
1542
|
+
verification_reason: llmResult.verification_reason || "",
|
|
1543
|
+
decision_source: llmResult.decision_source || "",
|
|
1544
|
+
fast_result: llmResult.fast_result || null,
|
|
1545
|
+
verify_result: llmResult.verify_result || null,
|
|
1546
|
+
error_code: llmResult.error_code || null,
|
|
1547
|
+
fatal: Boolean(llmResult.fatal),
|
|
1548
|
+
fatal_reason: llmResult.fatal_reason || "",
|
|
1464
1549
|
error: llmResult.error || null,
|
|
1465
1550
|
screened_at: llmResult.screened_at || null
|
|
1466
1551
|
};
|
|
@@ -1477,11 +1562,12 @@ export function llmResultToScreening(llmResult, candidate) {
|
|
|
1477
1562
|
}
|
|
1478
1563
|
|
|
1479
1564
|
export function isRecoverableLlmScreeningError(error) {
|
|
1480
|
-
return /(?:LLM response missing boolean passed decision|LLM response missing brief summary|LLM response was not valid JSON)/i
|
|
1565
|
+
return /(?:LLM response missing boolean passed decision|LLM response missing brief summary|LLM response missing boolean review_required decision|LLM response was not valid JSON)/i
|
|
1481
1566
|
.test(String(error?.message || error || ""));
|
|
1482
1567
|
}
|
|
1483
1568
|
|
|
1484
1569
|
export function createFailedLlmScreeningResult(error) {
|
|
1570
|
+
const fatalClassification = classifyFatalLlmProviderError(error);
|
|
1485
1571
|
return {
|
|
1486
1572
|
ok: false,
|
|
1487
1573
|
passed: false,
|
|
@@ -1496,6 +1582,9 @@ export function createFailedLlmScreeningResult(error) {
|
|
|
1496
1582
|
attempt_count: Number(error?.llm_attempt_count) || 0,
|
|
1497
1583
|
fallback_count: Array.isArray(error?.llm_model_failures) ? error.llm_model_failures.length : 0,
|
|
1498
1584
|
llm_model_failures: Array.isArray(error?.llm_model_failures) ? error.llm_model_failures : [],
|
|
1585
|
+
error_code: fatalClassification?.code || error?.code || null,
|
|
1586
|
+
fatal: Boolean(fatalClassification),
|
|
1587
|
+
fatal_reason: fatalClassification?.reason || "",
|
|
1499
1588
|
error: error?.message || String(error || "unknown"),
|
|
1500
1589
|
screened_at: nowIso()
|
|
1501
1590
|
};
|
|
@@ -1505,6 +1594,7 @@ export function buildScreeningLlmMessages({
|
|
|
1505
1594
|
candidate,
|
|
1506
1595
|
criteria,
|
|
1507
1596
|
thinkingLevel = "low",
|
|
1597
|
+
requireReviewDecision = false,
|
|
1508
1598
|
imageEvidence = null,
|
|
1509
1599
|
imagePaths = [],
|
|
1510
1600
|
imageInputs = null,
|
|
@@ -1514,7 +1604,8 @@ export function buildScreeningLlmMessages({
|
|
|
1514
1604
|
const safeCriteria = normalizeText(criteria || "判断候选人是否符合本次招聘筛选标准");
|
|
1515
1605
|
const safeText = String(candidate?.text?.raw || candidate?.text || "");
|
|
1516
1606
|
const normalizedThinkingLevel = normalizeLlmThinkingLevel(thinkingLevel) || "low";
|
|
1517
|
-
const
|
|
1607
|
+
const requestReviewDecision = Boolean(requireReviewDecision);
|
|
1608
|
+
const requestSummary = requestReviewDecision || normalizedThinkingLevel === "current";
|
|
1518
1609
|
const images = Array.isArray(imageInputs)
|
|
1519
1610
|
? imageInputs
|
|
1520
1611
|
: buildScreeningLlmImageInputs({
|
|
@@ -1523,23 +1614,43 @@ export function buildScreeningLlmMessages({
|
|
|
1523
1614
|
maxImages,
|
|
1524
1615
|
detail: imageDetail
|
|
1525
1616
|
});
|
|
1526
|
-
const outputShape =
|
|
1527
|
-
? "
|
|
1528
|
-
+ "{\"passed\": true/false, \"summary\": \"少于100个中文词的筛选总结\"}"
|
|
1529
|
-
:
|
|
1530
|
-
|
|
1617
|
+
const outputShape = requestReviewDecision
|
|
1618
|
+
? "最后只返回 JSON,格式为:"
|
|
1619
|
+
+ "{\"passed\": true/false, \"summary\": \"少于100个中文词的筛选总结\", \"review_required\": true/false}"
|
|
1620
|
+
: requestSummary
|
|
1621
|
+
? "7) 只返回 JSON,格式为:"
|
|
1622
|
+
+ "{\"passed\": true/false, \"summary\": \"少于100个中文词的筛选总结\"}"
|
|
1623
|
+
: "7) 只返回 JSON,格式为:"
|
|
1624
|
+
+ "{\"passed\": true/false}";
|
|
1625
|
+
const failFastInstructions = [
|
|
1626
|
+
"3) 按筛选标准原文顺序拆解并检查硬性淘汰项;一旦某项可确定不满足,必须立即返回 passed=false,不要继续评估后续条件。",
|
|
1627
|
+
"4) 若筛选标准规定证据不足、业务线无法判断、任期无法判断、学校无法判断等情况不通过,这类缺失证据就是可确定不满足。",
|
|
1628
|
+
"5) 只有当前淘汰项本身存在截图不清、信息冲突、或可能被后续可见简历内容澄清时,才继续阅读以核实该项。"
|
|
1629
|
+
].join("\n");
|
|
1630
|
+
const fastReviewInstructions = requestReviewDecision
|
|
1631
|
+
? [
|
|
1632
|
+
"7) review_required 必须是布尔值;当证据缺失、证据冲突、截图/文本不完整或不清晰、结论接近规则边界、或你依赖假设时设为 true;若结论明确且无需更深推理核验,设为 false。",
|
|
1633
|
+
"8) 只有当某个硬性不通过条件有直接、明确、无可见反向证据的简历证据时,才可返回 review_required=false。",
|
|
1634
|
+
"9) 如果简历中存在任何可能满足当前被否定条件的反向证据、边界证据或可计入经历,即使你倾向 passed=false,也必须 review_required=true。",
|
|
1635
|
+
"10) 反向证据包括但不限于:可能合格的学校/学历、可能合格的产品或行业、可能合格的职责或指标、可能合格的海外/语言证据、或需要精确计算的任期/年限证据。",
|
|
1636
|
+
"11) 不要因为候选人其他经历不匹配,就忽略某一段可能匹配的经历;只要任一可见经历可能满足被否定条件,就必须交给复核。",
|
|
1637
|
+
"12) 当 review_required=false 时,summary 必须点明确切的决定性硬性失败,并说明没有可见反向证据或边界证据。"
|
|
1638
|
+
].join("\n")
|
|
1639
|
+
: "";
|
|
1531
1640
|
const prompt =
|
|
1532
1641
|
`请根据以下标准判断候选人是否通过筛选。\n\n筛选标准:\n${safeCriteria}\n\n`
|
|
1533
1642
|
+ `候选人信息:\n${safeText || "候选人的完整简历信息在后续截图中,请按截图顺序阅读。"}\n\n`
|
|
1534
1643
|
+ (images.length
|
|
1535
|
-
? `候选人简历截图共 ${images.length}
|
|
1644
|
+
? `候选人简历截图共 ${images.length} 张,按从上到下的滚动顺序排列。若截图是拼接长图,请按图内从上到下顺序阅读;若已出现明确硬性淘汰项,可停止后续评估。\n\n`
|
|
1536
1645
|
: "")
|
|
1537
1646
|
+ "要求:\n"
|
|
1538
1647
|
+ "1) 只能依据候选人信息或截图中真实出现的内容判断。\n"
|
|
1539
1648
|
+ "2) 若证据不足或截图无法确认,必须返回 passed=false。\n"
|
|
1649
|
+
+ failFastInstructions + "\n"
|
|
1540
1650
|
+ (requestSummary
|
|
1541
|
-
? "
|
|
1542
|
-
: "
|
|
1651
|
+
? "6) summary 必须为少于100个中文词的简短筛选总结,可包含核心依据和主要风险;不要输出推理过程。\n"
|
|
1652
|
+
: "6) 不要输出评估原因、证据列表、解释或额外文字。\n")
|
|
1653
|
+
+ (fastReviewInstructions ? `${fastReviewInstructions}\n` : "")
|
|
1543
1654
|
+ outputShape;
|
|
1544
1655
|
const userContent = images.length
|
|
1545
1656
|
? [
|
|
@@ -1554,8 +1665,10 @@ export function buildScreeningLlmMessages({
|
|
|
1554
1665
|
{
|
|
1555
1666
|
role: "system",
|
|
1556
1667
|
content:
|
|
1557
|
-
"
|
|
1558
|
-
+ (
|
|
1668
|
+
"你是一位严谨的招聘筛选助手。必须按筛选标准顺序严格阅读和判断,严禁编造不存在的候选人经历;一旦确定命中硬性淘汰项,可立即给出最终不通过结论。"
|
|
1669
|
+
+ (requestReviewDecision
|
|
1670
|
+
? "只能返回严格 JSON。必须包含 passed、summary 和 review_required;summary 用中文,少于100个词,只概括筛选结论、核心依据和主要风险,不要输出推理过程;review_required 只能是 true 或 false。只有在硬性失败直接明确且无可见反向证据时,review_required 才能为 false。"
|
|
1671
|
+
: requestSummary
|
|
1559
1672
|
? "只能返回严格 JSON。必须包含 passed 和 summary;summary 用中文,少于100个词,只概括筛选结论、核心依据和主要风险,不要输出推理过程。"
|
|
1560
1673
|
: "只能返回严格 JSON,不要输出原因、证据或额外文字。")
|
|
1561
1674
|
},
|
|
@@ -1589,6 +1702,7 @@ async function callScreeningLlmWithProvider({
|
|
|
1589
1702
|
criteria,
|
|
1590
1703
|
config = {},
|
|
1591
1704
|
timeoutMs = 60000,
|
|
1705
|
+
requireReviewDecision = false,
|
|
1592
1706
|
imageEvidence = null,
|
|
1593
1707
|
imagePaths = [],
|
|
1594
1708
|
maxImages = 8,
|
|
@@ -1611,7 +1725,7 @@ async function callScreeningLlmWithProvider({
|
|
|
1611
1725
|
}
|
|
1612
1726
|
|
|
1613
1727
|
const thinkingLevel = config.llmThinkingLevel || config.thinkingLevel || config.reasoningEffort || "low";
|
|
1614
|
-
const outputTokenBudget = resolveLlmOutputTokenBudget(config, thinkingLevel);
|
|
1728
|
+
const outputTokenBudget = resolveLlmOutputTokenBudget(config, thinkingLevel, { requireReviewDecision });
|
|
1615
1729
|
const payload = {
|
|
1616
1730
|
model,
|
|
1617
1731
|
temperature: parseFiniteNumber(config.temperature, 0.1),
|
|
@@ -1620,6 +1734,7 @@ async function callScreeningLlmWithProvider({
|
|
|
1620
1734
|
candidate,
|
|
1621
1735
|
criteria,
|
|
1622
1736
|
thinkingLevel,
|
|
1737
|
+
requireReviewDecision,
|
|
1623
1738
|
imageInputs
|
|
1624
1739
|
})
|
|
1625
1740
|
};
|
|
@@ -1642,7 +1757,10 @@ async function callScreeningLlmWithProvider({
|
|
|
1642
1757
|
const maxRetries = normalizeLlmMaxRetries(config.llmMaxRetries ?? config.maxRetries);
|
|
1643
1758
|
const maxAttempts = maxRetries + 1;
|
|
1644
1759
|
let lastError = null;
|
|
1645
|
-
|
|
1760
|
+
let attempt = 0;
|
|
1761
|
+
let fatalProviderAttempts = 0;
|
|
1762
|
+
while (true) {
|
|
1763
|
+
attempt += 1;
|
|
1646
1764
|
const controller = new AbortController();
|
|
1647
1765
|
const timer = setTimeout(() => controller.abort(), effectiveTimeoutMs);
|
|
1648
1766
|
try {
|
|
@@ -1663,6 +1781,10 @@ async function callScreeningLlmWithProvider({
|
|
|
1663
1781
|
if (!response.ok) {
|
|
1664
1782
|
const error = new Error(`LLM request failed: ${response.status} ${responseText.slice(0, 400)}`);
|
|
1665
1783
|
error.status = response.status;
|
|
1784
|
+
const providerError = tryParseJson(responseText)?.error || {};
|
|
1785
|
+
error.provider_error_code = providerError.code || null;
|
|
1786
|
+
error.provider_error_type = providerError.type || null;
|
|
1787
|
+
error.provider_error_message = providerError.message || null;
|
|
1666
1788
|
throw error;
|
|
1667
1789
|
}
|
|
1668
1790
|
const json = tryParseJson(responseText);
|
|
@@ -1679,13 +1801,19 @@ async function callScreeningLlmWithProvider({
|
|
|
1679
1801
|
}
|
|
1680
1802
|
const normalizedThinkingLevel = normalizeLlmThinkingLevel(thinkingLevel) || "low";
|
|
1681
1803
|
const summary = normalizeBlockText(parsed?.summary || parsed?.screen_summary || parsed?.brief_summary);
|
|
1682
|
-
if (normalizedThinkingLevel === "current" && !summary) {
|
|
1683
|
-
throw new Error(`LLM response missing brief summary for current thinking level: ${content.slice(0, 240)}`);
|
|
1804
|
+
if ((normalizedThinkingLevel === "current" || requireReviewDecision) && !summary) {
|
|
1805
|
+
throw new Error(`LLM response missing brief summary for current thinking level or fast-first strategy: ${content.slice(0, 240)}`);
|
|
1806
|
+
}
|
|
1807
|
+
const reviewRequired = requireReviewDecision
|
|
1808
|
+
? parsePassedDecision(parsed?.review_required ?? parsed?.reviewRequired ?? parsed?.needs_review ?? parsed?.needsReview)
|
|
1809
|
+
: null;
|
|
1810
|
+
if (requireReviewDecision && reviewRequired === null) {
|
|
1811
|
+
throw new Error(`LLM response missing boolean review_required decision: ${content.slice(0, 240)}`);
|
|
1684
1812
|
}
|
|
1685
1813
|
const evidence = Array.isArray(parsed?.evidence)
|
|
1686
1814
|
? parsed.evidence.map(normalizeText).filter(Boolean)
|
|
1687
1815
|
: [];
|
|
1688
|
-
const decisionCot = normalizedThinkingLevel === "current"
|
|
1816
|
+
const decisionCot = (normalizedThinkingLevel === "current" || requireReviewDecision)
|
|
1689
1817
|
? summary
|
|
1690
1818
|
: (firstReasoningText([
|
|
1691
1819
|
parsed?.cot,
|
|
@@ -1712,6 +1840,7 @@ async function callScreeningLlmWithProvider({
|
|
|
1712
1840
|
max_completion_tokens: payload.max_completion_tokens || null
|
|
1713
1841
|
},
|
|
1714
1842
|
passed,
|
|
1843
|
+
review_required: reviewRequired,
|
|
1715
1844
|
reason: "",
|
|
1716
1845
|
evidence,
|
|
1717
1846
|
cot: decisionCot,
|
|
@@ -1730,7 +1859,23 @@ async function callScreeningLlmWithProvider({
|
|
|
1730
1859
|
return result;
|
|
1731
1860
|
} catch (error) {
|
|
1732
1861
|
lastError = error;
|
|
1733
|
-
|
|
1862
|
+
const fatalClassification = classifyFatalLlmProviderError(error);
|
|
1863
|
+
if (fatalClassification) {
|
|
1864
|
+
fatalProviderAttempts += 1;
|
|
1865
|
+
error.code = fatalClassification.code;
|
|
1866
|
+
error.llm_fatal_provider_error = true;
|
|
1867
|
+
error.llm_fatal_reason = fatalClassification.reason;
|
|
1868
|
+
if (fatalProviderAttempts <= FATAL_LLM_PROVIDER_MAX_RETRIES) {
|
|
1869
|
+
await sleepMs(Math.min(2500, 500 * fatalProviderAttempts));
|
|
1870
|
+
continue;
|
|
1871
|
+
}
|
|
1872
|
+
error.image_input_count = imageInputs.length;
|
|
1873
|
+
error.image_inputs = summarizeLlmImageInputs(imageInputs);
|
|
1874
|
+
error.llm_attempt_count = attempt;
|
|
1875
|
+
throw error;
|
|
1876
|
+
}
|
|
1877
|
+
const retryable = isRetryableLlmRequestError(error) || isRecoverableLlmScreeningError(error);
|
|
1878
|
+
if (attempt >= maxAttempts || !retryable) {
|
|
1734
1879
|
error.image_input_count = imageInputs.length;
|
|
1735
1880
|
error.image_inputs = summarizeLlmImageInputs(imageInputs);
|
|
1736
1881
|
error.llm_attempt_count = attempt;
|
|
@@ -1748,7 +1893,110 @@ async function callScreeningLlmWithProvider({
|
|
|
1748
1893
|
throw lastError;
|
|
1749
1894
|
}
|
|
1750
1895
|
|
|
1751
|
-
|
|
1896
|
+
function compactStrategyLlmResult(result) {
|
|
1897
|
+
if (!result) return null;
|
|
1898
|
+
const compact = compactScreeningLlmResult(result);
|
|
1899
|
+
if (compact) {
|
|
1900
|
+
compact.fast_result = null;
|
|
1901
|
+
compact.verify_result = null;
|
|
1902
|
+
}
|
|
1903
|
+
return compact;
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
function attachFastFirstScreeningMetadata(result, {
|
|
1907
|
+
fastThinkingLevel = "current",
|
|
1908
|
+
verifyThinkingLevel = "low",
|
|
1909
|
+
verified = false,
|
|
1910
|
+
verificationReason = "",
|
|
1911
|
+
decisionSource = "fast",
|
|
1912
|
+
fastResult = null,
|
|
1913
|
+
verifyResult = null
|
|
1914
|
+
} = {}) {
|
|
1915
|
+
return {
|
|
1916
|
+
...result,
|
|
1917
|
+
screening_strategy: "fast_first_verified",
|
|
1918
|
+
fast_thinking_level: fastThinkingLevel,
|
|
1919
|
+
verify_thinking_level: verifyThinkingLevel,
|
|
1920
|
+
verified: Boolean(verified),
|
|
1921
|
+
verification_reason: verificationReason,
|
|
1922
|
+
decision_source: decisionSource,
|
|
1923
|
+
fast_result: compactStrategyLlmResult(fastResult),
|
|
1924
|
+
verify_result: compactStrategyLlmResult(verifyResult)
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
function normalizeStrategyThinkingLevel(value, fallback) {
|
|
1929
|
+
return normalizeLlmThinkingLevel(value) || normalizeLlmThinkingLevel(fallback) || fallback;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
function resolveStrategyPassMaxTokens(entry = {}, inherited = {}, pass = "fast", fallback = null) {
|
|
1933
|
+
const value = pass === "verify"
|
|
1934
|
+
? firstConfiguredValue(
|
|
1935
|
+
entry.llmVerifyMaxTokens,
|
|
1936
|
+
entry.verifyMaxTokens,
|
|
1937
|
+
entry.verify_max_tokens,
|
|
1938
|
+
inherited.llmVerifyMaxTokens,
|
|
1939
|
+
inherited.verifyMaxTokens,
|
|
1940
|
+
inherited.verify_max_tokens
|
|
1941
|
+
)
|
|
1942
|
+
: firstConfiguredValue(
|
|
1943
|
+
entry.llmFastMaxTokens,
|
|
1944
|
+
entry.fastMaxTokens,
|
|
1945
|
+
entry.fast_max_tokens,
|
|
1946
|
+
inherited.llmFastMaxTokens,
|
|
1947
|
+
inherited.fastMaxTokens,
|
|
1948
|
+
inherited.fast_max_tokens
|
|
1949
|
+
);
|
|
1950
|
+
return parsePositiveInteger(value, fallback);
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
function applyForcedOutputTokenBudget(config = {}, outputTokenBudget = null) {
|
|
1954
|
+
const budget = parsePositiveInteger(outputTokenBudget, null);
|
|
1955
|
+
if (!budget) return config;
|
|
1956
|
+
const next = {
|
|
1957
|
+
...config,
|
|
1958
|
+
llmMaxTokens: budget,
|
|
1959
|
+
maxTokens: budget
|
|
1960
|
+
};
|
|
1961
|
+
delete next.llmMaxCompletionTokens;
|
|
1962
|
+
delete next.maxCompletionTokens;
|
|
1963
|
+
return next;
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
function withForcedLlmThinkingLevel(config = {}, thinkingLevel = "low", options = {}) {
|
|
1967
|
+
const normalizedThinkingLevel = normalizeStrategyThinkingLevel(thinkingLevel, "low");
|
|
1968
|
+
const forceEntry = (entry) => {
|
|
1969
|
+
const objectEntry = typeof entry === "string" ? { model: entry } : { ...(entry || {}) };
|
|
1970
|
+
const forced = {
|
|
1971
|
+
...objectEntry,
|
|
1972
|
+
llmThinkingLevel: normalizedThinkingLevel,
|
|
1973
|
+
thinkingLevel: normalizedThinkingLevel,
|
|
1974
|
+
reasoningEffort: normalizedThinkingLevel
|
|
1975
|
+
};
|
|
1976
|
+
const entryBudget = options.pass
|
|
1977
|
+
? resolveStrategyPassMaxTokens(objectEntry, config, options.pass, options.defaultMaxTokens)
|
|
1978
|
+
: options.outputTokenBudget;
|
|
1979
|
+
return applyForcedOutputTokenBudget(forced, entryBudget);
|
|
1980
|
+
};
|
|
1981
|
+
const baseBudget = options.pass
|
|
1982
|
+
? resolveStrategyPassMaxTokens(config, {}, options.pass, options.defaultMaxTokens)
|
|
1983
|
+
: options.outputTokenBudget;
|
|
1984
|
+
const next = applyForcedOutputTokenBudget({
|
|
1985
|
+
...(config || {}),
|
|
1986
|
+
llmThinkingLevel: normalizedThinkingLevel,
|
|
1987
|
+
thinkingLevel: normalizedThinkingLevel,
|
|
1988
|
+
reasoningEffort: normalizedThinkingLevel
|
|
1989
|
+
}, baseBudget);
|
|
1990
|
+
if (Array.isArray(config?.llmModels)) {
|
|
1991
|
+
next.llmModels = config.llmModels.map(forceEntry);
|
|
1992
|
+
}
|
|
1993
|
+
if (Array.isArray(config?.models)) {
|
|
1994
|
+
next.models = config.models.map(forceEntry);
|
|
1995
|
+
}
|
|
1996
|
+
return next;
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
async function callSinglePassScreeningLlm(args = {}) {
|
|
1752
2000
|
const providers = normalizeLlmProviderConfigs(args.config || {});
|
|
1753
2001
|
if (providers.length <= 1) {
|
|
1754
2002
|
return callScreeningLlmWithProvider({
|
|
@@ -1782,6 +2030,7 @@ export async function callScreeningLlm(args = {}) {
|
|
|
1782
2030
|
fallback_count: providerFailures.length
|
|
1783
2031
|
};
|
|
1784
2032
|
} catch (error) {
|
|
2033
|
+
if (isFatalLlmProviderError(error)) throw error;
|
|
1785
2034
|
lastError = error;
|
|
1786
2035
|
providerFailures.push(compactLlmProviderFailure(error, providerConfig, index));
|
|
1787
2036
|
if (index < providers.length - 1) {
|
|
@@ -1802,3 +2051,85 @@ export async function callScreeningLlm(args = {}) {
|
|
|
1802
2051
|
finalError.image_inputs = lastError?.image_inputs || [];
|
|
1803
2052
|
throw finalError;
|
|
1804
2053
|
}
|
|
2054
|
+
|
|
2055
|
+
async function callFastFirstVerifiedScreeningLlm(args = {}) {
|
|
2056
|
+
const config = args.config || {};
|
|
2057
|
+
const fastThinkingLevel = normalizeStrategyThinkingLevel(config.llmFastThinkingLevel ?? config.fastThinkingLevel, "current");
|
|
2058
|
+
const verifyThinkingLevel = normalizeStrategyThinkingLevel(config.llmVerifyThinkingLevel ?? config.verifyThinkingLevel, "low");
|
|
2059
|
+
const fastArgs = {
|
|
2060
|
+
...args,
|
|
2061
|
+
config: withForcedLlmThinkingLevel(config, fastThinkingLevel, {
|
|
2062
|
+
pass: "fast",
|
|
2063
|
+
defaultMaxTokens: FAST_FIRST_DEFAULT_FAST_MAX_TOKENS
|
|
2064
|
+
}),
|
|
2065
|
+
requireReviewDecision: true
|
|
2066
|
+
};
|
|
2067
|
+
let fastResult = null;
|
|
2068
|
+
let verifyReason = "";
|
|
2069
|
+
try {
|
|
2070
|
+
fastResult = await callSinglePassScreeningLlm(fastArgs);
|
|
2071
|
+
if (fastResult.passed === true) {
|
|
2072
|
+
verifyReason = fastResult.review_required === true ? "fast_passed_review_required" : "fast_passed";
|
|
2073
|
+
} else if (fastResult.review_required === true) {
|
|
2074
|
+
verifyReason = "fast_review_required";
|
|
2075
|
+
}
|
|
2076
|
+
} catch (error) {
|
|
2077
|
+
if (isFatalLlmProviderError(error)) throw error;
|
|
2078
|
+
fastResult = createFailedLlmScreeningResult(error);
|
|
2079
|
+
verifyReason = "fast_invalid_response";
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
if (!verifyReason) {
|
|
2083
|
+
return attachFastFirstScreeningMetadata(fastResult, {
|
|
2084
|
+
fastThinkingLevel,
|
|
2085
|
+
verifyThinkingLevel,
|
|
2086
|
+
verified: false,
|
|
2087
|
+
decisionSource: "fast",
|
|
2088
|
+
fastResult
|
|
2089
|
+
});
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
const verifyArgs = {
|
|
2093
|
+
...args,
|
|
2094
|
+
config: withForcedLlmThinkingLevel(config, verifyThinkingLevel, {
|
|
2095
|
+
pass: "verify"
|
|
2096
|
+
}),
|
|
2097
|
+
requireReviewDecision: false
|
|
2098
|
+
};
|
|
2099
|
+
try {
|
|
2100
|
+
const verifyResult = await callSinglePassScreeningLlm(verifyArgs);
|
|
2101
|
+
return attachFastFirstScreeningMetadata(verifyResult, {
|
|
2102
|
+
fastThinkingLevel,
|
|
2103
|
+
verifyThinkingLevel,
|
|
2104
|
+
verified: true,
|
|
2105
|
+
verificationReason: verifyReason,
|
|
2106
|
+
decisionSource: "verify",
|
|
2107
|
+
fastResult,
|
|
2108
|
+
verifyResult
|
|
2109
|
+
});
|
|
2110
|
+
} catch (error) {
|
|
2111
|
+
if (isFatalLlmProviderError(error)) throw error;
|
|
2112
|
+
const failedVerifyResult = createFailedLlmScreeningResult(error);
|
|
2113
|
+
return attachFastFirstScreeningMetadata(failedVerifyResult, {
|
|
2114
|
+
fastThinkingLevel,
|
|
2115
|
+
verifyThinkingLevel,
|
|
2116
|
+
verified: true,
|
|
2117
|
+
verificationReason: verifyReason,
|
|
2118
|
+
decisionSource: "verify_error",
|
|
2119
|
+
fastResult,
|
|
2120
|
+
verifyResult: failedVerifyResult
|
|
2121
|
+
});
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
export async function callScreeningLlm(args = {}) {
|
|
2126
|
+
const strategy = normalizeLlmScreeningStrategy(
|
|
2127
|
+
args.config?.llmScreeningStrategy
|
|
2128
|
+
?? args.config?.screeningStrategy
|
|
2129
|
+
?? args.config?.screening_strategy
|
|
2130
|
+
);
|
|
2131
|
+
if (strategy === "fast_first_verified") {
|
|
2132
|
+
return callFastFirstVerifiedScreeningLlm(args);
|
|
2133
|
+
}
|
|
2134
|
+
return callSinglePassScreeningLlm(args);
|
|
2135
|
+
}
|
|
@@ -40,6 +40,8 @@ import {
|
|
|
40
40
|
} from "../../core/run/timing.js";
|
|
41
41
|
import {
|
|
42
42
|
callScreeningLlm,
|
|
43
|
+
createFatalLlmRunError,
|
|
44
|
+
isFatalLlmProviderError,
|
|
43
45
|
normalizeText,
|
|
44
46
|
screenCandidate
|
|
45
47
|
} from "../../core/screening/index.js";
|
|
@@ -108,19 +110,31 @@ function compactLlmResult(llmResult) {
|
|
|
108
110
|
ok: Boolean(llmResult.ok),
|
|
109
111
|
provider: llmResult.provider || null,
|
|
110
112
|
passed: llmResult.passed,
|
|
113
|
+
review_required: typeof llmResult.review_required === "boolean" ? llmResult.review_required : null,
|
|
111
114
|
cot: llmResult.cot || llmResult.decision_cot || "",
|
|
112
|
-
reasoning_content: llmResult.reasoning_content || "",
|
|
113
|
-
raw_model_output: llmResult.raw_model_output || "",
|
|
114
|
-
evidence_count: llmResult.evidence?.length || 0,
|
|
115
|
-
usage: llmResult.usage || null,
|
|
115
|
+
reasoning_content: llmResult.reasoning_content || "",
|
|
116
|
+
raw_model_output: llmResult.raw_model_output || "",
|
|
117
|
+
evidence_count: llmResult.evidence?.length || 0,
|
|
118
|
+
usage: llmResult.usage || null,
|
|
116
119
|
finish_reason: llmResult.finish_reason || null,
|
|
117
|
-
image_input_count: llmResult.image_input_count || 0,
|
|
118
|
-
attempt_count: llmResult.attempt_count || 0,
|
|
119
|
-
fallback_count: llmResult.fallback_count || 0,
|
|
120
|
-
llm_model_failures: Array.isArray(llmResult.llm_model_failures) ? llmResult.llm_model_failures : [],
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
image_input_count: llmResult.image_input_count || 0,
|
|
121
|
+
attempt_count: llmResult.attempt_count || 0,
|
|
122
|
+
fallback_count: llmResult.fallback_count || 0,
|
|
123
|
+
llm_model_failures: Array.isArray(llmResult.llm_model_failures) ? llmResult.llm_model_failures : [],
|
|
124
|
+
screening_strategy: llmResult.screening_strategy || "",
|
|
125
|
+
fast_thinking_level: llmResult.fast_thinking_level || "",
|
|
126
|
+
verify_thinking_level: llmResult.verify_thinking_level || "",
|
|
127
|
+
verified: typeof llmResult.verified === "boolean" ? llmResult.verified : null,
|
|
128
|
+
verification_reason: llmResult.verification_reason || "",
|
|
129
|
+
decision_source: llmResult.decision_source || "",
|
|
130
|
+
fast_result: llmResult.fast_result || null,
|
|
131
|
+
verify_result: llmResult.verify_result || null,
|
|
132
|
+
error_code: llmResult.error_code || null,
|
|
133
|
+
fatal: Boolean(llmResult.fatal),
|
|
134
|
+
fatal_reason: llmResult.fatal_reason || "",
|
|
135
|
+
error: llmResult.error || null
|
|
136
|
+
};
|
|
137
|
+
}
|
|
124
138
|
|
|
125
139
|
function compactCandidate(candidate) {
|
|
126
140
|
return {
|
|
@@ -339,9 +353,9 @@ function isRecoverableLlmScreeningError(error) {
|
|
|
339
353
|
}
|
|
340
354
|
|
|
341
355
|
function createFailedLlmResult(error) {
|
|
342
|
-
return {
|
|
343
|
-
ok: false,
|
|
344
|
-
passed: false,
|
|
356
|
+
return {
|
|
357
|
+
ok: false,
|
|
358
|
+
passed: false,
|
|
345
359
|
reason: "",
|
|
346
360
|
evidence: [],
|
|
347
361
|
cot: "",
|
|
@@ -349,12 +363,15 @@ function createFailedLlmResult(error) {
|
|
|
349
363
|
reasoning_content: "",
|
|
350
364
|
raw_model_output: "",
|
|
351
365
|
attempt_count: Number(error?.llm_attempt_count) || 0,
|
|
352
|
-
fallback_count: Array.isArray(error?.llm_model_failures) ? error.llm_model_failures.length : 0,
|
|
353
|
-
llm_model_failures: Array.isArray(error?.llm_model_failures) ? error.llm_model_failures : [],
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
366
|
+
fallback_count: Array.isArray(error?.llm_model_failures) ? error.llm_model_failures.length : 0,
|
|
367
|
+
llm_model_failures: Array.isArray(error?.llm_model_failures) ? error.llm_model_failures : [],
|
|
368
|
+
error_code: error?.code || null,
|
|
369
|
+
fatal: Boolean(isFatalLlmProviderError(error)),
|
|
370
|
+
fatal_reason: error?.llm_fatal_reason || "",
|
|
371
|
+
error: error?.message || String(error || "unknown"),
|
|
372
|
+
screened_at: new Date().toISOString()
|
|
373
|
+
};
|
|
374
|
+
}
|
|
358
375
|
|
|
359
376
|
function normalizeScreeningMode(value) {
|
|
360
377
|
const normalized = String(value || "llm").trim().toLowerCase();
|
|
@@ -1510,9 +1527,15 @@ export async function runChatWorkflow({
|
|
|
1510
1527
|
maxImages: llmImageLimit,
|
|
1511
1528
|
imageDetail: llmImageDetail
|
|
1512
1529
|
}));
|
|
1513
|
-
} catch (error) {
|
|
1514
|
-
|
|
1515
|
-
|
|
1530
|
+
} catch (error) {
|
|
1531
|
+
if (isFatalLlmProviderError(error)) {
|
|
1532
|
+
throw createFatalLlmRunError(error, {
|
|
1533
|
+
domain: "chat",
|
|
1534
|
+
candidate: detailResult.candidate
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
llmResult = createFailedLlmResult(error);
|
|
1538
|
+
}
|
|
1516
1539
|
}
|
|
1517
1540
|
}
|
|
1518
1541
|
} else {
|
|
@@ -1583,9 +1606,15 @@ export async function runChatWorkflow({
|
|
|
1583
1606
|
maxImages: llmImageLimit,
|
|
1584
1607
|
imageDetail: llmImageDetail
|
|
1585
1608
|
}));
|
|
1586
|
-
} catch (error) {
|
|
1587
|
-
|
|
1588
|
-
|
|
1609
|
+
} catch (error) {
|
|
1610
|
+
if (isFatalLlmProviderError(error)) {
|
|
1611
|
+
throw createFatalLlmRunError(error, {
|
|
1612
|
+
domain: "chat",
|
|
1613
|
+
candidate: detailResult.candidate
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
llmResult = createFailedLlmResult(error);
|
|
1617
|
+
}
|
|
1589
1618
|
}
|
|
1590
1619
|
}
|
|
1591
1620
|
}
|
|
@@ -41,7 +41,9 @@ import { createViewportRunGuard } from "../../core/self-heal/index.js";
|
|
|
41
41
|
import {
|
|
42
42
|
callScreeningLlm,
|
|
43
43
|
compactScreeningLlmResult,
|
|
44
|
+
createFatalLlmRunError,
|
|
44
45
|
createFailedLlmScreeningResult,
|
|
46
|
+
isFatalLlmProviderError,
|
|
45
47
|
llmResultToScreening,
|
|
46
48
|
screenCandidate
|
|
47
49
|
} from "../../core/screening/index.js";
|
|
@@ -1343,9 +1345,15 @@ export async function runRecommendWorkflow({
|
|
|
1343
1345
|
maxImages: llmImageLimit,
|
|
1344
1346
|
imageDetail: llmImageDetail
|
|
1345
1347
|
}));
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1348
|
+
} catch (error) {
|
|
1349
|
+
if (isFatalLlmProviderError(error)) {
|
|
1350
|
+
throw createFatalLlmRunError(error, {
|
|
1351
|
+
domain: "recommend",
|
|
1352
|
+
candidate: screeningCandidate
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
llmResult = createFailedLlmScreeningResult(error);
|
|
1356
|
+
}
|
|
1349
1357
|
}
|
|
1350
1358
|
if (detailResult) detailResult.llm_result = llmResult;
|
|
1351
1359
|
}
|
|
@@ -39,7 +39,9 @@ import { createViewportRunGuard } from "../../core/self-heal/index.js";
|
|
|
39
39
|
import {
|
|
40
40
|
callScreeningLlm,
|
|
41
41
|
compactScreeningLlmResult,
|
|
42
|
+
createFatalLlmRunError,
|
|
42
43
|
createFailedLlmScreeningResult,
|
|
44
|
+
isFatalLlmProviderError,
|
|
43
45
|
llmResultToScreening,
|
|
44
46
|
screenCandidate
|
|
45
47
|
} from "../../core/screening/index.js";
|
|
@@ -1022,6 +1024,12 @@ export async function runRecruitWorkflow({
|
|
|
1022
1024
|
imageDetail: llmImageDetail
|
|
1023
1025
|
}));
|
|
1024
1026
|
} catch (error) {
|
|
1027
|
+
if (isFatalLlmProviderError(error)) {
|
|
1028
|
+
throw createFatalLlmRunError(error, {
|
|
1029
|
+
domain: "recruit",
|
|
1030
|
+
candidate: screeningCandidate
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1025
1033
|
llmResult = createFailedLlmScreeningResult(error);
|
|
1026
1034
|
}
|
|
1027
1035
|
}
|