lazlo-ai 1.0.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 (126) hide show
  1. package/.env.example +9 -0
  2. package/README.md +278 -0
  3. package/dist/cache/semantic.d.ts +39 -0
  4. package/dist/cache/semantic.d.ts.map +1 -0
  5. package/dist/cache/semantic.js +134 -0
  6. package/dist/cache/semantic.js.map +1 -0
  7. package/dist/chains/llmchain.d.ts +65 -0
  8. package/dist/chains/llmchain.d.ts.map +1 -0
  9. package/dist/chains/llmchain.js +137 -0
  10. package/dist/chains/llmchain.js.map +1 -0
  11. package/dist/chains/rag.d.ts +23 -0
  12. package/dist/chains/rag.d.ts.map +1 -0
  13. package/dist/chains/rag.js +47 -0
  14. package/dist/chains/rag.js.map +1 -0
  15. package/dist/core/types.d.ts +130 -0
  16. package/dist/core/types.d.ts.map +1 -0
  17. package/dist/core/types.js +8 -0
  18. package/dist/core/types.js.map +1 -0
  19. package/dist/document_loaders/index.d.ts +61 -0
  20. package/dist/document_loaders/index.d.ts.map +1 -0
  21. package/dist/document_loaders/index.js +183 -0
  22. package/dist/document_loaders/index.js.map +1 -0
  23. package/dist/embeddings/google.d.ts +43 -0
  24. package/dist/embeddings/google.d.ts.map +1 -0
  25. package/dist/embeddings/google.js +90 -0
  26. package/dist/embeddings/google.js.map +1 -0
  27. package/dist/embeddings/local.d.ts +64 -0
  28. package/dist/embeddings/local.d.ts.map +1 -0
  29. package/dist/embeddings/local.js +95 -0
  30. package/dist/embeddings/local.js.map +1 -0
  31. package/dist/evals/judge.d.ts +22 -0
  32. package/dist/evals/judge.d.ts.map +1 -0
  33. package/dist/evals/judge.js +77 -0
  34. package/dist/evals/judge.js.map +1 -0
  35. package/dist/index.d.ts +28 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +84 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/memory/buffer.d.ts +64 -0
  40. package/dist/memory/buffer.d.ts.map +1 -0
  41. package/dist/memory/buffer.js +168 -0
  42. package/dist/memory/buffer.js.map +1 -0
  43. package/dist/parsers/output.d.ts +64 -0
  44. package/dist/parsers/output.d.ts.map +1 -0
  45. package/dist/parsers/output.js +148 -0
  46. package/dist/parsers/output.js.map +1 -0
  47. package/dist/prompts/registry.d.ts +65 -0
  48. package/dist/prompts/registry.d.ts.map +1 -0
  49. package/dist/prompts/registry.js +170 -0
  50. package/dist/prompts/registry.js.map +1 -0
  51. package/dist/providers/ollama.d.ts +30 -0
  52. package/dist/providers/ollama.d.ts.map +1 -0
  53. package/dist/providers/ollama.js +104 -0
  54. package/dist/providers/ollama.js.map +1 -0
  55. package/dist/providers/openai.d.ts +46 -0
  56. package/dist/providers/openai.d.ts.map +1 -0
  57. package/dist/providers/openai.js +228 -0
  58. package/dist/providers/openai.js.map +1 -0
  59. package/dist/retrievers/index.d.ts +71 -0
  60. package/dist/retrievers/index.d.ts.map +1 -0
  61. package/dist/retrievers/index.js +130 -0
  62. package/dist/retrievers/index.js.map +1 -0
  63. package/dist/router/smartrouter.d.ts +36 -0
  64. package/dist/router/smartrouter.d.ts.map +1 -0
  65. package/dist/router/smartrouter.js +132 -0
  66. package/dist/router/smartrouter.js.map +1 -0
  67. package/dist/text_splitters/index.d.ts +28 -0
  68. package/dist/text_splitters/index.d.ts.map +1 -0
  69. package/dist/text_splitters/index.js +109 -0
  70. package/dist/text_splitters/index.js.map +1 -0
  71. package/dist/tools/decorator.d.ts +26 -0
  72. package/dist/tools/decorator.d.ts.map +1 -0
  73. package/dist/tools/decorator.js +102 -0
  74. package/dist/tools/decorator.js.map +1 -0
  75. package/dist/tools/index.d.ts +7 -0
  76. package/dist/tools/index.d.ts.map +1 -0
  77. package/dist/tools/index.js +6 -0
  78. package/dist/tools/index.js.map +1 -0
  79. package/dist/tools/keiro.d.ts +20 -0
  80. package/dist/tools/keiro.d.ts.map +1 -0
  81. package/dist/tools/keiro.js +67 -0
  82. package/dist/tools/keiro.js.map +1 -0
  83. package/dist/tracing/tracer.d.ts +56 -0
  84. package/dist/tracing/tracer.d.ts.map +1 -0
  85. package/dist/tracing/tracer.js +125 -0
  86. package/dist/tracing/tracer.js.map +1 -0
  87. package/dist/utils/logger.d.ts +25 -0
  88. package/dist/utils/logger.d.ts.map +1 -0
  89. package/dist/utils/logger.js +50 -0
  90. package/dist/utils/logger.js.map +1 -0
  91. package/dist/utils/pricing.d.ts +31 -0
  92. package/dist/utils/pricing.d.ts.map +1 -0
  93. package/dist/utils/pricing.js +108 -0
  94. package/dist/utils/pricing.js.map +1 -0
  95. package/dist/vectorstores/index.d.ts +62 -0
  96. package/dist/vectorstores/index.d.ts.map +1 -0
  97. package/dist/vectorstores/index.js +244 -0
  98. package/dist/vectorstores/index.js.map +1 -0
  99. package/package.json +48 -0
  100. package/src/cache/semantic.ts +175 -0
  101. package/src/chains/llmchain.ts +194 -0
  102. package/src/chains/rag.ts +65 -0
  103. package/src/core/types.ts +178 -0
  104. package/src/document_loaders/index.ts +223 -0
  105. package/src/embeddings/google.ts +119 -0
  106. package/src/embeddings/local.ts +118 -0
  107. package/src/evals/judge.ts +99 -0
  108. package/src/index.ts +121 -0
  109. package/src/memory/buffer.ts +222 -0
  110. package/src/parsers/output.ts +195 -0
  111. package/src/prompts/registry.ts +205 -0
  112. package/src/providers/ollama.ts +151 -0
  113. package/src/providers/openai.ts +320 -0
  114. package/src/retrievers/index.ts +182 -0
  115. package/src/router/smartrouter.ts +172 -0
  116. package/src/text_splitters/index.ts +145 -0
  117. package/src/tools/decorator.ts +145 -0
  118. package/src/tools/index.ts +7 -0
  119. package/src/tools/keiro.ts +92 -0
  120. package/src/tracing/tracer.ts +178 -0
  121. package/src/utils/logger.ts +62 -0
  122. package/src/utils/pricing.ts +133 -0
  123. package/src/vectorstores/index.ts +338 -0
  124. package/test-full.mjs +552 -0
  125. package/test.mjs +74 -0
  126. package/tsconfig.json +30 -0
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Ollama Provider - Local LLM inference
3
+ */
4
+
5
+ import axios, { AxiosInstance } from 'axios';
6
+ import {
7
+ BaseChatModel,
8
+ Message,
9
+ ChatResponse,
10
+ ToolDefinition,
11
+ StreamChunk
12
+ } from '../core/types.js';
13
+ import { logger } from '../utils/logger.js';
14
+
15
+ // ============================================================================
16
+ // Ollama Provider
17
+ // ============================================================================
18
+
19
+ export interface OllamaOptions {
20
+ model?: string;
21
+ baseURL?: string;
22
+ timeout?: number;
23
+ }
24
+
25
+ export class Ollama implements BaseChatModel {
26
+ readonly supportsTools = true;
27
+ readonly supportsStructuredOutput = true;
28
+
29
+ private model: string;
30
+ private baseURL: string;
31
+ private client: AxiosInstance;
32
+
33
+ constructor(options: OllamaOptions = {}) {
34
+ this.model = options.model ?? 'llama3.2';
35
+ this.baseURL = (options.baseURL ?? 'http://localhost:11434').replace(/\/$/, '');
36
+
37
+ this.client = axios.create({
38
+ baseURL: this.baseURL,
39
+ headers: { 'Content-Type': 'application/json' },
40
+ timeout: options.timeout ?? 300000,
41
+ });
42
+
43
+ logger.info(`Ollama Provider ready. Model: ${this.model}. Endpoint: ${this.baseURL}`);
44
+ }
45
+
46
+ private formatMessages(messages: Message[]): { role: string; content: string }[] {
47
+ return messages.map(m => ({
48
+ role: m.role,
49
+ content: m.content,
50
+ }));
51
+ }
52
+
53
+ async invoke(
54
+ messages: Message[],
55
+ options: {
56
+ model?: string;
57
+ tools?: ToolDefinition[];
58
+ temperature?: number;
59
+ } = {}
60
+ ): Promise<ChatResponse> {
61
+ const model = options.model ?? this.model;
62
+
63
+ const payload: Record<string, unknown> = {
64
+ model,
65
+ messages: this.formatMessages(messages),
66
+ stream: false,
67
+ };
68
+
69
+ if (options.tools) {
70
+ payload.tools = options.tools;
71
+ }
72
+
73
+ try {
74
+ const response = await this.client.post('/api/chat', payload);
75
+ const data = response.data;
76
+ const message = data.message;
77
+
78
+ return {
79
+ content: message?.content ?? '',
80
+ tool_calls: message?.tool_calls,
81
+ model: model,
82
+ finish_reason: data.done ? 'stop' : null,
83
+ };
84
+ } catch (error: any) {
85
+ if (error.code === 'ECONNREFUSED') {
86
+ throw new Error(`Cannot connect to Ollama at ${this.baseURL}. Is Ollama running?`);
87
+ }
88
+ logger.error(`Ollama invoke error: ${error.message}`);
89
+ throw error;
90
+ }
91
+ }
92
+
93
+ async *stream(
94
+ messages: Message[],
95
+ options: {
96
+ model?: string;
97
+ } = {}
98
+ ): AsyncGenerator<StreamChunk> {
99
+ const model = options.model ?? this.model;
100
+
101
+ const payload = {
102
+ model,
103
+ messages: this.formatMessages(messages),
104
+ stream: true,
105
+ };
106
+
107
+ const response = await this.client.post('/api/chat', payload, {
108
+ responseType: 'stream',
109
+ });
110
+
111
+ for await (const chunk of response.data) {
112
+ const lines = chunk.toString().split('\n').filter((l: string) => l.trim());
113
+
114
+ for (const line of lines) {
115
+ try {
116
+ const data = JSON.parse(line);
117
+ const content = data.message?.content ?? '';
118
+
119
+ if (content) {
120
+ yield { delta: content };
121
+ }
122
+
123
+ if (data.done) {
124
+ return;
125
+ }
126
+ } catch {
127
+ // Skip invalid JSON
128
+ }
129
+ }
130
+ }
131
+ }
132
+
133
+ async listModels(): Promise<string[]> {
134
+ try {
135
+ const response = await this.client.get('/api/tags');
136
+ return response.data.models?.map((m: any) => m.name) ?? [];
137
+ } catch (error) {
138
+ logger.error(`Could not list Ollama models: ${error}`);
139
+ return [];
140
+ }
141
+ }
142
+
143
+ close(): void {
144
+ logger.info('Ollama client closed.');
145
+ }
146
+ }
147
+
148
+ // Factory
149
+ export function createOllama(options?: OllamaOptions): Ollama {
150
+ return new Ollama(options);
151
+ }
@@ -0,0 +1,320 @@
1
+ /**
2
+ * OpenAI Provider
3
+ *
4
+ * Direct API access to OpenAI models. No massive SDKs, just axios calls.
5
+ */
6
+
7
+ import axios, { AxiosInstance, AxiosError } from 'axios';
8
+ import {
9
+ BaseChatModel,
10
+ Message,
11
+ ChatResponse,
12
+ ToolDefinition,
13
+ ResponseFormat,
14
+ StreamChunk
15
+ } from '../core/types.js';
16
+ import { logger } from '../utils/logger.js';
17
+ import { calculateCost } from '../utils/pricing.js';
18
+
19
+ // ============================================================================
20
+ // Circuit Breaker
21
+ // ============================================================================
22
+
23
+ class CircuitBreaker {
24
+ private failures = 0;
25
+ private lastFailure = 0;
26
+ private state: 'closed' | 'open' | 'half-open' = 'closed';
27
+
28
+ constructor(
29
+ private failMax = 5,
30
+ private resetTimeout = 60000
31
+ ) {}
32
+
33
+ async execute<T>(fn: () => Promise<T>): Promise<T> {
34
+ if (this.state === 'open') {
35
+ if (Date.now() - this.lastFailure > this.resetTimeout) {
36
+ this.state = 'half-open';
37
+ } else {
38
+ throw new Error('Circuit breaker open');
39
+ }
40
+ }
41
+
42
+ try {
43
+ const result = await fn();
44
+ if (this.state === 'half-open') {
45
+ this.state = 'closed';
46
+ this.failures = 0;
47
+ }
48
+ return result;
49
+ } catch (error) {
50
+ this.failures++;
51
+ this.lastFailure = Date.now();
52
+ if (this.failures >= this.failMax) {
53
+ this.state = 'open';
54
+ }
55
+ throw error;
56
+ }
57
+ }
58
+ }
59
+
60
+ const openAICircuitBreaker = new CircuitBreaker();
61
+
62
+ // ============================================================================
63
+ // Helper Functions
64
+ // ============================================================================
65
+
66
+ function isRetryableError(error: AxiosError): boolean {
67
+ if (axios.isAxiosError(error)) {
68
+ const status = error.response?.status;
69
+ return status === undefined || status === 429 || status >= 500;
70
+ }
71
+ return true;
72
+ }
73
+
74
+ function parseResponseFormat(format?: ResponseFormat): Record<string, unknown> | undefined {
75
+ if (!format) return undefined;
76
+
77
+ if (format.type === 'json_schema' && format.json_schema) {
78
+ return {
79
+ type: 'json_schema',
80
+ json_schema: {
81
+ name: format.json_schema.name,
82
+ schema: format.json_schema.schema,
83
+ strict: format.json_schema.strict ?? true,
84
+ },
85
+ };
86
+ }
87
+
88
+ return { type: format.type };
89
+ }
90
+
91
+ // Simple rate limiting
92
+ let lastRequestTime = 0;
93
+ const MIN_REQUEST_INTERVAL = 100; // ms between requests
94
+
95
+ async function rateLimit(): Promise<void> {
96
+ const now = Date.now();
97
+ const elapsed = now - lastRequestTime;
98
+ if (elapsed < MIN_REQUEST_INTERVAL) {
99
+ await new Promise(resolve => setTimeout(resolve, MIN_REQUEST_INTERVAL - elapsed));
100
+ }
101
+ lastRequestTime = Date.now();
102
+ }
103
+
104
+ // ============================================================================
105
+ // OpenAI Provider
106
+ // ============================================================================
107
+
108
+ export class OpenAI implements BaseChatModel {
109
+ readonly supportsTools = true;
110
+ readonly supportsStructuredOutput = true;
111
+
112
+ private client: AxiosInstance;
113
+ private defaultModel: string;
114
+
115
+ constructor(
116
+ apiKey?: string,
117
+ defaultModel = 'gpt-4o',
118
+ options: {
119
+ baseURL?: string;
120
+ timeout?: number;
121
+ } = {}
122
+ ) {
123
+ const key = apiKey ?? (typeof process !== 'undefined' ? (process as any).env?.OPENAI_API_KEY : undefined);
124
+ if (!key) {
125
+ throw new Error('OPENAI_API_KEY is required');
126
+ }
127
+
128
+ this.defaultModel = defaultModel;
129
+ this.client = axios.create({
130
+ baseURL: options.baseURL ?? 'https://api.openai.com/v1',
131
+ headers: {
132
+ 'Content-Type': 'application/json',
133
+ Authorization: `Bearer ${key}`,
134
+ },
135
+ timeout: options.timeout ?? 120000,
136
+ });
137
+
138
+ logger.info(`OpenAI Provider ready. Default Model: ${this.defaultModel}`);
139
+ }
140
+
141
+ private preparePayload(
142
+ model: string,
143
+ messages: Message[],
144
+ options: {
145
+ tools?: ToolDefinition[];
146
+ responseFormat?: ResponseFormat;
147
+ temperature?: number;
148
+ maxTokens?: number;
149
+ topP?: number;
150
+ stop?: string[];
151
+ stream?: boolean;
152
+ } = {}
153
+ ): Record<string, unknown> {
154
+ const payload: Record<string, unknown> = {
155
+ model,
156
+ messages,
157
+ };
158
+
159
+ if (options.tools) {
160
+ payload.tools = options.tools;
161
+ }
162
+
163
+ if (options.responseFormat) {
164
+ payload.response_format = parseResponseFormat(options.responseFormat);
165
+ }
166
+
167
+ if (options.temperature !== undefined) payload.temperature = options.temperature;
168
+ if (options.maxTokens !== undefined) payload.max_tokens = options.maxTokens;
169
+ if (options.topP !== undefined) payload.top_p = options.topP;
170
+ if (options.stop !== undefined) payload.stop = options.stop;
171
+ if (options.stream !== undefined) payload.stream = options.stream;
172
+
173
+ return payload;
174
+ }
175
+
176
+ async invoke(
177
+ messages: Message[],
178
+ options: {
179
+ model?: string;
180
+ tools?: ToolDefinition[];
181
+ responseFormat?: ResponseFormat;
182
+ temperature?: number;
183
+ maxTokens?: number;
184
+ topP?: number;
185
+ frequencyPenalty?: number;
186
+ presencePenalty?: number;
187
+ stop?: string | string[];
188
+ } = {}
189
+ ): Promise<ChatResponse> {
190
+ const model = options.model ?? this.defaultModel;
191
+
192
+ // Handle string input
193
+ const formattedMessages: Message[] = typeof messages === 'string'
194
+ ? [{ role: 'user', content: messages }]
195
+ : messages;
196
+
197
+ // Convert stop to array if needed
198
+ const stop = options.stop
199
+ ? (Array.isArray(options.stop) ? options.stop : [options.stop])
200
+ : undefined;
201
+
202
+ const payload = this.preparePayload(model, formattedMessages, {
203
+ ...options,
204
+ stop,
205
+ });
206
+
207
+ return openAICircuitBreaker.execute(async () => {
208
+ await rateLimit();
209
+
210
+ try {
211
+ const response = await this.client.post('/chat/completions', payload);
212
+ const data = response.data;
213
+
214
+ if (!data.choices?.length) {
215
+ throw new Error('Empty response from OpenAI');
216
+ }
217
+
218
+ const choice = data.choices[0];
219
+ const usage = data.usage ?? {};
220
+
221
+ const result: ChatResponse = {
222
+ content: choice.message?.content ?? '',
223
+ tool_calls: choice.message?.tool_calls,
224
+ usage: {
225
+ prompt_tokens: usage.prompt_tokens ?? 0,
226
+ completion_tokens: usage.completion_tokens ?? 0,
227
+ total_tokens: usage.total_tokens ?? 0,
228
+ },
229
+ model: model,
230
+ finish_reason: choice.finish_reason,
231
+ };
232
+
233
+ logger.debug(`OpenAI: ${usage.total_tokens ?? 0} tokens, $${calculateCost(model, usage.prompt_tokens ?? 0, usage.completion_tokens ?? 0).toFixed(6)}`);
234
+
235
+ return result;
236
+ } catch (error) {
237
+ if (axios.isAxiosError(error)) {
238
+ logger.error(`OpenAI API error: ${error.message}`);
239
+ }
240
+ throw error;
241
+ }
242
+ });
243
+ }
244
+
245
+ async *stream(
246
+ messages: Message[],
247
+ options: {
248
+ model?: string;
249
+ temperature?: number;
250
+ maxTokens?: number;
251
+ topP?: number;
252
+ stop?: string[];
253
+ } = {}
254
+ ): AsyncGenerator<StreamChunk> {
255
+ const model = options.model ?? this.defaultModel;
256
+
257
+ const formattedMessages: Message[] = typeof messages === 'string'
258
+ ? [{ role: 'user', content: messages }]
259
+ : messages;
260
+
261
+ const payload = this.preparePayload(model, formattedMessages, { ...options, stream: true });
262
+
263
+ const response = await this.client.post('/chat/completions', payload, {
264
+ responseType: 'stream',
265
+ });
266
+
267
+ for await (const chunk of response.data) {
268
+ const lines = chunk.toString().split('\n').filter((line: string) => line.trim() !== '');
269
+
270
+ for (const line of lines) {
271
+ if (line.startsWith('data: ')) {
272
+ const data = line.slice(6);
273
+
274
+ if (data === '[DONE]') {
275
+ return;
276
+ }
277
+
278
+ try {
279
+ const parsed = JSON.parse(data);
280
+ const delta = parsed.choices?.[0]?.delta?.content;
281
+
282
+ if (delta) {
283
+ yield {
284
+ delta,
285
+ finish_reason: parsed.choices[0]?.finish_reason,
286
+ index: parsed.choices[0]?.index,
287
+ };
288
+ }
289
+ } catch {
290
+ // Skip malformed JSON
291
+ }
292
+ }
293
+ }
294
+ }
295
+ }
296
+
297
+ close(): void {
298
+ logger.info('OpenAI client closed.');
299
+ }
300
+
301
+ // Alias for compatibility - async version calls the same implementation
302
+ async ainvoke(messages: Message[], options?: {
303
+ model?: string;
304
+ tools?: ToolDefinition[];
305
+ responseFormat?: ResponseFormat;
306
+ temperature?: number;
307
+ maxTokens?: number;
308
+ topP?: number;
309
+ }): Promise<ChatResponse> {
310
+ return this.invoke(messages, options);
311
+ }
312
+ }
313
+
314
+ // ============================================================================
315
+ // Factory Function
316
+ // ============================================================================
317
+
318
+ export function createOpenAI(apiKey?: string, defaultModel?: string): OpenAI {
319
+ return new OpenAI(apiKey, defaultModel);
320
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Retrievers
3
+ *
4
+ * Base interfaces for document retrieval
5
+ */
6
+
7
+ import { InMemoryVectorStore } from '../vectorstores/index.js';
8
+ import { logger } from '../utils/logger.js';
9
+
10
+ export interface BaseRetriever {
11
+ getRelevantDocuments(query: string): Promise<Document[]>;
12
+ }
13
+
14
+ export interface Document {
15
+ pageContent: string;
16
+ metadata: Record<string, unknown>;
17
+ }
18
+
19
+ export interface VectorStore {
20
+ similaritySearch(query: string, k?: number): Promise<Document[]>;
21
+ similaritySearchWithScore(query: string, k?: number): Promise<[Document, number][]>;
22
+ addDocuments(documents: Document[]): Promise<void>;
23
+ }
24
+
25
+ /**
26
+ * Contextual Compression Retriever
27
+ *
28
+ * Compresses retrieved documents based on query context
29
+ */
30
+ export class ContextualCompressionRetriever implements BaseRetriever {
31
+ private baseRetriever: BaseRetriever;
32
+ private compressor?: (doc: Document, query: string) => Promise<Document>;
33
+
34
+ constructor(options: {
35
+ baseRetriever: BaseRetriever;
36
+ compressor?: (doc: Document, query: string) => Promise<Document>;
37
+ }) {
38
+ this.baseRetriever = options.baseRetriever;
39
+ this.compressor = options.compressor;
40
+ }
41
+
42
+ async getRelevantDocuments(query: string): Promise<Document[]> {
43
+ const docs = await this.baseRetriever.getRelevantDocuments(query);
44
+
45
+ if (!this.compressor) {
46
+ return docs;
47
+ }
48
+
49
+ // Compress each document
50
+ const compressed: Document[] = [];
51
+ for (const doc of docs) {
52
+ const compressedDoc = await this.compressor(doc, query);
53
+ compressed.push(compressedDoc);
54
+ }
55
+
56
+ return compressed;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Parent Document Retriever
62
+ *
63
+ * Retrieves full parent documents from embedded chunks
64
+ */
65
+ export class ParentDocumentRetriever implements BaseRetriever {
66
+ private childRetriever: BaseRetriever;
67
+ private idKey: string;
68
+ private parentDocuments: Map<string, Document> = new Map();
69
+
70
+ constructor(options: {
71
+ childRetriever: BaseRetriever;
72
+ idKey?: string;
73
+ }) {
74
+ this.childRetriever = options.childRetriever;
75
+ this.idKey = options.idKey || 'parent_id';
76
+ }
77
+
78
+ /**
79
+ * Add parent documents
80
+ */
81
+ addParentDocuments(documents: Document[]): void {
82
+ for (const doc of documents) {
83
+ const id = doc.metadata[this.idKey] as string || crypto.randomUUID();
84
+ this.parentDocuments.set(id, doc);
85
+ }
86
+ }
87
+
88
+ async getRelevantDocuments(query: string): Promise<Document[]> {
89
+ const childDocs = await this.childRetriever.getRelevantDocuments(query);
90
+
91
+ // Get unique parent IDs
92
+ const parentIds = new Set<string>();
93
+ for (const doc of childDocs) {
94
+ const parentId = doc.metadata[this.idKey] as string;
95
+ if (parentId) {
96
+ parentIds.add(parentId);
97
+ }
98
+ }
99
+
100
+ // Return parent documents
101
+ const parents: Document[] = [];
102
+ for (const id of parentIds) {
103
+ const parent = this.parentDocuments.get(id);
104
+ if (parent) {
105
+ parents.push(parent);
106
+ }
107
+ }
108
+
109
+ return parents.length > 0 ? parents : childDocs;
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Ensemble Retriever
115
+ *
116
+ * Combines multiple retrievers with weighted scoring
117
+ */
118
+ export class EnsembleRetriever implements BaseRetriever {
119
+ private retrievers: { retriever: BaseRetriever; weight: number }[];
120
+
121
+ constructor(retrievers: { retriever: BaseRetriever; weight: number }[]) {
122
+ this.retrievers = retrievers;
123
+ }
124
+
125
+ async getRelevantDocuments(query: string): Promise<Document[]> {
126
+ const allDocs: Map<string, { doc: Document; score: number }> = new Map();
127
+
128
+ // Fetch from all retrievers
129
+ for (const { retriever, weight } of this.retrievers) {
130
+ try {
131
+ const docs = await retriever.getRelevantDocuments(query);
132
+
133
+ for (let i = 0; i < docs.length; i++) {
134
+ const doc = docs[i];
135
+ const key = `${doc.pageContent.slice(0, 50)}`;
136
+
137
+ const score = weight * (docs.length - i) / docs.length;
138
+
139
+ if (allDocs.has(key)) {
140
+ allDocs.get(key)!.score += score;
141
+ } else {
142
+ allDocs.set(key, { doc, score });
143
+ }
144
+ }
145
+ } catch (error) {
146
+ logger.warn(`[EnsembleRetriever] Error from retriever: ${error}`);
147
+ }
148
+ }
149
+
150
+ // Sort by score and return
151
+ const sorted = Array.from(allDocs.values())
152
+ .sort((a, b) => b.score - a.score)
153
+ .map(({ doc }) => doc);
154
+
155
+ return sorted;
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Create a vector store retriever
161
+ */
162
+ export function createVectorStoreRetriever(
163
+ vectorStore: VectorStore,
164
+ options: {
165
+ k?: number;
166
+ filter?: (doc: Document) => boolean;
167
+ } = {}
168
+ ): BaseRetriever {
169
+ const k = options.k || 4;
170
+
171
+ return {
172
+ async getRelevantDocuments(query: string): Promise<Document[]> {
173
+ let docs = await vectorStore.similaritySearch(query, k);
174
+
175
+ if (options.filter) {
176
+ docs = docs.filter(options.filter);
177
+ }
178
+
179
+ return docs;
180
+ }
181
+ };
182
+ }