@chatman-media/conversation-engine 1.2.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.
- package/LICENSE +21 -0
- package/README.md +22 -0
- package/dist/compact-conversation.d.ts +16 -0
- package/dist/compact-conversation.d.ts.map +1 -0
- package/dist/compact-conversation.test.d.ts +2 -0
- package/dist/compact-conversation.test.d.ts.map +1 -0
- package/dist/contact-resolver.d.ts +16 -0
- package/dist/contact-resolver.d.ts.map +1 -0
- package/dist/conversation-resolver.d.ts +17 -0
- package/dist/conversation-resolver.d.ts.map +1 -0
- package/dist/dal/channel-identities.d.ts +26 -0
- package/dist/dal/channel-identities.d.ts.map +1 -0
- package/dist/dal/contacts.d.ts +30 -0
- package/dist/dal/contacts.d.ts.map +1 -0
- package/dist/dal/conversations.d.ts +67 -0
- package/dist/dal/conversations.d.ts.map +1 -0
- package/dist/dal/experiments.d.ts +47 -0
- package/dist/dal/experiments.d.ts.map +1 -0
- package/dist/dal/index.d.ts +14 -0
- package/dist/dal/index.d.ts.map +1 -0
- package/dist/dal/kb-store.d.ts +58 -0
- package/dist/dal/kb-store.d.ts.map +1 -0
- package/dist/dal/kb-suggestions.d.ts +26 -0
- package/dist/dal/kb-suggestions.d.ts.map +1 -0
- package/dist/dal/leads.d.ts +38 -0
- package/dist/dal/leads.d.ts.map +1 -0
- package/dist/dal/messages.d.ts +48 -0
- package/dist/dal/messages.d.ts.map +1 -0
- package/dist/dal/notifications.d.ts +32 -0
- package/dist/dal/notifications.d.ts.map +1 -0
- package/dist/dal/outbound.d.ts +70 -0
- package/dist/dal/outbound.d.ts.map +1 -0
- package/dist/dal/skill-outcomes.d.ts +58 -0
- package/dist/dal/skill-outcomes.d.ts.map +1 -0
- package/dist/dal/styles.d.ts +44 -0
- package/dist/dal/styles.d.ts.map +1 -0
- package/dist/dal/types.d.ts +27 -0
- package/dist/dal/types.d.ts.map +1 -0
- package/dist/dispatch-reply.d.ts +49 -0
- package/dist/dispatch-reply.d.ts.map +1 -0
- package/dist/dispatch-reply.test.d.ts +2 -0
- package/dist/dispatch-reply.test.d.ts.map +1 -0
- package/dist/experiment-router.d.ts +15 -0
- package/dist/experiment-router.d.ts.map +1 -0
- package/dist/experiment-router.test.d.ts +2 -0
- package/dist/experiment-router.test.d.ts.map +1 -0
- package/dist/extract-fields.test.d.ts +2 -0
- package/dist/extract-fields.test.d.ts.map +1 -0
- package/dist/funnel-machine.d.ts +43 -0
- package/dist/funnel-machine.d.ts.map +1 -0
- package/dist/funnel-machine.test.d.ts +2 -0
- package/dist/funnel-machine.test.d.ts.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2024 -0
- package/dist/lead-lifecycle.d.ts +46 -0
- package/dist/lead-lifecycle.d.ts.map +1 -0
- package/dist/lead-lifecycle.test.d.ts +2 -0
- package/dist/lead-lifecycle.test.d.ts.map +1 -0
- package/dist/memory-extractor.d.ts +62 -0
- package/dist/memory-extractor.d.ts.map +1 -0
- package/dist/memory-extractor.test.d.ts +2 -0
- package/dist/memory-extractor.test.d.ts.map +1 -0
- package/dist/notifications.d.ts +32 -0
- package/dist/notifications.d.ts.map +1 -0
- package/dist/notifications.test.d.ts +2 -0
- package/dist/notifications.test.d.ts.map +1 -0
- package/dist/operator-bot-handler.d.ts +13 -0
- package/dist/operator-bot-handler.d.ts.map +1 -0
- package/dist/operator-bot-handler.test.d.ts +2 -0
- package/dist/operator-bot-handler.test.d.ts.map +1 -0
- package/dist/outbound-dispatch.d.ts +17 -0
- package/dist/outbound-dispatch.d.ts.map +1 -0
- package/dist/process-inbound.d.ts +126 -0
- package/dist/process-inbound.d.ts.map +1 -0
- package/dist/process-inbound.test.d.ts +2 -0
- package/dist/process-inbound.test.d.ts.map +1 -0
- package/dist/reply-strategy/index.d.ts +3 -0
- package/dist/reply-strategy/index.d.ts.map +1 -0
- package/dist/reply-strategy/llm-reply.d.ts +69 -0
- package/dist/reply-strategy/llm-reply.d.ts.map +1 -0
- package/dist/reply-strategy/llm-reply.test.d.ts +2 -0
- package/dist/reply-strategy/llm-reply.test.d.ts.map +1 -0
- package/dist/reply-strategy/rag-reply.d.ts +175 -0
- package/dist/reply-strategy/rag-reply.d.ts.map +1 -0
- package/dist/rls-guard.d.ts +23 -0
- package/dist/rls-guard.d.ts.map +1 -0
- package/dist/rls-guard.integration.test.d.ts +2 -0
- package/dist/rls-guard.integration.test.d.ts.map +1 -0
- package/dist/secrets.d.ts +27 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.test.d.ts +2 -0
- package/dist/secrets.test.d.ts.map +1 -0
- package/dist/stage-classifier.d.ts +48 -0
- package/dist/stage-classifier.d.ts.map +1 -0
- package/dist/testkit.d.ts +82 -0
- package/dist/testkit.d.ts.map +1 -0
- package/dist/transcriber.d.ts +15 -0
- package/dist/transcriber.d.ts.map +1 -0
- package/dist/types.d.ts +98 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/with-tenant.d.ts +25 -0
- package/dist/with-tenant.d.ts.map +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { VerticalTemplate } from "@chatman-media/verticals";
|
|
2
|
+
import type { LeadRow, LeadsRepo } from "./dal/leads.ts";
|
|
3
|
+
import type { NotificationService } from "./notifications.ts";
|
|
4
|
+
/**
|
|
5
|
+
* Find-or-create Lead на (tenant, contact). При создании stage берётся
|
|
6
|
+
* из template.funnelStages[0] (обычно 'intake_pending'). Vertical-hook
|
|
7
|
+
* onLeadStageChange при создании НЕ вызывается — у нас нет fromState;
|
|
8
|
+
* хук рассчитан на смены состояния, а не на initial-create.
|
|
9
|
+
*/
|
|
10
|
+
export declare function ensureLead(opts: {
|
|
11
|
+
contactId: number;
|
|
12
|
+
template: VerticalTemplate;
|
|
13
|
+
leads: LeadsRepo;
|
|
14
|
+
nowEpoch: number;
|
|
15
|
+
}): Promise<{
|
|
16
|
+
lead: LeadRow;
|
|
17
|
+
created: boolean;
|
|
18
|
+
}>;
|
|
19
|
+
/**
|
|
20
|
+
* Контекст для vertical-hooks — мини-DI прокидывается в каждый хук.
|
|
21
|
+
* Зачем нужен сам объект (вместо просто полей): vertical-template'у
|
|
22
|
+
* нужен escape-hatch чтобы пробросить дополнительные deps через cast'у.
|
|
23
|
+
*/
|
|
24
|
+
export interface LeadHookContext {
|
|
25
|
+
tenantId: number;
|
|
26
|
+
contactId: number;
|
|
27
|
+
leadId: number;
|
|
28
|
+
template: VerticalTemplate;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Переход lead.state с валидацией по template.funnelStages и вызовом
|
|
32
|
+
* хуков. Атомарность: UPDATE leads.state делается ДО вызова хука —
|
|
33
|
+
* если хук бросит, мы НЕ откатываем состояние, потому что хук может
|
|
34
|
+
* быть side-effect'ом (например, послать DM кандидату). Хуки должны
|
|
35
|
+
* быть идемпотентны.
|
|
36
|
+
*/
|
|
37
|
+
export declare function transitionLeadState(opts: {
|
|
38
|
+
lead: LeadRow;
|
|
39
|
+
toState: string;
|
|
40
|
+
template: VerticalTemplate;
|
|
41
|
+
leads: LeadsRepo;
|
|
42
|
+
notifications?: NotificationService;
|
|
43
|
+
nowEpoch: number;
|
|
44
|
+
hookContext?: Partial<LeadHookContext>;
|
|
45
|
+
}): Promise<LeadRow>;
|
|
46
|
+
//# sourceMappingURL=lead-lifecycle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lead-lifecycle.d.ts","sourceRoot":"","sources":["../src/lead-lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEzD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE9D;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,KAAK,EAAE,SAAS,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAS/C;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,gBAAgB,CAAC;CAC5B;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE;IAC9C,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,KAAK,EAAE,SAAS,CAAC;IACjB,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;CACxC,GAAG,OAAO,CAAC,OAAO,CAAC,CAuCnB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lead-lifecycle.test.d.ts","sourceRoot":"","sources":["../src/lead-lifecycle.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ChatClient } from "@chatman-media/llm-router";
|
|
2
|
+
import type { ContactsRepo } from "./dal/contacts.ts";
|
|
3
|
+
import type { MessagesRepo } from "./dal/messages.ts";
|
|
4
|
+
/**
|
|
5
|
+
* Memory extractor — LLM-based анализ диалога, извлекает факты о кандидате
|
|
6
|
+
* (name/city/age/experience/...) и merge'ит их в contact.attributes_json.
|
|
7
|
+
*
|
|
8
|
+
* Дополняет vertical-template'овский hooks.extractFields:
|
|
9
|
+
* - extractFields — regex/regex-based, дёшево и быстро, но узко (имя/возраст
|
|
10
|
+
* по паттерну)
|
|
11
|
+
* - LlmMemoryExtractor — LLM-call, дороже, но универсально (русский NER,
|
|
12
|
+
* импликации типа "работала в Дубае" → country_target=Dubai)
|
|
13
|
+
*
|
|
14
|
+
* Вызывается после persist user-message в pipeline. Не блокирует reply-loop:
|
|
15
|
+
* - exception → log + continue
|
|
16
|
+
* - extractor сам skip'ает LLM call если нет user-сообщений
|
|
17
|
+
*/
|
|
18
|
+
export interface MemoryExtractor {
|
|
19
|
+
/** Извлекает facts; возвращает только новые/обновлённые. Empty = no-op. */
|
|
20
|
+
extract(opts: {
|
|
21
|
+
tenantId: number;
|
|
22
|
+
conversationId: number;
|
|
23
|
+
contactId: number;
|
|
24
|
+
existingFacts: Record<string, string>;
|
|
25
|
+
}): Promise<Record<string, string>>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* LLM-based реализация через @chatman-media/kb.extractUserFacts.
|
|
29
|
+
* MessagesRepo — для загрузки history, ChatClient — для LLM-вызова.
|
|
30
|
+
*/
|
|
31
|
+
export declare class LlmMemoryExtractor implements MemoryExtractor {
|
|
32
|
+
private readonly opts;
|
|
33
|
+
private readonly messagesRepoFor;
|
|
34
|
+
constructor(opts: {
|
|
35
|
+
historyLimit?: number;
|
|
36
|
+
resolveChat: (tenantId: number) => ChatClient;
|
|
37
|
+
}, messagesRepoFor: (tenantId: number) => MessagesRepo);
|
|
38
|
+
extract(input: {
|
|
39
|
+
tenantId: number;
|
|
40
|
+
conversationId: number;
|
|
41
|
+
contactId: number;
|
|
42
|
+
existingFacts: Record<string, string>;
|
|
43
|
+
}): Promise<Record<string, string>>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Дёргает extractor + merge'ит результат в contact.attributes_json.
|
|
47
|
+
* Используется из process-inbound (опционально через memoryExtractor dep).
|
|
48
|
+
*
|
|
49
|
+
* Контракт: existing facts читаются из contact.attributesJson; только string-
|
|
50
|
+
* value поля считаются "facts" (rag's extractUserFacts возвращает
|
|
51
|
+
* Record<string,string>). Не-string полей (age:number и т.п.) сохраняются
|
|
52
|
+
* как-есть.
|
|
53
|
+
*/
|
|
54
|
+
export declare function runMemoryExtraction(opts: {
|
|
55
|
+
extractor: MemoryExtractor;
|
|
56
|
+
tenantId: number;
|
|
57
|
+
conversationId: number;
|
|
58
|
+
contactId: number;
|
|
59
|
+
contacts: ContactsRepo;
|
|
60
|
+
nowEpoch: number;
|
|
61
|
+
}): Promise<Record<string, string>>;
|
|
62
|
+
//# sourceMappingURL=memory-extractor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-extractor.d.ts","sourceRoot":"","sources":["../src/memory-extractor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAiC,MAAM,2BAA2B,CAAC;AAE3F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAc,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAElE;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,eAAe;IAC9B,2EAA2E;IAC3E,OAAO,CAAC,IAAI,EAAE;QACZ,QAAQ,EAAE,MAAM,CAAC;QACjB,cAAc,EAAE,MAAM,CAAC;QACvB,SAAS,EAAE,MAAM,CAAC;QAClB,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACvC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACrC;AAED;;;GAGG;AACH,qBAAa,kBAAmB,YAAW,eAAe;IAEtD,OAAO,CAAC,QAAQ,CAAC,IAAI;IAIrB,OAAO,CAAC,QAAQ,CAAC,eAAe;gBAJf,IAAI,EAAE;QACrB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,WAAW,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,UAAU,CAAC;KAC/C,EACgB,eAAe,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,YAAY;IAGhE,OAAO,CAAC,KAAK,EAAE;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,cAAc,EAAE,MAAM,CAAC;QACvB,SAAS,EAAE,MAAM,CAAC;QAClB,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACvC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAepC;AAcD;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE;IAC9C,SAAS,EAAE,eAAe,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,YAAY,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAoBlC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-extractor.test.d.ts","sourceRoot":"","sources":["../src/memory-extractor.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { NotificationRule, NotificationsRepo } from "./dal/notifications.ts";
|
|
2
|
+
export interface NotificationEvent {
|
|
3
|
+
tenantId: number;
|
|
4
|
+
eventType: string;
|
|
5
|
+
leadId?: number;
|
|
6
|
+
conversationId?: number;
|
|
7
|
+
contactId?: number;
|
|
8
|
+
/** assignedAdminId — если задан, проверяется notifyOnAssignedOnly */
|
|
9
|
+
assignedAdminId?: number;
|
|
10
|
+
data: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
export declare class NotificationService {
|
|
13
|
+
private readonly repo;
|
|
14
|
+
private readonly botToken;
|
|
15
|
+
private readonly appUrl;
|
|
16
|
+
private client;
|
|
17
|
+
constructor(repo: NotificationsRepo, botToken: string, appUrl: string);
|
|
18
|
+
notify(event: NotificationEvent): Promise<void>;
|
|
19
|
+
sendTestMessage(chatId: string): Promise<{
|
|
20
|
+
ok: boolean;
|
|
21
|
+
error?: string;
|
|
22
|
+
}>;
|
|
23
|
+
private sendMessage;
|
|
24
|
+
matchesCondition(rule: NotificationRule, event: NotificationEvent): boolean;
|
|
25
|
+
renderTemplate(body: string, event: NotificationEvent): string;
|
|
26
|
+
formatMessage(event: NotificationEvent): string;
|
|
27
|
+
private formatButtons;
|
|
28
|
+
private getEventEmoji;
|
|
29
|
+
private getEventTitle;
|
|
30
|
+
private formatKey;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=notifications.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notifications.d.ts","sourceRoot":"","sources":["../src/notifications.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAElF,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qEAAqE;IACrE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAED,qBAAa,mBAAmB;IAI5B,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM;IALzB,OAAO,CAAC,MAAM,CAA+B;gBAG1B,IAAI,EAAE,iBAAiB,EACvB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM;IAO3B,MAAM,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAyC/C,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;YAcjE,WAAW;IAazB,gBAAgB,CAAC,IAAI,EAAE,gBAAgB,EAAE,KAAK,EAAE,iBAAiB,GAAG,OAAO;IAa3E,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,GAAG,MAAM;IAc9D,aAAa,CAAC,KAAK,EAAE,iBAAiB,GAAG,MAAM;IAsB/C,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,aAAa;IAYrB,OAAO,CAAC,aAAa;IAYrB,OAAO,CAAC,SAAS;CAUlB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notifications.test.d.ts","sourceRoot":"","sources":["../src/notifications.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type TgUpdate } from "@chatman-media/channel-telegram";
|
|
2
|
+
import type { NotificationsRepo } from "./dal/notifications.ts";
|
|
3
|
+
export declare class OperatorBotHandler {
|
|
4
|
+
private readonly repo;
|
|
5
|
+
private readonly botToken;
|
|
6
|
+
private client;
|
|
7
|
+
constructor(repo: NotificationsRepo, botToken: string);
|
|
8
|
+
handleUpdate(update: TgUpdate): Promise<void>;
|
|
9
|
+
private handleLinkToken;
|
|
10
|
+
private handleGroupLinkToken;
|
|
11
|
+
private handleSetupGroup;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=operator-bot-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"operator-bot-handler.d.ts","sourceRoot":"","sources":["../src/operator-bot-handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAChF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhE,qBAAa,kBAAkB;IAI3B,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAJ3B,OAAO,CAAC,MAAM,CAA+B;gBAG1B,IAAI,EAAE,iBAAiB,EACvB,QAAQ,EAAE,MAAM;IAO7B,YAAY,CAAC,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;YAwCrC,eAAe;YAoBf,oBAAoB;YAmCpB,gBAAgB;CAkB/B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"operator-bot-handler.test.d.ts","sourceRoot":"","sources":["../src/operator-bot-handler.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { OutboundEnvelope } from "@chatman-media/channel-core";
|
|
2
|
+
import type { OutboundQueueRepo, OutboundQueueRow } from "./dal/index.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Поставить envelope в outbound_queue. Worker'у дальше pop'ить
|
|
5
|
+
* pending записи и слать через ChannelAdapter.send().
|
|
6
|
+
*
|
|
7
|
+
* `idempotencyKey` на envelope защищает от дублей при retry processInbound'а.
|
|
8
|
+
* Рекомендованный формат ключа — `${channelId}:${externalUserId}:${reasonHash}`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function dispatchOutbound(opts: {
|
|
11
|
+
channelDbId: number;
|
|
12
|
+
conversationId: number | null;
|
|
13
|
+
envelope: OutboundEnvelope;
|
|
14
|
+
outbound: OutboundQueueRepo;
|
|
15
|
+
nowEpoch: number;
|
|
16
|
+
}): Promise<OutboundQueueRow>;
|
|
17
|
+
//# sourceMappingURL=outbound-dispatch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outbound-dispatch.d.ts","sourceRoot":"","sources":["../src/outbound-dispatch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAE1E;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAO5B"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Inbound, OutboundEnvelope } from "@chatman-media/channel-core";
|
|
2
|
+
import type { VerticalTemplate } from "@chatman-media/verticals";
|
|
3
|
+
import type { ChannelIdentitiesRepo, ContactsRepo, ConversationsRepo, MessagesRepo, OutboundQueueRepo } from "./dal/index.ts";
|
|
4
|
+
import { type MemoryExtractor } from "./memory-extractor.ts";
|
|
5
|
+
import { type StageClassifier } from "./stage-classifier.ts";
|
|
6
|
+
import type { ITranscriber } from "./transcriber.ts";
|
|
7
|
+
import type { NotificationService } from "./notifications.ts";
|
|
8
|
+
import { ChannelContext, type Clock, type PipelineSink, type ProcessInboundResult, type TenantContext } from "./types.ts";
|
|
9
|
+
/**
|
|
10
|
+
* Стратегия генерации ответа. Pipeline сам не дёргает LLM напрямую —
|
|
11
|
+
* вместо этого consumer (apps/worker) инжектит ReplyStrategy. Это
|
|
12
|
+
* позволяет:
|
|
13
|
+
* - заменять стратегию для тестов на fake (вернёт фиксированный ответ)
|
|
14
|
+
* - поэтапно вводить RAG / sales-engine / vertical-hooks без trogания
|
|
15
|
+
* pipeline'а
|
|
16
|
+
* - выключать reply вообще (mediaOnly turn, режим оператора, и т.д.)
|
|
17
|
+
*
|
|
18
|
+
* Возвращает null — значит бот молчит. Возвращает envelope'ы — они
|
|
19
|
+
* пушатся в outbound_queue в порядке возврата.
|
|
20
|
+
*/
|
|
21
|
+
export interface ReplyStrategy {
|
|
22
|
+
generate(opts: {
|
|
23
|
+
tenant: TenantContext;
|
|
24
|
+
channel: ChannelContext;
|
|
25
|
+
conversationId: number;
|
|
26
|
+
contactId: number;
|
|
27
|
+
inbound: Inbound;
|
|
28
|
+
userMessageText: string;
|
|
29
|
+
}): Promise<OutboundEnvelope[] | null>;
|
|
30
|
+
}
|
|
31
|
+
export interface ProcessInboundDeps {
|
|
32
|
+
tenant: TenantContext;
|
|
33
|
+
channel: ChannelContext;
|
|
34
|
+
channelDbId: number;
|
|
35
|
+
contacts: ContactsRepo;
|
|
36
|
+
identities: ChannelIdentitiesRepo;
|
|
37
|
+
conversations: ConversationsRepo;
|
|
38
|
+
messages: MessagesRepo;
|
|
39
|
+
outbound: OutboundQueueRepo;
|
|
40
|
+
/** Стратегия ответа. null = pipeline сохраняет inbound и не отвечает. */
|
|
41
|
+
reply?: ReplyStrategy | null;
|
|
42
|
+
/**
|
|
43
|
+
* Vertical-template для текущей conversation. Если задан и в нём
|
|
44
|
+
* есть hooks.extractFields — pipeline после persist user-message
|
|
45
|
+
* дёрнет hook и merge'нёт извлечённые поля в contact.attributes_json.
|
|
46
|
+
* Это даёт автозаполнение questionnaire (имя/возраст/паспорт/...)
|
|
47
|
+
* без блокировки reply-loop.
|
|
48
|
+
*/
|
|
49
|
+
template?: VerticalTemplate;
|
|
50
|
+
/**
|
|
51
|
+
* Опциональный LLM-based memory extractor. Если задан, после persist
|
|
52
|
+
* user-message pipeline вытащит из истории facts через
|
|
53
|
+
* extractUserFacts и merge'нёт в contact.attributes_json. Дополняет
|
|
54
|
+
* template.hooks.extractFields (тот — regex; этот — LLM).
|
|
55
|
+
*/
|
|
56
|
+
memoryExtractor?: MemoryExtractor;
|
|
57
|
+
/**
|
|
58
|
+
* Опциональный stage classifier (opener|qualify|pitch|objection|close).
|
|
59
|
+
* Если задан, после persist user-message pipeline классифицирует stage
|
|
60
|
+
* и пишет в conversations.current_stage (если отличается от previous).
|
|
61
|
+
*
|
|
62
|
+
* Sales-engine использует current_stage для выбора stage-specific
|
|
63
|
+
* промптов в composeSystemPrompt; admin-UI — для отображения позиции
|
|
64
|
+
* кандидата в воронке.
|
|
65
|
+
*/
|
|
66
|
+
stageClassifier?: StageClassifier;
|
|
67
|
+
/**
|
|
68
|
+
* Drizzle db, нужен stage classifier'у для UPDATE conversations.current_stage.
|
|
69
|
+
* Когда stageClassifier=null — может быть опущен.
|
|
70
|
+
*/
|
|
71
|
+
db?: import("./dal/types.ts").Db;
|
|
72
|
+
/**
|
|
73
|
+
* Когда true — pipeline ЗАВЕРШАЕТСЯ после persist + classify + memory
|
|
74
|
+
* extract БЕЗ вызова `reply.generate` и enqueue outbound. Result содержит
|
|
75
|
+
* `replyDeferred: true`, `userMessageText`, `mediaOnly` — caller должен
|
|
76
|
+
* затем вызвать `generateReplyAndEnqueue(...)` ВНЕ открытой Postgres-tx.
|
|
77
|
+
*
|
|
78
|
+
* Зачем: reply.generate — это LLM call ~1-2s, который не должен держать
|
|
79
|
+
* Postgres pool connection. Pool=10, под нагрузкой это становится
|
|
80
|
+
* bottleneck'ом. Split на phases освобождает connection для других
|
|
81
|
+
* inbound'ов пока ждём LLM.
|
|
82
|
+
*
|
|
83
|
+
* Default false (legacy single-tx path для existing callers).
|
|
84
|
+
*/
|
|
85
|
+
deferReply?: boolean;
|
|
86
|
+
sink?: PipelineSink;
|
|
87
|
+
clock?: Clock;
|
|
88
|
+
notifications?: NotificationService;
|
|
89
|
+
/**
|
|
90
|
+
* Опциональный STT-транскрибер. Если задан и inbound содержит voice-part —
|
|
91
|
+
* pipeline транскрибирует аудио перед persist'ом. Транскрипт становится
|
|
92
|
+
* текстом сообщения; остальной pipeline работает без изменений.
|
|
93
|
+
* downloadVoice должен быть задан вместе с transcriber.
|
|
94
|
+
*/
|
|
95
|
+
transcriber?: ITranscriber | null;
|
|
96
|
+
/** Загружает аудиофайл. Нужен для transcriber. externalUserId нужен userbot-адаптеру. */
|
|
97
|
+
downloadVoice?: ((mediaRef: import("@chatman-media/channel-core").MediaRef, externalUserId: string) => Promise<Response>) | null;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Основной pipeline. Channel-agnostic, tenant-scoped. apps/worker дёргает
|
|
101
|
+
* это для каждого Inbound из ChannelAdapter.receive().
|
|
102
|
+
*
|
|
103
|
+
* Шаги:
|
|
104
|
+
* 1. resolveContact — find/create Contact + ChannelIdentity
|
|
105
|
+
* 2. resolveConversation — find/create Conversation per (contact, kind)
|
|
106
|
+
* 3. Persist user message в messages (с idempotency через
|
|
107
|
+
* uniq_msg_user_tg)
|
|
108
|
+
* 4. Если conversation.mode === 'ai' и reply-strategy задана → сгенерить
|
|
109
|
+
* OutboundEnvelope[]. Для 'queued'/'human' — pipeline пропускает
|
|
110
|
+
* генерацию (оператор отвечает сам).
|
|
111
|
+
* 5. Push каждого envelope в outbound_queue.
|
|
112
|
+
* 6. Touch conversations.last_message_at.
|
|
113
|
+
*
|
|
114
|
+
* Реализовано: vertical-hooks.extractFields (шаг 5), LLM-memory extraction
|
|
115
|
+
* (шаг 5b), stage classifier (шаг 5a-bis), RAG + LLM reply через
|
|
116
|
+
* ReplyStrategy (шаг 6). Оставшиеся TODO:
|
|
117
|
+
* - conversation summarization при длинных диалогах (context overflow)
|
|
118
|
+
* - escalation rules (не отвечать N часов → перевод в queued)
|
|
119
|
+
* - A/B routing (styleId → experiment allocation через ExperimentsRepo)
|
|
120
|
+
*
|
|
121
|
+
* Photo classification + passport OCR реализованы в apps/api/src/lib/
|
|
122
|
+
* photo-processor.ts — выполняется ПОСЛЕ pipeline'а (Phase 4, async,
|
|
123
|
+
* без tx) через PhotoProcessor.process().
|
|
124
|
+
*/
|
|
125
|
+
export declare function processInbound(inbound: Inbound, deps: ProcessInboundDeps): Promise<ProcessInboundResult>;
|
|
126
|
+
//# sourceMappingURL=process-inbound.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process-inbound.d.ts","sourceRoot":"","sources":["../src/process-inbound.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC7E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAGjE,OAAO,KAAK,EACV,qBAAqB,EACrB,YAAY,EACZ,iBAAiB,EACjB,YAAY,EACZ,iBAAiB,EAClB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,KAAK,eAAe,EAAuB,MAAM,uBAAuB,CAAC;AAElF,OAAO,EAAwB,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACnF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EACL,cAAc,EACd,KAAK,KAAK,EACV,KAAK,YAAY,EACjB,KAAK,oBAAoB,EAEzB,KAAK,aAAa,EACnB,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE;QACb,MAAM,EAAE,aAAa,CAAC;QACtB,OAAO,EAAE,cAAc,CAAC;QACxB,cAAc,EAAE,MAAM,CAAC;QACvB,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,OAAO,CAAC;QACjB,eAAe,EAAE,MAAM,CAAC;KACzB,GAAG,OAAO,CAAC,gBAAgB,EAAE,GAAG,IAAI,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,aAAa,CAAC;IACtB,OAAO,EAAE,cAAc,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,YAAY,CAAC;IACvB,UAAU,EAAE,qBAAqB,CAAC;IAClC,aAAa,EAAE,iBAAiB,CAAC;IACjC,QAAQ,EAAE,YAAY,CAAC;IACvB,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,yEAAyE;IACzE,KAAK,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IAC7B;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B;;;;;OAKG;IACH,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC;;;;;;;;OAQG;IACH,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC;;;OAGG;IACH,EAAE,CAAC,EAAE,OAAO,gBAAgB,EAAE,EAAE,CAAC;IACjC;;;;;;;;;;;;OAYG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC;;;;;OAKG;IACH,WAAW,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IAClC,yFAAyF;IACzF,aAAa,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,OAAO,6BAA6B,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC;CAClI;AAwBD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,kBAAkB,GACvB,OAAO,CAAC,oBAAoB,CAAC,CA6T/B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process-inbound.test.d.ts","sourceRoot":"","sources":["../src/process-inbound.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/reply-strategy/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,KAAK,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAC7E,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,KAAK,oBAAoB,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { OutboundEnvelope } from "@chatman-media/channel-core";
|
|
2
|
+
import type { ChatClient } from "@chatman-media/llm-router";
|
|
3
|
+
import type { VerticalTemplate } from "@chatman-media/verticals";
|
|
4
|
+
import type { MessagesRepo } from "../dal/messages.ts";
|
|
5
|
+
import type { ReplyStrategy } from "../process-inbound.ts";
|
|
6
|
+
/**
|
|
7
|
+
* Минимальный LLM-based ReplyStrategy. Шаги на каждый inbound:
|
|
8
|
+
* 1. Загрузить последние N сообщений из conversation (history).
|
|
9
|
+
* 2. Собрать system prompt (template.systemPromptFragment + base).
|
|
10
|
+
* 3. Послать history → ChatClient.complete().
|
|
11
|
+
* 4. Вернуть OutboundEnvelope с text-частью.
|
|
12
|
+
*
|
|
13
|
+
* Что отсутствует (полная RAG / sales — следующая итерация):
|
|
14
|
+
* - KB search + chunks в контекст
|
|
15
|
+
* - sales-style selection и A/B routing
|
|
16
|
+
* - memory extraction в contacts.attributes_json
|
|
17
|
+
* - conversation summarization для длинных history
|
|
18
|
+
* - extractFields hook на user message
|
|
19
|
+
* - photo/voice handling (сейчас игнорируем, нет multimodal через chat)
|
|
20
|
+
*
|
|
21
|
+
* Truncated ответы (ChatTruncatedError) ловятся выше — strategy
|
|
22
|
+
* пробрасывает их в processInbound, где попадают в sink.log; envelope
|
|
23
|
+
* НЕ ставится в outbound_queue, бот молчит вместо half-формы.
|
|
24
|
+
*/
|
|
25
|
+
export interface LlmReplyStrategyOpts {
|
|
26
|
+
template: VerticalTemplate;
|
|
27
|
+
/**
|
|
28
|
+
* Лимит сообщений в history-prompt'е. Default 20.
|
|
29
|
+
* При больших значениях нужен conversation summary (следующая итерация).
|
|
30
|
+
*/
|
|
31
|
+
historyLimit?: number;
|
|
32
|
+
/** Per-call temperature, default 0.7. */
|
|
33
|
+
temperature?: number;
|
|
34
|
+
/** Output token cap, default 600. */
|
|
35
|
+
maxOutputTokens?: number;
|
|
36
|
+
/**
|
|
37
|
+
* Resolver ChatClient'а. apps/api прокидывает функцию которая знает
|
|
38
|
+
* tenant_id из request scope: (tenantId) => llmRouter.resolveChat(tenantId, 'chat').
|
|
39
|
+
* Это даёт per-call swap клиента — если оператор поменял конфиг через
|
|
40
|
+
* admin-UI и invalidate'нул router, следующий resolveChat() построит
|
|
41
|
+
* нового.
|
|
42
|
+
*/
|
|
43
|
+
resolveChat: (tenantId: number) => ChatClient;
|
|
44
|
+
/** Если возвращает true — стадия лида помечена supportMode, бот молчит. */
|
|
45
|
+
resolveIsSupport?: (input: {
|
|
46
|
+
tenantId: number;
|
|
47
|
+
contactId: number;
|
|
48
|
+
}) => Promise<boolean>;
|
|
49
|
+
}
|
|
50
|
+
export declare class LlmReplyStrategy implements ReplyStrategy {
|
|
51
|
+
private readonly opts;
|
|
52
|
+
private readonly messagesRepoFor;
|
|
53
|
+
constructor(opts: LlmReplyStrategyOpts, messagesRepoFor: (tenantId: number) => MessagesRepo);
|
|
54
|
+
generate(input: {
|
|
55
|
+
tenant: {
|
|
56
|
+
tenantId: number;
|
|
57
|
+
};
|
|
58
|
+
channel: {
|
|
59
|
+
channelId: number;
|
|
60
|
+
};
|
|
61
|
+
conversationId: number;
|
|
62
|
+
contactId: number;
|
|
63
|
+
inbound: {
|
|
64
|
+
externalUserId: string;
|
|
65
|
+
};
|
|
66
|
+
userMessageText: string;
|
|
67
|
+
}): Promise<OutboundEnvelope[] | null>;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=llm-reply.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"llm-reply.d.ts","sourceRoot":"","sources":["../../src/reply-strategy/llm-reply.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,KAAK,EAAE,UAAU,EAAe,MAAM,2BAA2B,CAAC;AACzE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,KAAK,EAAc,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,gBAAgB,CAAC;IAC3B;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yCAAyC;IACzC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qCAAqC;IACrC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;OAMG;IACH,WAAW,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,UAAU,CAAC;IAC9C,2EAA2E;IAC3E,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACzF;AAkBD,qBAAa,gBAAiB,YAAW,aAAa;IAElD,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,eAAe;gBADf,IAAI,EAAE,oBAAoB,EAC1B,eAAe,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,YAAY;IAGhE,QAAQ,CAAC,KAAK,EAAE;QACpB,MAAM,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAA;SAAE,CAAC;QAC7B,OAAO,EAAE;YAAE,SAAS,EAAE,MAAM,CAAA;SAAE,CAAC;QAC/B,cAAc,EAAE,MAAM,CAAC;QACvB,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE;YAAE,cAAc,EAAE,MAAM,CAAA;SAAE,CAAC;QACpC,eAAe,EAAE,MAAM,CAAC;KACzB,GAAG,OAAO,CAAC,gBAAgB,EAAE,GAAG,IAAI,CAAC;CAqCvC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"llm-reply.test.d.ts","sourceRoot":"","sources":["../../src/reply-strategy/llm-reply.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type { OutboundEnvelope } from "@chatman-media/channel-core";
|
|
2
|
+
import type { ChatClient, EmbeddingClient as RagEmbeddingClient } from "@chatman-media/llm-router";
|
|
3
|
+
import { type AnyRagTool, type DirectorHookForPrompt, type IKbStore, type Reranker, type SkillForPrompt, type Style } from "@chatman-media/kb";
|
|
4
|
+
import type { VerticalTemplate } from "@chatman-media/verticals";
|
|
5
|
+
import type { ConversationsRepo } from "../dal/conversations.ts";
|
|
6
|
+
import type { KbSuggestionsRepo } from "../dal/kb-suggestions.ts";
|
|
7
|
+
import type { MessagesRepo } from "../dal/messages.ts";
|
|
8
|
+
import type { ReplyStrategy } from "../process-inbound.ts";
|
|
9
|
+
/**
|
|
10
|
+
* RAG-аware ReplyStrategy. На каждый user message:
|
|
11
|
+
* 1. Загрузить последние N сообщений (history).
|
|
12
|
+
* 2. Эмбеддинг user message → KbStore.hybridSearch → chunks.
|
|
13
|
+
* 3. answerWithRag(chunks + history + system prompt) → text reply.
|
|
14
|
+
* 4. Вернуть OutboundEnvelope.
|
|
15
|
+
*
|
|
16
|
+
* answerWithRag из @chatman-media/kb сам строит system prompt из chunks,
|
|
17
|
+
* делает query rewriting + reflection (если флаги включены) и
|
|
18
|
+
* sanitize'ит output. Мы только инжектим деп'ы и history.
|
|
19
|
+
*
|
|
20
|
+
* Vertical-template'овский systemPromptFragment пробрасывается через
|
|
21
|
+
* persona.role — это не идеально, но answerWithRag не предоставляет
|
|
22
|
+
* прямой override system-промпта; нужная extension-точка появится при
|
|
23
|
+
* рефакторе rag (вне scope этого pkg'а).
|
|
24
|
+
*/
|
|
25
|
+
export interface RagReplyStrategyOpts {
|
|
26
|
+
template: VerticalTemplate;
|
|
27
|
+
/** Лимит history сообщений (default 12 — answerWithRag сам ужмёт через summary). */
|
|
28
|
+
historyLimit?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Включить hybrid retrieval (vector + BM25 RRF). Default true.
|
|
31
|
+
* False = pure vector — быстрее, но хуже на keyword-questions.
|
|
32
|
+
*/
|
|
33
|
+
hybridSearch?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Query rewriting перед retrieval (LLM-вызов до search). Default true.
|
|
36
|
+
* Помогает на ambiguous follow-up'ах ("а что насчёт оплаты?" → "оплата контракта в UAE").
|
|
37
|
+
*/
|
|
38
|
+
rewriteQueryBeforeRetrieval?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Reflection-guard после генерации (LLM фактчекит chunks vs answer).
|
|
41
|
+
* Default false — дополнительная LLM-стоимость и latency.
|
|
42
|
+
*/
|
|
43
|
+
reflect?: boolean;
|
|
44
|
+
/** topK chunks для контекста. Default 5. */
|
|
45
|
+
topK?: number;
|
|
46
|
+
/** Per-call temperature, default 0.7. */
|
|
47
|
+
temperature?: number;
|
|
48
|
+
maxOutputTokens?: number;
|
|
49
|
+
resolveChat: (tenantId: number) => ChatClient;
|
|
50
|
+
resolveEmbed: (tenantId: number) => RagEmbeddingClient;
|
|
51
|
+
resolveKb: (tenantId: number) => IKbStore;
|
|
52
|
+
/**
|
|
53
|
+
* Опциональный sales-style resolver. Если задан и возвращает Style —
|
|
54
|
+
* answerWithRag использует его для построения system-prompt (persona,
|
|
55
|
+
* sales framework, hooks, skills). При null/undefined — rag fallback'нет
|
|
56
|
+
* на DEFAULT_PERSONA. Лёгкое расширение для A/B routing в будущем:
|
|
57
|
+
* resolveStyle может смотреть на conversation.style_id или experiment_id.
|
|
58
|
+
*/
|
|
59
|
+
resolveStyle?: (input: {
|
|
60
|
+
tenantId: number;
|
|
61
|
+
conversationId: number;
|
|
62
|
+
contactId: number;
|
|
63
|
+
}) => Promise<Style | null> | Style | null;
|
|
64
|
+
/**
|
|
65
|
+
* Опциональная проверка support-mode. Если возвращает true — стадия лида
|
|
66
|
+
* помечена как supportMode и бот не отвечает (возвращает null). Оператор
|
|
67
|
+
* ведёт диалог вручную пока лид не переведут на другую стадию.
|
|
68
|
+
*/
|
|
69
|
+
resolveIsSupport?: (input: {
|
|
70
|
+
tenantId: number;
|
|
71
|
+
contactId: number;
|
|
72
|
+
}) => Promise<boolean>;
|
|
73
|
+
/**
|
|
74
|
+
* Загрузить включённые навыки убеждения для тенанта. Возвращает список
|
|
75
|
+
* SkillForPrompt, уже отфильтрованных по is_enabled = true.
|
|
76
|
+
* Stage-фильтрация (intake/active/always) выполняется внутри
|
|
77
|
+
* composeSystemPrompt по applicableStages.
|
|
78
|
+
* Если не задан — навыки не инжектируются (silent fallback).
|
|
79
|
+
*/
|
|
80
|
+
resolveSkills?: (input: {
|
|
81
|
+
tenantId: number;
|
|
82
|
+
}) => Promise<readonly SkillForPrompt[]>;
|
|
83
|
+
/**
|
|
84
|
+
* Загрузить активные директорские хуки тенанта (is_active = true).
|
|
85
|
+
* Если не задан — хуки не инжектируются.
|
|
86
|
+
*/
|
|
87
|
+
resolveDirectorHooks?: (input: {
|
|
88
|
+
tenantId: number;
|
|
89
|
+
}) => Promise<readonly DirectorHookForPrompt[]>;
|
|
90
|
+
/**
|
|
91
|
+
* Загрузить активные agentic tools для тенанта.
|
|
92
|
+
* Вызывается один раз на каждый входящий message.
|
|
93
|
+
* Если не задан или возвращает пустой массив — tool-loop не запускается
|
|
94
|
+
* (поведение как раньше: один LLM-вызов без инструментов).
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```ts
|
|
98
|
+
* resolveTools: async ({ tenantId }) => {
|
|
99
|
+
* const url = await getBookingUrl(tenantId);
|
|
100
|
+
* return url ? [makeBookingLinkTool(url)] : [];
|
|
101
|
+
* }
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
resolveTools?: (input: {
|
|
105
|
+
tenantId: number;
|
|
106
|
+
conversationId: number;
|
|
107
|
+
}) => Promise<AnyRagTool[]> | AnyRagTool[];
|
|
108
|
+
/**
|
|
109
|
+
* Если true — когда RAG не находит контекста (NO_CONTEXT_MARKER) бот всё
|
|
110
|
+
* равно отвечает через `generateSoftFallback` (честное «уточню и вернусь»
|
|
111
|
+
* без выдумывания конкретики). Вопрос при этом логируется в kb_suggestions.
|
|
112
|
+
*
|
|
113
|
+
* Если false (по умолчанию) — бот молчит (возвращает null), как раньше.
|
|
114
|
+
*/
|
|
115
|
+
softFallback?: boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Фабрика KbSuggestionsRepo для fire-and-forget логирования незакрытых вопросов.
|
|
118
|
+
* Используется только если softFallback=true.
|
|
119
|
+
*/
|
|
120
|
+
resolveSuggestions?: (tenantId: number) => KbSuggestionsRepo;
|
|
121
|
+
/**
|
|
122
|
+
* Conversation compaction: если кол-во сообщений в диалоге достигает порога —
|
|
123
|
+
* pipeline генерирует резюме и сохраняет его в conversations.summary_json.
|
|
124
|
+
* Резюме передаётся в answerWithRag как conversationSummary, сокращая effective
|
|
125
|
+
* history window и предотвращая overflow LLM context.
|
|
126
|
+
*
|
|
127
|
+
* Default: 20. Отключить: 0 или Infinity.
|
|
128
|
+
*/
|
|
129
|
+
compactAfterMessages?: number;
|
|
130
|
+
/**
|
|
131
|
+
* Фабрика ConversationsRepo для сохранения compaction summary.
|
|
132
|
+
* Если не задан — compaction происходит в памяти (summary не сохраняется).
|
|
133
|
+
*/
|
|
134
|
+
resolveConversations?: (tenantId: number) => ConversationsRepo;
|
|
135
|
+
/**
|
|
136
|
+
* Optional cross-encoder reranker resolver. Called once per turn — should
|
|
137
|
+
* return a `Reranker` instance (Jina or Cohere) configured for the tenant,
|
|
138
|
+
* or null/undefined if no reranker is configured. Results are expected to be
|
|
139
|
+
* cached by the caller (building a reranker per-call is cheap — the API key
|
|
140
|
+
* lookup is the expensive part, which the caller should cache).
|
|
141
|
+
*/
|
|
142
|
+
resolveReranker?: (input: {
|
|
143
|
+
tenantId: number;
|
|
144
|
+
}) => Promise<Reranker | null> | Reranker | null;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Helper: распарсить style.config_json (из storage StyleRow) в типизированный
|
|
148
|
+
* rag's Style через zod StyleSchema. Возвращает null если JSON невалидный —
|
|
149
|
+
* pipeline'у тогда fall back на DEFAULT_PERSONA вместо crash'а.
|
|
150
|
+
*
|
|
151
|
+
* Использовать снаружи (apps/api) при построении resolveStyle hook'а:
|
|
152
|
+
* const styleRow = await stylesRepo.findActiveBySlug(slug);
|
|
153
|
+
* return parseStyleConfig(styleRow.configJson);
|
|
154
|
+
*/
|
|
155
|
+
export declare function parseStyleConfig(configJson: string): Style | null;
|
|
156
|
+
export declare class RagReplyStrategy implements ReplyStrategy {
|
|
157
|
+
private readonly opts;
|
|
158
|
+
private readonly messagesRepoFor;
|
|
159
|
+
constructor(opts: RagReplyStrategyOpts, messagesRepoFor: (tenantId: number) => MessagesRepo);
|
|
160
|
+
generate(input: {
|
|
161
|
+
tenant: {
|
|
162
|
+
tenantId: number;
|
|
163
|
+
};
|
|
164
|
+
channel: {
|
|
165
|
+
channelId: number;
|
|
166
|
+
};
|
|
167
|
+
conversationId: number;
|
|
168
|
+
contactId: number;
|
|
169
|
+
inbound: {
|
|
170
|
+
externalUserId: string;
|
|
171
|
+
};
|
|
172
|
+
userMessageText: string;
|
|
173
|
+
}): Promise<OutboundEnvelope[] | null>;
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=rag-reply.d.ts.map
|