@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,49 @@
1
+ import { type ChunkOptions } from "./chunk.ts";
2
+ import type { EmbeddingClient } from "@chatman-media/llm-router";
3
+ import type { IKbStore } from "./types.ts";
4
+ export interface IngestDeps {
5
+ kb: IKbStore;
6
+ embedder: EmbeddingClient;
7
+ chunk?: Partial<ChunkOptions>;
8
+ topic?: string | null;
9
+ /** Overrides the default `file://<abs>` document source. Callers that
10
+ * ingest from a throwaway temp path (e.g. an admin upload route) pass a
11
+ * stable, content-addressed source here so re-uploads dedupe. */
12
+ source?: string;
13
+ /** Overrides the default `basename(abs)` document title — useful when the
14
+ * on-disk filename is a temp name rather than something operator-facing. */
15
+ title?: string;
16
+ }
17
+ export interface IngestFileResult {
18
+ source: string;
19
+ documentId: number;
20
+ chunks: number;
21
+ created: boolean;
22
+ }
23
+ export declare function ingestFile(path: string, deps: IngestDeps): Promise<IngestFileResult>;
24
+ /**
25
+ * Ingest a raw text document directly (no filesystem). Used by admin-UI
26
+ * paste path. Source is synthesised as `inline:<sha256[:12]>` so subsequent
27
+ * uploads of identical content dedupe via the `source` UNIQUE lookup.
28
+ */
29
+ export declare function ingestText(input: {
30
+ title: string;
31
+ body: string;
32
+ }, deps: IngestDeps): Promise<IngestFileResult>;
33
+ export interface IngestDirectorySummary {
34
+ documents: number;
35
+ chunks: number;
36
+ skipped: number;
37
+ }
38
+ export declare function ingestDirectory(dir: string, deps: IngestDeps): Promise<IngestDirectorySummary>;
39
+ /**
40
+ * Returns the immediate sub-directory name of `file` relative to `root`,
41
+ * or null when the file sits directly under `root`. Exported for unit tests.
42
+ */
43
+ export declare function deriveTopicFromPath(file: string, root: string): string | null;
44
+ /**
45
+ * Removes YAML frontmatter and HTML comments from Markdown — noise that
46
+ * pollutes embeddings. The visible body is preserved as-is.
47
+ */
48
+ export declare function stripNonContent(raw: string): string;
49
+ //# sourceMappingURL=ingest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ingest.d.ts","sourceRoot":"","sources":["../src/ingest.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,YAAY,EAAa,MAAM,YAAY,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAEjE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAI3C,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,QAAQ,CAAC;IACb,QAAQ,EAAE,eAAe,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB;;sEAEkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;iFAC6E;IAC7E,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA+C1F;AAED;;;;GAIG;AACH,wBAAsB,UAAU,CAC9B,KAAK,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACtC,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,gBAAgB,CAAC,CA4C3B;AAED,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,eAAe,CACnC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,sBAAsB,CAAC,CAejC;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK7E;AAaD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAMnD"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Multi-query expansion for RAG retrieval.
3
+ *
4
+ * Instead of searching with a single query, generate N semantically-equivalent
5
+ * rephrases in parallel using a fast LLM call, then search with each and merge
6
+ * the results via RRF (Reciprocal Rank Fusion). This covers synonym gaps and
7
+ * different formulations that a single embedding vector misses.
8
+ *
9
+ * Example — query "сколько стоит квартира в ЖК Марина":
10
+ * → "цена апартаментов в Marina Gate"
11
+ * → "стоимость юнитов Marina Dubai"
12
+ *
13
+ * Falls back gracefully to the original query on any LLM error.
14
+ */
15
+ import type { ChatClient, ChatMessage } from "@chatman-media/llm-router";
16
+ export interface ExpandQueriesInput {
17
+ question: string;
18
+ history?: ChatMessage[];
19
+ chat: ChatClient;
20
+ /** Number of ADDITIONAL variants to generate (not counting the original). Default: 2. */
21
+ count?: number;
22
+ }
23
+ /**
24
+ * Generate N alternative search queries for the given question.
25
+ * Always includes the original question as the first element.
26
+ * Returns `[question]` (single item) on any error.
27
+ */
28
+ export declare function expandQueries(input: ExpandQueriesInput): Promise<string[]>;
29
+ //# sourceMappingURL=multi-query.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"multi-query.d.ts","sourceRoot":"","sources":["../src/multi-query.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAGzE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,IAAI,EAAE,UAAU,CAAC;IACjB,yFAAyF;IACzF,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAuBD;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CA0BhF"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Extract plain text from a PDF file. Pages are joined with double newlines
3
+ * so paragraph boundaries survive. Throws on corrupt/encrypted files.
4
+ */
5
+ export declare function parsePdf(filePath: string): Promise<string>;
6
+ /**
7
+ * Extract plain text directly from a PDF buffer (Uint8Array). Use this when
8
+ * the PDF is already in memory (e.g. HTTP multipart upload) to avoid writing
9
+ * a temporary file.
10
+ *
11
+ * Throws on corrupt or encrypted PDFs.
12
+ */
13
+ export declare function parsePdfBuffer(buffer: Uint8Array): Promise<string>;
14
+ //# sourceMappingURL=parse-pdf.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-pdf.d.ts","sourceRoot":"","sources":["../src/parse-pdf.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGhE;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAIxE"}
@@ -0,0 +1,51 @@
1
+ import type { Persona } from "./answer-types.ts";
2
+ /**
3
+ * True when the message is asking about the persona's nature (bot vs human
4
+ * vs AI). Detected separately from the name/identity smalltalk because the
5
+ * answer depends on `persona.role`, not just on `persona.name`. Returns a
6
+ * single deterministic reply via `botPresenceReply` — bypassing RAG.
7
+ *
8
+ * Why this guard exists: the RAG system prompt contains an example reply
9
+ * ("конечно нет, я ${name}, чем помочь?") meant for "ты бот?". Without this
10
+ * shortcut, the LLM parrots that template literally — including for
11
+ * "ты человек?", inverting the answer ("конечно нет" = "I'm not human").
12
+ */
13
+ export declare function isBotPresenceQuestion(question: string): boolean;
14
+ /**
15
+ * Deterministic reply for `isBotPresenceQuestion`. For role=human the bot
16
+ * always asserts being human regardless of the question form ("ты бот?" /
17
+ * "ты человек?" both → same human reply). For role=assistant it admits
18
+ * being an AI helper.
19
+ */
20
+ export declare function botPresenceReply(persona: Persona): string;
21
+ /**
22
+ * True when the message is only smalltalk about identity (name / who are you).
23
+ * Catches the common bare forms candidates actually type — "как зовут?",
24
+ * "имя?", "представься" — not just the textbook "как тебя зовут".
25
+ *
26
+ * Returns false when the message also has any work/offer intent — even
27
+ * "как тебя зовут есть работа в китае?" should land in RAG, not the
28
+ * smalltalk shortcut, because the candidate is asking about a job.
29
+ */
30
+ export declare function isPersonaSmalltalkQuestion(question: string): boolean;
31
+ /**
32
+ * Returns a fact key ("city" | "age" | "status" | "experience") when the
33
+ * question is ONLY about that personal attribute of the persona, or `null`
34
+ * when it also contains job/offer intent (route to RAG in that case).
35
+ *
36
+ * Mirrors the `isPersonaSmalltalkQuestion` guard: same job-intent block list,
37
+ * same design — pure function, no side effects, safe to call unconditionally.
38
+ */
39
+ export declare function isPersonalFactQuestion(question: string): string | null;
40
+ /**
41
+ * Builds a short deterministic reply from `persona.facts[key]`.
42
+ * Returns `null` when the fact is not configured (caller falls through to RAG).
43
+ *
44
+ * "city" / "age" values are wrapped in natural templates; "status" /
45
+ * "experience" values are returned verbatim — the operator writes the full
46
+ * natural reply for these (e.g. "Не замужем, работа всё время занимает").
47
+ */
48
+ export declare function personaFactReply(persona: Persona, key: string): string | null;
49
+ /** Short reply derived from persona - no KB required. */
50
+ export declare function personaSmalltalkReply(persona: Persona): string;
51
+ //# sourceMappingURL=persona-shortcuts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persona-shortcuts.d.ts","sourceRoot":"","sources":["../src/persona-shortcuts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEjD;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAwB/D;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAQzD;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CA+CpE;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA2DtE;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAgB7E;AAgCD,yDAAyD;AACzD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAe9D"}
@@ -0,0 +1,9 @@
1
+ import type { ComposeOptions, FunnelStage, Style } from "./styles.ts";
2
+ /**
3
+ * Builds the system prompt for one turn of conversation in the given style
4
+ * and stage. Up to 8 sections: persona, voice, framework, hooks, stage,
5
+ * KB-grounding reminder (conditional), guardrails, few-shot (conditional),
6
+ * and KB context (conditional).
7
+ */
8
+ export declare function composeSystemPrompt(style: Style, stage: FunnelStage, preFetchedKbContext?: string | null, options?: ComposeOptions): string;
9
+ //# sourceMappingURL=prompt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../src/prompt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAQ,KAAK,EAAE,MAAM,aAAa,CAAC;AA6C5E;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,WAAW,EAClB,mBAAmB,GAAE,MAAM,GAAG,IAAW,EACzC,OAAO,GAAE,cAAmB,GAC3B,MAAM,CAqIR"}
@@ -0,0 +1,29 @@
1
+ import type { ChatClient } from "@chatman-media/llm-router";
2
+ /**
3
+ * Verifies that all factual claims in `answer` are grounded in `context`
4
+ * (the KB chunks retrieved for this turn). Used as a post-generation
5
+ * hallucination guard — if the LLM invented a number/city/condition, this
6
+ * catches it before the message reaches the candidate.
7
+ *
8
+ * Returns `{ grounded: true }` when the answer is fully supported by the
9
+ * context, or `{ grounded: false, reason }` when it isn't. The webhook
10
+ * caller is responsible for deciding what to do with `grounded:false` —
11
+ * typically: drop the reply (silent → mode stays "ai") or escalate.
12
+ */
13
+ export interface ReflectInput {
14
+ question: string;
15
+ answer: string;
16
+ /** The same KB CONTEXT that was passed to the generator. */
17
+ context: string;
18
+ chat: ChatClient;
19
+ }
20
+ export interface ReflectResult {
21
+ grounded: boolean;
22
+ reason?: string;
23
+ }
24
+ export declare function verifyAnswer(input: ReflectInput): Promise<ReflectResult>;
25
+ /** Parses the verifier's JSON output. Defaults to `grounded:true` on parse
26
+ * failure — false negatives are cheap (one wasted reply), but false positives
27
+ * here would silently drop legitimate answers. Exported for unit tests. */
28
+ export declare function parseReflection(raw: string): ReflectResult;
29
+ //# sourceMappingURL=reflect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reflect.d.ts","sourceRoot":"","sources":["../src/reflect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAe,MAAM,2BAA2B,CAAC;AAGzE;;;;;;;;;;GAUG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,UAAU,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAmBD,wBAAsB,YAAY,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC,CA6B9E;AAED;;4EAE4E;AAC5E,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAoB1D"}
@@ -0,0 +1,71 @@
1
+ import type { KbSearchHit } from "./types.ts";
2
+ /**
3
+ * Cross-encoder reranker interface. Called after initial retrieval (vector /
4
+ * hybrid) to re-score and re-order hits using a more expensive but accurate
5
+ * relevance model. Optional third stage in the retrieval pipeline.
6
+ */
7
+ export interface Reranker {
8
+ rerank(query: string, hits: KbSearchHit[], topK?: number): Promise<KbSearchHit[]>;
9
+ }
10
+ export interface CohereRerankerOptions {
11
+ apiKey: string;
12
+ /** Default: "rerank-v3.5" */
13
+ model?: string;
14
+ /** Base URL. Default: "https://api.cohere.com/v2" */
15
+ baseUrl?: string;
16
+ /** Per-request timeout ms. Default: 30_000. */
17
+ timeoutMs?: number;
18
+ fetch?: typeof fetch;
19
+ }
20
+ /**
21
+ * Reranker backed by the Cohere Rerank API.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * import { CohereReranker } from "@chatman-media/kb";
26
+ *
27
+ * const reranker = new CohereReranker({ apiKey: process.env.COHERE_API_KEY! });
28
+ * const reranked = await reranker.rerank(question, hits, 5);
29
+ * ```
30
+ */
31
+ export declare class CohereReranker implements Reranker {
32
+ private readonly apiKey;
33
+ private readonly model;
34
+ private readonly baseUrl;
35
+ private readonly timeoutMs;
36
+ private readonly fetchImpl;
37
+ constructor(opts: CohereRerankerOptions);
38
+ rerank(query: string, hits: KbSearchHit[], topK?: number): Promise<KbSearchHit[]>;
39
+ }
40
+ export interface JinaRerankerOptions {
41
+ apiKey: string;
42
+ /** Default: "jina-reranker-v2-base-multilingual" */
43
+ model?: string;
44
+ /** Base URL. Default: "https://api.jina.ai/v1" */
45
+ baseUrl?: string;
46
+ /** Per-request timeout ms. Default: 30_000. */
47
+ timeoutMs?: number;
48
+ fetch?: typeof fetch;
49
+ }
50
+ /**
51
+ * Reranker backed by the Jina Reranker API.
52
+ * The default model is multilingual — works well for Russian and Chinese KB.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * import { JinaReranker } from "@chatman-media/kb";
57
+ *
58
+ * const reranker = new JinaReranker({ apiKey: process.env.JINA_API_KEY! });
59
+ * const reranked = await reranker.rerank(question, hits, 5);
60
+ * ```
61
+ */
62
+ export declare class JinaReranker implements Reranker {
63
+ private readonly apiKey;
64
+ private readonly model;
65
+ private readonly baseUrl;
66
+ private readonly timeoutMs;
67
+ private readonly fetchImpl;
68
+ constructor(opts: JinaRerankerOptions);
69
+ rerank(query: string, hits: KbSearchHit[], topK?: number): Promise<KbSearchHit[]>;
70
+ }
71
+ //# sourceMappingURL=reranker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reranker.d.ts","sourceRoot":"","sources":["../src/reranker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;GAIG;AACH,MAAM,WAAW,QAAQ;IACvB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;CACnF;AAID,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,6BAA6B;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+CAA+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AAOD;;;;;;;;;;GAUG;AACH,qBAAa,cAAe,YAAW,QAAQ;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;gBAE7B,IAAI,EAAE,qBAAqB;IASjC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;CA+BxF;AAID,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+CAA+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AAOD;;;;;;;;;;;GAWG;AACH,qBAAa,YAAa,YAAW,QAAQ;IAC3C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;gBAE7B,IAAI,EAAE,mBAAmB;IAS/B,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;CA8BxF"}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Post-retrieval utilities for the RAG pipeline.
3
+ *
4
+ * Three independent transforms, applied in order:
5
+ *
6
+ * 0. **`rrfMerge`** — merge multiple hit-lists (from multi-query expansion) via
7
+ * Reciprocal Rank Fusion. Deduplicates by chunk_id, boosts chunks that rank
8
+ * high across multiple queries.
9
+ *
10
+ * 1. **`applyDynamicThreshold`** — trim hits that are "too far" from the query.
11
+ * Prevents hallucination-inducing weak matches from polluting the context.
12
+ *
13
+ * 2. **`mmrDiversify`** — re-rank via Maximal Marginal Relevance so that the
14
+ * final context window covers diverse sub-topics rather than repeating the
15
+ * same dense cluster of near-duplicate chunks.
16
+ *
17
+ * All functions are pure (no I/O) and operate on the {@link KbSearchHit} array
18
+ * that comes back from `IKbStore.search` / `IKbStore.hybridSearch`.
19
+ */
20
+ import type { KbSearchHit } from "./types.ts";
21
+ export interface RrfMergeOpts {
22
+ /**
23
+ * RRF smoothing constant. Higher = more weight to lower-ranked items.
24
+ * Standard value: 60.
25
+ */
26
+ k?: number;
27
+ /**
28
+ * Maximum number of hits to return. Defaults to all unique hits.
29
+ */
30
+ topN?: number;
31
+ }
32
+ /**
33
+ * Reciprocal Rank Fusion — merge multiple retrieval result lists into one.
34
+ *
35
+ * Each hit's score is the sum of `1 / (k + rank)` across all lists where it
36
+ * appears (1-based rank). Hits that appear in multiple lists get boosted.
37
+ * Deduplication is by `chunk_id`.
38
+ *
39
+ * The output uses the distance convention (lower = better):
40
+ * `distance = 1 / (1 + rrf_score)` so values stay in (0, 1].
41
+ *
42
+ * @param hitLists One list per query, each sorted best-first.
43
+ */
44
+ export declare function rrfMerge(hitLists: KbSearchHit[][], opts?: RrfMergeOpts): KbSearchHit[];
45
+ export interface DynamicThresholdOpts {
46
+ /**
47
+ * Drop hits whose `distance` exceeds this value.
48
+ * Cosine distance is in [0, 2]; typical "useful" range is ≤ 0.4.
49
+ * Default: 0.45.
50
+ */
51
+ threshold?: number;
52
+ /**
53
+ * Always keep at least this many hits even if they all exceed the threshold.
54
+ * Prevents the context from going completely empty.
55
+ * Default: 1.
56
+ */
57
+ minHits?: number;
58
+ }
59
+ /**
60
+ * Trim hits that exceed a distance threshold, keeping at least `minHits`.
61
+ *
62
+ * When the best match is already weak (high distance), the whole batch is
63
+ * likely unhelpful — cap it so the model isn't given noise.
64
+ */
65
+ export declare function applyDynamicThreshold(hits: KbSearchHit[], opts?: DynamicThresholdOpts): KbSearchHit[];
66
+ export interface MmrOpts {
67
+ /**
68
+ * Trade-off between relevance and diversity.
69
+ * - 1.0 → pure relevance (same as original ranking)
70
+ * - 0.0 → pure diversity (greedy maximum coverage)
71
+ * Default: 0.6.
72
+ */
73
+ lambda?: number;
74
+ /**
75
+ * Maximum number of hits to return after diversification.
76
+ * Defaults to the full input length.
77
+ */
78
+ topK?: number;
79
+ }
80
+ /**
81
+ * Maximal Marginal Relevance re-ranking.
82
+ *
83
+ * Iteratively selects the next chunk that maximises:
84
+ * `score = λ * relevance - (1 - λ) * max_similarity_to_already_selected`
85
+ *
86
+ * Relevance is derived from the search distance (lower distance = higher
87
+ * relevance). Inter-chunk similarity is approximated with **Jaccard overlap on
88
+ * trigrams** — cheap, no extra embedder call required, and surprisingly
89
+ * effective at detecting paraphrase duplicates.
90
+ *
91
+ * @param hits Sorted by relevance (best first), as returned by the store.
92
+ */
93
+ export declare function mmrDiversify(hits: KbSearchHit[], opts?: MmrOpts): KbSearchHit[];
94
+ //# sourceMappingURL=retrieval-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retrieval-utils.d.ts","sourceRoot":"","sources":["../src/retrieval-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAI9C,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,CAAC,CAAC,EAAE,MAAM,CAAC;IACX;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,EAAE,IAAI,GAAE,YAAiB,GAAG,WAAW,EAAE,CA8B1F;AAID,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,WAAW,EAAE,EACnB,IAAI,GAAE,oBAAyB,GAC9B,WAAW,EAAE,CAMf;AAID,MAAM,WAAW,OAAO;IACtB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,EAAE,IAAI,GAAE,OAAY,GAAG,WAAW,EAAE,CAwCnF"}
@@ -0,0 +1,53 @@
1
+ import { type ChatClient } from "@chatman-media/llm-router";
2
+ import { type EmbeddingClient } from "@chatman-media/llm-router";
3
+ export interface RetryOptions {
4
+ /**
5
+ * Maximum number of attempts (including the first). Default: 3.
6
+ */
7
+ maxAttempts?: number;
8
+ /**
9
+ * Initial backoff in ms before the second attempt. Doubles on each retry.
10
+ * Default: 500.
11
+ */
12
+ initialDelayMs?: number;
13
+ /**
14
+ * Cap on backoff delay in ms. Default: 30_000.
15
+ */
16
+ maxDelayMs?: number;
17
+ /**
18
+ * HTTP status codes that should trigger a retry.
19
+ * Default: [429, 500, 502, 503, 504].
20
+ */
21
+ retryOn?: number[];
22
+ }
23
+ /**
24
+ * Wraps any `ChatClient` with automatic retry + exponential backoff.
25
+ *
26
+ * Retries on transient HTTP errors (429, 5xx) with jittered exponential
27
+ * backoff. Non-retryable errors (4xx other than 429) propagate immediately.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * import { withRetryChatClient, OpenAIChatClient } from "@chatman-media/kb";
32
+ *
33
+ * const chat = withRetryChatClient(
34
+ * new OpenAIChatClient({ apiKey, baseUrl, model }),
35
+ * { maxAttempts: 4, initialDelayMs: 1000 },
36
+ * );
37
+ * ```
38
+ */
39
+ export declare function withRetryChatClient(client: ChatClient, opts?: RetryOptions): ChatClient;
40
+ /**
41
+ * Wraps any `EmbeddingClient` with automatic retry + exponential backoff.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * import { withRetryEmbeddingClient, OpenAIEmbeddingClient } from "@chatman-media/kb";
46
+ *
47
+ * const embedder = withRetryEmbeddingClient(
48
+ * new OpenAIEmbeddingClient({ apiKey, baseUrl, model, dim: 1536 }),
49
+ * );
50
+ * ```
51
+ */
52
+ export declare function withRetryEmbeddingClient(client: EmbeddingClient, opts?: RetryOptions): EmbeddingClient;
53
+ //# sourceMappingURL=retry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../src/retry.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,UAAU,EAGhB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAqB,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAEpF,MAAM,WAAW,YAAY;IAC3B;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAwCD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,GAAE,YAAiB,GAAG,UAAU,CA4B3F;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,eAAe,EACvB,IAAI,GAAE,YAAiB,GACtB,eAAe,CAUjB"}
@@ -0,0 +1,30 @@
1
+ import type { ChatClient, ChatMessage } from "@chatman-media/llm-router";
2
+ /**
3
+ * Rewrites a user question into a search-friendly query using recent
4
+ * conversation history. Resolves pronouns ("это", "там", "то"), expands
5
+ * elliptical follow-ups ("а сколько платят?" → "сколько платят моделям в
6
+ * Дубае"), and folds in named entities from prior turns.
7
+ *
8
+ * Why this matters: vector search on the raw user message misses precision
9
+ * on follow-ups because embeddings of "а в дубае?" sit nowhere near the
10
+ * actual KB chunks about Dubai contracts. Rewriting bridges that gap.
11
+ */
12
+ export interface RewriteQueryInput {
13
+ question: string;
14
+ /** Recent dialog (oldest first), excluding the current question. */
15
+ history?: ChatMessage[];
16
+ chat: ChatClient;
17
+ /** Cap output length to avoid the model writing essays. Default 200 chars. */
18
+ maxLength?: number;
19
+ }
20
+ /**
21
+ * Heuristic: skip rewriting when the question is already self-contained.
22
+ * Saves an LLM call (and thus latency + $) on the majority of inbound
23
+ * messages which are full standalone questions, not follow-ups.
24
+ */
25
+ export declare function questionNeedsRewrite(question: string, history?: ChatMessage[]): boolean;
26
+ export declare function rewriteQuery(input: RewriteQueryInput): Promise<string>;
27
+ /** Strips think-tags, "ответ:" prefixes, markdown, line breaks. Falls back
28
+ * to original on empty/garbage output. Exported for unit tests. */
29
+ export declare function sanitizeRewritten(raw: string, fallback: string, maxLength: number): string;
30
+ //# sourceMappingURL=rewrite-query.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rewrite-query.d.ts","sourceRoot":"","sources":["../src/rewrite-query.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAGzE;;;;;;;;;GASG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,IAAI,EAAE,UAAU,CAAC;IACjB,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAwBD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,EAAE,GAAG,OAAO,CAwBvF;AAED,wBAAsB,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAgC5E;AAED;oEACoE;AACpE,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAY1F"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Strip `<think>…</think>` reasoning blocks some chat models emit despite
3
+ * system instructions (qwen3, deepseek-r1 style). Both well-formed
4
+ * paired and an unclosed leading block are handled.
5
+ */
6
+ export declare function stripThinkBlocks(raw: string): string;
7
+ /** Strip markdown code fences (` ``` ` and ` ```json `). Useful when an LLM
8
+ * wraps its JSON answer in a fenced block despite "only JSON" instructions. */
9
+ export declare function stripCodeFences(raw: string): string;
10
+ /**
11
+ * Strip artifacts some chat models emit despite system instructions:
12
+ * - `<think>…</think>` reasoning blocks (qwen3, deepseek-r1 style).
13
+ * - leading "Answer:" / "Ответ:" / "Согласно контексту" prefixes.
14
+ * - surrounding whitespace.
15
+ * - "AI tells" — em-/en-dashes, unicode ellipsis, "Конечно!" lead-ins
16
+ * (see `text-style-rules.ts` for the full list).
17
+ *
18
+ * Exported for unit tests.
19
+ */
20
+ export declare function sanitizeLlmOutput(raw: string): string;
21
+ //# sourceMappingURL=sanitize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../src/sanitize.ts"],"names":[],"mappings":"AAWA;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;gFACgF;AAChF,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAOrD"}
@@ -0,0 +1,70 @@
1
+ import type { AnswerResult } from "./answer-types.ts";
2
+ import type { EmbeddingClient } from "@chatman-media/llm-router";
3
+ export interface SemanticCacheOptions {
4
+ /**
5
+ * Cosine similarity threshold for a cache hit (0–1). Higher = stricter.
6
+ * Default: 0.92 — catches paraphrases and minor reformulations.
7
+ */
8
+ threshold?: number;
9
+ /**
10
+ * Maximum number of entries to keep in memory. Oldest entries are evicted
11
+ * when the limit is reached. Default: 500.
12
+ */
13
+ maxEntries?: number;
14
+ /**
15
+ * TTL in milliseconds. Entries older than this are ignored. Default: 1 hour.
16
+ * Set to `Infinity` to disable expiry.
17
+ */
18
+ ttlMs?: number;
19
+ }
20
+ /**
21
+ * In-memory semantic cache for `answerWithRag` results.
22
+ *
23
+ * Stores (question embedding → AnswerResult) pairs. On lookup, computes
24
+ * cosine similarity between the incoming question embedding and all cached
25
+ * embeddings, returning the best match above `threshold`.
26
+ *
27
+ * Use `SemanticCache.wrap()` to get a drop-in cached version of any async
28
+ * function that accepts an `EmbeddingClient` and a question string.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * import { SemanticCache, answerWithRag } from "@chatman-media/kb";
33
+ *
34
+ * const cache = new SemanticCache(embedder, { threshold: 0.93, ttlMs: 30 * 60_000 });
35
+ *
36
+ * const result = await cache.getOrSet(
37
+ * input.question,
38
+ * () => answerWithRag(input),
39
+ * );
40
+ * ```
41
+ */
42
+ export declare class SemanticCache {
43
+ private readonly embedder;
44
+ private readonly threshold;
45
+ private readonly maxEntries;
46
+ private readonly ttlMs;
47
+ private entries;
48
+ /** Total cache hits since creation. */
49
+ hits: number;
50
+ /** Total cache misses since creation. */
51
+ misses: number;
52
+ constructor(embedder: EmbeddingClient, opts?: SemanticCacheOptions);
53
+ /**
54
+ * Look up `question` in the cache. If a similar question was cached and is
55
+ * still fresh, returns the cached `AnswerResult` with `telemetry.path`
56
+ * overridden to `"cache_hit"`. Otherwise calls `fn()`, stores the result,
57
+ * and returns it.
58
+ */
59
+ getOrSet(question: string, fn: () => Promise<AnswerResult>): Promise<AnswerResult>;
60
+ /** Manually insert a question→result pair (e.g. from a warm-up script). */
61
+ prime(question: string, result: AnswerResult): Promise<void>;
62
+ /** Remove all entries. */
63
+ clear(): void;
64
+ /** Number of live (non-expired) entries. */
65
+ get size(): number;
66
+ private findBestMatch;
67
+ private store;
68
+ private evictExpired;
69
+ }
70
+ //# sourceMappingURL=semantic-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semantic-cache.d.ts","sourceRoot":"","sources":["../src/semantic-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAmB,MAAM,mBAAmB,CAAC;AACvE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAEjE,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAQD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkB;IAC3C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,OAAO,CAAoB;IAEnC,uCAAuC;IACvC,IAAI,SAAK;IACT,yCAAyC;IACzC,MAAM,SAAK;gBAEC,QAAQ,EAAE,eAAe,EAAE,IAAI,GAAE,oBAAyB;IAOtE;;;;;OAKG;IACG,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,YAAY,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC;IAqBxF,2EAA2E;IACrE,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAKlE,0BAA0B;IAC1B,KAAK,IAAI,IAAI;IAIb,4CAA4C;IAC5C,IAAI,IAAI,IAAI,MAAM,CAGjB;IAID,OAAO,CAAC,aAAa;IAarB,OAAO,CAAC,KAAK;IAQb,OAAO,CAAC,YAAY;CAKrB"}