@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
@@ -0,0 +1,64 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * A tool the LLM can call during `answerWithRag`.
4
+ *
5
+ * Pass one or more tools via `AnswerInput.tools`. When the model decides to
6
+ * use a tool, the library executes `execute()` automatically and feeds the
7
+ * result back to the model for a final answer (single-cycle — one tool call
8
+ * per request).
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { answerWithRag, type RagTool } from "@chatman-media/kb";
13
+ * import { z } from "zod";
14
+ *
15
+ * const slotsTool: RagTool = {
16
+ * name: "getAvailableSlots",
17
+ * description: "Returns available booking slots for a given date (YYYY-MM-DD).",
18
+ * parameters: z.object({ date: z.string() }),
19
+ * execute: async ({ date }) => fetchSlotsFromCRM(date),
20
+ * };
21
+ *
22
+ * const result = await answerWithRag({ question, kb, chat, embedder, tools: [slotsTool] });
23
+ * // result.telemetry.toolCall — name + result if a tool was invoked
24
+ * ```
25
+ */
26
+ export interface RagTool<TParams extends z.ZodTypeAny = z.ZodTypeAny> {
27
+ name: string;
28
+ description: string;
29
+ /** Zod schema for the tool's input parameters. Used to build the JSON Schema sent to the model. */
30
+ parameters: TParams;
31
+ execute: (args: z.infer<TParams>) => Promise<unknown>;
32
+ }
33
+ export type AnyRagTool = RagTool<any>;
34
+ /** Converts a `RagTool` to the OpenAI function-calling format. */
35
+ export declare function toolToOpenAIFunction(tool: AnyRagTool): OpenAIToolDefinition;
36
+ export interface OpenAIToolDefinition {
37
+ type: "function";
38
+ function: {
39
+ name: string;
40
+ description: string;
41
+ parameters: Record<string, unknown>;
42
+ };
43
+ }
44
+ export interface ToolCallRequest {
45
+ id: string;
46
+ type: "function";
47
+ function: {
48
+ name: string;
49
+ arguments: string;
50
+ };
51
+ }
52
+ export interface ToolCallResult {
53
+ /** Tool call id returned by the model. */
54
+ id: string;
55
+ name: string;
56
+ args: Record<string, unknown>;
57
+ }
58
+ export interface CompleteWithToolsResult {
59
+ /** Model text when no tool was called. */
60
+ content: string | null;
61
+ /** Parsed tool calls when the model chose to call a tool. */
62
+ toolCalls: ToolCallResult[];
63
+ }
64
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,WAAW,OAAO,CAAC,OAAO,SAAS,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC,UAAU;IAClE,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,mGAAmG;IACnG,UAAU,EAAE,OAAO,CAAC;IACpB,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACvD;AAGD,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;AAEtC,kEAAkE;AAClE,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,GAAG,oBAAoB,CAS3E;AAID,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE;QACR,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACrC,CAAC;CACH;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/C;AAED,MAAM,WAAW,cAAc;IAC7B,0CAA0C;IAC1C,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,uBAAuB;IACtC,0CAA0C;IAC1C,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,6DAA6D;IAC7D,SAAS,EAAE,cAAc,EAAE,CAAC;CAC7B"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Returns a single topic slug when exactly ONE topic pattern matches, or
3
+ * `null` when zero or multiple match. Multi-match is treated as ambiguous —
4
+ * forcing one topic would silently drop docs from the other.
5
+ */
6
+ export declare function classifyTopic(question: string): string | null;
7
+ /** Exposed for tests + admin debugging — lets callers see ALL matches. */
8
+ export declare function classifyTopicAll(question: string): string[];
9
+ /** All defined topic slugs. Useful for ingest CLI validation and admin UI. */
10
+ export declare const KNOWN_TOPICS: readonly string[];
11
+ //# sourceMappingURL=topic-classifier.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"topic-classifier.d.ts","sourceRoot":"","sources":["../src/topic-classifier.ts"],"names":[],"mappings":"AAqFA;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQ7D;AAED,0EAA0E;AAC1E,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAO3D;AAED,8EAA8E;AAC9E,eAAO,MAAM,YAAY,EAAE,SAAS,MAAM,EAAuC,CAAC"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Storage interfaces for the RAG engine.
3
+ *
4
+ * The package defines these interfaces; consumers provide implementations.
5
+ * The reference implementation (`PgKbStore`) ships with the lead-engine platform
6
+ * project and wraps the PostgreSQL + pgvector repos.
7
+ */
8
+ export interface KbSearchHit {
9
+ chunk_id: number;
10
+ /** Cosine distance (vector search) or negated BM25 rank (FTS). Lower = closer. */
11
+ distance: number;
12
+ text: string;
13
+ document_id: number;
14
+ source: string;
15
+ title: string;
16
+ }
17
+ /**
18
+ * Minimal contract the RAG engine needs from whatever storage backend you
19
+ * plug in. Split into two logical groups:
20
+ *
21
+ * - **Search** — called by `answerWithRag` at query time (read-only).
22
+ * - **Ingest** — called by `ingestFile` / `ingestText` / `ingestDirectory`
23
+ * when indexing documents (read+write).
24
+ */
25
+ export interface IKbStore {
26
+ /**
27
+ * Pure vector search (cosine distance via pgvector or equivalent).
28
+ * Returns up to `k` hits, optionally filtered by `topic`.
29
+ */
30
+ search(embedding: number[], k: number, topic?: string | null): Promise<KbSearchHit[]>;
31
+ /**
32
+ * Hybrid retrieval: fuse vector + BM25 results via Reciprocal Rank Fusion.
33
+ * Falls back to vector-only when the FTS index returns nothing.
34
+ */
35
+ hybridSearch(input: {
36
+ embedding: number[];
37
+ query: string;
38
+ k?: number;
39
+ topic?: string | null;
40
+ }): Promise<KbSearchHit[]>;
41
+ /**
42
+ * Books-priority search: tries the "books" topic first, falls back to
43
+ * global search when no books-tagged chunks match. Used when `booksPriority`
44
+ * is set on `AnswerInput`.
45
+ */
46
+ prioritySearch(input: {
47
+ embedding: number[];
48
+ query: string;
49
+ k?: number;
50
+ vectorOnly?: boolean;
51
+ }): Promise<KbSearchHit[]>;
52
+ /** Look up an existing document by its source URI. */
53
+ getDocumentBySource(source: string): Promise<{
54
+ id: number;
55
+ content_hash: string;
56
+ } | null>;
57
+ /** Count indexed chunks for a document (used for dedup short-circuit). */
58
+ countChunksForDocument(documentId: number): Promise<number>;
59
+ /** Delete a document and all its chunks (called before re-indexing). */
60
+ deleteDocument(id: number): Promise<boolean>;
61
+ /** Upsert a document record; returns the canonical row id. */
62
+ upsertDocument(input: {
63
+ source: string;
64
+ title: string;
65
+ contentHash: string;
66
+ topic?: string | null;
67
+ }): Promise<{
68
+ id: number;
69
+ }>;
70
+ /** Insert one chunk with its embedding vector. */
71
+ insertChunkWithEmbedding(input: {
72
+ documentId: number;
73
+ chunkIndex: number;
74
+ text: string;
75
+ tokenCount: number;
76
+ embedding: number[];
77
+ }): Promise<void>;
78
+ }
79
+ /** Optional write-only interface for logging unanswered questions. */
80
+ export interface IKbSuggestionsStore {
81
+ log(question: string, conversationId: string): Promise<void>;
82
+ }
83
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,QAAQ;IAGvB;;;OAGG;IACH,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAEtF;;;OAGG;IACH,YAAY,CAAC,KAAK,EAAE;QAClB,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,CAAC;IAE3B;;;;OAIG;IACH,cAAc,CAAC,KAAK,EAAE;QACpB,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,CAAC;IAI3B,sDAAsD;IACtD,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IAE1F,0EAA0E;IAC1E,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAE5D,wEAAwE;IACxE,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE7C,8DAA8D;IAC9D,cAAc,CAAC,KAAK,EAAE;QACpB,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,CAAC;IAE5B,kDAAkD;IAClD,wBAAwB,CAAC,KAAK,EAAE;QAC9B,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,CAAC;CACnB;AAED,sEAAsE;AACtE,MAAM,WAAW,mBAAmB;IAClC,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9D"}
@@ -0,0 +1,19 @@
1
+ import type { KbSearchHit } from "./types.ts";
2
+ /**
3
+ * Reciprocal Rank Fusion — fuses two ranked result lists into one.
4
+ *
5
+ * Standard formula: score(d) = Σ 1/(k + rank(d))
6
+ * where k=60 is the smoothing constant (Cormack et al., 2009).
7
+ *
8
+ * Returns hits sorted by descending fused score. The `distance` field is
9
+ * remapped to `1 - score` so callers get a consistent "lower = better"
10
+ * interpretation regardless of whether the underlying search was vector or BM25.
11
+ *
12
+ * @param vectorHits Results from vector (cosine) search, ranked by ascending distance.
13
+ * @param bm25Hits Results from BM25 search, ranked by descending BM25 score.
14
+ * @param k Number of top results to return.
15
+ * @param rrfK RRF smoothing constant (default 60).
16
+ */
17
+ export declare function reciprocalRankFusion(vectorHits: KbSearchHit[], bm25Hits: KbSearchHit[], k: number, rrfK?: number): KbSearchHit[];
18
+ export declare function sanitizeFtsQuery(raw: string): string;
19
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,WAAW,EAAE,EACzB,QAAQ,EAAE,WAAW,EAAE,EACvB,CAAC,EAAE,MAAM,EACT,IAAI,SAAK,GACR,WAAW,EAAE,CA0Bf;AAuBD,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CASpD"}
@@ -0,0 +1,72 @@
1
+ import type { FetchLike } from "@chatman-media/llm-router";
2
+ /** AI provider for the vision model — both expose an OpenAI-compatible API. */
3
+ export type VisionProvider = "openrouter" | "openai";
4
+ /**
5
+ * Photo classification via a vision-capable model.
6
+ *
7
+ * The recruiting funnel collects two distinct kinds of photo from a
8
+ * candidate — full-body shots and a photo of her international passport
9
+ * (загранпаспорт) — plus assorted regular photos. The bot needs to tell
10
+ * them apart so the lead intake counters are accurate (see
11
+ * `src/leads/intake.ts`), instead of the old "total photos >= 7" guess.
12
+ *
13
+ * Works with either OpenRouter or OpenAI (set via `provider`). Both expose
14
+ * an OpenAI-compatible `/chat/completions` endpoint; vision input is the
15
+ * standard `image_url` content part with a data URL. The OpenRouter-only
16
+ * `reasoning` request param is sent only for that provider.
17
+ */
18
+ export declare const PHOTO_CLASSES: readonly ["passport", "full_body", "portrait", "other"];
19
+ export type PhotoClass = (typeof PHOTO_CLASSES)[number];
20
+ export interface ClassifyPhotoOptions {
21
+ /** Raw image bytes (as downloaded from Telegram). */
22
+ bytes: ArrayBuffer;
23
+ /** MIME type, e.g. "image/jpeg". Falls back to image/jpeg when empty. */
24
+ mimeType?: string;
25
+ /** Vision-capable model id (OpenRouter slug or OpenAI model name). */
26
+ model: string;
27
+ apiKey: string;
28
+ /** AI provider. Default: "openrouter". */
29
+ provider?: VisionProvider;
30
+ /** Default: https://openrouter.ai/api/v1 */
31
+ baseUrl?: string;
32
+ /** Per-request timeout in ms. Default 30_000. */
33
+ timeoutMs?: number;
34
+ fetch?: FetchLike;
35
+ }
36
+ /** Maps a free-form model reply onto a `PhotoClass`, defaulting to `other`. */
37
+ export declare function parsePhotoClass(raw: string): PhotoClass;
38
+ /**
39
+ * Downloads nothing — caller passes raw bytes. Returns the classified
40
+ * category. Throws on transport / API errors so the caller can decide
41
+ * to leave the photo unclassified and retry on the next turn.
42
+ */
43
+ export declare function classifyPhoto(opts: ClassifyPhotoOptions): Promise<PhotoClass>;
44
+ /**
45
+ * Identity fields read off a candidate's international passport
46
+ * (загранпаспорт). Latin spelling — the form is what the visa anketa
47
+ * needs and what the MRZ carries unambiguously. Every field optional:
48
+ * a blurry scan may yield only some, or none.
49
+ */
50
+ export interface PassportIdentity {
51
+ /** Surname, Latin, as printed in the passport / MRZ. */
52
+ family_name?: string;
53
+ /** Given name(s), Latin. */
54
+ given_name?: string;
55
+ passport_number?: string;
56
+ /** Expiration date, formatted dd.mm.yyyy to match the intake field. */
57
+ passport_expiry?: string;
58
+ }
59
+ /**
60
+ * Strips markdown / think-tags and parses the model's passport JSON.
61
+ * Validates value types and length. Exported for unit tests.
62
+ */
63
+ export declare function parsePassportJson(raw: string): PassportIdentity;
64
+ /**
65
+ * Reads identity fields off a passport photo via the same vision model
66
+ * used for classification. Caller should only invoke this for photos
67
+ * already classified as `passport`. Throws on transport / API errors so
68
+ * the caller can leave the photo for retry; a successful-but-empty model
69
+ * reply returns `{}` (a valid "nothing readable" result).
70
+ */
71
+ export declare function extractPassportIdentity(opts: ClassifyPhotoOptions): Promise<PassportIdentity>;
72
+ //# sourceMappingURL=vision.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vision.d.ts","sourceRoot":"","sources":["../src/vision.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAE3D,+EAA+E;AAC/E,MAAM,MAAM,cAAc,GAAG,YAAY,GAAG,QAAQ,CAAC;AAErD;;;;;;;;;;;;;GAaG;AAEH,eAAO,MAAM,aAAa,yDAA0D,CAAC;AACrF,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAcxD,MAAM,WAAW,oBAAoB;IACnC,qDAAqD;IACrD,KAAK,EAAE,WAAW,CAAC;IACnB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,4CAA4C;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC;CACnB;AAUD,+EAA+E;AAC/E,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAIvD;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,UAAU,CAAC,CA4DnF;AAED;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4BAA4B;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,uEAAuE;IACvE,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAqBD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,CAwB/D;AAED;;;;;;GAMG;AACH,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,oBAAoB,GACzB,OAAO,CAAC,gBAAgB,CAAC,CAwD3B"}
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@chatman-media/kb",
3
+ "version": "1.3.0",
4
+ "description": "Tenant-scoped Knowledge Base: hybrid retrieval (pgvector + BM25), ingest, answer pipeline, persona/skill composition. LLM I/O живёт в @chatman-media/llm-router.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "bun": "./src/index.ts",
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "src",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "scripts": {
23
+ "build": "bun build ./src/index.ts --outdir ./dist --target bun --format esm && tsc -p tsconfig.build.json",
24
+ "typecheck": "tsc --noEmit",
25
+ "check": "biome check ./src ./test",
26
+ "format": "biome format --write ./src ./test",
27
+ "test": "bun test",
28
+ "prepublishOnly": "bun run build"
29
+ },
30
+ "engines": {
31
+ "bun": ">=1.0.0"
32
+ },
33
+ "keywords": [
34
+ "rag",
35
+ "retrieval-augmented-generation",
36
+ "pgvector",
37
+ "bm25",
38
+ "hybrid-search",
39
+ "llm",
40
+ "chatbot",
41
+ "sales-bot",
42
+ "telegram-bot",
43
+ "ollama",
44
+ "openai",
45
+ "openrouter"
46
+ ],
47
+ "author": "Alexander Kireev",
48
+ "license": "MIT",
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/chatman-media/lead-engine.git",
52
+ "directory": "packages/kb"
53
+ },
54
+ "homepage": "https://github.com/chatman-media/lead-engine/tree/main/packages/kb#readme",
55
+ "bugs": {
56
+ "url": "https://github.com/chatman-media/lead-engine/issues"
57
+ },
58
+ "dependencies": {
59
+ "@chatman-media/llm-router": "1.0.0",
60
+ "unpdf": "^1.6.2",
61
+ "zod": "^4.4.1"
62
+ },
63
+ "devDependencies": {
64
+ "@biomejs/biome": "^2.4.16",
65
+ "@semantic-release/changelog": "^6.0.3",
66
+ "@semantic-release/git": "^10.0.1",
67
+ "@semantic-release/github": "^12.0.8",
68
+ "@semantic-release/npm": "^13.1.5",
69
+ "@types/bun": "latest",
70
+ "semantic-release": "^25.0.3",
71
+ "typescript": "^6.0.3"
72
+ },
73
+ "publishConfig": {
74
+ "access": "public"
75
+ }
76
+ }
@@ -0,0 +1,118 @@
1
+ import type { AnswerTelemetry } from "./answer-types.ts";
2
+ import type { Style } from "./styles.ts";
3
+
4
+ export interface ABVariant {
5
+ /** Weight relative to other variants. Default 1. */
6
+ weight?: number;
7
+ style: Style;
8
+ }
9
+
10
+ export interface ABRouterOptions {
11
+ variants: ABVariant[];
12
+ /**
13
+ * Called after every answer with the assigned variant slug and telemetry.
14
+ * Use this to log impressions, latency, and conversion signals to your DB.
15
+ */
16
+ onResult?: (variantSlug: string, telemetry: AnswerTelemetry) => void;
17
+ /**
18
+ * Salt added to the userId before hashing. Change it to re-randomise
19
+ * assignments without changing user ids.
20
+ */
21
+ salt?: string;
22
+ }
23
+
24
+ /**
25
+ * Deterministic A/B style router.
26
+ *
27
+ * Assigns each user to a variant by hashing `userId + salt` — the same user
28
+ * always gets the same variant within an experiment. Weights are respected:
29
+ * a variant with weight 2 gets twice as many users as one with weight 1.
30
+ *
31
+ * Pass the returned `style` directly to `answerWithRag`. Log results via
32
+ * `onResult` to measure conversion by variant.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * import { ABRouter, answerWithRag } from "@chatman-media/kb";
37
+ *
38
+ * const router = new ABRouter({
39
+ * variants: [
40
+ * { style: nepqStyle, weight: 1 },
41
+ * { style: straightLineStyle, weight: 1 },
42
+ * ],
43
+ * onResult: (slug, telemetry) => db.logImpression(slug, telemetry),
44
+ * });
45
+ *
46
+ * const { style, onTelemetry } = router.assign(userId);
47
+ * const result = await answerWithRag({ question, kb, chat, embedder, style, onTelemetry });
48
+ * ```
49
+ */
50
+ export class ABRouter {
51
+ private readonly variants: Required<ABVariant>[];
52
+ private readonly totalWeight: number;
53
+ private readonly onResult: ABRouterOptions["onResult"];
54
+ private readonly salt: string;
55
+
56
+ constructor(opts: ABRouterOptions) {
57
+ if (opts.variants.length === 0) throw new Error("ABRouter: at least one variant required");
58
+ this.variants = opts.variants.map((v) => ({ weight: v.weight ?? 1, style: v.style }));
59
+ this.totalWeight = this.variants.reduce((s, v) => s + v.weight, 0);
60
+ this.onResult = opts.onResult;
61
+ this.salt = opts.salt ?? "";
62
+ }
63
+
64
+ /**
65
+ * Assign a user to a variant. Returns the `style` to pass to `answerWithRag`
66
+ * and an `onTelemetry` callback that forwards results to `opts.onResult`.
67
+ */
68
+ assign(userId: string): {
69
+ style: Style;
70
+ variantSlug: string;
71
+ onTelemetry: (t: AnswerTelemetry) => void;
72
+ } {
73
+ const style = this.pickVariant(userId);
74
+ const variantSlug = style.slug;
75
+ const onResult = this.onResult;
76
+
77
+ return {
78
+ style,
79
+ variantSlug,
80
+ onTelemetry: (telemetry: AnswerTelemetry) => {
81
+ onResult?.(variantSlug, telemetry);
82
+ },
83
+ };
84
+ }
85
+
86
+ /** Distribution map — slug → fraction of users assigned. */
87
+ get distribution(): Record<string, number> {
88
+ const out: Record<string, number> = {};
89
+ for (const v of this.variants) {
90
+ out[v.style.slug] = v.weight / this.totalWeight;
91
+ }
92
+ return out;
93
+ }
94
+
95
+ // ── Private ───────────────────────────────────────────────────────────────
96
+
97
+ private pickVariant(userId: string): Style {
98
+ const hash = simpleHash(userId + this.salt);
99
+ const bucket = ((hash % this.totalWeight) + this.totalWeight) % this.totalWeight;
100
+ let cumulative = 0;
101
+ for (const v of this.variants) {
102
+ cumulative += v.weight;
103
+ if (bucket < cumulative) return v.style;
104
+ }
105
+ // Fallback — should never be reached
106
+ return (this.variants[this.variants.length - 1] as Required<ABVariant>).style;
107
+ }
108
+ }
109
+
110
+ /** Fast non-cryptographic integer hash (FNV-1a 32-bit). */
111
+ function simpleHash(s: string): number {
112
+ let h = 2166136261;
113
+ for (let i = 0; i < s.length; i++) {
114
+ h ^= s.charCodeAt(i);
115
+ h = (h * 16777619) >>> 0;
116
+ }
117
+ return h;
118
+ }
@@ -0,0 +1,191 @@
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
+
9
+ export const NO_CONTEXT_MARKER = "__NO_CONTEXT__";
10
+
11
+ export interface Persona {
12
+ name: string;
13
+ role: "human" | "assistant";
14
+ company?: string;
15
+ /**
16
+ * Fixed personal facts about the persona — bypasses RAG for direct personal
17
+ * questions and injects into the system prompt as inviolable grounding.
18
+ * Known keys: "city", "age", "status", "experience", "phone".
19
+ */
20
+ facts?: Record<string, string>;
21
+ }
22
+
23
+ export interface AnswerInput {
24
+ question: string;
25
+ kb: IKbStore;
26
+ embedder: EmbeddingClient;
27
+ chat: ChatClient;
28
+ history?: ChatMessage[];
29
+ topK?: number;
30
+ maxDistance?: number;
31
+ persona?: Persona;
32
+ style?: Style;
33
+ stage?: FunnelStage;
34
+ includeFewShot?: boolean;
35
+ numPredict?: number;
36
+ userFacts?: Record<string, string>;
37
+ rewriteQueryBeforeRetrieval?: boolean;
38
+ reflect?: boolean;
39
+ hybridSearch?: boolean;
40
+ conversationSummary?: string;
41
+ topicRouting?: boolean;
42
+ vacanciesBlock?: string;
43
+ vacancyGuard?: boolean;
44
+ skills?: readonly SkillForPrompt[];
45
+ /**
46
+ * Tenant-specific director hooks to inject into the system prompt.
47
+ * Loaded from `director_hooks` table filtered by `is_active = true`.
48
+ * Always injected (not stage-filtered) — the LLM decides when to apply.
49
+ */
50
+ directorHooks?: readonly DirectorHookForPrompt[];
51
+ booksPriority?: boolean;
52
+ /**
53
+ * Support mode — set when the lead is past the sales stage and waiting on a
54
+ * downstream process (`docs` = collecting their documents, `submitted` =
55
+ * filed). When set AND a `style` is active, the composed system prompt drops
56
+ * the sales framework / hooks / skills / few-shot / funnel-stage guidance and
57
+ * uses a calm FAQ-support block instead — answering questions without selling.
58
+ */
59
+ supportPhase?: "docs" | "submitted";
60
+ /**
61
+ * Called after every `answerWithRag` or `answerWithRagStream` call with the
62
+ * final telemetry. Useful for logging, metrics, or A/B experiment recording
63
+ * without having to unwrap the return value.
64
+ */
65
+ onTelemetry?: (telemetry: AnswerTelemetry) => void;
66
+ /**
67
+ * Optional tools the LLM can call during answer generation (single-cycle).
68
+ * When the model decides to call a tool, `execute()` is called automatically
69
+ * and the result is fed back for a final answer.
70
+ * Requires `chat` to implement `completeWithTools` (e.g. `OpenAIChatClient`),
71
+ * otherwise falls back to prompt-based tool injection.
72
+ */
73
+ tools?: AnyRagTool[];
74
+ /**
75
+ * Maximum number of agentic tool-calling cycles. Each cycle is one LLM call
76
+ * that may request tools, followed by execution of those tools. When the
77
+ * limit is reached, a final answer is forced WITHOUT tools. Only relevant
78
+ * when `tools` is set. Default: 4 (caps at 5 LLM calls including the final
79
+ * answer — note the latency and cost of a long tool chain).
80
+ */
81
+ maxToolCycles?: number;
82
+ /**
83
+ * Enable Maximal Marginal Relevance re-ranking after retrieval.
84
+ * Selects a diverse set of chunks so that repeated near-duplicate passages
85
+ * do not crowd out different sub-topics. Default: false.
86
+ */
87
+ mmr?: boolean;
88
+ /**
89
+ * λ (lambda) for MMR: trade-off between relevance and diversity.
90
+ * 1.0 = pure relevance, 0.0 = pure diversity. Default: 0.6.
91
+ * Only has effect when `mmr` is true.
92
+ */
93
+ mmrLambda?: number;
94
+ /**
95
+ * Trim retrieved hits that exceed a cosine-distance threshold before passing
96
+ * them to the LLM. Reduces hallucinations caused by weak/unrelated matches.
97
+ * Default: false (no trimming).
98
+ */
99
+ autoTrimDistance?: boolean;
100
+ /**
101
+ * Distance threshold used when `autoTrimDistance` is true.
102
+ * Cosine distance in [0, 2]; typical useful range is ≤ 0.4.
103
+ * Default: 0.45.
104
+ */
105
+ autoTrimThreshold?: number;
106
+ /**
107
+ * Enable multi-query expansion: generate `multiQueryCount` rephrased variants
108
+ * of the question with a fast LLM call, search with each in parallel, and
109
+ * merge all result lists via Reciprocal Rank Fusion (RRF). Improves recall
110
+ * for synonym gaps and differently-phrased concepts.
111
+ * Default: false.
112
+ */
113
+ multiQuery?: boolean;
114
+ /**
115
+ * Number of ADDITIONAL query variants to generate (not counting the original).
116
+ * Only has effect when `multiQuery` is true. Default: 2.
117
+ */
118
+ multiQueryCount?: number;
119
+ /**
120
+ * Optional cross-encoder reranker applied after vector/hybrid retrieval.
121
+ * Retrieves `topK * 3` candidates, passes them to the reranker, then keeps
122
+ * the top `topK`. Use `JinaReranker` or `CohereReranker` from this package.
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * import { JinaReranker } from "@chatman-media/kb";
127
+ * await answerWithRag({ ..., reranker: new JinaReranker({ apiKey: process.env.JINA_API_KEY! }) });
128
+ * ```
129
+ */
130
+ reranker?: Reranker;
131
+ /**
132
+ * When provided, the LLM is instructed to return a JSON object matching this
133
+ * Zod schema. The parsed and validated value is available as `result.output`.
134
+ * Uses OpenAI's native `response_format` when available; falls back to prompt
135
+ * injection for other providers (Ollama, OpenRouter, etc.).
136
+ *
137
+ * @example
138
+ * ```ts
139
+ * const result = await answerWithRag({
140
+ * question: "Classify this lead",
141
+ * kb, chat, embedder,
142
+ * outputSchema: z.object({
143
+ * intent: z.enum(["buy", "info", "not_interested"]),
144
+ * budget: z.number().optional(),
145
+ * nextAction: z.string(),
146
+ * }),
147
+ * });
148
+ * console.log(result.output.intent); // "buy" | "info" | "not_interested"
149
+ * ```
150
+ */
151
+ outputSchema?: z.ZodTypeAny;
152
+ }
153
+
154
+ export interface AnswerTelemetry {
155
+ path: "smalltalk" | "persona_fact" | "no_context" | "ungrounded" | "ok" | "cache_hit";
156
+ total_ms?: number;
157
+ retrieval_ms?: number;
158
+ generation_ms?: number;
159
+ top_distances?: number[];
160
+ hybrid?: boolean;
161
+ topic?: string | null;
162
+ original_query?: string;
163
+ rewritten_query?: string;
164
+ factCheck?: { grounded: boolean; vacancyOk: boolean; reason?: string };
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?: { name: string; result: unknown };
170
+ /**
171
+ * Every tool call executed across all agentic cycles, in order. Note that
172
+ * `args` may contain user-derived input — treat as sensitive if your tools
173
+ * receive PII.
174
+ */
175
+ toolCalls?: Array<{
176
+ name: string;
177
+ args: Record<string, unknown>;
178
+ result: unknown;
179
+ error?: boolean;
180
+ cycle: number;
181
+ }>;
182
+ }
183
+
184
+ export interface AnswerResult<TOutput = unknown> {
185
+ text: string;
186
+ usedChunkIds: number[];
187
+ hits: KbSearchHit[];
188
+ telemetry: AnswerTelemetry;
189
+ /** Parsed and validated output when `outputSchema` was passed to `answerWithRag`. */
190
+ output?: TOutput;
191
+ }