@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.
- package/LICENSE +21 -0
- package/README.md +169 -0
- package/dist/ab-router.d.ts +66 -0
- package/dist/ab-router.d.ts.map +1 -0
- package/dist/answer-types.d.ts +194 -0
- package/dist/answer-types.d.ts.map +1 -0
- package/dist/answer.d.ts +59 -0
- package/dist/answer.d.ts.map +1 -0
- package/dist/built-in-tools/calendly.d.ts +19 -0
- package/dist/built-in-tools/calendly.d.ts.map +1 -0
- package/dist/chunk.d.ts +48 -0
- package/dist/chunk.d.ts.map +1 -0
- package/dist/conversation-store.d.ts +76 -0
- package/dist/conversation-store.d.ts.map +1 -0
- package/dist/eval.d.ts +64 -0
- package/dist/eval.d.ts.map +1 -0
- package/dist/extract-user-facts.d.ts +27 -0
- package/dist/extract-user-facts.d.ts.map +1 -0
- package/dist/fact-checker.d.ts +46 -0
- package/dist/fact-checker.d.ts.map +1 -0
- package/dist/grade-skills.d.ts +29 -0
- package/dist/grade-skills.d.ts.map +1 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +62655 -0
- package/dist/ingest.d.ts +49 -0
- package/dist/ingest.d.ts.map +1 -0
- package/dist/multi-query.d.ts +29 -0
- package/dist/multi-query.d.ts.map +1 -0
- package/dist/parse-pdf.d.ts +14 -0
- package/dist/parse-pdf.d.ts.map +1 -0
- package/dist/persona-shortcuts.d.ts +51 -0
- package/dist/persona-shortcuts.d.ts.map +1 -0
- package/dist/prompt.d.ts +9 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/reflect.d.ts +29 -0
- package/dist/reflect.d.ts.map +1 -0
- package/dist/reranker.d.ts +71 -0
- package/dist/reranker.d.ts.map +1 -0
- package/dist/retrieval-utils.d.ts +94 -0
- package/dist/retrieval-utils.d.ts.map +1 -0
- package/dist/retry.d.ts +53 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/rewrite-query.d.ts +30 -0
- package/dist/rewrite-query.d.ts.map +1 -0
- package/dist/sanitize.d.ts +21 -0
- package/dist/sanitize.d.ts.map +1 -0
- package/dist/semantic-cache.d.ts +70 -0
- package/dist/semantic-cache.d.ts.map +1 -0
- package/dist/server.d.ts +77 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/stores/memory-store.d.ts +72 -0
- package/dist/stores/memory-store.d.ts.map +1 -0
- package/dist/structured-output.d.ts +21 -0
- package/dist/structured-output.d.ts.map +1 -0
- package/dist/styles.d.ts +186 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/summarize-conversation.d.ts +31 -0
- package/dist/summarize-conversation.d.ts.map +1 -0
- package/dist/system-prompt.d.ts +11 -0
- package/dist/system-prompt.d.ts.map +1 -0
- package/dist/text-style-rules.d.ts +133 -0
- package/dist/text-style-rules.d.ts.map +1 -0
- package/dist/tool-loop.d.ts +44 -0
- package/dist/tool-loop.d.ts.map +1 -0
- package/dist/tools.d.ts +64 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/topic-classifier.d.ts +11 -0
- package/dist/topic-classifier.d.ts.map +1 -0
- package/dist/types.d.ts +83 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils.d.ts +19 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/vision.d.ts +72 -0
- package/dist/vision.d.ts.map +1 -0
- package/package.json +76 -0
- package/src/ab-router.ts +118 -0
- package/src/answer-types.ts +191 -0
- package/src/answer.ts +696 -0
- package/src/built-in-tools/calendly.ts +32 -0
- package/src/chunk.ts +198 -0
- package/src/conversation-store.ts +138 -0
- package/src/eval.ts +127 -0
- package/src/extract-user-facts.ts +120 -0
- package/src/fact-checker.ts +171 -0
- package/src/grade-skills.ts +79 -0
- package/src/index.ts +191 -0
- package/src/ingest.ts +193 -0
- package/src/multi-query.ts +89 -0
- package/src/parse-pdf.ts +24 -0
- package/src/persona-shortcuts.ts +255 -0
- package/src/prompt.ts +190 -0
- package/src/reflect.ts +99 -0
- package/src/reranker.ts +166 -0
- package/src/retrieval-utils.ts +209 -0
- package/src/retry.ts +139 -0
- package/src/rewrite-query.ts +124 -0
- package/src/sanitize.ts +44 -0
- package/src/semantic-cache.ts +154 -0
- package/src/server.ts +164 -0
- package/src/stores/memory-store.ts +249 -0
- package/src/structured-output.ts +47 -0
- package/src/styles.ts +138 -0
- package/src/summarize-conversation.ts +88 -0
- package/src/system-prompt.ts +118 -0
- package/src/text-style-rules.ts +244 -0
- package/src/tool-loop.ts +110 -0
- package/src/tools.ts +79 -0
- package/src/topic-classifier.ts +112 -0
- package/src/types.ts +91 -0
- package/src/utils.ts +81 -0
- package/src/vision.ts +265 -0
package/dist/tools.d.ts
ADDED
|
@@ -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"}
|
package/dist/types.d.ts
ADDED
|
@@ -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"}
|
package/dist/utils.d.ts
ADDED
|
@@ -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"}
|
package/dist/vision.d.ts
ADDED
|
@@ -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
|
+
}
|
package/src/ab-router.ts
ADDED
|
@@ -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
|
+
}
|