@roll-agent/smart-reply-agent 0.1.0 → 0.1.1
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/dist/ai/model-registry.d.ts +0 -1
- package/dist/ai/model-registry.js +1 -205
- package/dist/ai/structured-output.d.ts +0 -1
- package/dist/ai/structured-output.js +1 -78
- package/dist/errors/app-error.d.ts +0 -1
- package/dist/errors/app-error.js +1 -95
- package/dist/errors/error-codes.d.ts +0 -1
- package/dist/errors/error-codes.js +1 -115
- package/dist/errors/error-factory.d.ts +0 -1
- package/dist/errors/error-factory.js +1 -86
- package/dist/errors/error-utils.d.ts +0 -1
- package/dist/errors/error-utils.js +1 -188
- package/dist/errors/index.d.ts +0 -1
- package/dist/errors/index.js +1 -5
- package/dist/index.d.ts +0 -1
- package/dist/index.js +1 -12
- package/dist/log-control.d.ts +0 -1
- package/dist/log-control.js +1 -15
- package/dist/pipeline/age-eligibility.d.ts +0 -1
- package/dist/pipeline/age-eligibility.js +1 -176
- package/dist/pipeline/candidate-context.d.ts +0 -1
- package/dist/pipeline/candidate-context.js +1 -31
- package/dist/pipeline/candidate-utils.d.ts +0 -1
- package/dist/pipeline/candidate-utils.js +1 -33
- package/dist/pipeline/classification.d.ts +0 -1
- package/dist/pipeline/classification.js +1 -206
- package/dist/pipeline/context-builder.d.ts +0 -1
- package/dist/pipeline/context-builder.js +1 -404
- package/dist/pipeline/pipeline-progress.d.ts +0 -1
- package/dist/pipeline/pipeline-progress.js +1 -33
- package/dist/pipeline/reply-gate.d.ts +0 -1
- package/dist/pipeline/reply-gate.js +1 -139
- package/dist/pipeline/smart-reply.d.ts +0 -1
- package/dist/pipeline/smart-reply.js +1 -418
- package/dist/pipeline.d.ts +0 -1
- package/dist/pipeline.js +1 -12
- package/dist/services/brand-alias.d.ts +0 -1
- package/dist/services/brand-alias.js +1 -184
- package/dist/services/brand-config-selectors.d.ts +0 -1
- package/dist/services/brand-config-selectors.js +1 -30
- package/dist/services/config-loader.d.ts +0 -1
- package/dist/services/config-loader.js +1 -45
- package/dist/services/duliday-api.d.ts +0 -1
- package/dist/services/duliday-api.js +1 -160
- package/dist/services/duliday-mapper.d.ts +0 -1
- package/dist/services/duliday-mapper.js +1 -536
- package/dist/tools/generate-reply.d.ts +0 -1
- package/dist/tools/generate-reply.js +1 -132
- package/dist/tools/sync-brand-data.d.ts +0 -1
- package/dist/tools/sync-brand-data.js +1 -114
- package/dist/types/brand-resolution.d.ts +0 -1
- package/dist/types/brand-resolution.js +1 -37
- package/dist/types/classification.d.ts +0 -1
- package/dist/types/classification.js +1 -30
- package/dist/types/config.d.ts +0 -1
- package/dist/types/config.js +1 -7
- package/dist/types/duliday-api.d.ts +0 -1
- package/dist/types/duliday-api.js +1 -235
- package/dist/types/geocoding.d.ts +0 -1
- package/dist/types/geocoding.js +1 -12
- package/dist/types/reply-policy.d.ts +0 -1
- package/dist/types/reply-policy.js +1 -332
- package/dist/types/zhipin.d.ts +0 -1
- package/dist/types/zhipin.js +1 -123
- package/package.json +3 -3
- package/dist/ai/model-registry.d.ts.map +0 -1
- package/dist/ai/model-registry.js.map +0 -1
- package/dist/ai/structured-output.d.ts.map +0 -1
- package/dist/ai/structured-output.js.map +0 -1
- package/dist/errors/app-error.d.ts.map +0 -1
- package/dist/errors/app-error.js.map +0 -1
- package/dist/errors/error-codes.d.ts.map +0 -1
- package/dist/errors/error-codes.js.map +0 -1
- package/dist/errors/error-factory.d.ts.map +0 -1
- package/dist/errors/error-factory.js.map +0 -1
- package/dist/errors/error-utils.d.ts.map +0 -1
- package/dist/errors/error-utils.js.map +0 -1
- package/dist/errors/index.d.ts.map +0 -1
- package/dist/errors/index.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/log-control.d.ts.map +0 -1
- package/dist/log-control.js.map +0 -1
- package/dist/pipeline/age-eligibility.d.ts.map +0 -1
- package/dist/pipeline/age-eligibility.js.map +0 -1
- package/dist/pipeline/candidate-context.d.ts.map +0 -1
- package/dist/pipeline/candidate-context.js.map +0 -1
- package/dist/pipeline/candidate-utils.d.ts.map +0 -1
- package/dist/pipeline/candidate-utils.js.map +0 -1
- package/dist/pipeline/classification.d.ts.map +0 -1
- package/dist/pipeline/classification.js.map +0 -1
- package/dist/pipeline/context-builder.d.ts.map +0 -1
- package/dist/pipeline/context-builder.js.map +0 -1
- package/dist/pipeline/pipeline-progress.d.ts.map +0 -1
- package/dist/pipeline/pipeline-progress.js.map +0 -1
- package/dist/pipeline/reply-gate.d.ts.map +0 -1
- package/dist/pipeline/reply-gate.js.map +0 -1
- package/dist/pipeline/smart-reply.d.ts.map +0 -1
- package/dist/pipeline/smart-reply.js.map +0 -1
- package/dist/pipeline.d.ts.map +0 -1
- package/dist/pipeline.js.map +0 -1
- package/dist/services/brand-alias.d.ts.map +0 -1
- package/dist/services/brand-alias.js.map +0 -1
- package/dist/services/brand-config-selectors.d.ts.map +0 -1
- package/dist/services/brand-config-selectors.js.map +0 -1
- package/dist/services/config-loader.d.ts.map +0 -1
- package/dist/services/config-loader.js.map +0 -1
- package/dist/services/duliday-api.d.ts.map +0 -1
- package/dist/services/duliday-api.js.map +0 -1
- package/dist/services/duliday-mapper.d.ts.map +0 -1
- package/dist/services/duliday-mapper.js.map +0 -1
- package/dist/tools/generate-reply.d.ts.map +0 -1
- package/dist/tools/generate-reply.js.map +0 -1
- package/dist/tools/sync-brand-data.d.ts.map +0 -1
- package/dist/tools/sync-brand-data.js.map +0 -1
- package/dist/types/brand-resolution.d.ts.map +0 -1
- package/dist/types/brand-resolution.js.map +0 -1
- package/dist/types/classification.d.ts.map +0 -1
- package/dist/types/classification.js.map +0 -1
- package/dist/types/config.d.ts.map +0 -1
- package/dist/types/config.js.map +0 -1
- package/dist/types/duliday-api.d.ts.map +0 -1
- package/dist/types/duliday-api.js.map +0 -1
- package/dist/types/geocoding.d.ts.map +0 -1
- package/dist/types/geocoding.js.map +0 -1
- package/dist/types/reply-policy.d.ts.map +0 -1
- package/dist/types/reply-policy.js.map +0 -1
- package/dist/types/zhipin.d.ts.map +0 -1
- package/dist/types/zhipin.js.map +0 -1
|
@@ -1,33 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
export function createPipelineProgress() {
|
|
3
|
-
let spinner = null;
|
|
4
|
-
return {
|
|
5
|
-
update(text) {
|
|
6
|
-
if (!spinner) {
|
|
7
|
-
spinner = ora({ text, stream: process.stderr }).start();
|
|
8
|
-
}
|
|
9
|
-
else {
|
|
10
|
-
spinner.text = text;
|
|
11
|
-
}
|
|
12
|
-
},
|
|
13
|
-
succeed(text) {
|
|
14
|
-
if (spinner) {
|
|
15
|
-
spinner.succeed(text);
|
|
16
|
-
spinner = null;
|
|
17
|
-
}
|
|
18
|
-
else {
|
|
19
|
-
console.error(`✓ ${text}`);
|
|
20
|
-
}
|
|
21
|
-
},
|
|
22
|
-
fail(text) {
|
|
23
|
-
if (spinner) {
|
|
24
|
-
spinner.fail(text);
|
|
25
|
-
spinner = null;
|
|
26
|
-
}
|
|
27
|
-
else {
|
|
28
|
-
console.error(`✗ ${text}`);
|
|
29
|
-
}
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
//# sourceMappingURL=pipeline-progress.js.map
|
|
1
|
+
import e from"ora";export function createPipelineProgress(){let r=null;return{update(t){r?r.text=t:r=e({text:t,stream:process.stderr}).start()},succeed(e){r?(r.succeed(e),r=null):console.error(`✓ ${e}`)},fail(e){r?(r.fail(e),r=null):console.error(`✗ ${e}`)}}}
|
|
@@ -1,139 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
export const REPLY_GATE_VIOLATION_CODES = [
|
|
3
|
-
"too_many_questions",
|
|
4
|
-
"audit_tone",
|
|
5
|
-
"premature_numeric_disclosure",
|
|
6
|
-
"off_axis_fact_disclosure",
|
|
7
|
-
"reply_overpacked",
|
|
8
|
-
];
|
|
9
|
-
const QUESTION_MARK_SEGMENT_PATTERN = /[^。!?!?]*[??]/g;
|
|
10
|
-
const FACT_FAMILY_PATTERNS = {
|
|
11
|
-
salary: {
|
|
12
|
-
mention: [/\d+(?:\.\d+)?\s*元(?:\/时|\/小时)?/i, /时薪|薪资|工资|底薪|收入/i],
|
|
13
|
-
concrete: [/\d+(?:\.\d+)?\s*元(?:\/时|\/小时)?/i],
|
|
14
|
-
},
|
|
15
|
-
schedule: {
|
|
16
|
-
mention: [/\d{1,2}:\d{2}\s*[~-]\s*\d{1,2}:\d{2}/, /班次|轮班|白班|晚班|工时|排班/i],
|
|
17
|
-
concrete: [/\d{1,2}:\d{2}\s*[~-]\s*\d{1,2}:\d{2}/, /轮班|白班|晚班|早班|中班|夜班/i],
|
|
18
|
-
},
|
|
19
|
-
location: {
|
|
20
|
-
mention: [/地址|地铁|附近|位于|门店|在.+(?:路|街|广场|商场|大厦)/i],
|
|
21
|
-
concrete: [/地址|位于|地铁\S*站|在.+(?:路|街|广场|商场|大厦)/i],
|
|
22
|
-
},
|
|
23
|
-
policy: {
|
|
24
|
-
mention: [/考勤|试用期|社保|五险一金|迟到|补班/i],
|
|
25
|
-
concrete: [/考勤|试用期|社保|五险一金|迟到|补班/i],
|
|
26
|
-
},
|
|
27
|
-
requirements: {
|
|
28
|
-
mention: [/年龄|学历|经验|健康证|要求/i],
|
|
29
|
-
concrete: [/年龄|学历|经验|健康证/i],
|
|
30
|
-
},
|
|
31
|
-
availability: {
|
|
32
|
-
mention: [/名额|空位|可用时段|可安排/i],
|
|
33
|
-
concrete: [/名额|空位|可用时段/i],
|
|
34
|
-
},
|
|
35
|
-
};
|
|
36
|
-
function resolveOutputGuards(policy) {
|
|
37
|
-
return policy?.outputGuards ?? DEFAULT_OUTPUT_GUARDS;
|
|
38
|
-
}
|
|
39
|
-
export function countQuestions(text) {
|
|
40
|
-
const questionMarkSegments = text.match(QUESTION_MARK_SEGMENT_PATTERN) ?? [];
|
|
41
|
-
const normalizedQuestionMarkSegments = new Set(questionMarkSegments.map((segment) => segment.replace(/[??]/g, "").trim()).filter(Boolean));
|
|
42
|
-
const endingQuestions = text
|
|
43
|
-
.split(/[。!?!?]/)
|
|
44
|
-
.map((part) => part.trim())
|
|
45
|
-
.filter(Boolean)
|
|
46
|
-
.filter((clause) => /[吗呢么]$/.test(clause) && !normalizedQuestionMarkSegments.has(clause)).length;
|
|
47
|
-
return questionMarkSegments.length + endingQuestions;
|
|
48
|
-
}
|
|
49
|
-
function countSentences(text) {
|
|
50
|
-
return text
|
|
51
|
-
.split(/[。!?!?]/)
|
|
52
|
-
.map((part) => part.trim())
|
|
53
|
-
.filter(Boolean).length;
|
|
54
|
-
}
|
|
55
|
-
function hasListMarkers(text) {
|
|
56
|
-
return /(?:^|\s)(?:\d+\.\s|- |•)/m.test(text);
|
|
57
|
-
}
|
|
58
|
-
function detectFactFamilies(text, matchMode = "mention") {
|
|
59
|
-
return Object.entries(FACT_FAMILY_PATTERNS)
|
|
60
|
-
.filter(([, patterns]) => patterns[matchMode].some((pattern) => pattern.test(text)))
|
|
61
|
-
.map(([family]) => family);
|
|
62
|
-
}
|
|
63
|
-
function hasBlockedAuditPhrase(text, blockedPhrases) {
|
|
64
|
-
return blockedPhrases.some((phrase) => phrase && text.includes(phrase));
|
|
65
|
-
}
|
|
66
|
-
function hasFirstTurnSpecificFacts(text) {
|
|
67
|
-
return (/\d+(?:\.\d+)?\s*元(?:\/时|\/小时)?/i.test(text) ||
|
|
68
|
-
/\d{1,2}:\d{2}\s*[~-]\s*\d{1,2}:\d{2}/.test(text) ||
|
|
69
|
-
/年龄\s*\d{1,2}\s*[-~到]\s*\d{1,2}\s*岁/i.test(text) ||
|
|
70
|
-
/\d{1,2}\s*[-~到]\s*\d{1,2}\s*岁/i.test(text) ||
|
|
71
|
-
/地址在|门店在|位于|在.+(?:路|街|广场|商场|大厦|地铁)/i.test(text));
|
|
72
|
-
}
|
|
73
|
-
function isReplyOverpacked(text, questionCount, factFamilies) {
|
|
74
|
-
let signals = 0;
|
|
75
|
-
if (countSentences(text) >= 4)
|
|
76
|
-
signals += 1;
|
|
77
|
-
if (hasListMarkers(text))
|
|
78
|
-
signals += 1;
|
|
79
|
-
if (questionCount >= 3)
|
|
80
|
-
signals += 1;
|
|
81
|
-
if (factFamilies.length >= 2)
|
|
82
|
-
signals += 1;
|
|
83
|
-
return signals >= 2;
|
|
84
|
-
}
|
|
85
|
-
export function detectConcreteFactFamilies(text) {
|
|
86
|
-
return detectFactFamilies(text, "concrete");
|
|
87
|
-
}
|
|
88
|
-
export function detectContextFactFamilies(contextInfo) {
|
|
89
|
-
const families = [];
|
|
90
|
-
if (/薪资:/.test(contextInfo))
|
|
91
|
-
families.push("salary");
|
|
92
|
-
if (/排班:|时间:|每周工时:/.test(contextInfo))
|
|
93
|
-
families.push("schedule");
|
|
94
|
-
if (/匹配到的门店信息:[\s\S]*• .*:/.test(contextInfo))
|
|
95
|
-
families.push("location");
|
|
96
|
-
if (/考勤:|出勤要求:/.test(contextInfo))
|
|
97
|
-
families.push("policy");
|
|
98
|
-
if (/要求:/.test(contextInfo))
|
|
99
|
-
families.push("requirements");
|
|
100
|
-
if (/可用时段:/.test(contextInfo))
|
|
101
|
-
families.push("availability");
|
|
102
|
-
return families;
|
|
103
|
-
}
|
|
104
|
-
function hasOffAxisFactDisclosure(allowedNeeds, factFamilies) {
|
|
105
|
-
const allowedFamilies = new Set(allowedNeeds.flatMap((need) => PRIMARY_NEED_FACT_MAP[need]));
|
|
106
|
-
if (allowedFamilies.size === 0)
|
|
107
|
-
return factFamilies.length > 0;
|
|
108
|
-
return factFamilies.some((family) => !allowedFamilies.has(family));
|
|
109
|
-
}
|
|
110
|
-
export function validateReply(options) {
|
|
111
|
-
const { text, turnIndex, mode, policy } = options;
|
|
112
|
-
const outputGuards = resolveOutputGuards(policy);
|
|
113
|
-
const questionCount = countQuestions(text);
|
|
114
|
-
const maxQuestions = outputGuards.maxQuestionsByMode[mode];
|
|
115
|
-
const factFamilies = detectConcreteFactFamilies(text);
|
|
116
|
-
const allowedNeeds = options.allowedNeeds?.length ? options.allowedNeeds : [options.primaryNeed];
|
|
117
|
-
const violations = [];
|
|
118
|
-
if (questionCount > maxQuestions)
|
|
119
|
-
violations.push("too_many_questions");
|
|
120
|
-
if (hasBlockedAuditPhrase(text, outputGuards.blockedAuditPhrases))
|
|
121
|
-
violations.push("audit_tone");
|
|
122
|
-
if (outputGuards.blockFirstTurnSpecificFacts &&
|
|
123
|
-
turnIndex === 1 &&
|
|
124
|
-
hasFirstTurnSpecificFacts(text)) {
|
|
125
|
-
violations.push("premature_numeric_disclosure");
|
|
126
|
-
}
|
|
127
|
-
if (hasOffAxisFactDisclosure(allowedNeeds, factFamilies)) {
|
|
128
|
-
violations.push("off_axis_fact_disclosure");
|
|
129
|
-
}
|
|
130
|
-
if (isReplyOverpacked(text, questionCount, factFamilies)) {
|
|
131
|
-
violations.push("reply_overpacked");
|
|
132
|
-
}
|
|
133
|
-
return {
|
|
134
|
-
violations,
|
|
135
|
-
questionCount,
|
|
136
|
-
factFamilies,
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
//# sourceMappingURL=reply-gate.js.map
|
|
1
|
+
import{DEFAULT_OUTPUT_GUARDS as t,PRIMARY_NEED_FACT_MAP as e}from"../types/reply-policy.js";export const REPLY_GATE_VIOLATION_CODES=["too_many_questions","audit_tone","premature_numeric_disclosure","off_axis_fact_disclosure","reply_overpacked"];const n=/[^。!?!?]*[??]/g,s={salary:{mention:[/\d+(?:\.\d+)?\s*元(?:\/时|\/小时)?/i,/时薪|薪资|工资|底薪|收入/i],concrete:[/\d+(?:\.\d+)?\s*元(?:\/时|\/小时)?/i]},schedule:{mention:[/\d{1,2}:\d{2}\s*[~-]\s*\d{1,2}:\d{2}/,/班次|轮班|白班|晚班|工时|排班/i],concrete:[/\d{1,2}:\d{2}\s*[~-]\s*\d{1,2}:\d{2}/,/轮班|白班|晚班|早班|中班|夜班/i]},location:{mention:[/地址|地铁|附近|位于|门店|在.+(?:路|街|广场|商场|大厦)/i],concrete:[/地址|位于|地铁\S*站|在.+(?:路|街|广场|商场|大厦)/i]},policy:{mention:[/考勤|试用期|社保|五险一金|迟到|补班/i],concrete:[/考勤|试用期|社保|五险一金|迟到|补班/i]},requirements:{mention:[/年龄|学历|经验|健康证|要求/i],concrete:[/年龄|学历|经验|健康证/i]},availability:{mention:[/名额|空位|可用时段|可安排/i],concrete:[/名额|空位|可用时段/i]}};function i(e){return e?.outputGuards??t}export function countQuestions(t){const e=t.match(n)??[],s=new Set(e.map(t=>t.replace(/[??]/g,"").trim()).filter(Boolean)),i=t.split(/[。!?!?]/).map(t=>t.trim()).filter(Boolean).filter(t=>/[吗呢么]$/.test(t)&&!s.has(t)).length;return e.length+i}function o(t){return t.split(/[。!?!?]/).map(t=>t.trim()).filter(Boolean).length}function r(t){return/(?:^|\s)(?:\d+\.\s|- |•)/m.test(t)}function c(t,e="mention"){return Object.entries(s).filter(([,n])=>n[e].some(e=>e.test(t))).map(([t])=>t)}function u(t,e){return e.some(e=>e&&t.includes(e))}function a(t){return/\d+(?:\.\d+)?\s*元(?:\/时|\/小时)?/i.test(t)||/\d{1,2}:\d{2}\s*[~-]\s*\d{1,2}:\d{2}/.test(t)||/年龄\s*\d{1,2}\s*[-~到]\s*\d{1,2}\s*岁/i.test(t)||/\d{1,2}\s*[-~到]\s*\d{1,2}\s*岁/i.test(t)||/地址在|门店在|位于|在.+(?:路|街|广场|商场|大厦|地铁)/i.test(t)}function l(t,e,n){let s=0;return o(t)>=4&&(s+=1),r(t)&&(s+=1),e>=3&&(s+=1),n.length>=2&&(s+=1),s>=2}export function detectConcreteFactFamilies(t){return c(t,"concrete")}export function detectContextFactFamilies(t){const e=[];return/薪资:/.test(t)&&e.push("salary"),/排班:|时间:|每周工时:/.test(t)&&e.push("schedule"),/匹配到的门店信息:[\s\S]*• .*:/.test(t)&&e.push("location"),/考勤:|出勤要求:/.test(t)&&e.push("policy"),/要求:/.test(t)&&e.push("requirements"),/可用时段:/.test(t)&&e.push("availability"),e}function d(t,n){const s=new Set(t.flatMap(t=>e[t]));return 0===s.size?n.length>0:n.some(t=>!s.has(t))}export function validateReply(t){const{text:e,turnIndex:n,mode:s,policy:o}=t,r=i(o),c=countQuestions(e),p=r.maxQuestionsByMode[s],m=detectConcreteFactFamilies(e),f=t.allowedNeeds?.length?t.allowedNeeds:[t.primaryNeed],h=[];return c>p&&h.push("too_many_questions"),u(e,r.blockedAuditPhrases)&&h.push("audit_tone"),r.blockFirstTurnSpecificFacts&&1===n&&a(e)&&h.push("premature_numeric_disclosure"),d(f,m)&&h.push("off_axis_fact_disclosure"),l(e,c,m)&&h.push("reply_overpacked"),{violations:h,questionCount:c,factFamilies:m}}
|
|
@@ -61,4 +61,3 @@ export declare function hasUnsupportedFactClaims(text: string, contextInfo: stri
|
|
|
61
61
|
export declare function resolveTurnIndex(conversationHistory: string[], explicitTurnIndex?: number | undefined): number;
|
|
62
62
|
export declare function resolveEffectiveDisclosureMode(turnIndex: number, stage: FunnelStage): EffectiveDisclosureMode;
|
|
63
63
|
export declare function generateSmartReply(options: SmartReplyAgentOptions): Promise<SmartReplyAgentResult>;
|
|
64
|
-
//# sourceMappingURL=smart-reply.d.ts.map
|
|
@@ -1,418 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { getAllStores, getAvailableBrandNames, resolveDefaultBrandName, resolvePrimaryCity, } from "../services/brand-config-selectors.js";
|
|
3
|
-
import { PRIMARY_NEED_FACT_MAP } from "../types/reply-policy.js";
|
|
4
|
-
import { planTurn, selectContextNeeds } from "./classification.js";
|
|
5
|
-
import { buildContextInfoByNeeds } from "./context-builder.js";
|
|
6
|
-
import { detectConcreteFactFamilies, detectContextFactFamilies, validateReply, } from "./reply-gate.js";
|
|
7
|
-
import { safeGenerateText } from "../ai/structured-output.js";
|
|
8
|
-
import { logError } from "../errors/index.js";
|
|
9
|
-
import { setSuppressVerboseLogs } from "../log-control.js";
|
|
10
|
-
import { createPipelineProgress } from "./pipeline-progress.js";
|
|
11
|
-
import { evaluateAgeEligibility } from "./age-eligibility.js";
|
|
12
|
-
import { resolveCandidateAge, resolveRegionName } from "./candidate-utils.js";
|
|
13
|
-
import { buildKnownCandidateContext } from "./candidate-context.js";
|
|
14
|
-
function formatAgeRange(summary) {
|
|
15
|
-
if (summary.minAgeObserved === null && summary.maxAgeObserved === null)
|
|
16
|
-
return null;
|
|
17
|
-
const min = summary.minAgeObserved ?? "?";
|
|
18
|
-
const max = summary.maxAgeObserved ?? "?";
|
|
19
|
-
return `${min}-${max}`;
|
|
20
|
-
}
|
|
21
|
-
function buildAgeQualificationConstraints(eligibility, policy, turnPlan) {
|
|
22
|
-
const agePolicy = policy?.qualificationPolicy?.age;
|
|
23
|
-
if (!eligibility || !agePolicy || !agePolicy.enabled)
|
|
24
|
-
return [];
|
|
25
|
-
if (eligibility.status === "unknown" && !turnPlan.riskFlags.includes("age_sensitive"))
|
|
26
|
-
return [];
|
|
27
|
-
const lines = ["[QualificationPolicy:Age]"];
|
|
28
|
-
const rangeText = formatAgeRange(eligibility.summary);
|
|
29
|
-
lines.push(`- gateStatus: ${eligibility.status}`);
|
|
30
|
-
lines.push(`- expressionStrategy: ${eligibility.appliedStrategy.strategy}`);
|
|
31
|
-
lines.push(`- redirect: ${agePolicy.allowRedirect ? `allowed priority=${agePolicy.redirectPriority}` : "not allowed"}`);
|
|
32
|
-
lines.push(`- revealRange: ${agePolicy.revealRange ? "allowed" : "not allowed"}`);
|
|
33
|
-
if (agePolicy.revealRange && rangeText)
|
|
34
|
-
lines.push(`- rangeObserved: ${rangeText}`);
|
|
35
|
-
if (eligibility.status === "pass") {
|
|
36
|
-
lines.push(`- writingConstraint: ${eligibility.appliedStrategy.strategy};匹配通过后推进下一步,避免强调年龄筛选`);
|
|
37
|
-
}
|
|
38
|
-
else if (eligibility.status === "fail") {
|
|
39
|
-
lines.push(`- writingConstraint: ${eligibility.appliedStrategy.strategy};礼貌说明不匹配,避免承诺或争辩`);
|
|
40
|
-
if (agePolicy.allowRedirect)
|
|
41
|
-
lines.push("- writingConstraint: 可提示其他岗位或门店选项");
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
lines.push(`- writingConstraint: 如确需涉及年龄或资格,用合规、轻量的方式核实,不要审查式逐条盘问`);
|
|
45
|
-
}
|
|
46
|
-
return lines;
|
|
47
|
-
}
|
|
48
|
-
function buildDisclosureOutputRules(mode) {
|
|
49
|
-
if (mode === "minimal") {
|
|
50
|
-
return [
|
|
51
|
-
"4. 当前为浅层沟通,优先泛化回答,不主动抛具体数字、时间、地址或筛选条件。",
|
|
52
|
-
"5. 若候选人追问具体事实,承接需求并引导进一步沟通,不编造细节。",
|
|
53
|
-
];
|
|
54
|
-
}
|
|
55
|
-
return [
|
|
56
|
-
"4. 围绕 primaryNeed 回答,上下文中有的事实可以正常引用,不要刻意回避。",
|
|
57
|
-
"5. 不主动展开其他事实轴;若候选人同时问两个点,只在上下文支持时简要带上次要问题。",
|
|
58
|
-
];
|
|
59
|
-
}
|
|
60
|
-
function buildPolicyPrompt(policy, turnPlan, contextNeeds, contextInfo, message, conversationHistory, turnIndex, effectiveDisclosureMode, knownCandidate, industryVoiceId, defaultWechatId, ageEligibility) {
|
|
61
|
-
if (!policy) {
|
|
62
|
-
return {
|
|
63
|
-
system: "你是招聘助手。遵循事实,不夸大承诺,回复简洁自然。",
|
|
64
|
-
prompt: `候选人消息:${message}\n\n上下文:\n${contextInfo}\n\n请直接回复候选人。`,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
const stagePolicy = policy.stageGoals[turnPlan.stage];
|
|
68
|
-
const voice = policy.industryVoices[industryVoiceId || policy.defaultIndustryVoiceId];
|
|
69
|
-
const maxQuestions = policy.outputGuards.maxQuestionsByMode[effectiveDisclosureMode];
|
|
70
|
-
const system = [
|
|
71
|
-
"你是政策驱动的招聘助手。",
|
|
72
|
-
`当前阶段:${turnPlan.stage}`,
|
|
73
|
-
`当前轮次:${turnIndex}`,
|
|
74
|
-
`当前披露模式:${effectiveDisclosureMode}`,
|
|
75
|
-
`主回答轴:${turnPlan.primaryNeed}`,
|
|
76
|
-
`阶段目标:${stagePolicy.primaryGoal}`,
|
|
77
|
-
`阶段成功标准:${stagePolicy.successCriteria.join(";")}`,
|
|
78
|
-
`推进策略:${stagePolicy.ctaStrategy}`,
|
|
79
|
-
stagePolicy.disallowedActions?.length
|
|
80
|
-
? `阶段禁止:${stagePolicy.disallowedActions.join(";")}`
|
|
81
|
-
: "",
|
|
82
|
-
`人格设定:语气=${policy.persona.tone},亲和度=${policy.persona.warmth},长度=${policy.persona.length},称呼=${policy.persona.addressStyle},提问风格=${policy.persona.questionStyle}`,
|
|
83
|
-
`共情策略:${policy.persona.empathyStrategy}`,
|
|
84
|
-
voice
|
|
85
|
-
? `行业指纹:${voice.name};背景=${voice.industryBackground};行业词=${voice.jargon.join("、")};避免=${voice.tabooPhrases.join("、")}`
|
|
86
|
-
: "",
|
|
87
|
-
`红线规则:${policy.hardConstraints.rules.map((r) => r.rule).join(";")}`,
|
|
88
|
-
`FactGate模式:${policy.factGate.mode};缺事实回退=${policy.factGate.fallbackBehavior}`,
|
|
89
|
-
`禁止审查措辞:${policy.outputGuards.blockedAuditPhrases.join("、")}`,
|
|
90
|
-
...(knownCandidate.knownFieldNames.length > 0
|
|
91
|
-
? [`候选人资料已确认:${knownCandidate.knownFieldNames.join("、")}。这些信息不得重复追问;如需引用,自然带过即可,不要像念资料一样复述。`]
|
|
92
|
-
: []),
|
|
93
|
-
...buildAgeQualificationConstraints(ageEligibility, policy, turnPlan),
|
|
94
|
-
defaultWechatId
|
|
95
|
-
? `如涉及换微信,优先引导平台交换,必要时可提供默认微信号:${defaultWechatId}`
|
|
96
|
-
: "如涉及换微信,优先引导平台交换,不编造联系方式。",
|
|
97
|
-
"必须口语化、简洁,不输出解释。",
|
|
98
|
-
]
|
|
99
|
-
.filter(Boolean)
|
|
100
|
-
.join("\n");
|
|
101
|
-
const prompt = [
|
|
102
|
-
`[回合规划]`,
|
|
103
|
-
`stage=${turnPlan.stage}`,
|
|
104
|
-
`subGoals=${turnPlan.subGoals.join("、") || "无"}`,
|
|
105
|
-
`contextNeeds=${contextNeeds.join("、") || "none"}`,
|
|
106
|
-
`primaryNeed=${turnPlan.primaryNeed}`,
|
|
107
|
-
`riskFlags=${turnPlan.riskFlags.join("、") || "无"}`,
|
|
108
|
-
`confidence=${turnPlan.confidence.toFixed(2)}`,
|
|
109
|
-
"",
|
|
110
|
-
`[对话历史]`,
|
|
111
|
-
conversationHistory.slice(-6).join("\n") || "无",
|
|
112
|
-
"",
|
|
113
|
-
`[业务上下文]`,
|
|
114
|
-
contextInfo,
|
|
115
|
-
"",
|
|
116
|
-
...(knownCandidate.factsText
|
|
117
|
-
? [`[候选人已知信息]`, knownCandidate.factsText, ""]
|
|
118
|
-
: []),
|
|
119
|
-
`[候选人消息]`,
|
|
120
|
-
message,
|
|
121
|
-
"",
|
|
122
|
-
`[输出要求]`,
|
|
123
|
-
"1. 直接给候选人的单条回复,不得输出多段解释或元信息。",
|
|
124
|
-
`2. 最多追问 ${maxQuestions} 个关键问题。`,
|
|
125
|
-
"3. 禁止使用“是否满足”“是否符合”“基本入职要求”等审查措辞。",
|
|
126
|
-
...buildDisclosureOutputRules(effectiveDisclosureMode),
|
|
127
|
-
].join("\n");
|
|
128
|
-
return { system, prompt };
|
|
129
|
-
}
|
|
130
|
-
export function hasUnsupportedFactClaims(text, contextInfo, allowedNeeds) {
|
|
131
|
-
const claimFamilies = detectConcreteFactFamilies(text);
|
|
132
|
-
if (claimFamilies.length === 0)
|
|
133
|
-
return false;
|
|
134
|
-
const contextFamilies = new Set(detectContextFactFamilies(contextInfo));
|
|
135
|
-
const allowedFamilies = new Set(allowedNeeds.flatMap((need) => PRIMARY_NEED_FACT_MAP[need]));
|
|
136
|
-
return claimFamilies.some((family) => !allowedFamilies.has(family) || !contextFamilies.has(family));
|
|
137
|
-
}
|
|
138
|
-
function shouldExchangeWechatByStage(stage) {
|
|
139
|
-
return stage === "private_channel" || stage === "interview_scheduling";
|
|
140
|
-
}
|
|
141
|
-
export function resolveTurnIndex(conversationHistory, explicitTurnIndex) {
|
|
142
|
-
if (Number.isInteger(explicitTurnIndex) && explicitTurnIndex !== undefined && explicitTurnIndex >= 1) {
|
|
143
|
-
return explicitTurnIndex;
|
|
144
|
-
}
|
|
145
|
-
return conversationHistory.length === 0 ? 1 : 2;
|
|
146
|
-
}
|
|
147
|
-
export function resolveEffectiveDisclosureMode(turnIndex, stage) {
|
|
148
|
-
if (turnIndex === 1 || stage === "trust_building" || stage === "private_channel") {
|
|
149
|
-
return "minimal";
|
|
150
|
-
}
|
|
151
|
-
return "focused";
|
|
152
|
-
}
|
|
153
|
-
function buildReplyGateFixInstructions(violations, maxQuestions) {
|
|
154
|
-
const instructions = ["- 只修正命中的违规点,没有命中的部分不要过度改写。"];
|
|
155
|
-
if (violations.includes("too_many_questions")) {
|
|
156
|
-
instructions.push(`- 删除多余追问,只保留最关键的 ${maxQuestions} 个问题。`);
|
|
157
|
-
}
|
|
158
|
-
if (violations.includes("audit_tone")) {
|
|
159
|
-
instructions.push("- 保留原意,但把审查式措辞改成自然口语,不要像筛选候选人。");
|
|
160
|
-
}
|
|
161
|
-
if (violations.includes("premature_numeric_disclosure")) {
|
|
162
|
-
instructions.push("- 把具体数字、时间和地址细节改成泛化表达,例如“细节我帮你确认”或“以门店安排为准”。");
|
|
163
|
-
}
|
|
164
|
-
if (violations.includes("off_axis_fact_disclosure")) {
|
|
165
|
-
instructions.push("- 删除不属于主回答轴的具体事实;如果要提到次要问题,只能做不带细节的承接。");
|
|
166
|
-
}
|
|
167
|
-
if (violations.includes("reply_overpacked")) {
|
|
168
|
-
instructions.push("- 压缩成最多两句,不要列表、不要枚举、不要一口气展开太多信息。");
|
|
169
|
-
}
|
|
170
|
-
return instructions;
|
|
171
|
-
}
|
|
172
|
-
async function rewriteForFactGate(text, model, contextInfo) {
|
|
173
|
-
const rewritePrompt = [
|
|
174
|
-
"请重写下面这条招聘回复。",
|
|
175
|
-
"要求:",
|
|
176
|
-
"- 不新增任何具体数字、地址、福利承诺。",
|
|
177
|
-
"- 仅保留泛化表达,强调可进一步沟通确认细节。",
|
|
178
|
-
"- 口语化、单行、简洁。",
|
|
179
|
-
"",
|
|
180
|
-
"[原回复]",
|
|
181
|
-
text,
|
|
182
|
-
"",
|
|
183
|
-
"[可用上下文]",
|
|
184
|
-
contextInfo,
|
|
185
|
-
].join("\n");
|
|
186
|
-
const rewritten = await safeGenerateText({
|
|
187
|
-
model,
|
|
188
|
-
prompt: rewritePrompt,
|
|
189
|
-
context: "SmartReplyFactGateRewrite",
|
|
190
|
-
timeoutMs: 20_000,
|
|
191
|
-
maxOutputTokens: 500,
|
|
192
|
-
});
|
|
193
|
-
if (!rewritten.success)
|
|
194
|
-
return { text };
|
|
195
|
-
return { text: rewritten.text, usage: rewritten.usage, latencyMs: rewritten.latencyMs };
|
|
196
|
-
}
|
|
197
|
-
async function rewriteForReplyGate(text, model, contextInfo, options) {
|
|
198
|
-
const { turnIndex, effectiveDisclosureMode, primaryNeed, allowedNeeds, violations, policy } = options;
|
|
199
|
-
const maxQuestions = policy?.outputGuards.maxQuestionsByMode[effectiveDisclosureMode] ?? 1;
|
|
200
|
-
const blockedPhrases = policy?.outputGuards.blockedAuditPhrases.join("、") ?? "";
|
|
201
|
-
const fixInstructions = buildReplyGateFixInstructions(violations, maxQuestions);
|
|
202
|
-
const secondaryNeeds = (allowedNeeds ?? []).filter((need) => need !== primaryNeed && need !== "none");
|
|
203
|
-
const rewritePrompt = [
|
|
204
|
-
"请重写下面这条招聘回复。",
|
|
205
|
-
"要求:",
|
|
206
|
-
`- 当前轮次=${turnIndex},披露模式=${effectiveDisclosureMode},主回答轴=${primaryNeed}。`,
|
|
207
|
-
secondaryNeeds.length > 0 ? `- 允许顺带覆盖的次要轴:${secondaryNeeds.join("、")}。` : "",
|
|
208
|
-
`- 当前违规点:${violations.join("、")}。`,
|
|
209
|
-
...fixInstructions,
|
|
210
|
-
"- 只保留单条口语化回复,不输出解释。",
|
|
211
|
-
`- 问题数最多 ${maxQuestions} 个。`,
|
|
212
|
-
"- 围绕主回答轴回答,不主动展开其他事实轴。",
|
|
213
|
-
"- 首轮时不要主动报具体数字、时间、地址或筛选条件。",
|
|
214
|
-
blockedPhrases ? `- 禁止使用这些措辞:${blockedPhrases}。` : "",
|
|
215
|
-
"",
|
|
216
|
-
"[原回复]",
|
|
217
|
-
text,
|
|
218
|
-
"",
|
|
219
|
-
"[可用上下文]",
|
|
220
|
-
contextInfo,
|
|
221
|
-
]
|
|
222
|
-
.filter(Boolean)
|
|
223
|
-
.join("\n");
|
|
224
|
-
const rewritten = await safeGenerateText({
|
|
225
|
-
model,
|
|
226
|
-
prompt: rewritePrompt,
|
|
227
|
-
context: "SmartReplyReplyGateRewrite",
|
|
228
|
-
timeoutMs: 20_000,
|
|
229
|
-
maxOutputTokens: 500,
|
|
230
|
-
});
|
|
231
|
-
if (!rewritten.success)
|
|
232
|
-
return { text };
|
|
233
|
-
return { text: rewritten.text, usage: rewritten.usage, latencyMs: rewritten.latencyMs };
|
|
234
|
-
}
|
|
235
|
-
export async function generateSmartReply(options) {
|
|
236
|
-
const progress = createPipelineProgress();
|
|
237
|
-
setSuppressVerboseLogs(true);
|
|
238
|
-
try {
|
|
239
|
-
return await generateSmartReplyInner(options, progress);
|
|
240
|
-
}
|
|
241
|
-
catch (error) {
|
|
242
|
-
progress.fail("回复生成失败");
|
|
243
|
-
throw error;
|
|
244
|
-
}
|
|
245
|
-
finally {
|
|
246
|
-
setSuppressVerboseLogs(false);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
async function generateSmartReplyInner(options, progress) {
|
|
250
|
-
const { modelConfig, preferredBrand, toolBrand, brandPriorityStrategy, conversationHistory = [], candidateMessage, configData, replyPolicy, candidateInfo, defaultWechatId, industryVoiceId, channelType, turnIndex, } = options;
|
|
251
|
-
const providerConfigs = modelConfig?.providerConfigs || DEFAULT_PROVIDER_CONFIGS;
|
|
252
|
-
const primaryCity = resolvePrimaryCity(configData);
|
|
253
|
-
const brandData = {
|
|
254
|
-
...(primaryCity ? { city: primaryCity } : {}),
|
|
255
|
-
defaultBrand: resolveDefaultBrandName(configData),
|
|
256
|
-
availableBrands: getAvailableBrandNames(configData),
|
|
257
|
-
storeCount: getAllStores(configData).length,
|
|
258
|
-
};
|
|
259
|
-
const knownCandidate = buildKnownCandidateContext(candidateInfo);
|
|
260
|
-
progress.update("分析对话意图...");
|
|
261
|
-
const turnPlan = await planTurn(candidateMessage, {
|
|
262
|
-
modelConfig: modelConfig || {},
|
|
263
|
-
conversationHistory,
|
|
264
|
-
brandData,
|
|
265
|
-
providerConfigs,
|
|
266
|
-
...(channelType !== undefined ? { channelType } : {}),
|
|
267
|
-
...(replyPolicy !== undefined ? { replyPolicy } : {}),
|
|
268
|
-
...(knownCandidate.knownFieldNames.length > 0
|
|
269
|
-
? { knownCandidateFields: knownCandidate.knownFieldNames }
|
|
270
|
-
: {}),
|
|
271
|
-
});
|
|
272
|
-
const resolvedTurnIndex = resolveTurnIndex(conversationHistory, turnIndex);
|
|
273
|
-
const effectiveDisclosureMode = resolveEffectiveDisclosureMode(resolvedTurnIndex, turnPlan.stage);
|
|
274
|
-
const contextNeeds = effectiveDisclosureMode === "focused"
|
|
275
|
-
? selectContextNeeds(turnPlan.primaryNeed, turnPlan.needs, candidateMessage, 2)
|
|
276
|
-
: [turnPlan.primaryNeed];
|
|
277
|
-
// toolBrand 优先;fallback 到 LLM 从消息中提取的 mentionedBrand
|
|
278
|
-
const effectiveToolBrand = toolBrand || turnPlan.extractedInfo.mentionedBrand || undefined;
|
|
279
|
-
progress.update("构建业务上下文...");
|
|
280
|
-
const { contextInfo, debugInfo, resolvedBrand } = await buildContextInfoByNeeds(configData, turnPlan, preferredBrand, effectiveToolBrand, brandPriorityStrategy, candidateInfo, replyPolicy, industryVoiceId, resolvedTurnIndex, effectiveDisclosureMode, contextNeeds);
|
|
281
|
-
progress.update("校验候选人资格...");
|
|
282
|
-
const candidateAge = resolveCandidateAge(turnPlan, candidateInfo);
|
|
283
|
-
const regionName = resolveRegionName(turnPlan, candidateInfo);
|
|
284
|
-
const ageEligibilityCity = turnPlan.extractedInfo.city ?? resolvePrimaryCity(configData, resolvedBrand);
|
|
285
|
-
const ageEligibility = await evaluateAgeEligibility({
|
|
286
|
-
...(candidateAge !== undefined ? { age: candidateAge } : {}),
|
|
287
|
-
brandAlias: resolvedBrand,
|
|
288
|
-
...(typeof ageEligibilityCity === "string" ? { cityName: ageEligibilityCity } : {}),
|
|
289
|
-
...(regionName !== undefined ? { regionName } : {}),
|
|
290
|
-
...(replyPolicy?.qualificationPolicy?.age !== undefined
|
|
291
|
-
? { strategy: replyPolicy.qualificationPolicy.age }
|
|
292
|
-
: {}),
|
|
293
|
-
});
|
|
294
|
-
const registry = getDynamicRegistry(providerConfigs);
|
|
295
|
-
const replyModel = (modelConfig?.replyModel || DEFAULT_MODEL_CONFIG.replyModel);
|
|
296
|
-
const model = registry.languageModel(replyModel);
|
|
297
|
-
const prompts = buildPolicyPrompt(replyPolicy, turnPlan, contextNeeds, contextInfo, candidateMessage, conversationHistory, resolvedTurnIndex, effectiveDisclosureMode, knownCandidate, industryVoiceId, defaultWechatId, ageEligibility);
|
|
298
|
-
progress.update("生成回复...");
|
|
299
|
-
const replyResult = await safeGenerateText({
|
|
300
|
-
model,
|
|
301
|
-
system: prompts.system,
|
|
302
|
-
prompt: prompts.prompt,
|
|
303
|
-
context: "SmartReply",
|
|
304
|
-
timeoutMs: 30_000,
|
|
305
|
-
maxOutputTokens: 2000,
|
|
306
|
-
});
|
|
307
|
-
if (!replyResult.success) {
|
|
308
|
-
progress.fail("回复生成失败");
|
|
309
|
-
logError("SmartReply 生成失败", replyResult.error);
|
|
310
|
-
return {
|
|
311
|
-
turnPlan,
|
|
312
|
-
suggestedReply: "",
|
|
313
|
-
confidence: 0,
|
|
314
|
-
shouldExchangeWechat: shouldExchangeWechatByStage(turnPlan.stage),
|
|
315
|
-
factGateRewritten: false,
|
|
316
|
-
replyGateRewritten: false,
|
|
317
|
-
gateViolations: [],
|
|
318
|
-
contextInfo,
|
|
319
|
-
debugInfo: {
|
|
320
|
-
...debugInfo,
|
|
321
|
-
resolvedBrand,
|
|
322
|
-
turnIndex: resolvedTurnIndex,
|
|
323
|
-
effectiveDisclosureMode,
|
|
324
|
-
primaryNeed: turnPlan.primaryNeed,
|
|
325
|
-
replyGateRewritten: false,
|
|
326
|
-
gateViolations: [],
|
|
327
|
-
gateStatus: ageEligibility.status,
|
|
328
|
-
appliedStrategy: ageEligibility.appliedStrategy,
|
|
329
|
-
ageRangeSummary: ageEligibility.summary,
|
|
330
|
-
},
|
|
331
|
-
usage: undefined,
|
|
332
|
-
error: replyResult.error,
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
let finalText = replyResult.text;
|
|
336
|
-
let finalUsage = replyResult.usage;
|
|
337
|
-
let finalLatencyMs = replyResult.latencyMs;
|
|
338
|
-
let factGateRewritten = false;
|
|
339
|
-
let replyGateRewritten = false;
|
|
340
|
-
let gateViolations = [];
|
|
341
|
-
progress.update("检查回复质量...");
|
|
342
|
-
if (replyPolicy?.factGate.mode === "strict") {
|
|
343
|
-
const violation = hasUnsupportedFactClaims(finalText, contextInfo, contextNeeds);
|
|
344
|
-
if (violation) {
|
|
345
|
-
factGateRewritten = true;
|
|
346
|
-
const rewritten = await rewriteForFactGate(finalText, model, contextInfo);
|
|
347
|
-
finalText = rewritten.text;
|
|
348
|
-
if (rewritten.usage)
|
|
349
|
-
finalUsage = rewritten.usage;
|
|
350
|
-
if (rewritten.latencyMs !== undefined) {
|
|
351
|
-
finalLatencyMs = (finalLatencyMs ?? 0) + rewritten.latencyMs;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
const gateValidation = validateReply({
|
|
356
|
-
text: finalText,
|
|
357
|
-
turnIndex: resolvedTurnIndex,
|
|
358
|
-
mode: effectiveDisclosureMode,
|
|
359
|
-
primaryNeed: turnPlan.primaryNeed,
|
|
360
|
-
allowedNeeds: contextNeeds,
|
|
361
|
-
policy: replyPolicy,
|
|
362
|
-
});
|
|
363
|
-
gateViolations = gateValidation.violations;
|
|
364
|
-
if (gateViolations.length > 0) {
|
|
365
|
-
progress.update("优化回复...");
|
|
366
|
-
replyGateRewritten = true;
|
|
367
|
-
const rewritten = await rewriteForReplyGate(finalText, model, contextInfo, {
|
|
368
|
-
turnIndex: resolvedTurnIndex,
|
|
369
|
-
effectiveDisclosureMode,
|
|
370
|
-
primaryNeed: turnPlan.primaryNeed,
|
|
371
|
-
allowedNeeds: contextNeeds,
|
|
372
|
-
violations: gateViolations,
|
|
373
|
-
policy: replyPolicy,
|
|
374
|
-
});
|
|
375
|
-
finalText = rewritten.text;
|
|
376
|
-
if (rewritten.usage)
|
|
377
|
-
finalUsage = rewritten.usage;
|
|
378
|
-
if (rewritten.latencyMs !== undefined) {
|
|
379
|
-
finalLatencyMs = (finalLatencyMs ?? 0) + rewritten.latencyMs;
|
|
380
|
-
}
|
|
381
|
-
gateViolations = validateReply({
|
|
382
|
-
text: finalText,
|
|
383
|
-
turnIndex: resolvedTurnIndex,
|
|
384
|
-
mode: effectiveDisclosureMode,
|
|
385
|
-
primaryNeed: turnPlan.primaryNeed,
|
|
386
|
-
allowedNeeds: contextNeeds,
|
|
387
|
-
policy: replyPolicy,
|
|
388
|
-
}).violations;
|
|
389
|
-
}
|
|
390
|
-
const totalLatency = finalLatencyMs ?? 0;
|
|
391
|
-
const totalTokens = finalUsage?.totalTokens ?? 0;
|
|
392
|
-
progress.succeed(`回复已生成 | ${turnPlan.stage} | ${totalLatency}ms | ${totalTokens} tokens${replyGateRewritten ? " | 已优化" : ""}`);
|
|
393
|
-
return {
|
|
394
|
-
turnPlan,
|
|
395
|
-
suggestedReply: finalText,
|
|
396
|
-
confidence: Math.max(0, Math.min(1, turnPlan.confidence)),
|
|
397
|
-
shouldExchangeWechat: shouldExchangeWechatByStage(turnPlan.stage),
|
|
398
|
-
factGateRewritten,
|
|
399
|
-
replyGateRewritten,
|
|
400
|
-
gateViolations,
|
|
401
|
-
contextInfo: `${contextInfo}\n当前品牌:${resolvedBrand}`,
|
|
402
|
-
debugInfo: {
|
|
403
|
-
...debugInfo,
|
|
404
|
-
resolvedBrand,
|
|
405
|
-
turnIndex: resolvedTurnIndex,
|
|
406
|
-
effectiveDisclosureMode,
|
|
407
|
-
primaryNeed: turnPlan.primaryNeed,
|
|
408
|
-
replyGateRewritten,
|
|
409
|
-
gateViolations,
|
|
410
|
-
gateStatus: ageEligibility.status,
|
|
411
|
-
appliedStrategy: ageEligibility.appliedStrategy,
|
|
412
|
-
ageRangeSummary: ageEligibility.summary,
|
|
413
|
-
},
|
|
414
|
-
usage: finalUsage,
|
|
415
|
-
latencyMs: finalLatencyMs,
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
//# sourceMappingURL=smart-reply.js.map
|
|
1
|
+
import{getDynamicRegistry as e,DEFAULT_MODEL_CONFIG as t,DEFAULT_PROVIDER_CONFIGS as a}from"../ai/model-registry.js";import{getAllStores as n,getAvailableBrandNames as r,resolveDefaultBrandName as o,resolvePrimaryCity as i}from"../services/brand-config-selectors.js";import{PRIMARY_NEED_FACT_MAP as s}from"../types/reply-policy.js";import{planTurn as l,selectContextNeeds as d}from"./classification.js";import{buildContextInfoByNeeds as u}from"./context-builder.js";import{detectConcreteFactFamilies as c,detectContextFactFamilies as p,validateReply as m}from"./reply-gate.js";import{safeGenerateText as y}from"../ai/structured-output.js";import{logError as g}from"../errors/index.js";import{setSuppressVerboseLogs as f}from"../log-control.js";import{createPipelineProgress as $}from"./pipeline-progress.js";import{evaluateAgeEligibility as x}from"./age-eligibility.js";import{resolveCandidateAge as h,resolveRegionName as v}from"./candidate-utils.js";import{buildKnownCandidateContext as w}from"./candidate-context.js";function j(e){if(null===e.minAgeObserved&&null===e.maxAgeObserved)return null;return`${e.minAgeObserved??"?"}-${e.maxAgeObserved??"?"}`}function N(e,t,a){const n=t?.qualificationPolicy?.age;if(!e||!n||!n.enabled)return[];if("unknown"===e.status&&!a.riskFlags.includes("age_sensitive"))return[];const r=["[QualificationPolicy:Age]"],o=j(e.summary);return r.push(`- gateStatus: ${e.status}`),r.push(`- expressionStrategy: ${e.appliedStrategy.strategy}`),r.push("- redirect: "+(n.allowRedirect?`allowed priority=${n.redirectPriority}`:"not allowed")),r.push("- revealRange: "+(n.revealRange?"allowed":"not allowed")),n.revealRange&&o&&r.push(`- rangeObserved: ${o}`),"pass"===e.status?r.push(`- writingConstraint: ${e.appliedStrategy.strategy};匹配通过后推进下一步,避免强调年龄筛选`):"fail"===e.status?(r.push(`- writingConstraint: ${e.appliedStrategy.strategy};礼貌说明不匹配,避免承诺或争辩`),n.allowRedirect&&r.push("- writingConstraint: 可提示其他岗位或门店选项")):r.push("- writingConstraint: 如确需涉及年龄或资格,用合规、轻量的方式核实,不要审查式逐条盘问"),r}function M(e){return"minimal"===e?["4. 当前为浅层沟通,优先泛化回答,不主动抛具体数字、时间、地址或筛选条件。","5. 若候选人追问具体事实,承接需求并引导进一步沟通,不编造细节。"]:["4. 围绕 primaryNeed 回答,上下文中有的事实可以正常引用,不要刻意回避。","5. 不主动展开其他事实轴;若候选人同时问两个点,只在上下文支持时简要带上次要问题。"]}function S(e,t,a,n,r,o,i,s,l,d,u,c){if(!e)return{system:"你是招聘助手。遵循事实,不夸大承诺,回复简洁自然。",prompt:`候选人消息:${r}\n\n上下文:\n${n}\n\n请直接回复候选人。`};const p=e.stageGoals[t.stage],m=e.industryVoices[d||e.defaultIndustryVoiceId],y=e.outputGuards.maxQuestionsByMode[s];return{system:["你是政策驱动的招聘助手。",`当前阶段:${t.stage}`,`当前轮次:${i}`,`当前披露模式:${s}`,`主回答轴:${t.primaryNeed}`,`阶段目标:${p.primaryGoal}`,`阶段成功标准:${p.successCriteria.join(";")}`,`推进策略:${p.ctaStrategy}`,p.disallowedActions?.length?`阶段禁止:${p.disallowedActions.join(";")}`:"",`人格设定:语气=${e.persona.tone},亲和度=${e.persona.warmth},长度=${e.persona.length},称呼=${e.persona.addressStyle},提问风格=${e.persona.questionStyle}`,`共情策略:${e.persona.empathyStrategy}`,m?`行业指纹:${m.name};背景=${m.industryBackground};行业词=${m.jargon.join("、")};避免=${m.tabooPhrases.join("、")}`:"",`红线规则:${e.hardConstraints.rules.map(e=>e.rule).join(";")}`,`FactGate模式:${e.factGate.mode};缺事实回退=${e.factGate.fallbackBehavior}`,`禁止审查措辞:${e.outputGuards.blockedAuditPhrases.join("、")}`,...l.knownFieldNames.length>0?[`候选人资料已确认:${l.knownFieldNames.join("、")}。这些信息不得重复追问;如需引用,自然带过即可,不要像念资料一样复述。`]:[],...N(c,e,t),u?`如涉及换微信,优先引导平台交换,必要时可提供默认微信号:${u}`:"如涉及换微信,优先引导平台交换,不编造联系方式。","必须口语化、简洁,不输出解释。"].filter(Boolean).join("\n"),prompt:["[回合规划]",`stage=${t.stage}`,`subGoals=${t.subGoals.join("、")||"无"}`,`contextNeeds=${a.join("、")||"none"}`,`primaryNeed=${t.primaryNeed}`,`riskFlags=${t.riskFlags.join("、")||"无"}`,`confidence=${t.confidence.toFixed(2)}`,"","[对话历史]",o.slice(-6).join("\n")||"无","","[业务上下文]",n,"",...l.factsText?["[候选人已知信息]",l.factsText,""]:[],"[候选人消息]",r,"","[输出要求]","1. 直接给候选人的单条回复,不得输出多段解释或元信息。",`2. 最多追问 ${y} 个关键问题。`,"3. 禁止使用“是否满足”“是否符合”“基本入职要求”等审查措辞。",...M(s)].join("\n")}}export function hasUnsupportedFactClaims(e,t,a){const n=c(e);if(0===n.length)return!1;const r=new Set(p(t)),o=new Set(a.flatMap(e=>s[e]));return n.some(e=>!o.has(e)||!r.has(e))}function b(e){return"private_channel"===e||"interview_scheduling"===e}export function resolveTurnIndex(e,t){return Number.isInteger(t)&&void 0!==t&&t>=1?t:0===e.length?1:2}export function resolveEffectiveDisclosureMode(e,t){return 1===e||"trust_building"===t||"private_channel"===t?"minimal":"focused"}function I(e,t){const a=["- 只修正命中的违规点,没有命中的部分不要过度改写。"];return e.includes("too_many_questions")&&a.push(`- 删除多余追问,只保留最关键的 ${t} 个问题。`),e.includes("audit_tone")&&a.push("- 保留原意,但把审查式措辞改成自然口语,不要像筛选候选人。"),e.includes("premature_numeric_disclosure")&&a.push("- 把具体数字、时间和地址细节改成泛化表达,例如“细节我帮你确认”或“以门店安排为准”。"),e.includes("off_axis_fact_disclosure")&&a.push("- 删除不属于主回答轴的具体事实;如果要提到次要问题,只能做不带细节的承接。"),e.includes("reply_overpacked")&&a.push("- 压缩成最多两句,不要列表、不要枚举、不要一口气展开太多信息。"),a}async function R(e,t,a){const n=["请重写下面这条招聘回复。","要求:","- 不新增任何具体数字、地址、福利承诺。","- 仅保留泛化表达,强调可进一步沟通确认细节。","- 口语化、单行、简洁。","","[原回复]",e,"","[可用上下文]",a].join("\n"),r=await y({model:t,prompt:n,context:"SmartReplyFactGateRewrite",timeoutMs:2e4,maxOutputTokens:500});return r.success?{text:r.text,usage:r.usage,latencyMs:r.latencyMs}:{text:e}}async function G(e,t,a,n){const{turnIndex:r,effectiveDisclosureMode:o,primaryNeed:i,allowedNeeds:s,violations:l,policy:d}=n,u=d?.outputGuards.maxQuestionsByMode[o]??1,c=d?.outputGuards.blockedAuditPhrases.join("、")??"",p=I(l,u),m=(s??[]).filter(e=>e!==i&&"none"!==e),g=["请重写下面这条招聘回复。","要求:",`- 当前轮次=${r},披露模式=${o},主回答轴=${i}。`,m.length>0?`- 允许顺带覆盖的次要轴:${m.join("、")}。`:"",`- 当前违规点:${l.join("、")}。`,...p,"- 只保留单条口语化回复,不输出解释。",`- 问题数最多 ${u} 个。`,"- 围绕主回答轴回答,不主动展开其他事实轴。","- 首轮时不要主动报具体数字、时间、地址或筛选条件。",c?`- 禁止使用这些措辞:${c}。`:"","","[原回复]",e,"","[可用上下文]",a].filter(Boolean).join("\n"),f=await y({model:t,prompt:g,context:"SmartReplyReplyGateRewrite",timeoutMs:2e4,maxOutputTokens:500});return f.success?{text:f.text,usage:f.usage,latencyMs:f.latencyMs}:{text:e}}export async function generateSmartReply(e){const t=$();f(!0);try{return await k(e,t)}catch(e){throw t.fail("回复生成失败"),e}finally{f(!1)}}async function k(s,c){const{modelConfig:p,preferredBrand:f,toolBrand:$,brandPriorityStrategy:j,conversationHistory:N=[],candidateMessage:M,configData:I,replyPolicy:k,candidateInfo:B,defaultWechatId:C,industryVoiceId:_,channelType:F,turnIndex:P}=s,A=p?.providerConfigs||a,T=i(I),D={...T?{city:T}:{},defaultBrand:o(I),availableBrands:r(I),storeCount:n(I).length},O=w(B);c.update("分析对话意图...");const V=await l(M,{modelConfig:p||{},conversationHistory:N,brandData:D,providerConfigs:A,...void 0!==F?{channelType:F}:{},...void 0!==k?{replyPolicy:k}:{},...O.knownFieldNames.length>0?{knownCandidateFields:O.knownFieldNames}:{}}),q=resolveTurnIndex(N,P),E=resolveEffectiveDisclosureMode(q,V.stage),Q="focused"===E?d(V.primaryNeed,V.needs,M,2):[V.primaryNeed],W=$||V.extractedInfo.mentionedBrand||void 0;c.update("构建业务上下文...");const{contextInfo:H,debugInfo:U,resolvedBrand:z}=await u(I,V,f,W,j,B,k,_,q,E,Q);c.update("校验候选人资格...");const J=h(V,B),K=v(V,B),L=V.extractedInfo.city??i(I,z),X=await x({...void 0!==J?{age:J}:{},brandAlias:z,..."string"==typeof L?{cityName:L}:{},...void 0!==K?{regionName:K}:{},...void 0!==k?.qualificationPolicy?.age?{strategy:k.qualificationPolicy.age}:{}}),Y=e(A),Z=p?.replyModel||t.replyModel,ee=Y.languageModel(Z),te=S(k,V,Q,H,M,N,q,E,O,_,C,X);c.update("生成回复...");const ae=await y({model:ee,system:te.system,prompt:te.prompt,context:"SmartReply",timeoutMs:3e4,maxOutputTokens:2e3});if(!ae.success)return c.fail("回复生成失败"),g("SmartReply 生成失败",ae.error),{turnPlan:V,suggestedReply:"",confidence:0,shouldExchangeWechat:b(V.stage),factGateRewritten:!1,replyGateRewritten:!1,gateViolations:[],contextInfo:H,debugInfo:{...U,resolvedBrand:z,turnIndex:q,effectiveDisclosureMode:E,primaryNeed:V.primaryNeed,replyGateRewritten:!1,gateViolations:[],gateStatus:X.status,appliedStrategy:X.appliedStrategy,ageRangeSummary:X.summary},usage:void 0,error:ae.error};let ne=ae.text,re=ae.usage,oe=ae.latencyMs,ie=!1,se=!1,le=[];if(c.update("检查回复质量..."),"strict"===k?.factGate.mode){if(hasUnsupportedFactClaims(ne,H,Q)){ie=!0;const e=await R(ne,ee,H);ne=e.text,e.usage&&(re=e.usage),void 0!==e.latencyMs&&(oe=(oe??0)+e.latencyMs)}}if(le=m({text:ne,turnIndex:q,mode:E,primaryNeed:V.primaryNeed,allowedNeeds:Q,policy:k}).violations,le.length>0){c.update("优化回复..."),se=!0;const e=await G(ne,ee,H,{turnIndex:q,effectiveDisclosureMode:E,primaryNeed:V.primaryNeed,allowedNeeds:Q,violations:le,policy:k});ne=e.text,e.usage&&(re=e.usage),void 0!==e.latencyMs&&(oe=(oe??0)+e.latencyMs),le=m({text:ne,turnIndex:q,mode:E,primaryNeed:V.primaryNeed,allowedNeeds:Q,policy:k}).violations}const de=oe??0,ue=re?.totalTokens??0;return c.succeed(`回复已生成 | ${V.stage} | ${de}ms | ${ue} tokens${se?" | 已优化":""}`),{turnPlan:V,suggestedReply:ne,confidence:Math.max(0,Math.min(1,V.confidence)),shouldExchangeWechat:b(V.stage),factGateRewritten:ie,replyGateRewritten:se,gateViolations:le,contextInfo:`${H}\n当前品牌:${z}`,debugInfo:{...U,resolvedBrand:z,turnIndex:q,effectiveDisclosureMode:E,primaryNeed:V.primaryNeed,replyGateRewritten:se,gateViolations:le,gateStatus:X.status,appliedStrategy:X.appliedStrategy,ageRangeSummary:X.summary},usage:re,latencyMs:oe}}
|
package/dist/pipeline.d.ts
CHANGED
|
@@ -18,4 +18,3 @@ export type { SafeGenerateTextUsage } from "./ai/structured-output.ts";
|
|
|
18
18
|
export type { AgeEligibilityStatus, AgeEligibilityAppliedStrategy, AgeEligibilitySummary, } from "./pipeline/age-eligibility.ts";
|
|
19
19
|
export type { StoreWithDistance } from "./types/geocoding.ts";
|
|
20
20
|
export type { AppError } from "./errors/index.ts";
|
|
21
|
-
//# sourceMappingURL=pipeline.d.ts.map
|
package/dist/pipeline.js
CHANGED
|
@@ -1,12 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Public pipeline API — 供外部项目直接 import 使用。
|
|
3
|
-
*
|
|
4
|
-
* @example
|
|
5
|
-
* ```ts
|
|
6
|
-
* import { generateSmartReply } from "@roll-agent/smart-reply-agent/pipeline";
|
|
7
|
-
* import type { SmartReplyAgentOptions, SmartReplyAgentResult } from "@roll-agent/smart-reply-agent/pipeline";
|
|
8
|
-
* ```
|
|
9
|
-
*/
|
|
10
|
-
// ---- core pipeline ----
|
|
11
|
-
export { generateSmartReply } from "./pipeline/smart-reply.js";
|
|
12
|
-
//# sourceMappingURL=pipeline.js.map
|
|
1
|
+
export{generateSmartReply}from"./pipeline/smart-reply.js";
|
|
@@ -2,4 +2,3 @@ export type BrandAliasMap = Map<string, string>;
|
|
|
2
2
|
export type BrandDictionary = Record<string, string[]>;
|
|
3
3
|
export declare function getSharedBrandDictionary(dulidayToken?: string): Promise<BrandDictionary>;
|
|
4
4
|
export declare function getSharedBrandAliasMap(dulidayToken?: string): Promise<BrandAliasMap>;
|
|
5
|
-
//# sourceMappingURL=brand-alias.d.ts.map
|