@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Alexander Kireev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# @chatman-media/conversation-engine
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@chatman-media/conversation-engine)
|
|
4
|
+
[](https://github.com/chatman-media/lead-engine/actions/workflows/ci.yml)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
[](https://bun.sh/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
Channel-agnostic inbound pipeline: contact-resolve → conversation lookup → mode routing → AI reply / queued / human → outbound. The heart of the data plane.
|
|
10
|
+
|
|
11
|
+
Part of the [**lead-engine**](https://github.com/chatman-media/lead-engine) monorepo — a multi-tenant SaaS platform for AI sales bots on Telegram / WhatsApp.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun add @chatman-media/conversation-engine # Bun
|
|
17
|
+
npm install @chatman-media/conversation-engine # npm / pnpm / yarn
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## License
|
|
21
|
+
|
|
22
|
+
[MIT](LICENSE) — Alexander Kireev / [chatman-media](https://github.com/chatman-media)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ChatClient, ChatMessage } from "@chatman-media/llm-router";
|
|
2
|
+
/**
|
|
3
|
+
* Conversation compaction — summarise a message list into a dense text block
|
|
4
|
+
* that fits into the `conversationSummary` field of answerWithRag.
|
|
5
|
+
*
|
|
6
|
+
* Produced summary is ≤ ~500 words. The call is intentionally cheap:
|
|
7
|
+
* - temperature = 0 (deterministic, no creativity needed)
|
|
8
|
+
* - short max-tokens
|
|
9
|
+
* - no KB retrieval — pure LLM condensation
|
|
10
|
+
*
|
|
11
|
+
* @param messages Recent messages chronologically (oldest first).
|
|
12
|
+
* @param chat Chat LLM client (same as the main pipeline).
|
|
13
|
+
* @returns Russian-language summary text, or null if messages list is empty.
|
|
14
|
+
*/
|
|
15
|
+
export declare function compactConversation(messages: ChatMessage[], chat: ChatClient): Promise<string | null>;
|
|
16
|
+
//# sourceMappingURL=compact-conversation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compact-conversation.d.ts","sourceRoot":"","sources":["../src/compact-conversation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAEzE;;;;;;;;;;;;GAYG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,WAAW,EAAE,EACvB,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA+BxB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compact-conversation.test.d.ts","sourceRoot":"","sources":["../src/compact-conversation.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Inbound } from "@chatman-media/channel-core";
|
|
2
|
+
import type { ChannelIdentitiesRepo, ContactRow, ContactsRepo } from "./dal/index.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Резолвит Contact по входящему сообщению. Если ChannelIdentity для
|
|
5
|
+
* (channel, externalUserId) уже есть — возвращает существующий Contact;
|
|
6
|
+
* иначе создаёт новый Contact + ChannelIdentity. Идемпотентно при
|
|
7
|
+
* параллельных webhooks (на уровне БД UNIQUE(channel_id, external_user_id)
|
|
8
|
+
* защищает от дублей).
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveContact(opts: {
|
|
11
|
+
inbound: Inbound;
|
|
12
|
+
channelDbId: number;
|
|
13
|
+
contacts: ContactsRepo;
|
|
14
|
+
identities: ChannelIdentitiesRepo;
|
|
15
|
+
}): Promise<ContactRow>;
|
|
16
|
+
//# sourceMappingURL=contact-resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contact-resolver.d.ts","sourceRoot":"","sources":["../src/contact-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,KAAK,EAAE,qBAAqB,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEtF;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE;IACzC,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,YAAY,CAAC;IACvB,UAAU,EAAE,qBAAqB,CAAC;CACnC,GAAG,OAAO,CAAC,UAAU,CAAC,CA0BtB"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ConversationRow, ConversationsRepo } from "./dal/index.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Find-or-create conversation для (contact, channel.kind). Уникальный
|
|
4
|
+
* conversations.user_id × source гарантирует одну строку на канал.
|
|
5
|
+
* Если контакт пишет одновременно в bot и userbot — получает две
|
|
6
|
+
* независимые conversations.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveConversation(opts: {
|
|
9
|
+
contactId: number;
|
|
10
|
+
channelKind: string;
|
|
11
|
+
conversations: ConversationsRepo;
|
|
12
|
+
nowEpoch: number;
|
|
13
|
+
}): Promise<{
|
|
14
|
+
conversation: ConversationRow;
|
|
15
|
+
created: boolean;
|
|
16
|
+
}>;
|
|
17
|
+
//# sourceMappingURL=conversation-resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversation-resolver.d.ts","sourceRoot":"","sources":["../src/conversation-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAqBzE;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,iBAAiB,CAAC;IACjC,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC;IAAE,YAAY,EAAE,eAAe,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAa/D"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { RepoCtx } from "./types.ts";
|
|
2
|
+
export interface ChannelIdentityRow {
|
|
3
|
+
id: number;
|
|
4
|
+
contactId: number;
|
|
5
|
+
channelId: number;
|
|
6
|
+
externalUserId: string;
|
|
7
|
+
createdAt: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* channel_identities не имеет tenant_id напрямую — scoped через channels.tenant_id.
|
|
11
|
+
* RepoCtx.tenantId всё равно прокидывается для defence-in-depth: SELECT'ы JOIN'ят
|
|
12
|
+
* с channels и фильтруют по tenant_id, чтобы случайный bug в channel-resolver'е
|
|
13
|
+
* не вернул foreign-tenant'овский row.
|
|
14
|
+
*/
|
|
15
|
+
export declare class ChannelIdentitiesRepo {
|
|
16
|
+
private readonly ctx;
|
|
17
|
+
constructor(ctx: RepoCtx);
|
|
18
|
+
/** Найти существующую идентичность по (channelId, externalUserId). */
|
|
19
|
+
find(channelId: number, externalUserId: string): Promise<ChannelIdentityRow | null>;
|
|
20
|
+
create(opts: {
|
|
21
|
+
contactId: number;
|
|
22
|
+
channelId: number;
|
|
23
|
+
externalUserId: string;
|
|
24
|
+
}): Promise<ChannelIdentityRow>;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=channel-identities.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel-identities.d.ts","sourceRoot":"","sources":["../../src/dal/channel-identities.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;GAKG;AACH,qBAAa,qBAAqB;IACpB,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,OAAO;IAEzC,sEAAsE;IAChE,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IAanF,MAAM,CAAC,IAAI,EAAE;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,cAAc,EAAE,MAAM,CAAC;KACxB,GAAG,OAAO,CAAC,kBAAkB,CAAC;CAYhC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { RepoCtx } from "./types.ts";
|
|
2
|
+
export interface ContactRow {
|
|
3
|
+
id: number;
|
|
4
|
+
tenantId: number;
|
|
5
|
+
displayName: string | null;
|
|
6
|
+
attributesJson: string | null;
|
|
7
|
+
createdAt: number;
|
|
8
|
+
updatedAt: number;
|
|
9
|
+
}
|
|
10
|
+
export declare class ContactsRepo {
|
|
11
|
+
private readonly ctx;
|
|
12
|
+
constructor(ctx: RepoCtx);
|
|
13
|
+
byId(id: number): Promise<ContactRow | null>;
|
|
14
|
+
create(opts: {
|
|
15
|
+
displayName?: string;
|
|
16
|
+
attributesJson?: string;
|
|
17
|
+
}): Promise<ContactRow>;
|
|
18
|
+
/**
|
|
19
|
+
* Merge'нет partialAttributes поверх существующих contact.attributes_json
|
|
20
|
+
* и обновит updated_at. Если partial пустой — no-op (без UPDATE).
|
|
21
|
+
*
|
|
22
|
+
* JSON merging — shallow: ключи верхнего уровня в partial перезаписывают
|
|
23
|
+
* существующие; вложенные объекты НЕ деep-merge'атся (это позволяет
|
|
24
|
+
* вертикали atomically заменить целый struct, например visa_docs).
|
|
25
|
+
*
|
|
26
|
+
* Возвращает обновлённый ContactRow.
|
|
27
|
+
*/
|
|
28
|
+
mergeAttributes(contactId: number, partial: Record<string, unknown>, nowEpoch: number): Promise<ContactRow | null>;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=contacts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contacts.d.ts","sourceRoot":"","sources":["../../src/dal/contacts.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,YAAY;IACX,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,OAAO;IAEnC,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAQ5C,MAAM,CAAC,IAAI,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,UAAU,CAAC;IAa1F;;;;;;;;;OASG;IACG,eAAe,CACnB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;CAmB9B"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { RepoCtx } from "./types.ts";
|
|
2
|
+
export interface ConversationRow {
|
|
3
|
+
id: number;
|
|
4
|
+
tenantId: number;
|
|
5
|
+
userId: number;
|
|
6
|
+
source: string;
|
|
7
|
+
mode: "ai" | "queued" | "human";
|
|
8
|
+
status: "open" | "pending" | "resolved";
|
|
9
|
+
unreadCount: number;
|
|
10
|
+
lastMessageText: string | null;
|
|
11
|
+
escalatedAt: number | null;
|
|
12
|
+
assignedAdminId: number | null;
|
|
13
|
+
lastMessageAt: number | null;
|
|
14
|
+
createdAt: number;
|
|
15
|
+
styleId: number | null;
|
|
16
|
+
experimentId: number | null;
|
|
17
|
+
currentStage: string | null;
|
|
18
|
+
summaryJson: string | null;
|
|
19
|
+
metaJson: string | null;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Conversations repo. Поле `userId` — legacy: было задумано как ссылка
|
|
23
|
+
* на старую таблицу users. Сейчас conversations.user_id хранит contact.id
|
|
24
|
+
* (мы reused колонку чтобы избежать схема-миграции при backfill).
|
|
25
|
+
* После Этапа 8 (миграция users → contacts) переименуем колонку в
|
|
26
|
+
* contact_id.
|
|
27
|
+
*
|
|
28
|
+
* `source` — legacy enum 'bot|userbot|self_play'. В будущей миграции
|
|
29
|
+
* заменяется на channel_id напрямую (FK на channels). Сейчас contactId
|
|
30
|
+
* прокидывается дополнительно — каждая conversation per (contact_id, source).
|
|
31
|
+
*/
|
|
32
|
+
export declare class ConversationsRepo {
|
|
33
|
+
private readonly ctx;
|
|
34
|
+
constructor(ctx: RepoCtx);
|
|
35
|
+
/**
|
|
36
|
+
* Найти существующую conversation per (contactId, source). UNIQUE
|
|
37
|
+
* uniq_conversations_user_source гарантирует одну строку.
|
|
38
|
+
*/
|
|
39
|
+
findByContactAndSource(contactId: number, source: string): Promise<ConversationRow | null>;
|
|
40
|
+
create(opts: {
|
|
41
|
+
contactId: number;
|
|
42
|
+
source: string;
|
|
43
|
+
mode?: "ai" | "queued" | "human";
|
|
44
|
+
styleId?: number | null;
|
|
45
|
+
experimentId?: number | null;
|
|
46
|
+
nowEpoch: number;
|
|
47
|
+
}): Promise<ConversationRow>;
|
|
48
|
+
/**
|
|
49
|
+
* Последние N conversations tenant'а, sorted DESC by last_message_at
|
|
50
|
+
* (NULLS LAST). Для admin-UI inbox.
|
|
51
|
+
*/
|
|
52
|
+
recent(limit: number): Promise<ConversationRow[]>;
|
|
53
|
+
touchLastMessageAt(conversationId: number, nowEpoch: number): Promise<void>;
|
|
54
|
+
updateInboxMetadata(conversationId: number, opts: {
|
|
55
|
+
lastMessageText?: string;
|
|
56
|
+
lastMessageAt?: number;
|
|
57
|
+
incrementUnread?: boolean;
|
|
58
|
+
status?: "open" | "pending" | "resolved";
|
|
59
|
+
}): Promise<void>;
|
|
60
|
+
markAsRead(conversationId: number): Promise<void>;
|
|
61
|
+
setAssignee(conversationId: number, adminId: number | null): Promise<void>;
|
|
62
|
+
/** Сохранить сжатое резюме диалога (conversation compaction). */
|
|
63
|
+
setSummaryJson(conversationId: number, summaryJson: string): Promise<void>;
|
|
64
|
+
/** Загрузить conversation по ID. Null если не найдена (или другой tenant). */
|
|
65
|
+
findById(conversationId: number): Promise<ConversationRow | null>;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=conversations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversations.d.ts","sourceRoot":"","sources":["../../src/dal/conversations.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,IAAI,GAAG,QAAQ,GAAG,OAAO,CAAC;IAChC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,UAAU,CAAC;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED;;;;;;;;;;GAUG;AACH,qBAAa,iBAAiB;IAChB,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,OAAO;IAEzC;;;OAGG;IACG,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAc1F,MAAM,CAAC,IAAI,EAAE;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,IAAI,GAAG,QAAQ,GAAG,OAAO,CAAC;QACjC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACxB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC7B,QAAQ,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CAAC,eAAe,CAAC;IAiB5B;;;OAGG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAUjD,kBAAkB,CAAC,cAAc,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAY3E,mBAAmB,CACvB,cAAc,EAAE,MAAM,EACtB,IAAI,EAAE;QACJ,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,eAAe,CAAC,EAAE,OAAO,CAAC;QAC1B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,UAAU,CAAC;KAC1C,GACA,OAAO,CAAC,IAAI,CAAC;IAsBV,UAAU,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYjD,WAAW,CAAC,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAYhF,iEAAiE;IAC3D,cAAc,CAAC,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYhF,8EAA8E;IACxE,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;CAYxE"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { RepoCtx } from "./types.ts";
|
|
2
|
+
export interface ExperimentRow {
|
|
3
|
+
id: number;
|
|
4
|
+
tenantId: number;
|
|
5
|
+
slug: string;
|
|
6
|
+
status: string;
|
|
7
|
+
allocationJson: string;
|
|
8
|
+
successMetric: string;
|
|
9
|
+
startedAt: number | null;
|
|
10
|
+
endedAt: number | null;
|
|
11
|
+
createdAt: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Experiments repo. allocation_json — TEXT с JSON-массивом
|
|
15
|
+
* [{ "style_slug": "empathetic-nepq-v1", "weight": 2 }, ...]
|
|
16
|
+
* status — enum 'draft|running|paused|done' (storage CHECK).
|
|
17
|
+
*/
|
|
18
|
+
export declare class ExperimentsRepo {
|
|
19
|
+
private readonly ctx;
|
|
20
|
+
constructor(ctx: RepoCtx);
|
|
21
|
+
byId(id: number): Promise<ExperimentRow | null>;
|
|
22
|
+
findRunningBySlug(slug: string): Promise<ExperimentRow | null>;
|
|
23
|
+
/**
|
|
24
|
+
* Все experiments tenant'а (для admin-UI list view, включая draft/done).
|
|
25
|
+
* Sorted DESC by created_at — recent first.
|
|
26
|
+
*/
|
|
27
|
+
listAll(): Promise<ExperimentRow[]>;
|
|
28
|
+
create(data: {
|
|
29
|
+
slug: string;
|
|
30
|
+
allocationJson: string;
|
|
31
|
+
successMetric: string;
|
|
32
|
+
}): Promise<ExperimentRow>;
|
|
33
|
+
update(id: number, data: Partial<{
|
|
34
|
+
allocationJson: string;
|
|
35
|
+
successMetric: string;
|
|
36
|
+
}>): Promise<ExperimentRow | null>;
|
|
37
|
+
setStatus(id: number, status: "running" | "paused" | "done"): Promise<ExperimentRow | null>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Parsed единица allocation_json. Weight default 1 если не указан.
|
|
41
|
+
*/
|
|
42
|
+
export interface ExperimentAllocationEntry {
|
|
43
|
+
styleSlug: string;
|
|
44
|
+
weight: number;
|
|
45
|
+
}
|
|
46
|
+
export declare function parseAllocation(allocationJson: string): ExperimentAllocationEntry[];
|
|
47
|
+
//# sourceMappingURL=experiments.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"experiments.d.ts","sourceRoot":"","sources":["../../src/dal/experiments.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,qBAAa,eAAe;IACd,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,OAAO;IAEnC,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAa/C,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAcpE;;;OAGG;IACG,OAAO,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;IASnC,MAAM,CAAC,IAAI,EAAE;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,cAAc,EAAE,MAAM,CAAC;QACvB,aAAa,EAAE,MAAM,CAAC;KACvB,GAAG,OAAO,CAAC,aAAa,CAAC;IAgBpB,MAAM,CACV,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,OAAO,CAAC;QAAE,cAAc,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC,GAC/D,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAS1B,SAAS,CACb,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,MAAM,GACpC,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;CAYjC;AAED;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,eAAe,CAAC,cAAc,EAAE,MAAM,GAAG,yBAAyB,EAAE,CAuBnF"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { ChannelIdentitiesRepo, type ChannelIdentityRow } from "./channel-identities.ts";
|
|
2
|
+
export { KbSuggestionsRepo } from "./kb-suggestions.ts";
|
|
3
|
+
export { ContactsRepo, type ContactRow } from "./contacts.ts";
|
|
4
|
+
export { ConversationsRepo, type ConversationRow } from "./conversations.ts";
|
|
5
|
+
export { DrizzleKbStore } from "./kb-store.ts";
|
|
6
|
+
export { type ExperimentAllocationEntry, ExperimentsRepo, type ExperimentRow, parseAllocation, } from "./experiments.ts";
|
|
7
|
+
export { LeadsRepo, type LeadRow } from "./leads.ts";
|
|
8
|
+
export { MessagesRepo, type MessageRow } from "./messages.ts";
|
|
9
|
+
export { OutboundQueueRepo, type OutboundQueueRow } from "./outbound.ts";
|
|
10
|
+
export { type SkillAggregateRow, type SkillOutcomeRow, SkillOutcomesRepo, } from "./skill-outcomes.ts";
|
|
11
|
+
export { StylesRepo, type StyleRow } from "./styles.ts";
|
|
12
|
+
export { NotificationsRepo, type NotificationRule, type OperatorSettings } from "./notifications.ts";
|
|
13
|
+
export type { Db, RepoCtx } from "./types.ts";
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/dal/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,KAAK,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AACzF,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,KAAK,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC7E,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EACL,KAAK,yBAAyB,EAC9B,eAAe,EACf,KAAK,aAAa,EAClB,eAAe,GAChB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,SAAS,EAAE,KAAK,OAAO,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACzE,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,iBAAiB,GAClB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,KAAK,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACrG,YAAY,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type IKbStore, type KbSearchHit } from "@chatman-media/kb";
|
|
2
|
+
import type { RepoCtx } from "./types.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Drizzle/Postgres implementation `IKbStore` контракта из @chatman-media/kb.
|
|
5
|
+
* Все запросы tenant_id-scoped: WHERE c.tenant_id = ? AND d.tenant_id = ?.
|
|
6
|
+
*
|
|
7
|
+
* Ingest-методы реализованы — пакет нужен и admin'ам (через
|
|
8
|
+
* @chatman-media/kb's ingestFile/Directory) и runtime (search).
|
|
9
|
+
*
|
|
10
|
+
* Особенности относительно legacy kb-store implementation:
|
|
11
|
+
* - tenant_id обязателен в KOAЖ-дый JOIN'е (защита от cross-tenant leak)
|
|
12
|
+
* - конструируем pgvector литерал как '[...]' строку → cast ::vector
|
|
13
|
+
* - sanitizeFtsQuery импортируется из @chatman-media/kb (общая логика)
|
|
14
|
+
* - hybridSearch использует RRF из @chatman-media/kb
|
|
15
|
+
*/
|
|
16
|
+
export declare class DrizzleKbStore implements IKbStore {
|
|
17
|
+
private readonly ctx;
|
|
18
|
+
constructor(ctx: RepoCtx);
|
|
19
|
+
private vec;
|
|
20
|
+
search(embedding: number[], k: number, topic?: string | null): Promise<KbSearchHit[]>;
|
|
21
|
+
private searchBm25;
|
|
22
|
+
/** BM25-only text search (без vector embeddings). Используется MCP-сервером. */
|
|
23
|
+
textSearch(query: string, k?: number, topic?: string | null): Promise<KbSearchHit[]>;
|
|
24
|
+
hybridSearch(input: {
|
|
25
|
+
embedding: number[];
|
|
26
|
+
query: string;
|
|
27
|
+
k?: number;
|
|
28
|
+
topic?: string | null;
|
|
29
|
+
}): Promise<KbSearchHit[]>;
|
|
30
|
+
prioritySearch(input: {
|
|
31
|
+
embedding: number[];
|
|
32
|
+
query: string;
|
|
33
|
+
k?: number;
|
|
34
|
+
vectorOnly?: boolean;
|
|
35
|
+
}): Promise<KbSearchHit[]>;
|
|
36
|
+
getDocumentBySource(source: string): Promise<{
|
|
37
|
+
id: number;
|
|
38
|
+
content_hash: string;
|
|
39
|
+
} | null>;
|
|
40
|
+
countChunksForDocument(documentId: number): Promise<number>;
|
|
41
|
+
deleteDocument(id: number): Promise<boolean>;
|
|
42
|
+
upsertDocument(input: {
|
|
43
|
+
source: string;
|
|
44
|
+
title: string;
|
|
45
|
+
contentHash: string;
|
|
46
|
+
topic?: string | null;
|
|
47
|
+
}): Promise<{
|
|
48
|
+
id: number;
|
|
49
|
+
}>;
|
|
50
|
+
insertChunkWithEmbedding(input: {
|
|
51
|
+
documentId: number;
|
|
52
|
+
chunkIndex: number;
|
|
53
|
+
text: string;
|
|
54
|
+
tokenCount: number;
|
|
55
|
+
embedding: number[];
|
|
56
|
+
}): Promise<void>;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=kb-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kb-store.d.ts","sourceRoot":"","sources":["../../src/dal/kb-store.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,WAAW,EAGjB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C;;;;;;;;;;;;GAYG;AACH,qBAAa,cAAe,YAAW,QAAQ;IACjC,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,OAAO;IAEzC,OAAO,CAAC,GAAG;IAML,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;YAuC7E,UAAU;IA2CxB,gFAAgF;IAC1E,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC,SAAI,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAI/E,YAAY,CAAC,KAAK,EAAE;QACxB,SAAS,EAAE,MAAM,EAAE,CAAC;QACpB,KAAK,EAAE,MAAM,CAAC;QACd,CAAC,CAAC,EAAE,MAAM,CAAC;QACX,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KACvB,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAapB,cAAc,CAAC,KAAK,EAAE;QAC1B,SAAS,EAAE,MAAM,EAAE,CAAC;QACpB,KAAK,EAAE,MAAM,CAAC;QACd,CAAC,CAAC,EAAE,MAAM,CAAC;QACX,UAAU,CAAC,EAAE,OAAO,CAAC;KACtB,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAWpB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAWzF,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAU3D,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAmB5C,cAAc,CAAC,KAAK,EAAE;QAC1B,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,MAAM,CAAC;QACd,WAAW,EAAE,MAAM,CAAC;QACpB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KACvB,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IAgBrB,wBAAwB,CAAC,KAAK,EAAE;QACpC,UAAU,EAAE,MAAM,CAAC;QACnB,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,EAAE,MAAM,CAAC;QACb,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,EAAE,MAAM,EAAE,CAAC;KACrB,GAAG,OAAO,CAAC,IAAI,CAAC;CAQlB"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { RepoCtx } from "./types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Repo для логирования вопросов, на которые бот не смог найти ответ в KB.
|
|
4
|
+
*
|
|
5
|
+
* Операторы видят эти записи в admin-UI и могут:
|
|
6
|
+
* - Принять (ingested) → написать документ и загрузить в KB.
|
|
7
|
+
* - Отклонить (rejected) → пометить как нерелевантный вопрос.
|
|
8
|
+
*
|
|
9
|
+
* Используется в RagReplyStrategy когда answerWithRag возвращает NO_CONTEXT_MARKER.
|
|
10
|
+
*/
|
|
11
|
+
export declare class KbSuggestionsRepo {
|
|
12
|
+
private readonly ctx;
|
|
13
|
+
constructor(ctx: RepoCtx);
|
|
14
|
+
/**
|
|
15
|
+
* Записать незакрытый вопрос.
|
|
16
|
+
* Fire-and-forget: ошибки залоггированы, но не пробрасываются — чтобы не
|
|
17
|
+
* блокировать основной pipeline если insert упадёт.
|
|
18
|
+
*/
|
|
19
|
+
log(opts: {
|
|
20
|
+
questionText: string;
|
|
21
|
+
sourceConversationId?: number;
|
|
22
|
+
sourceMessageId?: number;
|
|
23
|
+
nowEpoch: number;
|
|
24
|
+
}): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=kb-suggestions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kb-suggestions.d.ts","sourceRoot":"","sources":["../../src/dal/kb-suggestions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C;;;;;;;;GAQG;AACH,qBAAa,iBAAiB;IAChB,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,OAAO;IAEzC;;;;OAIG;IACG,GAAG,CAAC,IAAI,EAAE;QACd,YAAY,EAAE,MAAM,CAAC;QACrB,oBAAoB,CAAC,EAAE,MAAM,CAAC;QAC9B,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,QAAQ,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CAAC,IAAI,CAAC;CAalB"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { RepoCtx } from "./types.ts";
|
|
2
|
+
export interface LeadRow {
|
|
3
|
+
id: number;
|
|
4
|
+
tenantId: number;
|
|
5
|
+
userId: number;
|
|
6
|
+
state: string;
|
|
7
|
+
assignedAdminId?: number | null;
|
|
8
|
+
intakeJson: string | null;
|
|
9
|
+
visaDocsJson: string | null;
|
|
10
|
+
applicationId: string | null;
|
|
11
|
+
opsChatId: number | null;
|
|
12
|
+
opsMessageId: number | null;
|
|
13
|
+
rejectedReason: string | null;
|
|
14
|
+
decidedByAdminId: number | null;
|
|
15
|
+
decidedAt: number | null;
|
|
16
|
+
lastCheckinAt: number | null;
|
|
17
|
+
visaInterviewField: string | null;
|
|
18
|
+
createdAt: number;
|
|
19
|
+
updatedAt: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Leads repo. Schema-колонка user_id хранит contact.id (см. ConversationsRepo
|
|
23
|
+
* docstring о legacy-имени). UNIQUE(user_id) гарантирует один лид на контакт
|
|
24
|
+
* — даже при нескольких conversations (bot + userbot) лид всё ещё один.
|
|
25
|
+
*/
|
|
26
|
+
export declare class LeadsRepo {
|
|
27
|
+
private readonly ctx;
|
|
28
|
+
constructor(ctx: RepoCtx);
|
|
29
|
+
byId(id: number): Promise<LeadRow | null>;
|
|
30
|
+
findByContactId(contactId: number): Promise<LeadRow | null>;
|
|
31
|
+
create(opts: {
|
|
32
|
+
contactId: number;
|
|
33
|
+
state: string;
|
|
34
|
+
nowEpoch: number;
|
|
35
|
+
}): Promise<LeadRow>;
|
|
36
|
+
updateState(leadId: number, newState: string, nowEpoch: number): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=leads.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"leads.d.ts","sourceRoot":"","sources":["../../src/dal/leads.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,qBAAa,SAAS;IACR,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,OAAO;IAEnC,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAQzC,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAU3D,MAAM,CAAC,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IAetF,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAMrF"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { RepoCtx } from "./types.ts";
|
|
2
|
+
export interface MessageRow {
|
|
3
|
+
id: number;
|
|
4
|
+
tenantId: number;
|
|
5
|
+
conversationId: number;
|
|
6
|
+
role: "user" | "assistant" | "human" | "system";
|
|
7
|
+
text: string;
|
|
8
|
+
tgMessageId: number | null;
|
|
9
|
+
metaJson: string | null;
|
|
10
|
+
createdAt: number;
|
|
11
|
+
stage: string | null;
|
|
12
|
+
deletedAt: number | null;
|
|
13
|
+
}
|
|
14
|
+
export declare class MessagesRepo {
|
|
15
|
+
private readonly ctx;
|
|
16
|
+
constructor(ctx: RepoCtx);
|
|
17
|
+
insert(opts: {
|
|
18
|
+
conversationId: number;
|
|
19
|
+
role: "user" | "assistant" | "human" | "system";
|
|
20
|
+
text: string;
|
|
21
|
+
externalMessageId?: string;
|
|
22
|
+
metaJson?: string;
|
|
23
|
+
stage?: string;
|
|
24
|
+
nowEpoch: number;
|
|
25
|
+
}): Promise<MessageRow>;
|
|
26
|
+
/**
|
|
27
|
+
* Дедупликационный path для пользовательских сообщений: один tg_message_id
|
|
28
|
+
* per (conversation_id, role='user') уже защищён partial UNIQUE
|
|
29
|
+
* uniq_msg_user_tg, но мы делаем явный pre-check чтобы вернуть существующую
|
|
30
|
+
* строку вместо ON CONFLICT (нужны её id и timestamp для downstream).
|
|
31
|
+
*/
|
|
32
|
+
findUserByExternalId(conversationId: number, externalMessageId: string): Promise<MessageRow | null>;
|
|
33
|
+
/**
|
|
34
|
+
* Последние N не-удалённых сообщений в conversation, в хронологическом
|
|
35
|
+
* порядке (oldest → newest). Используется ReplyStrategy для построения
|
|
36
|
+
* history-промпта.
|
|
37
|
+
*
|
|
38
|
+
* Skip'ает soft-deleted (deleted_at NOT NULL) и system-role (их не
|
|
39
|
+
* показываем LLM как историю — они для внутренних флагов).
|
|
40
|
+
*/
|
|
41
|
+
recent(conversationId: number, limit: number): Promise<MessageRow[]>;
|
|
42
|
+
/**
|
|
43
|
+
* Подсчитать общее кол-во non-deleted, non-system сообщений в диалоге.
|
|
44
|
+
* Используется для compaction threshold check.
|
|
45
|
+
*/
|
|
46
|
+
countByConversation(conversationId: number): Promise<number>;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=messages.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/dal/messages.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,OAAO,GAAG,QAAQ,CAAC;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,qBAAa,YAAY;IACX,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,OAAO;IAEnC,MAAM,CAAC,IAAI,EAAE;QACjB,cAAc,EAAE,MAAM,CAAC;QACvB,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,OAAO,GAAG,QAAQ,CAAC;QAChD,IAAI,EAAE,MAAM,CAAC;QACb,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CAAC,UAAU,CAAC;IAoBvB;;;;;OAKG;IACG,oBAAoB,CACxB,cAAc,EAAE,MAAM,EACtB,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAiB7B;;;;;;;OAOG;IACG,MAAM,CAAC,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAiB1E;;;OAGG;IACG,mBAAmB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAcnE"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
2
|
+
import { type notificationRules as nrTable, type operatorSettings as osTable, type notificationTemplates as ntTable } from "@chatman-media/storage";
|
|
3
|
+
export type NotificationRule = typeof nrTable.$inferSelect;
|
|
4
|
+
export type OperatorSettings = typeof osTable.$inferSelect;
|
|
5
|
+
export type NotificationTemplate = typeof ntTable.$inferSelect;
|
|
6
|
+
export declare class NotificationsRepo {
|
|
7
|
+
private readonly db;
|
|
8
|
+
constructor(db: PostgresJsDatabase);
|
|
9
|
+
findRulesByEvent(tenantId: number, eventType: string): Promise<NotificationRule[]>;
|
|
10
|
+
findOperatorSettings(adminId: number): Promise<OperatorSettings | undefined>;
|
|
11
|
+
findByLinkToken(token: string): Promise<OperatorSettings | undefined>;
|
|
12
|
+
generateLinkToken(adminId: number, tenantId: number): Promise<string>;
|
|
13
|
+
linkChat(adminId: number, telegramChatId: string): Promise<void>;
|
|
14
|
+
findOperatorSettingsByTenant(tenantId: number): Promise<OperatorSettings[]>;
|
|
15
|
+
partialUpdateSettings(adminId: number, tenantId: number, fields: Partial<Pick<OperatorSettings, "telegramChatId" | "notifyOnAssignedOnly">>): Promise<void>;
|
|
16
|
+
upsertOperatorSettings(settings: Omit<OperatorSettings, "id" | "updatedAt">): Promise<void>;
|
|
17
|
+
createRule(rule: Omit<NotificationRule, "id" | "createdAt" | "updatedAt">): Promise<NotificationRule>;
|
|
18
|
+
listRules(tenantId: number): Promise<NotificationRule[]>;
|
|
19
|
+
deleteRule(tenantId: number, id: number): Promise<void>;
|
|
20
|
+
findTemplate(tenantId: number, slug: string): Promise<NotificationTemplate | undefined>;
|
|
21
|
+
upsertTemplate(tpl: Omit<NotificationTemplate, "id" | "updatedAt">): Promise<void>;
|
|
22
|
+
listTemplates(tenantId: number): Promise<NotificationTemplate[]>;
|
|
23
|
+
deleteTemplate(tenantId: number, slug: string): Promise<void>;
|
|
24
|
+
generateGroupLinkToken(tenantId: number, adminId: number, eventType: string): Promise<string>;
|
|
25
|
+
findGroupLinkToken(token: string): Promise<{
|
|
26
|
+
tenantId: number;
|
|
27
|
+
adminId: number;
|
|
28
|
+
eventType: string;
|
|
29
|
+
} | undefined>;
|
|
30
|
+
deleteGroupLinkToken(token: string): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=notifications.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notifications.d.ts","sourceRoot":"","sources":["../../src/dal/notifications.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,EAAuF,KAAK,iBAAiB,IAAI,OAAO,EAAE,KAAK,gBAAgB,IAAI,OAAO,EAAE,KAAK,qBAAqB,IAAI,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAEzO,MAAM,MAAM,gBAAgB,GAAG,OAAO,OAAO,CAAC,YAAY,CAAC;AAC3D,MAAM,MAAM,gBAAgB,GAAG,OAAO,OAAO,CAAC,YAAY,CAAC;AAC3D,MAAM,MAAM,oBAAoB,GAAG,OAAO,OAAO,CAAC,YAAY,CAAC;AAE/D,qBAAa,iBAAiB;IAChB,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,kBAAkB;IAE7C,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAalF,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC;IAS5E,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC;IAerE,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAwBrE,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYhE,4BAA4B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAO3E,qBAAqB,CACzB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,gBAAgB,EAAE,gBAAgB,GAAG,sBAAsB,CAAC,CAAC,GACjF,OAAO,CAAC,IAAI,CAAC;IAWV,sBAAsB,CAAC,QAAQ,EAAE,IAAI,CAAC,gBAAgB,EAAE,IAAI,GAAG,WAAW,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAc3F,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,gBAAgB,EAAE,IAAI,GAAG,WAAW,GAAG,WAAW,CAAC,GAAG,OAAO,CAAC,gBAAgB,CAAC;IASrG,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAOxD,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQvD,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC;IASvF,cAAc,CAAC,GAAG,EAAE,IAAI,CAAC,oBAAoB,EAAE,IAAI,GAAG,WAAW,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAalF,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAOhE,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAM7D,sBAAsB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAc7F,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;IAUhH,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGzD"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { OutboundEnvelope } from "@chatman-media/channel-core";
|
|
2
|
+
import type { RepoCtx } from "./types.ts";
|
|
3
|
+
export interface OutboundQueueRow {
|
|
4
|
+
id: number;
|
|
5
|
+
tenantId: number;
|
|
6
|
+
channelId: number;
|
|
7
|
+
conversationId: number | null;
|
|
8
|
+
payloadJson: string;
|
|
9
|
+
idempotencyKey: string | null;
|
|
10
|
+
scheduledAt: number;
|
|
11
|
+
status: "pending" | "processing" | "sent" | "failed" | "cancelled";
|
|
12
|
+
attempt: number;
|
|
13
|
+
lastError: string | null;
|
|
14
|
+
externalMessageId: string | null;
|
|
15
|
+
sentAt: number | null;
|
|
16
|
+
createdAt: number;
|
|
17
|
+
}
|
|
18
|
+
export declare class OutboundQueueRepo {
|
|
19
|
+
private readonly ctx;
|
|
20
|
+
constructor(ctx: RepoCtx);
|
|
21
|
+
/**
|
|
22
|
+
* Поставить envelope в очередь. Идемпотентный insert через
|
|
23
|
+
* idempotency_key — если строка уже существует, возвращается имеющаяся
|
|
24
|
+
* вместо дубля. Это защищает от retry'я process-inbound'а после краша
|
|
25
|
+
* worker'а (Telegram повторно постит webhook).
|
|
26
|
+
*/
|
|
27
|
+
enqueue(opts: {
|
|
28
|
+
channelId: number;
|
|
29
|
+
conversationId?: number | null;
|
|
30
|
+
envelope: OutboundEnvelope;
|
|
31
|
+
scheduledAt?: number;
|
|
32
|
+
nowEpoch: number;
|
|
33
|
+
}): Promise<OutboundQueueRow>;
|
|
34
|
+
/**
|
|
35
|
+
* Атомарно claim'ает до `limit` pending envelope'ов: UPDATE'ом переводит
|
|
36
|
+
* их в status='processing' и returning'ом отдаёт worker'у. Использует
|
|
37
|
+
* `FOR UPDATE SKIP LOCKED` в inner SELECT — multi-worker safe: каждая
|
|
38
|
+
* row claim'ается ровно одним worker'ом, остальные пропускают её
|
|
39
|
+
* без блокировки.
|
|
40
|
+
*
|
|
41
|
+
* Worker'у обязательно вызвать markSent или markFailed после processing —
|
|
42
|
+
* row застрянет в processing иначе. Cleanup-cron для возврата
|
|
43
|
+
* processing→pending после timeout'а — отдельный job в Issue #3 / M-2.
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* @param opts.kinds Опциональный whitelist `channels.kind` для claim'а.
|
|
47
|
+
* Когда задан, JOIN'имся с `channels` и берём только rows для каналов
|
|
48
|
+
* указанных типов. Используется чтобы worker не claim'ил web-rows
|
|
49
|
+
* (адаптер web-канала живёт в apps/api in-memory с WS-connection'ом —
|
|
50
|
+
* worker до него не достанется и mark'нет fail спустя no_adapter).
|
|
51
|
+
* undefined → не фильтруем (legacy / тесты).
|
|
52
|
+
*/
|
|
53
|
+
claimPending(opts: {
|
|
54
|
+
limit: number;
|
|
55
|
+
nowEpoch: number;
|
|
56
|
+
kinds?: string[];
|
|
57
|
+
}): Promise<OutboundQueueRow[]>;
|
|
58
|
+
/**
|
|
59
|
+
* Откатить зависшие processing rows обратно в pending. Идёт по rows
|
|
60
|
+
* у которых status='processing' дольше `stuckSec` секунд (worker умер
|
|
61
|
+
* не дойдя до markSent/markFailed). Cron'ится из apps/worker раз в N минут.
|
|
62
|
+
*/
|
|
63
|
+
releaseStuckProcessing(opts: {
|
|
64
|
+
nowEpoch: number;
|
|
65
|
+
stuckSec: number;
|
|
66
|
+
}): Promise<number>;
|
|
67
|
+
markSent(id: number, externalMessageId: string, nowEpoch: number): Promise<void>;
|
|
68
|
+
markFailed(id: number, error: string): Promise<void>;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=outbound.d.ts.map
|