@chatman-media/kb 1.3.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 (112) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +169 -0
  3. package/dist/ab-router.d.ts +66 -0
  4. package/dist/ab-router.d.ts.map +1 -0
  5. package/dist/answer-types.d.ts +194 -0
  6. package/dist/answer-types.d.ts.map +1 -0
  7. package/dist/answer.d.ts +59 -0
  8. package/dist/answer.d.ts.map +1 -0
  9. package/dist/built-in-tools/calendly.d.ts +19 -0
  10. package/dist/built-in-tools/calendly.d.ts.map +1 -0
  11. package/dist/chunk.d.ts +48 -0
  12. package/dist/chunk.d.ts.map +1 -0
  13. package/dist/conversation-store.d.ts +76 -0
  14. package/dist/conversation-store.d.ts.map +1 -0
  15. package/dist/eval.d.ts +64 -0
  16. package/dist/eval.d.ts.map +1 -0
  17. package/dist/extract-user-facts.d.ts +27 -0
  18. package/dist/extract-user-facts.d.ts.map +1 -0
  19. package/dist/fact-checker.d.ts +46 -0
  20. package/dist/fact-checker.d.ts.map +1 -0
  21. package/dist/grade-skills.d.ts +29 -0
  22. package/dist/grade-skills.d.ts.map +1 -0
  23. package/dist/index.d.ts +76 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +62655 -0
  26. package/dist/ingest.d.ts +49 -0
  27. package/dist/ingest.d.ts.map +1 -0
  28. package/dist/multi-query.d.ts +29 -0
  29. package/dist/multi-query.d.ts.map +1 -0
  30. package/dist/parse-pdf.d.ts +14 -0
  31. package/dist/parse-pdf.d.ts.map +1 -0
  32. package/dist/persona-shortcuts.d.ts +51 -0
  33. package/dist/persona-shortcuts.d.ts.map +1 -0
  34. package/dist/prompt.d.ts +9 -0
  35. package/dist/prompt.d.ts.map +1 -0
  36. package/dist/reflect.d.ts +29 -0
  37. package/dist/reflect.d.ts.map +1 -0
  38. package/dist/reranker.d.ts +71 -0
  39. package/dist/reranker.d.ts.map +1 -0
  40. package/dist/retrieval-utils.d.ts +94 -0
  41. package/dist/retrieval-utils.d.ts.map +1 -0
  42. package/dist/retry.d.ts +53 -0
  43. package/dist/retry.d.ts.map +1 -0
  44. package/dist/rewrite-query.d.ts +30 -0
  45. package/dist/rewrite-query.d.ts.map +1 -0
  46. package/dist/sanitize.d.ts +21 -0
  47. package/dist/sanitize.d.ts.map +1 -0
  48. package/dist/semantic-cache.d.ts +70 -0
  49. package/dist/semantic-cache.d.ts.map +1 -0
  50. package/dist/server.d.ts +77 -0
  51. package/dist/server.d.ts.map +1 -0
  52. package/dist/stores/memory-store.d.ts +72 -0
  53. package/dist/stores/memory-store.d.ts.map +1 -0
  54. package/dist/structured-output.d.ts +21 -0
  55. package/dist/structured-output.d.ts.map +1 -0
  56. package/dist/styles.d.ts +186 -0
  57. package/dist/styles.d.ts.map +1 -0
  58. package/dist/summarize-conversation.d.ts +31 -0
  59. package/dist/summarize-conversation.d.ts.map +1 -0
  60. package/dist/system-prompt.d.ts +11 -0
  61. package/dist/system-prompt.d.ts.map +1 -0
  62. package/dist/text-style-rules.d.ts +133 -0
  63. package/dist/text-style-rules.d.ts.map +1 -0
  64. package/dist/tool-loop.d.ts +44 -0
  65. package/dist/tool-loop.d.ts.map +1 -0
  66. package/dist/tools.d.ts +64 -0
  67. package/dist/tools.d.ts.map +1 -0
  68. package/dist/topic-classifier.d.ts +11 -0
  69. package/dist/topic-classifier.d.ts.map +1 -0
  70. package/dist/types.d.ts +83 -0
  71. package/dist/types.d.ts.map +1 -0
  72. package/dist/utils.d.ts +19 -0
  73. package/dist/utils.d.ts.map +1 -0
  74. package/dist/vision.d.ts +72 -0
  75. package/dist/vision.d.ts.map +1 -0
  76. package/package.json +76 -0
  77. package/src/ab-router.ts +118 -0
  78. package/src/answer-types.ts +191 -0
  79. package/src/answer.ts +696 -0
  80. package/src/built-in-tools/calendly.ts +32 -0
  81. package/src/chunk.ts +198 -0
  82. package/src/conversation-store.ts +138 -0
  83. package/src/eval.ts +127 -0
  84. package/src/extract-user-facts.ts +120 -0
  85. package/src/fact-checker.ts +171 -0
  86. package/src/grade-skills.ts +79 -0
  87. package/src/index.ts +191 -0
  88. package/src/ingest.ts +193 -0
  89. package/src/multi-query.ts +89 -0
  90. package/src/parse-pdf.ts +24 -0
  91. package/src/persona-shortcuts.ts +255 -0
  92. package/src/prompt.ts +190 -0
  93. package/src/reflect.ts +99 -0
  94. package/src/reranker.ts +166 -0
  95. package/src/retrieval-utils.ts +209 -0
  96. package/src/retry.ts +139 -0
  97. package/src/rewrite-query.ts +124 -0
  98. package/src/sanitize.ts +44 -0
  99. package/src/semantic-cache.ts +154 -0
  100. package/src/server.ts +164 -0
  101. package/src/stores/memory-store.ts +249 -0
  102. package/src/structured-output.ts +47 -0
  103. package/src/styles.ts +138 -0
  104. package/src/summarize-conversation.ts +88 -0
  105. package/src/system-prompt.ts +118 -0
  106. package/src/text-style-rules.ts +244 -0
  107. package/src/tool-loop.ts +110 -0
  108. package/src/tools.ts +79 -0
  109. package/src/topic-classifier.ts +112 -0
  110. package/src/types.ts +91 -0
  111. package/src/utils.ts +81 -0
  112. package/src/vision.ts +265 -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,169 @@
1
+ # @chatman-media/kb
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@chatman-media/kb?logo=npm&color=22c55e)](https://www.npmjs.com/package/@chatman-media/kb)
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
+ Tenant-scoped Knowledge Base for the Lead Engine platform. Provides a full
10
+ retrieval-augmented generation (RAG) pipeline: hybrid search (pgvector cosine +
11
+ BM25 with RRF fusion), multi-query expansion, cross-encoder reranking, MMR
12
+ diversification, dynamic distance threshold, semantic caching, persona/skill
13
+ composition, photo classification and passport OCR.
14
+
15
+ LLM I/O lives in `@chatman-media/llm-router`.
16
+
17
+ ## Key modules
18
+
19
+ | Module | What it does |
20
+ |---|---|
21
+ | `answer.ts` | Full RAG answer pipeline: retrieve → filter → diversify → rerank → generate |
22
+ | `ingest.ts` | Document ingest: parse → chunk → embed → upsert |
23
+ | `hybrid-search.ts` | pgvector cosine + BM25 keyword fusion via RRF |
24
+ | `retrieval-utils.ts` | `rrfMerge`, `applyDynamicThreshold`, `mmrDiversify` — post-retrieval transforms |
25
+ | `multi-query.ts` | `expandQueries` — LLM-generated query variants for parallel search |
26
+ | `reranker.ts` | `JinaReranker`, `CohereReranker` — cross-encoder second-pass reranking |
27
+ | `rewrite-query.ts` | Context-aware query rewriting (resolves pronouns / ellipsis via history) |
28
+ | `semantic-cache.ts` | Vector-similarity cache for identical/near-identical questions |
29
+ | `vision.ts` | `classifyPhoto()` + `extractPassportIdentity()` — passport OCR via vision LLM |
30
+ | `ab-router.ts` | A/B experiment allocation for styles/personas |
31
+ | `grade-skills.ts` | ELO-based skill grading via judge LLM |
32
+ | `prompt.ts` | `composeSystemPrompt()` — assemble sales persona + KB context + style |
33
+
34
+ ## RAG pipeline
35
+
36
+ Each call to `answerWithRag` / `answerWithRagStream` goes through these stages:
37
+
38
+ ```
39
+ 1. [opt] Query rewrite — resolves pronouns, expands ellipsis (rewriteQueryBeforeRetrieval)
40
+ 2. [opt] Multi-query — generate N variants → embed all in parallel (multiQuery)
41
+ 3. Vector / hybrid search — pgvector cosine or RRF(vector+BM25)
42
+ 4. [opt] RRF merge — fuse N result lists if multi-query was used (rrfMerge)
43
+ 5. [opt] Distance filter — drop hits > threshold (autoTrimDistance)
44
+ 6. [opt] MMR diversify — reduce duplicate chunks (mmr)
45
+ 7. [opt] Cross-encoder — reranker.rerank(query, candidates, topK) (reranker)
46
+ 8. Prompt composition — style + persona + context + skills + hooks
47
+ 9. LLM generation — stream or single response
48
+ 10.[opt] Fact-checker — hallucination guard (reflect)
49
+ ```
50
+
51
+ All stages are optional and controlled per-request via `AnswerInput` fields.
52
+
53
+ ## API
54
+
55
+ ### `answerWithRag(input: AnswerInput): Promise<AnswerResult>`
56
+
57
+ ```ts
58
+ import { answerWithRag, JinaReranker } from "@chatman-media/kb";
59
+
60
+ const result = await answerWithRag({
61
+ question: "сколько стоит квартира в ЖК Марина?",
62
+ kb, // IKbStore implementation
63
+ embedder, // EmbeddingClient
64
+ chat, // ChatClient
65
+
66
+ // ── Retrieval tuning ──────────────────────────────
67
+ topK: 5, // final chunks to pass to the LLM
68
+ hybridSearch: true, // vector + BM25 fusion
69
+ rewriteQueryBeforeRetrieval: true, // resolve "там" / "он" via history
70
+
71
+ // Multi-query expansion (generate 2 rephrases, search 3 in parallel)
72
+ multiQuery: true,
73
+ multiQueryCount: 2,
74
+
75
+ // Drop chunks with cosine distance > 0.45 (reduce hallucinations)
76
+ autoTrimDistance: true,
77
+ autoTrimThreshold: 0.45,
78
+
79
+ // Maximal Marginal Relevance — diversify results
80
+ mmr: true,
81
+ mmrLambda: 0.6, // 1.0 = pure relevance, 0.0 = pure diversity
82
+
83
+ // Cross-encoder reranker (retrieves topK×3 candidates, returns topK)
84
+ reranker: new JinaReranker({ apiKey: process.env.JINA_API_KEY! }),
85
+
86
+ // ── Generation ────────────────────────────────────
87
+ history, // ChatMessage[] — conversation context
88
+ persona, // Persona — bot identity
89
+ style, // Style — sales methodology (SPIN / NEPQ / AIDA)
90
+ stage, // FunnelStage — current funnel stage
91
+ skills, // SkillForPrompt[] — active persuasion skills
92
+ reflect: true, // hallucination guard (LLM judge)
93
+
94
+ onTelemetry: (t) => console.log(t), // retrieval_ms, top_distances, path, ...
95
+ });
96
+
97
+ console.log(result.text); // generated reply
98
+ console.log(result.hits); // KbSearchHit[] — chunks used
99
+ console.log(result.usedChunkIds); // chunk IDs referenced in the reply
100
+ ```
101
+
102
+ ### Rerankers
103
+
104
+ ```ts
105
+ import { JinaReranker, CohereReranker } from "@chatman-media/kb";
106
+
107
+ // Jina — multilingual, good for Russian (jina-reranker-v2-base-multilingual)
108
+ const reranker = new JinaReranker({
109
+ apiKey: process.env.JINA_API_KEY!,
110
+ model: "jina-reranker-v2-base-multilingual", // default
111
+ });
112
+
113
+ // Cohere — also multilingual (rerank-v3.5)
114
+ const reranker = new CohereReranker({
115
+ apiKey: process.env.COHERE_API_KEY!,
116
+ model: "rerank-v3.5", // default
117
+ });
118
+ ```
119
+
120
+ ### Post-retrieval utilities
121
+
122
+ ```ts
123
+ import { rrfMerge, applyDynamicThreshold, mmrDiversify } from "@chatman-media/kb";
124
+
125
+ // Merge results from multiple queries via Reciprocal Rank Fusion
126
+ const merged = rrfMerge([hitsFromQuery1, hitsFromQuery2, hitsFromQuery3], { topN: 15 });
127
+
128
+ // Drop hits with cosine distance > 0.4 (keep at least 1)
129
+ const trimmed = applyDynamicThreshold(hits, { threshold: 0.4, minHits: 1 });
130
+
131
+ // Maximal Marginal Relevance — diversify, reduce duplicates
132
+ const diverse = mmrDiversify(hits, { lambda: 0.6, topK: 5 });
133
+ ```
134
+
135
+ ### Vision
136
+
137
+ ```ts
138
+ import { classifyPhoto, extractPassportIdentity } from "@chatman-media/kb";
139
+
140
+ const cls = await classifyPhoto({
141
+ bytes: await res.arrayBuffer(),
142
+ model: "gpt-4o",
143
+ apiKey: "sk-...",
144
+ provider: "openai",
145
+ });
146
+ // cls → "passport" | "full_body" | "portrait" | "other"
147
+
148
+ if (cls === "passport") {
149
+ const identity = await extractPassportIdentity({ bytes, model, apiKey, provider });
150
+ // identity → { family_name?, given_name?, passport_number?, passport_expiry? }
151
+ }
152
+ ```
153
+
154
+ In `apps/api`, photo classification is wired automatically via `photo-processor.ts`:
155
+ when a tenant has a `vision` LLM config, every incoming photo is classified
156
+ and passport data is merged into `contact.attributes_json`.
157
+
158
+ ## Install
159
+
160
+ ```bash
161
+ bun add @chatman-media/kb # Bun
162
+ npm install @chatman-media/kb # npm / pnpm / yarn
163
+ ```
164
+
165
+ 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.
166
+
167
+ ## License
168
+
169
+ [MIT](LICENSE) — Alexander Kireev / [chatman-media](https://github.com/chatman-media)
@@ -0,0 +1,66 @@
1
+ import type { AnswerTelemetry } from "./answer-types.ts";
2
+ import type { Style } from "./styles.ts";
3
+ export interface ABVariant {
4
+ /** Weight relative to other variants. Default 1. */
5
+ weight?: number;
6
+ style: Style;
7
+ }
8
+ export interface ABRouterOptions {
9
+ variants: ABVariant[];
10
+ /**
11
+ * Called after every answer with the assigned variant slug and telemetry.
12
+ * Use this to log impressions, latency, and conversion signals to your DB.
13
+ */
14
+ onResult?: (variantSlug: string, telemetry: AnswerTelemetry) => void;
15
+ /**
16
+ * Salt added to the userId before hashing. Change it to re-randomise
17
+ * assignments without changing user ids.
18
+ */
19
+ salt?: string;
20
+ }
21
+ /**
22
+ * Deterministic A/B style router.
23
+ *
24
+ * Assigns each user to a variant by hashing `userId + salt` — the same user
25
+ * always gets the same variant within an experiment. Weights are respected:
26
+ * a variant with weight 2 gets twice as many users as one with weight 1.
27
+ *
28
+ * Pass the returned `style` directly to `answerWithRag`. Log results via
29
+ * `onResult` to measure conversion by variant.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * import { ABRouter, answerWithRag } from "@chatman-media/kb";
34
+ *
35
+ * const router = new ABRouter({
36
+ * variants: [
37
+ * { style: nepqStyle, weight: 1 },
38
+ * { style: straightLineStyle, weight: 1 },
39
+ * ],
40
+ * onResult: (slug, telemetry) => db.logImpression(slug, telemetry),
41
+ * });
42
+ *
43
+ * const { style, onTelemetry } = router.assign(userId);
44
+ * const result = await answerWithRag({ question, kb, chat, embedder, style, onTelemetry });
45
+ * ```
46
+ */
47
+ export declare class ABRouter {
48
+ private readonly variants;
49
+ private readonly totalWeight;
50
+ private readonly onResult;
51
+ private readonly salt;
52
+ constructor(opts: ABRouterOptions);
53
+ /**
54
+ * Assign a user to a variant. Returns the `style` to pass to `answerWithRag`
55
+ * and an `onTelemetry` callback that forwards results to `opts.onResult`.
56
+ */
57
+ assign(userId: string): {
58
+ style: Style;
59
+ variantSlug: string;
60
+ onTelemetry: (t: AnswerTelemetry) => void;
61
+ };
62
+ /** Distribution map — slug → fraction of users assigned. */
63
+ get distribution(): Record<string, number>;
64
+ private pickVariant;
65
+ }
66
+ //# sourceMappingURL=ab-router.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ab-router.d.ts","sourceRoot":"","sources":["../src/ab-router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEzC,MAAM,WAAW,SAAS;IACxB,oDAAoD;IACpD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,KAAK,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,SAAS,EAAE,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,eAAe,KAAK,IAAI,CAAC;IACrE;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAwB;IACjD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA8B;IACvD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;gBAElB,IAAI,EAAE,eAAe;IAQjC;;;OAGG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG;QACtB,KAAK,EAAE,KAAK,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,WAAW,EAAE,CAAC,CAAC,EAAE,eAAe,KAAK,IAAI,CAAC;KAC3C;IAcD,4DAA4D;IAC5D,IAAI,YAAY,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMzC;IAID,OAAO,CAAC,WAAW;CAWpB"}
@@ -0,0 +1,194 @@
1
+ import type { z } from "zod";
2
+ import type { ChatClient, ChatMessage } from "@chatman-media/llm-router";
3
+ import type { EmbeddingClient } from "@chatman-media/llm-router";
4
+ import type { DirectorHookForPrompt, FunnelStage, SkillForPrompt, Style } from "./styles.ts";
5
+ import type { Reranker } from "./reranker.ts";
6
+ import type { AnyRagTool } from "./tools.ts";
7
+ import type { IKbStore, KbSearchHit } from "./types.ts";
8
+ export declare const NO_CONTEXT_MARKER = "__NO_CONTEXT__";
9
+ export interface Persona {
10
+ name: string;
11
+ role: "human" | "assistant";
12
+ company?: string;
13
+ /**
14
+ * Fixed personal facts about the persona — bypasses RAG for direct personal
15
+ * questions and injects into the system prompt as inviolable grounding.
16
+ * Known keys: "city", "age", "status", "experience", "phone".
17
+ */
18
+ facts?: Record<string, string>;
19
+ }
20
+ export interface AnswerInput {
21
+ question: string;
22
+ kb: IKbStore;
23
+ embedder: EmbeddingClient;
24
+ chat: ChatClient;
25
+ history?: ChatMessage[];
26
+ topK?: number;
27
+ maxDistance?: number;
28
+ persona?: Persona;
29
+ style?: Style;
30
+ stage?: FunnelStage;
31
+ includeFewShot?: boolean;
32
+ numPredict?: number;
33
+ userFacts?: Record<string, string>;
34
+ rewriteQueryBeforeRetrieval?: boolean;
35
+ reflect?: boolean;
36
+ hybridSearch?: boolean;
37
+ conversationSummary?: string;
38
+ topicRouting?: boolean;
39
+ vacanciesBlock?: string;
40
+ vacancyGuard?: boolean;
41
+ skills?: readonly SkillForPrompt[];
42
+ /**
43
+ * Tenant-specific director hooks to inject into the system prompt.
44
+ * Loaded from `director_hooks` table filtered by `is_active = true`.
45
+ * Always injected (not stage-filtered) — the LLM decides when to apply.
46
+ */
47
+ directorHooks?: readonly DirectorHookForPrompt[];
48
+ booksPriority?: boolean;
49
+ /**
50
+ * Support mode — set when the lead is past the sales stage and waiting on a
51
+ * downstream process (`docs` = collecting their documents, `submitted` =
52
+ * filed). When set AND a `style` is active, the composed system prompt drops
53
+ * the sales framework / hooks / skills / few-shot / funnel-stage guidance and
54
+ * uses a calm FAQ-support block instead — answering questions without selling.
55
+ */
56
+ supportPhase?: "docs" | "submitted";
57
+ /**
58
+ * Called after every `answerWithRag` or `answerWithRagStream` call with the
59
+ * final telemetry. Useful for logging, metrics, or A/B experiment recording
60
+ * without having to unwrap the return value.
61
+ */
62
+ onTelemetry?: (telemetry: AnswerTelemetry) => void;
63
+ /**
64
+ * Optional tools the LLM can call during answer generation (single-cycle).
65
+ * When the model decides to call a tool, `execute()` is called automatically
66
+ * and the result is fed back for a final answer.
67
+ * Requires `chat` to implement `completeWithTools` (e.g. `OpenAIChatClient`),
68
+ * otherwise falls back to prompt-based tool injection.
69
+ */
70
+ tools?: AnyRagTool[];
71
+ /**
72
+ * Maximum number of agentic tool-calling cycles. Each cycle is one LLM call
73
+ * that may request tools, followed by execution of those tools. When the
74
+ * limit is reached, a final answer is forced WITHOUT tools. Only relevant
75
+ * when `tools` is set. Default: 4 (caps at 5 LLM calls including the final
76
+ * answer — note the latency and cost of a long tool chain).
77
+ */
78
+ maxToolCycles?: number;
79
+ /**
80
+ * Enable Maximal Marginal Relevance re-ranking after retrieval.
81
+ * Selects a diverse set of chunks so that repeated near-duplicate passages
82
+ * do not crowd out different sub-topics. Default: false.
83
+ */
84
+ mmr?: boolean;
85
+ /**
86
+ * λ (lambda) for MMR: trade-off between relevance and diversity.
87
+ * 1.0 = pure relevance, 0.0 = pure diversity. Default: 0.6.
88
+ * Only has effect when `mmr` is true.
89
+ */
90
+ mmrLambda?: number;
91
+ /**
92
+ * Trim retrieved hits that exceed a cosine-distance threshold before passing
93
+ * them to the LLM. Reduces hallucinations caused by weak/unrelated matches.
94
+ * Default: false (no trimming).
95
+ */
96
+ autoTrimDistance?: boolean;
97
+ /**
98
+ * Distance threshold used when `autoTrimDistance` is true.
99
+ * Cosine distance in [0, 2]; typical useful range is ≤ 0.4.
100
+ * Default: 0.45.
101
+ */
102
+ autoTrimThreshold?: number;
103
+ /**
104
+ * Enable multi-query expansion: generate `multiQueryCount` rephrased variants
105
+ * of the question with a fast LLM call, search with each in parallel, and
106
+ * merge all result lists via Reciprocal Rank Fusion (RRF). Improves recall
107
+ * for synonym gaps and differently-phrased concepts.
108
+ * Default: false.
109
+ */
110
+ multiQuery?: boolean;
111
+ /**
112
+ * Number of ADDITIONAL query variants to generate (not counting the original).
113
+ * Only has effect when `multiQuery` is true. Default: 2.
114
+ */
115
+ multiQueryCount?: number;
116
+ /**
117
+ * Optional cross-encoder reranker applied after vector/hybrid retrieval.
118
+ * Retrieves `topK * 3` candidates, passes them to the reranker, then keeps
119
+ * the top `topK`. Use `JinaReranker` or `CohereReranker` from this package.
120
+ *
121
+ * @example
122
+ * ```ts
123
+ * import { JinaReranker } from "@chatman-media/kb";
124
+ * await answerWithRag({ ..., reranker: new JinaReranker({ apiKey: process.env.JINA_API_KEY! }) });
125
+ * ```
126
+ */
127
+ reranker?: Reranker;
128
+ /**
129
+ * When provided, the LLM is instructed to return a JSON object matching this
130
+ * Zod schema. The parsed and validated value is available as `result.output`.
131
+ * Uses OpenAI's native `response_format` when available; falls back to prompt
132
+ * injection for other providers (Ollama, OpenRouter, etc.).
133
+ *
134
+ * @example
135
+ * ```ts
136
+ * const result = await answerWithRag({
137
+ * question: "Classify this lead",
138
+ * kb, chat, embedder,
139
+ * outputSchema: z.object({
140
+ * intent: z.enum(["buy", "info", "not_interested"]),
141
+ * budget: z.number().optional(),
142
+ * nextAction: z.string(),
143
+ * }),
144
+ * });
145
+ * console.log(result.output.intent); // "buy" | "info" | "not_interested"
146
+ * ```
147
+ */
148
+ outputSchema?: z.ZodTypeAny;
149
+ }
150
+ export interface AnswerTelemetry {
151
+ path: "smalltalk" | "persona_fact" | "no_context" | "ungrounded" | "ok" | "cache_hit";
152
+ total_ms?: number;
153
+ retrieval_ms?: number;
154
+ generation_ms?: number;
155
+ top_distances?: number[];
156
+ hybrid?: boolean;
157
+ topic?: string | null;
158
+ original_query?: string;
159
+ rewritten_query?: string;
160
+ factCheck?: {
161
+ grounded: boolean;
162
+ vacancyOk: boolean;
163
+ reason?: string;
164
+ };
165
+ /**
166
+ * @deprecated Use `toolCalls`. Retained for backward compatibility — set to
167
+ * the first tool call when any tools ran during answer generation.
168
+ */
169
+ toolCall?: {
170
+ name: string;
171
+ result: unknown;
172
+ };
173
+ /**
174
+ * Every tool call executed across all agentic cycles, in order. Note that
175
+ * `args` may contain user-derived input — treat as sensitive if your tools
176
+ * receive PII.
177
+ */
178
+ toolCalls?: Array<{
179
+ name: string;
180
+ args: Record<string, unknown>;
181
+ result: unknown;
182
+ error?: boolean;
183
+ cycle: number;
184
+ }>;
185
+ }
186
+ export interface AnswerResult<TOutput = unknown> {
187
+ text: string;
188
+ usedChunkIds: number[];
189
+ hits: KbSearchHit[];
190
+ telemetry: AnswerTelemetry;
191
+ /** Parsed and validated output when `outputSchema` was passed to `answerWithRag`. */
192
+ output?: TOutput;
193
+ }
194
+ //# sourceMappingURL=answer-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"answer-types.d.ts","sourceRoot":"","sources":["../src/answer-types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ import type { z } from "zod";
2
+ import { type AnswerInput, type AnswerResult, type Persona } from "./answer-types.ts";
3
+ import type { ChatClient, ChatMessage } from "@chatman-media/llm-router";
4
+ import type { KbSearchHit } from "./types.ts";
5
+ export { type AnswerInput, type AnswerResult, type AnswerTelemetry, NO_CONTEXT_MARKER, type Persona, } from "./answer-types.ts";
6
+ export { botPresenceReply, isBotPresenceQuestion, isPersonalFactQuestion, isPersonaSmalltalkQuestion, personaFactReply, personaSmalltalkReply, } from "./persona-shortcuts.ts";
7
+ export { sanitizeLlmOutput } from "./sanitize.ts";
8
+ export { buildSystemPrompt, legacyRagSamplingTemperature, renderSummaryBlock, renderUserFactsBlock, } from "./system-prompt.ts";
9
+ export interface RetrievalResult {
10
+ hits: KbSearchHit[];
11
+ retrievalMs: number;
12
+ searchQuery: string;
13
+ queries: string[];
14
+ /** null when topicRouting is off or booksPriority path was used. */
15
+ usedTopic: string | null;
16
+ }
17
+ /**
18
+ * Shared retrieval logic for both `answerWithRag` and `answerWithRagStream`.
19
+ *
20
+ * Steps:
21
+ * 1. Optional query rewrite (LLM resolves pronouns/ellipsis via history).
22
+ * 2. Optional multi-query expansion (LLM generates N variants).
23
+ * 3. Embed all queries in one batch.
24
+ * 4. booksPriority path OR normal path (multi-query → RRF | single → topic fallback).
25
+ * 5. maxDistance filter → applyDynamicThreshold → mmrDiversify → reranker.
26
+ * 6. Slice to topK.
27
+ */
28
+ export declare function retrieveHits(input: AnswerInput): Promise<RetrievalResult>;
29
+ /**
30
+ * Streaming variant of `answerWithRag`. Yields raw text tokens as they arrive
31
+ * from the LLM. The final telemetry is delivered via `input.onTelemetry` (if
32
+ * set). Falls back to `complete()` when the chat client has no `stream()`.
33
+ *
34
+ * Note: hallucination guard (`reflect`, `vacancyGuard`) is not applied during
35
+ * streaming — fact-checking requires the full answer. Use `answerWithRag()` when
36
+ * fact-checking is required.
37
+ */
38
+ export declare function answerWithRagStream(input: AnswerInput): AsyncIterable<string>;
39
+ export declare function answerWithRag<T extends z.ZodTypeAny>(input: AnswerInput & {
40
+ outputSchema: T;
41
+ }): Promise<AnswerResult<z.infer<T>>>;
42
+ export declare function answerWithRag(input: AnswerInput): Promise<AnswerResult>;
43
+ /**
44
+ * Soft fallback reply for turns where RAG produced nothing groundable — no KB
45
+ * hit, or the fact-checker dropped the draft as ungrounded. Instead of going
46
+ * silent, the bot answers in its own persona voice, but is hard-constrained
47
+ * NOT to invent any specifics (salaries, dates, visa terms, cities, prices).
48
+ *
49
+ * Concrete questions get an honest "I'll clarify and come back"; general
50
+ * questions get a normal conversational answer. The caller is still expected
51
+ * to log the unanswered question (kb_suggestions) for a later precise reply.
52
+ */
53
+ export declare function generateSoftFallback(input: {
54
+ question: string;
55
+ chat: ChatClient;
56
+ persona: Persona;
57
+ history?: ChatMessage[];
58
+ }): Promise<string>;
59
+ //# sourceMappingURL=answer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"answer.d.ts","sourceRoot":"","sources":["../src/answer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,YAAY,EAGjB,KAAK,OAAO,EACb,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AA8BzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAG9C,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,iBAAiB,EACjB,KAAK,OAAO,GACb,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,gBAAgB,EAChB,qBAAqB,EACrB,sBAAsB,EACtB,0BAA0B,EAC1B,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EACL,iBAAiB,EACjB,4BAA4B,EAC5B,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAI5B,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,WAAW,EAAE,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,oEAAoE;IACpE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,YAAY,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,eAAe,CAAC,CAgG/E;AA2OD;;;;;;;;GAQG;AACH,wBAAuB,mBAAmB,CAAC,KAAK,EAAE,WAAW,GAAG,aAAa,CAAC,MAAM,CAAC,CA0HpF;AAED,wBAAsB,aAAa,CAAC,CAAC,SAAS,CAAC,CAAC,UAAU,EACxD,KAAK,EAAE,WAAW,GAAG;IAAE,YAAY,EAAE,CAAC,CAAA;CAAE,GACvC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACrC,wBAAsB,aAAa,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;AAiG/E;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,CAAC,KAAK,EAAE;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,UAAU,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;CACzB,GAAG,OAAO,CAAC,MAAM,CAAC,CA8BlB"}
@@ -0,0 +1,19 @@
1
+ import type { AnyRagTool } from "../tools.ts";
2
+ /**
3
+ * Встроенный tool: "offer_booking_link".
4
+ *
5
+ * Когда лид/кандидат хочет записаться на звонок, демо или встречу — LLM
6
+ * вызывает этот tool, получает ссылку и вставляет её в ответ.
7
+ * Работает с любой платформой бронирования: Calendly, Cal.com, Tidycal, и т.д.
8
+ *
9
+ * @param schedulingUrl — публичная ссылка на страницу бронирования тенанта
10
+ * Пример: "https://calendly.com/your-agency/30min"
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const tool = makeBookingLinkTool("https://calendly.com/my-agency/demo");
15
+ * const result = await answerWithRag({ ..., tools: [tool] });
16
+ * ```
17
+ */
18
+ export declare function makeBookingLinkTool(schedulingUrl: string): AnyRagTool;
19
+ //# sourceMappingURL=calendly.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calendly.d.ts","sourceRoot":"","sources":["../../src/built-in-tools/calendly.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,MAAM,GAAG,UAAU,CAYrE"}
@@ -0,0 +1,48 @@
1
+ export interface ChunkOptions {
2
+ maxChars: number;
3
+ overlapChars: number;
4
+ }
5
+ export interface Chunk {
6
+ index: number;
7
+ text: string;
8
+ /** Rough token estimate: ~4 chars per token. */
9
+ tokenCount: number;
10
+ }
11
+ /** Approximate token count for English/mixed text: ~4 chars per token. */
12
+ export declare function estimateTokens(text: string): number;
13
+ /**
14
+ * Splits `text` into overlapping chunks bounded by `maxChars`. Tries to break
15
+ * on paragraph boundaries (\n\n) first, then on whitespace, before slicing
16
+ * mid-word. Empty/whitespace-only input yields an empty array.
17
+ */
18
+ export declare function chunkText(text: string, opts?: Partial<ChunkOptions>): Chunk[];
19
+ export interface SectionChunk extends Chunk {
20
+ /** Heading that introduces this section, or null for the document preamble. */
21
+ heading: string | null;
22
+ /** Heading level (1–6) derived from the number of `#` characters, or null. */
23
+ headingLevel: number | null;
24
+ }
25
+ /**
26
+ * Semantic chunker that splits Markdown/plain-text documents by headings
27
+ * (`#`, `##`, …) and paragraph breaks, keeping each heading with its content.
28
+ *
29
+ * Compared to `chunkText()`:
30
+ * - Never breaks a heading away from its first paragraph
31
+ * - Each chunk carries the heading context, so retrieval results are
32
+ * self-contained even without surrounding text
33
+ * - Falls back to `chunkText()` for sections that exceed `maxChars`
34
+ *
35
+ * Use this for structured knowledge-base documents (FAQs, wikis, product
36
+ * pages). Use `chunkText()` for unstructured long-form prose.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * import { chunkBySections } from "@chatman-media/kb";
41
+ *
42
+ * const chunks = chunkBySections(markdownString, { maxChars: 1200 });
43
+ * // chunks[0].heading → "Installation"
44
+ * // chunks[0].text → "## Installation\n\nRun `bun add …`"
45
+ * ```
46
+ */
47
+ export declare function chunkBySections(text: string, opts?: Partial<ChunkOptions>): SectionChunk[];
48
+ //# sourceMappingURL=chunk.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunk.d.ts","sourceRoot":"","sources":["../src/chunk.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,KAAK;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,gDAAgD;IAChD,UAAU,EAAE,MAAM,CAAC;CACpB;AAOD,0EAA0E;AAC1E,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,OAAO,CAAC,YAAY,CAAM,GAAG,KAAK,EAAE,CAqDjF;AAED,MAAM,WAAW,YAAa,SAAQ,KAAK;IACzC,+EAA+E;IAC/E,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,8EAA8E;IAC9E,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,OAAO,CAAC,YAAY,CAAM,GAAG,YAAY,EAAE,CA2E9F"}