@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.
Files changed (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +22 -0
  3. package/dist/compact-conversation.d.ts +16 -0
  4. package/dist/compact-conversation.d.ts.map +1 -0
  5. package/dist/compact-conversation.test.d.ts +2 -0
  6. package/dist/compact-conversation.test.d.ts.map +1 -0
  7. package/dist/contact-resolver.d.ts +16 -0
  8. package/dist/contact-resolver.d.ts.map +1 -0
  9. package/dist/conversation-resolver.d.ts +17 -0
  10. package/dist/conversation-resolver.d.ts.map +1 -0
  11. package/dist/dal/channel-identities.d.ts +26 -0
  12. package/dist/dal/channel-identities.d.ts.map +1 -0
  13. package/dist/dal/contacts.d.ts +30 -0
  14. package/dist/dal/contacts.d.ts.map +1 -0
  15. package/dist/dal/conversations.d.ts +67 -0
  16. package/dist/dal/conversations.d.ts.map +1 -0
  17. package/dist/dal/experiments.d.ts +47 -0
  18. package/dist/dal/experiments.d.ts.map +1 -0
  19. package/dist/dal/index.d.ts +14 -0
  20. package/dist/dal/index.d.ts.map +1 -0
  21. package/dist/dal/kb-store.d.ts +58 -0
  22. package/dist/dal/kb-store.d.ts.map +1 -0
  23. package/dist/dal/kb-suggestions.d.ts +26 -0
  24. package/dist/dal/kb-suggestions.d.ts.map +1 -0
  25. package/dist/dal/leads.d.ts +38 -0
  26. package/dist/dal/leads.d.ts.map +1 -0
  27. package/dist/dal/messages.d.ts +48 -0
  28. package/dist/dal/messages.d.ts.map +1 -0
  29. package/dist/dal/notifications.d.ts +32 -0
  30. package/dist/dal/notifications.d.ts.map +1 -0
  31. package/dist/dal/outbound.d.ts +70 -0
  32. package/dist/dal/outbound.d.ts.map +1 -0
  33. package/dist/dal/skill-outcomes.d.ts +58 -0
  34. package/dist/dal/skill-outcomes.d.ts.map +1 -0
  35. package/dist/dal/styles.d.ts +44 -0
  36. package/dist/dal/styles.d.ts.map +1 -0
  37. package/dist/dal/types.d.ts +27 -0
  38. package/dist/dal/types.d.ts.map +1 -0
  39. package/dist/dispatch-reply.d.ts +49 -0
  40. package/dist/dispatch-reply.d.ts.map +1 -0
  41. package/dist/dispatch-reply.test.d.ts +2 -0
  42. package/dist/dispatch-reply.test.d.ts.map +1 -0
  43. package/dist/experiment-router.d.ts +15 -0
  44. package/dist/experiment-router.d.ts.map +1 -0
  45. package/dist/experiment-router.test.d.ts +2 -0
  46. package/dist/experiment-router.test.d.ts.map +1 -0
  47. package/dist/extract-fields.test.d.ts +2 -0
  48. package/dist/extract-fields.test.d.ts.map +1 -0
  49. package/dist/funnel-machine.d.ts +43 -0
  50. package/dist/funnel-machine.d.ts.map +1 -0
  51. package/dist/funnel-machine.test.d.ts +2 -0
  52. package/dist/funnel-machine.test.d.ts.map +1 -0
  53. package/dist/index.d.ts +21 -0
  54. package/dist/index.d.ts.map +1 -0
  55. package/dist/index.js +2024 -0
  56. package/dist/lead-lifecycle.d.ts +46 -0
  57. package/dist/lead-lifecycle.d.ts.map +1 -0
  58. package/dist/lead-lifecycle.test.d.ts +2 -0
  59. package/dist/lead-lifecycle.test.d.ts.map +1 -0
  60. package/dist/memory-extractor.d.ts +62 -0
  61. package/dist/memory-extractor.d.ts.map +1 -0
  62. package/dist/memory-extractor.test.d.ts +2 -0
  63. package/dist/memory-extractor.test.d.ts.map +1 -0
  64. package/dist/notifications.d.ts +32 -0
  65. package/dist/notifications.d.ts.map +1 -0
  66. package/dist/notifications.test.d.ts +2 -0
  67. package/dist/notifications.test.d.ts.map +1 -0
  68. package/dist/operator-bot-handler.d.ts +13 -0
  69. package/dist/operator-bot-handler.d.ts.map +1 -0
  70. package/dist/operator-bot-handler.test.d.ts +2 -0
  71. package/dist/operator-bot-handler.test.d.ts.map +1 -0
  72. package/dist/outbound-dispatch.d.ts +17 -0
  73. package/dist/outbound-dispatch.d.ts.map +1 -0
  74. package/dist/process-inbound.d.ts +126 -0
  75. package/dist/process-inbound.d.ts.map +1 -0
  76. package/dist/process-inbound.test.d.ts +2 -0
  77. package/dist/process-inbound.test.d.ts.map +1 -0
  78. package/dist/reply-strategy/index.d.ts +3 -0
  79. package/dist/reply-strategy/index.d.ts.map +1 -0
  80. package/dist/reply-strategy/llm-reply.d.ts +69 -0
  81. package/dist/reply-strategy/llm-reply.d.ts.map +1 -0
  82. package/dist/reply-strategy/llm-reply.test.d.ts +2 -0
  83. package/dist/reply-strategy/llm-reply.test.d.ts.map +1 -0
  84. package/dist/reply-strategy/rag-reply.d.ts +175 -0
  85. package/dist/reply-strategy/rag-reply.d.ts.map +1 -0
  86. package/dist/rls-guard.d.ts +23 -0
  87. package/dist/rls-guard.d.ts.map +1 -0
  88. package/dist/rls-guard.integration.test.d.ts +2 -0
  89. package/dist/rls-guard.integration.test.d.ts.map +1 -0
  90. package/dist/secrets.d.ts +27 -0
  91. package/dist/secrets.d.ts.map +1 -0
  92. package/dist/secrets.test.d.ts +2 -0
  93. package/dist/secrets.test.d.ts.map +1 -0
  94. package/dist/stage-classifier.d.ts +48 -0
  95. package/dist/stage-classifier.d.ts.map +1 -0
  96. package/dist/testkit.d.ts +82 -0
  97. package/dist/testkit.d.ts.map +1 -0
  98. package/dist/transcriber.d.ts +15 -0
  99. package/dist/transcriber.d.ts.map +1 -0
  100. package/dist/types.d.ts +98 -0
  101. package/dist/types.d.ts.map +1 -0
  102. package/dist/with-tenant.d.ts +25 -0
  103. package/dist/with-tenant.d.ts.map +1 -0
  104. 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
+ [![npm version](https://img.shields.io/npm/v/@chatman-media/conversation-engine?logo=npm&color=22c55e)](https://www.npmjs.com/package/@chatman-media/conversation-engine)
4
+ [![CI](https://github.com/chatman-media/lead-engine/actions/workflows/ci.yml/badge.svg)](https://github.com/chatman-media/lead-engine/actions/workflows/ci.yml)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
6
+ [![Bun](https://img.shields.io/badge/Bun-compatible-fbf0df?logo=bun&logoColor=black)](https://bun.sh/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=compact-conversation.test.d.ts.map
@@ -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