@chatman-media/kb 1.4.0 → 1.6.0

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.
@@ -63,6 +63,16 @@ export interface AnswerInput {
63
63
  goal: string;
64
64
  guidance?: string;
65
65
  };
66
+ /**
67
+ * Dynamic per-request context (R4 / multi-request): the guest's current
68
+ * request_type + how many open requests. Injected as a separate prompt block.
69
+ */
70
+ requestContext?: string;
71
+ /**
72
+ * The guest's current lead is on an `awaiting_operator` stage (R5): the bot
73
+ * holds and defers pricing/decisions to a human operator.
74
+ */
75
+ awaitingOperator?: boolean;
66
76
  /**
67
77
  * Called after every `answerWithRag` or `answerWithRagStream` call with the
68
78
  * final telemetry. Useful for logging, metrics, or A/B experiment recording
@@ -1 +1 @@
1
- {"version":3,"file":"answer-types.d.ts","sourceRoot":"","sources":["../src/answer-types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"answer-types.d.ts","sourceRoot":"","sources":["../src/answer-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AACzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,KAAK,EAAE,qBAAqB,EAAE,WAAW,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAC7F,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAExD,eAAO,MAAM,iBAAiB,mBAAmB,CAAC;AAElD,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,GAAG,WAAW,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,EAAE,EAAE,QAAQ,CAAC;IACb,QAAQ,EAAE,eAAe,CAAC;IAC1B,IAAI,EAAE,UAAU,CAAC;IACjB,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,MAAM,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IACnC;;;;OAIG;IACH,aAAa,CAAC,EAAE,SAAS,qBAAqB,EAAE,CAAC;IACjD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC;IACpC;;;;OAIG;IACH,aAAa,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACpD;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,SAAS,EAAE,eAAe,KAAK,IAAI,CAAC;IACnD;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC;IACrB;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,GAAG,CAAC,EAAE,OAAO,CAAC;IACd;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,YAAY,CAAC,EAAE,CAAC,CAAC,UAAU,CAAC;CAC7B;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,WAAW,GAAG,cAAc,GAAG,YAAY,GAAG,YAAY,GAAG,IAAI,GAAG,WAAW,CAAC;IACtF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,SAAS,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACvE;;;OAGG;IACH,QAAQ,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE,CAAC;IAC7C;;;;OAIG;IACH,SAAS,CAAC,EAAE,KAAK,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAC9B,MAAM,EAAE,OAAO,CAAC;QAChB,KAAK,CAAC,EAAE,OAAO,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;KACf,CAAC,CAAC;CACJ;AAED,MAAM,WAAW,YAAY,CAAC,OAAO,GAAG,OAAO;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,IAAI,EAAE,WAAW,EAAE,CAAC;IACpB,SAAS,EAAE,eAAe,CAAC;IAC3B,qFAAqF;IACrF,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB"}
@@ -1 +1 @@
1
- {"version":3,"file":"answer.d.ts","sourceRoot":"","sources":["../src/answer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,YAAY,EAGjB,KAAK,OAAO,EACb,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AA8BzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAG9C,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,iBAAiB,EACjB,KAAK,OAAO,GACb,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,gBAAgB,EAChB,qBAAqB,EACrB,sBAAsB,EACtB,0BAA0B,EAC1B,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EACL,iBAAiB,EACjB,4BAA4B,EAC5B,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAI5B,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,WAAW,EAAE,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,oEAAoE;IACpE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,YAAY,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,eAAe,CAAC,CAgG/E;AA4OD;;;;;;;;GAQG;AACH,wBAAuB,mBAAmB,CAAC,KAAK,EAAE,WAAW,GAAG,aAAa,CAAC,MAAM,CAAC,CA2HpF;AAED,wBAAsB,aAAa,CAAC,CAAC,SAAS,CAAC,CAAC,UAAU,EACxD,KAAK,EAAE,WAAW,GAAG;IAAE,YAAY,EAAE,CAAC,CAAA;CAAE,GACvC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACrC,wBAAsB,aAAa,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;AAiG/E;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,CAAC,KAAK,EAAE;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,UAAU,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;CACzB,GAAG,OAAO,CAAC,MAAM,CAAC,CA8BlB"}
1
+ {"version":3,"file":"answer.d.ts","sourceRoot":"","sources":["../src/answer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,YAAY,EAGjB,KAAK,OAAO,EACb,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAmCzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAG9C,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,iBAAiB,EACjB,KAAK,OAAO,GACb,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,gBAAgB,EAChB,qBAAqB,EACrB,sBAAsB,EACtB,0BAA0B,EAC1B,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EACL,iBAAiB,EACjB,4BAA4B,EAC5B,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAa5B,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,WAAW,EAAE,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,oEAAoE;IACpE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,YAAY,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,eAAe,CAAC,CAgG/E;AAgPD;;;;;;;;GAQG;AACH,wBAAuB,mBAAmB,CAAC,KAAK,EAAE,WAAW,GAAG,aAAa,CAAC,MAAM,CAAC,CA6HpF;AAED,wBAAsB,aAAa,CAAC,CAAC,SAAS,CAAC,CAAC,UAAU,EACxD,KAAK,EAAE,WAAW,GAAG;IAAE,YAAY,EAAE,CAAC,CAAA;CAAE,GACvC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACrC,wBAAsB,aAAa,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;AAiG/E;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,CAAC,KAAK,EAAE;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,UAAU,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;CACzB,GAAG,OAAO,CAAC,MAAM,CAAC,CA8BlB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=answer.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"answer.test.d.ts","sourceRoot":"","sources":["../src/answer.test.ts"],"names":[],"mappings":""}
package/dist/index.js CHANGED
@@ -45106,6 +45106,8 @@ ${triggerLine}${h.body}`;
45106
45106
  ` + `\u0426\u0415\u041B\u042C \u042D\u0422\u0410\u041F\u0410: ${effGoal}.` + (effGuidance ? `
45107
45107
  \u041A\u0410\u041A: ${effGuidance}` : "") + (stageCfg?.groundingRequired ? `
45108
45108
  GROUNDING: \u043D\u0430 \u044D\u0442\u043E\u043C \u044D\u0442\u0430\u043F\u0435 \u0432\u0441\u0435 \u043A\u043E\u043D\u043A\u0440\u0435\u0442\u043D\u044B\u0435 \u0444\u0430\u043A\u0442\u044B (\u0446\u0438\u0444\u0440\u044B, \u0441\u0443\u043C\u043C\u044B, \u0441\u0440\u043E\u043A\u0438) \u0431\u0435\u0440\u0438 \u0422\u041E\u041B\u042C\u041A\u041E \u0438\u0437 \u0441\u0435\u043A\u0446\u0438\u0438 KB CONTEXT \u043D\u0438\u0436\u0435. \u0415\u0441\u043B\u0438 \u0435\u0451 \u043D\u0435\u0442 \u0438\u043B\u0438 \u043D\u0443\u0436\u043D\u043E\u0433\u043E \u0444\u0430\u043A\u0442\u0430 \u0432 \u043D\u0435\u0439 \u043D\u0435\u0442 \u2014 \u043D\u0435 \u0432\u044B\u0434\u0443\u043C\u044B\u0432\u0430\u0439, \u0441\u043A\u0430\u0436\u0438 \u0447\u0442\u043E \u0443\u0442\u043E\u0447\u043D\u0438\u0448\u044C.` : "") : `\u0422\u0415\u041A\u0423\u0429\u0418\u0419 \u042D\u0422\u0410\u041F: ${stage}. (\u0421\u043F\u0435\u0446\u0438\u0444\u0438\u0447\u0435\u0441\u043A\u0438\u0445 \u043F\u0440\u0430\u0432\u0438\u043B \u0434\u043B\u044F \u044D\u0442\u0430\u043F\u0430 \u043D\u0435\u0442 \u2014 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439 \u043E\u0431\u0449\u0438\u0439 \u0441\u0442\u0438\u043B\u044C.)`;
45109
+ const requestBlock = options.requestContext ? `\u0417\u0410\u041F\u0420\u041E\u0421 \u0413\u041E\u0421\u0422\u042F: ${options.requestContext}` : "";
45110
+ const operatorBlock = options.awaitingOperator ? `\u041E\u0416\u0418\u0414\u0410\u041D\u0418\u0415 \u041E\u041F\u0415\u0420\u0410\u0422\u041E\u0420\u0410: \u043D\u0430 \u044D\u0442\u043E\u0439 \u0441\u0442\u0430\u0434\u0438\u0438 \u0443\u0441\u043B\u043E\u0432\u0438\u044F/\u0446\u0435\u043D\u0443/\u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u0433\u043E\u0442\u043E\u0432\u0438\u0442 \u043A\u043E\u043B\u043B\u0435\u0433\u0430-\u0447\u0435\u043B\u043E\u0432\u0435\u043A. \u041D\u0415 \u0432\u044B\u0434\u0443\u043C\u044B\u0432\u0430\u0439 \u0447\u0438\u0441\u043B\u0430 \u0438 \u0434\u0435\u0442\u0430\u043B\u0438. \u0421\u043A\u0430\u0436\u0438 \u0433\u043E\u0441\u0442\u044E \u043A\u043E\u0440\u043E\u0442\u043A\u043E, \u0447\u0442\u043E \u0443\u0442\u043E\u0447\u043D\u044F\u0435\u0448\u044C \u0438 \u0432\u0435\u0440\u043D\u0451\u0448\u044C\u0441\u044F \u0441 \u043E\u0442\u0432\u0435\u0442\u043E\u043C \u2014 \u0438 \u0436\u0434\u0438, \u043D\u0435 \u0434\u0430\u0432\u0438 \u0438 \u043D\u0435 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u0439 \u0441\u0434\u0435\u043B\u043A\u0443.` : "";
45109
45111
  const minorRule = guardrails.noMinors ? "- \u0415\u0441\u043B\u0438 prospect <18 \u043B\u0435\u0442 \u2014 \u0432\u0435\u0436\u043B\u0438\u0432\u043E \u0437\u0430\u0432\u0435\u0440\u0448\u0438 \u0434\u0438\u0430\u043B\u043E\u0433." : "";
45110
45112
  const topicsRule = guardrails.forbiddenTopics.length ? `- \u0417\u0430\u043F\u0440\u0435\u0449\u0451\u043D\u043D\u044B\u0435 \u0442\u0435\u043C\u044B: ${guardrails.forbiddenTopics.join(", ")}.` : "";
45111
45113
  const brevityRule = persona.role === "human" ? `- \u041F\u0438\u0448\u0438 \u043A\u0430\u043A \u0432 \u0436\u0438\u0432\u043E\u043C \u0447\u0430\u0442\u0435: 2\u20136 \u043A\u043E\u0440\u043E\u0442\u043A\u0438\u0445 \u0444\u0440\u0430\u0437 \u043C\u043E\u0436\u043D\u043E, \u0435\u0441\u043B\u0438 \u043D\u0443\u0436\u043D\u043E \u043F\u0435\u0440\u0435\u0434\u0430\u0442\u044C \u0443\u0441\u043B\u043E\u0432\u0438\u044F. \u0411\u0435\u0437 markdown-\u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043A\u043E\u0432. ` + `\u0421\u043F\u0438\u0441\u043A\u043E\u043C \u0441 \u043D\u043E\u043C\u0435\u0440\u0430\u043C\u0438 \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u0435\u0441\u043B\u0438 \u0447\u0435\u043B\u043E\u0432\u0435\u043A \u0441\u0430\u043C \u043F\u0440\u043E\u0441\u0438\u0442 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0443.` : `- \u041F\u0438\u0448\u0438 \u043A\u043E\u0440\u043E\u0442\u043A\u043E: 1-3 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F. \u0411\u0435\u0437 markdown-\u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043A\u043E\u0432 \u0438 \u043D\u0443\u043C\u0435\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0445 \u0441\u043F\u0438\u0441\u043A\u043E\u0432.`;
@@ -45132,6 +45134,8 @@ ${preFetchedKbContext}` : "";
45132
45134
  support ? "" : directorHooksBlock,
45133
45135
  support ? "" : skillsBlock,
45134
45136
  support || stageBlock,
45137
+ operatorBlock,
45138
+ requestBlock,
45135
45139
  summaryBlock,
45136
45140
  userFactsBlock,
45137
45141
  needsGroundingReminder ? kbGroundingReminder(persona.role) : "",
@@ -59799,6 +59803,17 @@ function classifyTopicAll(question) {
59799
59803
  var KNOWN_TOPICS = TOPIC_PATTERNS.map((p) => p.topic);
59800
59804
 
59801
59805
  // src/answer.ts
59806
+ function toolCallsToGroundingContext(records) {
59807
+ if (!records || records.length === 0)
59808
+ return "";
59809
+ const lines = records.map((record2, index) => {
59810
+ const result = typeof record2.result === "string" ? record2.result : JSON.stringify(record2.result);
59811
+ return `[#tool-${index + 1}] ${record2.name}: ${result ?? ""}`;
59812
+ });
59813
+ return `TOOL RESULTS:
59814
+ ${lines.join(`
59815
+ `)}`;
59816
+ }
59802
59817
  async function retrieveHits(input) {
59803
59818
  const topK = input.topK ?? 5;
59804
59819
  const candidateK = input.reranker ? topK * 3 : topK;
@@ -59908,7 +59923,9 @@ ${kbContextStr}` : vacBlock : kbContextStr;
59908
59923
  ...input.skills && input.skills.length > 0 ? { skills: input.skills } : {},
59909
59924
  ...input.directorHooks && input.directorHooks.length > 0 ? { directorHooks: input.directorHooks } : {},
59910
59925
  ...input.supportPhase ? { supportPhase: input.supportPhase } : {},
59911
- ...input.stageOverride ? { stageOverride: input.stageOverride } : {}
59926
+ ...input.stageOverride ? { stageOverride: input.stageOverride } : {},
59927
+ ...input.requestContext ? { requestContext: input.requestContext } : {},
59928
+ ...input.awaitingOperator ? { awaitingOperator: true } : {}
59912
59929
  });
59913
59930
  temperature = input.style.model.temperature;
59914
59931
  } else {
@@ -59995,10 +60012,14 @@ ${kbContextStr}` : vacBlock : kbContextStr;
59995
60012
  const groundingExempt = input.stage !== undefined && GROUNDING_EXEMPT_STAGES.has(input.stage);
59996
60013
  const runFactCheck = (input.reflect || runVacancyCheck) && text !== NO_CONTEXT_MARKER && text.trim().length > 0 && !(groundingExempt && !runVacancyCheck);
59997
60014
  if (runFactCheck) {
60015
+ const toolContext = toolCallsToGroundingContext(multiCycleToolCalls);
60016
+ const groundingContext = toolContext ? [context, toolContext].filter(Boolean).join(`
60017
+
60018
+ `) : context;
59998
60019
  const verdict = await checkFacts({
59999
60020
  question: input.question,
60000
60021
  answer: text,
60001
- context,
60022
+ context: groundingContext,
60002
60023
  chat: input.chat,
60003
60024
  ...runVacancyCheck ? { vacanciesBlock: vacBlock } : {}
60004
60025
  });
@@ -60096,7 +60117,9 @@ ${kbContextStr}` : vacBlock : kbContextStr;
60096
60117
  ...input.skills && input.skills.length > 0 ? { skills: input.skills } : {},
60097
60118
  ...input.directorHooks && input.directorHooks.length > 0 ? { directorHooks: input.directorHooks } : {},
60098
60119
  ...input.supportPhase ? { supportPhase: input.supportPhase } : {},
60099
- ...input.stageOverride ? { stageOverride: input.stageOverride } : {}
60120
+ ...input.stageOverride ? { stageOverride: input.stageOverride } : {},
60121
+ ...input.requestContext ? { requestContext: input.requestContext } : {},
60122
+ ...input.awaitingOperator ? { awaitingOperator: true } : {}
60100
60123
  });
60101
60124
  temperature = input.style.model.temperature;
60102
60125
  } else {
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ingest.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ingest.test.d.ts","sourceRoot":"","sources":["../src/ingest.test.ts"],"names":[],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../src/prompt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAQ,KAAK,EAAE,MAAM,aAAa,CAAC;AA6C5E;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,WAAW,EAClB,mBAAmB,GAAE,MAAM,GAAG,IAAW,EACzC,OAAO,GAAE,cAAmB,GAC3B,MAAM,CAyIR"}
1
+ {"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../src/prompt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAQ,KAAK,EAAE,MAAM,aAAa,CAAC;AA6C5E;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,WAAW,EAClB,mBAAmB,GAAE,MAAM,GAAG,IAAW,EACzC,OAAO,GAAE,cAAmB,GAC3B,MAAM,CAsJR"}
package/dist/styles.d.ts CHANGED
@@ -191,5 +191,16 @@ export interface ComposeOptions {
191
191
  goal: string;
192
192
  guidance?: string;
193
193
  };
194
+ /**
195
+ * Динамический контекст текущего запроса гостя (multi-request / concierge):
196
+ * какой тип запроса ведётся + сколько открыто. Инжектится блоком «ЗАПРОС
197
+ * ГОСТЯ». null/absent для линейных вертикалей.
198
+ */
199
+ requestContext?: string;
200
+ /**
201
+ * Лид стоит на стадии awaiting_operator (R5): цену/условия даёт человек-оператор.
202
+ * Бот придерживает гостя и не выдумывает детали. Блок «ОЖИДАНИЕ ОПЕРАТОРА».
203
+ */
204
+ awaitingOperator?: boolean;
194
205
  }
195
206
  //# sourceMappingURL=styles.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../src/styles.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,aAAa,+DAAgE,CAAC;AAC3F,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzD,eAAO,MAAM,gBAAgB,2DAA4D,CAAC;AAC1F,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE/D,eAAO,MAAM,UAAU,2FAOb,CAAC;AACX,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnD,eAAO,MAAM,UAAU;;;;;;;;;;iBAGrB,CAAC;AACH,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAE9C,eAAO,MAAM,iBAAiB;;;;;iBAK5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,eAAO,MAAM,aAAa;;;;;;;;iBAKxB,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAEzD,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAsCtB,CAAC;AACH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEhD;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB;;;;;OAKG;IACH,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,MAAM,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IACnC;;;;OAIG;IACH,aAAa,CAAC,EAAE,SAAS,qBAAqB,EAAE,CAAC;IACjD;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC;IACpC;;;;OAIG;IACH,aAAa,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACrD"}
1
+ {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../src/styles.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,aAAa,+DAAgE,CAAC;AAC3F,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzD,eAAO,MAAM,gBAAgB,2DAA4D,CAAC;AAC1F,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE/D,eAAO,MAAM,UAAU,2FAOb,CAAC;AACX,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnD,eAAO,MAAM,UAAU;;;;;;;;;;iBAGrB,CAAC;AACH,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAE9C,eAAO,MAAM,iBAAiB;;;;;iBAK5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,eAAO,MAAM,aAAa;;;;;;;;iBAKxB,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAEzD,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAsCtB,CAAC;AACH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEhD;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB;;;;;OAKG;IACH,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,MAAM,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IACnC;;;;OAIG;IACH,aAAa,CAAC,EAAE,SAAS,qBAAqB,EAAE,CAAC;IACjD;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC;IACpC;;;;OAIG;IACH,aAAa,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACpD;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=system-prompt.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"system-prompt.test.d.ts","sourceRoot":"","sources":["../src/system-prompt.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=vision.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vision.test.d.ts","sourceRoot":"","sources":["../src/vision.test.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatman-media/kb",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "Tenant-scoped Knowledge Base: hybrid retrieval (pgvector + BM25), ingest, answer pipeline, persona/skill composition. LLM I/O живёт в @chatman-media/llm-router.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -63,6 +63,16 @@ export interface AnswerInput {
63
63
  * per-sales-stage config when composing the prompt.
64
64
  */
65
65
  stageOverride?: { goal: string; guidance?: string };
66
+ /**
67
+ * Dynamic per-request context (R4 / multi-request): the guest's current
68
+ * request_type + how many open requests. Injected as a separate prompt block.
69
+ */
70
+ requestContext?: string;
71
+ /**
72
+ * The guest's current lead is on an `awaiting_operator` stage (R5): the bot
73
+ * holds and defers pricing/decisions to a human operator.
74
+ */
75
+ awaitingOperator?: boolean;
66
76
  /**
67
77
  * Called after every `answerWithRag` or `answerWithRagStream` call with the
68
78
  * final telemetry. Useful for logging, metrics, or A/B experiment recording
@@ -0,0 +1,380 @@
1
+ // Unit tests for the RAG answer pipeline (answer.ts): retrieveHits,
2
+ // answerWithRag (+ its answerFromHits branches), answerWithRagStream and
3
+ // generateSoftFallback. The KB store, embedder and chat client are faked so
4
+ // every branch (shortcuts, no-context, structured output, tool loop, fact
5
+ // check drops, retrieval failure, streaming) runs without network or DB.
6
+
7
+ import { describe, expect, it } from "bun:test";
8
+ import type { ChatClient, ChatMessage, EmbeddingClient } from "@chatman-media/llm-router";
9
+ import { z } from "zod";
10
+ import {
11
+ answerWithRag,
12
+ answerWithRagStream,
13
+ generateSoftFallback,
14
+ NO_CONTEXT_MARKER,
15
+ retrieveHits,
16
+ } from "./answer.ts";
17
+ import type { AnswerInput } from "./answer-types.ts";
18
+ import type { KbSearchHit } from "./types.ts";
19
+
20
+ const hit = (id: number, over: Partial<KbSearchHit> = {}): KbSearchHit => ({
21
+ chunk_id: id,
22
+ distance: 0.1,
23
+ text: `chunk ${id}`,
24
+ document_id: 1,
25
+ source: "src",
26
+ title: `title ${id}`,
27
+ ...over,
28
+ });
29
+
30
+ const embedder: EmbeddingClient = {
31
+ embed: async (inputs: string[]) => inputs.map(() => [1, 0, 0]),
32
+ dim: 3,
33
+ };
34
+
35
+ function fakeKb(
36
+ over: Partial<Record<keyof FakeKbCalls, unknown>> = {},
37
+ hits: KbSearchHit[] = [hit(1)],
38
+ ) {
39
+ const calls: FakeKbCalls = { search: 0, hybrid: 0, priority: 0 };
40
+ const store = {
41
+ search: async (_e: number[], _k: number, topic?: string | null) => {
42
+ calls.search++;
43
+ calls.lastTopic = topic ?? null;
44
+ return (over.search as KbSearchHit[] | undefined) ?? hits;
45
+ },
46
+ hybridSearch: async () => {
47
+ calls.hybrid++;
48
+ return (over.hybrid as KbSearchHit[] | undefined) ?? hits;
49
+ },
50
+ prioritySearch: async () => {
51
+ calls.priority++;
52
+ return (over.priority as KbSearchHit[] | undefined) ?? hits;
53
+ },
54
+ } as unknown as AnswerInput["kb"];
55
+ return { store, calls };
56
+ }
57
+ interface FakeKbCalls {
58
+ search: number;
59
+ hybrid: number;
60
+ priority: number;
61
+ lastTopic?: string | null;
62
+ }
63
+
64
+ /** Chat client whose complete() returns a fixed string and records messages. */
65
+ function fakeChat(
66
+ reply: string | ((m: ChatMessage[]) => string),
67
+ extra: Partial<ChatClient> = {},
68
+ ): ChatClient & { lastMessages?: ChatMessage[] } {
69
+ const c: ChatClient & { lastMessages?: ChatMessage[] } = {
70
+ complete: async (messages: ChatMessage[]) => {
71
+ c.lastMessages = messages;
72
+ return typeof reply === "function" ? reply(messages) : reply;
73
+ },
74
+ ...extra,
75
+ };
76
+ return c;
77
+ }
78
+
79
+ const baseInput = (over: Partial<AnswerInput> = {}): AnswerInput => {
80
+ const { store } = fakeKb();
81
+ return {
82
+ question: "Расскажи про вакансию в Китае и условия", // job intent → not a shortcut
83
+ kb: store,
84
+ embedder,
85
+ chat: fakeChat("ответ модели"),
86
+ ...over,
87
+ };
88
+ };
89
+
90
+ describe("retrieveHits", () => {
91
+ it("single-query vector search, без topic", async () => {
92
+ const { store, calls } = fakeKb();
93
+ const r = await retrieveHits(baseInput({ kb: store }));
94
+ expect(r.hits).toHaveLength(1);
95
+ expect(calls.search).toBe(1);
96
+ expect(r.searchQuery).toContain("вакансию");
97
+ });
98
+
99
+ it("hybridSearch path", async () => {
100
+ const { store, calls } = fakeKb();
101
+ await retrieveHits(baseInput({ kb: store, hybridSearch: true }));
102
+ expect(calls.hybrid).toBe(1);
103
+ expect(calls.search).toBe(0);
104
+ });
105
+
106
+ it("booksPriority path", async () => {
107
+ const { store, calls } = fakeKb();
108
+ const r = await retrieveHits(baseInput({ kb: store, booksPriority: true }));
109
+ expect(calls.priority).toBe(1);
110
+ expect(r.usedTopic).toBeNull();
111
+ });
112
+
113
+ it("topicRouting: пустой результат по topic → fallback на global (usedTopic=null)", async () => {
114
+ let first = true;
115
+ const store = {
116
+ search: async () => {
117
+ if (first) {
118
+ first = false;
119
+ return [];
120
+ }
121
+ return [hit(2)];
122
+ },
123
+ hybridSearch: async () => [],
124
+ prioritySearch: async () => [],
125
+ } as unknown as AnswerInput["kb"];
126
+ const r = await retrieveHits(
127
+ baseInput({ kb: store, topicRouting: true, question: "виза china" }),
128
+ );
129
+ // fell back to global → got the hit, usedTopic reset to null
130
+ expect(r.usedTopic).toBeNull();
131
+ });
132
+
133
+ it("maxDistance фильтрует далёкие хиты (non-hybrid)", async () => {
134
+ const { store } = fakeKb({}, [hit(1, { distance: 0.1 }), hit(2, { distance: 0.9 })]);
135
+ const r = await retrieveHits(baseInput({ kb: store, maxDistance: 0.5 }));
136
+ expect(r.hits.map((h) => h.chunk_id)).toEqual([1]);
137
+ });
138
+
139
+ it("multiQuery → expandQueries + RRF merge", async () => {
140
+ const { store, calls } = fakeKb({}, [hit(1), hit(2)]);
141
+ // expandQueries uses chat to generate variants; a chat returning lines works,
142
+ // but multiQuery merges per-query result lists regardless of expansion output.
143
+ const r = await retrieveHits(
144
+ baseInput({ kb: store, multiQuery: true, chat: fakeChat("вариант1\nвариант2") }),
145
+ );
146
+ expect(r.queries.length).toBeGreaterThanOrEqual(1);
147
+ expect(calls.search).toBeGreaterThanOrEqual(1);
148
+ });
149
+
150
+ it("embedder без вектора → throw", async () => {
151
+ const empty: EmbeddingClient = { embed: async () => [], dim: 3 };
152
+ await expect(retrieveHits(baseInput({ embedder: empty }))).rejects.toThrow(/no vector/);
153
+ });
154
+ });
155
+
156
+ describe("answerWithRag — persona shortcuts", () => {
157
+ it("smalltalk «кто ты?» → path=smalltalk, без LLM", async () => {
158
+ const chat = fakeChat(() => {
159
+ throw new Error("should not call LLM");
160
+ });
161
+ const r = await answerWithRag(baseInput({ question: "кто ты?", chat }));
162
+ expect(r.telemetry.path).toBe("smalltalk");
163
+ expect(r.hits).toHaveLength(0);
164
+ });
165
+
166
+ it("bot-presence «ты бот?» → path=smalltalk", async () => {
167
+ const r = await answerWithRag(baseInput({ question: "ты бот?" }));
168
+ expect(r.telemetry.path).toBe("smalltalk");
169
+ });
170
+
171
+ it("personal-fact «где ты живёшь?» с persona.facts → path=persona_fact", async () => {
172
+ const r = await answerWithRag(
173
+ baseInput({
174
+ question: "где ты живёшь?",
175
+ persona: { name: "Аня", role: "human", facts: { city: "Москва" } },
176
+ }),
177
+ );
178
+ expect(r.telemetry.path).toBe("persona_fact");
179
+ expect(r.text).toContain("Москва");
180
+ });
181
+ });
182
+
183
+ describe("answerWithRag — main paths", () => {
184
+ it("happy path: hits + LLM → path=ok, usedChunkIds", async () => {
185
+ const onTel: unknown[] = [];
186
+ const r = await answerWithRag(baseInput({ onTelemetry: (t) => onTel.push(t) }));
187
+ expect(r.telemetry.path).toBe("ok");
188
+ expect(r.text.toLowerCase()).toBe("ответ модели");
189
+ expect(r.usedChunkIds).toEqual([1]);
190
+ expect(onTel).toHaveLength(1);
191
+ });
192
+
193
+ it("нет hits, нет style/vac/tools → NO_CONTEXT_MARKER", async () => {
194
+ const { store } = fakeKb({ search: [] });
195
+ const r = await answerWithRag(baseInput({ kb: store }));
196
+ expect(r.text).toBe(NO_CONTEXT_MARKER);
197
+ expect(r.telemetry.path).toBe("no_context");
198
+ });
199
+
200
+ it("retrieval падает → отвечаем без базы (graceful)", async () => {
201
+ const store = {
202
+ search: async () => {
203
+ throw new Error("vector down");
204
+ },
205
+ hybridSearch: async () => [],
206
+ prioritySearch: async () => [],
207
+ } as unknown as AnswerInput["kb"];
208
+ // no style → with zero hits answerFromHits returns no_context (no throw)
209
+ const r = await answerWithRag(baseInput({ kb: store }));
210
+ expect(r.text).toBe(NO_CONTEXT_MARKER);
211
+ });
212
+
213
+ it("structured output через completeStructured", async () => {
214
+ const schema = z.object({ ok: z.boolean() });
215
+ const chat = fakeChat("unused", {
216
+ completeStructured: async () => '{"ok":true}',
217
+ });
218
+ const r = await answerWithRag({ ...baseInput({ chat }), outputSchema: schema });
219
+ expect(r.output).toEqual({ ok: true });
220
+ expect(r.telemetry.path).toBe("ok");
221
+ });
222
+
223
+ it("structured output fallback (нет completeStructured) → inject + complete temp0", async () => {
224
+ const schema = z.object({ n: z.number() });
225
+ const chat = fakeChat('{"n":7}');
226
+ const r = await answerWithRag({ ...baseInput({ chat }), outputSchema: schema });
227
+ expect(r.output).toEqual({ n: 7 });
228
+ });
229
+
230
+ it("fact-check (reflect): ungrounded → дроп в NO_CONTEXT_MARKER, path=ungrounded", async () => {
231
+ const chat = fakeChat((m) => {
232
+ // checkFacts uses the last system prompt asking for JSON verdict
233
+ const sys = m[0]?.content ?? "";
234
+ return sys.includes("grounded")
235
+ ? '{"grounded":false,"vacancyOk":true,"reason":"выдумал"}'
236
+ : "сырой ответ";
237
+ });
238
+ const r = await answerWithRag(baseInput({ chat, reflect: true, stage: "pitch" }));
239
+ expect(r.telemetry.path).toBe("ungrounded");
240
+ expect(r.text).toBe(NO_CONTEXT_MARKER);
241
+ });
242
+
243
+ it("vacancyGuard: mismatched vacancy → дроп", async () => {
244
+ const chat = fakeChat((m) => {
245
+ const sys = m[0]?.content ?? "";
246
+ return sys.includes("grounded")
247
+ ? '{"grounded":true,"vacancyOk":false,"reason":"цифры"}'
248
+ : "ответ с вакансией";
249
+ });
250
+ const r = await answerWithRag(
251
+ baseInput({ chat, vacanciesBlock: "Вакансия 2000$", stage: "pitch" }),
252
+ );
253
+ expect(r.telemetry.path).toBe("ungrounded");
254
+ });
255
+
256
+ it("grounding-exempt стадия (qualify) без vacancy → fact-check скипается", async () => {
257
+ let factCheckCalled = false;
258
+ const chat = fakeChat((m) => {
259
+ if ((m[0]?.content ?? "").includes("grounded")) {
260
+ factCheckCalled = true;
261
+ return '{"grounded":false,"vacancyOk":true}';
262
+ }
263
+ return "вопрос кандидату";
264
+ });
265
+ const r = await answerWithRag(baseInput({ chat, reflect: true, stage: "qualify" }));
266
+ expect(factCheckCalled).toBe(false);
267
+ expect(r.telemetry.path).toBe("ok");
268
+ });
269
+
270
+ it("tool-loop: модель отвечает без вызова tools → early-return ok", async () => {
271
+ const chat = fakeChat("unused", {
272
+ completeWithTools: async () => ({ content: "финальный ответ", toolCalls: [] }),
273
+ }) as ChatClient;
274
+ const tool = {
275
+ name: "quote",
276
+ description: "d",
277
+ parameters: z.object({}),
278
+ execute: async () => "x",
279
+ };
280
+ const r = await answerWithRag(
281
+ baseInput({ chat, tools: [tool] as unknown as AnswerInput["tools"] }),
282
+ );
283
+ expect(r.text.toLowerCase()).toBe("финальный ответ");
284
+ expect(r.telemetry.path).toBe("ok");
285
+ });
286
+
287
+ it("fact-check sees tool results as grounding context", async () => {
288
+ let calls = 0;
289
+ let checkerPrompt = "";
290
+ const chat = fakeChat("unused", {
291
+ completeWithTools: async () => {
292
+ calls++;
293
+ if (calls === 1) {
294
+ return {
295
+ content: null,
296
+ toolCalls: [{ id: "q1", name: "quote", args: { asset: "USDT", amount: 335 } }],
297
+ };
298
+ }
299
+ return { content: "Курс 31.5, получите 10553 THB.", toolCalls: [] };
300
+ },
301
+ complete: async (messages: ChatMessage[]) => {
302
+ if ((messages[0]?.content ?? "").includes("grounded")) {
303
+ checkerPrompt = messages[1]?.content ?? "";
304
+ return '{"grounded":true,"vacancyOk":true}';
305
+ }
306
+ return "Курс 31.5, получите 10553 THB.";
307
+ },
308
+ }) as ChatClient;
309
+ const tool = {
310
+ name: "quote",
311
+ description: "d",
312
+ parameters: z.object({ asset: z.string(), amount: z.number() }),
313
+ execute: async () => ({ rate: 31.5, amountToThb: 10553 }),
314
+ };
315
+ const r = await answerWithRag(
316
+ baseInput({ chat, reflect: true, tools: [tool] as unknown as AnswerInput["tools"] }),
317
+ );
318
+ expect(r.telemetry.path).toBe("ok");
319
+ expect(r.text).toContain("10553 THB");
320
+ expect(checkerPrompt).toContain("TOOL RESULTS");
321
+ expect(checkerPrompt).toContain("quote");
322
+ expect(checkerPrompt).toContain("10553");
323
+ });
324
+ });
325
+
326
+ describe("answerWithRagStream", () => {
327
+ async function collect(it: AsyncIterable<string>): Promise<string> {
328
+ let out = "";
329
+ for await (const t of it) out += t;
330
+ return out;
331
+ }
332
+
333
+ it("smalltalk shortcut → yield + telemetry", async () => {
334
+ const tel: { path?: string }[] = [];
335
+ const out = await collect(
336
+ answerWithRagStream(baseInput({ question: "кто ты?", onTelemetry: (t) => tel.push(t) })),
337
+ );
338
+ expect(out.length).toBeGreaterThan(0);
339
+ expect(tel[0]?.path).toBe("smalltalk");
340
+ });
341
+
342
+ it("no-context → yield NO_CONTEXT_MARKER", async () => {
343
+ const { store } = fakeKb({ search: [] });
344
+ const out = await collect(answerWithRagStream(baseInput({ kb: store })));
345
+ expect(out).toBe(NO_CONTEXT_MARKER);
346
+ });
347
+
348
+ it("stream() доступен → токены отдаются по одному", async () => {
349
+ const chat = fakeChat("unused", {
350
+ // eslint-disable-next-line require-yield
351
+ stream: async function* () {
352
+ yield "при";
353
+ yield "вет";
354
+ },
355
+ }) as ChatClient;
356
+ const out = await collect(answerWithRagStream(baseInput({ chat })));
357
+ expect(out).toBe("привет");
358
+ });
359
+
360
+ it("нет stream() → fallback на complete()", async () => {
361
+ const out = await collect(answerWithRagStream(baseInput({ chat: fakeChat("полный ответ") })));
362
+ // complete() fallback runs sanitizeLlmOutput (capitalises the first letter)
363
+ expect(out.toLowerCase()).toBe("полный ответ");
364
+ });
365
+ });
366
+
367
+ describe("generateSoftFallback", () => {
368
+ it("строит persona-промпт и санитизирует ответ", async () => {
369
+ const chat = fakeChat("Уточню и вернусь!");
370
+ const r = await generateSoftFallback({
371
+ question: "сколько платят?",
372
+ chat,
373
+ persona: { name: "Аня", role: "human", company: "Acme" },
374
+ });
375
+ expect(r).toBe("Уточню и вернусь!");
376
+ const sys = chat.lastMessages?.[0]?.content ?? "";
377
+ expect(sys).toContain("Аня");
378
+ expect(sys).toContain("Acme");
379
+ });
380
+ });
package/src/answer.ts CHANGED
@@ -33,7 +33,12 @@ import {
33
33
  legacyRagSamplingTemperature,
34
34
  } from "./system-prompt.ts";
35
35
  import { applyStyleRules } from "./text-style-rules.ts";
36
- import { buildToolTelemetry, DEFAULT_MAX_TOOL_CYCLES, runToolLoop } from "./tool-loop.ts";
36
+ import {
37
+ buildToolTelemetry,
38
+ DEFAULT_MAX_TOOL_CYCLES,
39
+ runToolLoop,
40
+ type ToolCallRecord,
41
+ } from "./tool-loop.ts";
37
42
  import type { AnyRagTool } from "./tools.ts";
38
43
  import { classifyTopic } from "./topic-classifier.ts";
39
44
  import type { KbSearchHit } from "./types.ts";
@@ -62,6 +67,15 @@ export {
62
67
  renderUserFactsBlock,
63
68
  } from "./system-prompt.ts";
64
69
 
70
+ function toolCallsToGroundingContext(records: ToolCallRecord[] | undefined): string {
71
+ if (!records || records.length === 0) return "";
72
+ const lines = records.map((record, index) => {
73
+ const result = typeof record.result === "string" ? record.result : JSON.stringify(record.result);
74
+ return `[#tool-${index + 1}] ${record.name}: ${result ?? ""}`;
75
+ });
76
+ return `TOOL RESULTS:\n${lines.join("\n")}`;
77
+ }
78
+
65
79
  // ── Shared retrieval ─────────────────────────────────────────────────────────
66
80
 
67
81
  export interface RetrievalResult {
@@ -235,6 +249,8 @@ async function answerFromHits(opts: {
235
249
  : {}),
236
250
  ...(input.supportPhase ? { supportPhase: input.supportPhase } : {}),
237
251
  ...(input.stageOverride ? { stageOverride: input.stageOverride } : {}),
252
+ ...(input.requestContext ? { requestContext: input.requestContext } : {}),
253
+ ...(input.awaitingOperator ? { awaitingOperator: true } : {}),
238
254
  });
239
255
  temperature = input.style.model.temperature;
240
256
  } else {
@@ -366,10 +382,12 @@ async function answerFromHits(opts: {
366
382
  !(groundingExempt && !runVacancyCheck);
367
383
 
368
384
  if (runFactCheck) {
385
+ const toolContext = toolCallsToGroundingContext(multiCycleToolCalls);
386
+ const groundingContext = toolContext ? [context, toolContext].filter(Boolean).join("\n\n") : context;
369
387
  const verdict = await checkFacts({
370
388
  question: input.question,
371
389
  answer: text,
372
- context,
390
+ context: groundingContext,
373
391
  chat: input.chat,
374
392
  ...(runVacancyCheck ? { vacanciesBlock: vacBlock } : {}),
375
393
  });
@@ -505,6 +523,8 @@ export async function* answerWithRagStream(input: AnswerInput): AsyncIterable<st
505
523
  : {}),
506
524
  ...(input.supportPhase ? { supportPhase: input.supportPhase } : {}),
507
525
  ...(input.stageOverride ? { stageOverride: input.stageOverride } : {}),
526
+ ...(input.requestContext ? { requestContext: input.requestContext } : {}),
527
+ ...(input.awaitingOperator ? { awaitingOperator: true } : {}),
508
528
  });
509
529
  temperature = input.style.model.temperature;
510
530
  } else {