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.
- package/.env.example +9 -0
- package/README.md +278 -0
- package/dist/cache/semantic.d.ts +39 -0
- package/dist/cache/semantic.d.ts.map +1 -0
- package/dist/cache/semantic.js +134 -0
- package/dist/cache/semantic.js.map +1 -0
- package/dist/chains/llmchain.d.ts +65 -0
- package/dist/chains/llmchain.d.ts.map +1 -0
- package/dist/chains/llmchain.js +137 -0
- package/dist/chains/llmchain.js.map +1 -0
- package/dist/chains/rag.d.ts +23 -0
- package/dist/chains/rag.d.ts.map +1 -0
- package/dist/chains/rag.js +47 -0
- package/dist/chains/rag.js.map +1 -0
- package/dist/core/types.d.ts +130 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +8 -0
- package/dist/core/types.js.map +1 -0
- package/dist/document_loaders/index.d.ts +61 -0
- package/dist/document_loaders/index.d.ts.map +1 -0
- package/dist/document_loaders/index.js +183 -0
- package/dist/document_loaders/index.js.map +1 -0
- package/dist/embeddings/google.d.ts +43 -0
- package/dist/embeddings/google.d.ts.map +1 -0
- package/dist/embeddings/google.js +90 -0
- package/dist/embeddings/google.js.map +1 -0
- package/dist/embeddings/local.d.ts +64 -0
- package/dist/embeddings/local.d.ts.map +1 -0
- package/dist/embeddings/local.js +95 -0
- package/dist/embeddings/local.js.map +1 -0
- package/dist/evals/judge.d.ts +22 -0
- package/dist/evals/judge.d.ts.map +1 -0
- package/dist/evals/judge.js +77 -0
- package/dist/evals/judge.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +84 -0
- package/dist/index.js.map +1 -0
- package/dist/memory/buffer.d.ts +64 -0
- package/dist/memory/buffer.d.ts.map +1 -0
- package/dist/memory/buffer.js +168 -0
- package/dist/memory/buffer.js.map +1 -0
- package/dist/parsers/output.d.ts +64 -0
- package/dist/parsers/output.d.ts.map +1 -0
- package/dist/parsers/output.js +148 -0
- package/dist/parsers/output.js.map +1 -0
- package/dist/prompts/registry.d.ts +65 -0
- package/dist/prompts/registry.d.ts.map +1 -0
- package/dist/prompts/registry.js +170 -0
- package/dist/prompts/registry.js.map +1 -0
- package/dist/providers/ollama.d.ts +30 -0
- package/dist/providers/ollama.d.ts.map +1 -0
- package/dist/providers/ollama.js +104 -0
- package/dist/providers/ollama.js.map +1 -0
- package/dist/providers/openai.d.ts +46 -0
- package/dist/providers/openai.d.ts.map +1 -0
- package/dist/providers/openai.js +228 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/retrievers/index.d.ts +71 -0
- package/dist/retrievers/index.d.ts.map +1 -0
- package/dist/retrievers/index.js +130 -0
- package/dist/retrievers/index.js.map +1 -0
- package/dist/router/smartrouter.d.ts +36 -0
- package/dist/router/smartrouter.d.ts.map +1 -0
- package/dist/router/smartrouter.js +132 -0
- package/dist/router/smartrouter.js.map +1 -0
- package/dist/text_splitters/index.d.ts +28 -0
- package/dist/text_splitters/index.d.ts.map +1 -0
- package/dist/text_splitters/index.js +109 -0
- package/dist/text_splitters/index.js.map +1 -0
- package/dist/tools/decorator.d.ts +26 -0
- package/dist/tools/decorator.d.ts.map +1 -0
- package/dist/tools/decorator.js +102 -0
- package/dist/tools/decorator.js.map +1 -0
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +6 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/keiro.d.ts +20 -0
- package/dist/tools/keiro.d.ts.map +1 -0
- package/dist/tools/keiro.js +67 -0
- package/dist/tools/keiro.js.map +1 -0
- package/dist/tracing/tracer.d.ts +56 -0
- package/dist/tracing/tracer.d.ts.map +1 -0
- package/dist/tracing/tracer.js +125 -0
- package/dist/tracing/tracer.js.map +1 -0
- package/dist/utils/logger.d.ts +25 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +50 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/pricing.d.ts +31 -0
- package/dist/utils/pricing.d.ts.map +1 -0
- package/dist/utils/pricing.js +108 -0
- package/dist/utils/pricing.js.map +1 -0
- package/dist/vectorstores/index.d.ts +62 -0
- package/dist/vectorstores/index.d.ts.map +1 -0
- package/dist/vectorstores/index.js +244 -0
- package/dist/vectorstores/index.js.map +1 -0
- package/package.json +48 -0
- package/src/cache/semantic.ts +175 -0
- package/src/chains/llmchain.ts +194 -0
- package/src/chains/rag.ts +65 -0
- package/src/core/types.ts +178 -0
- package/src/document_loaders/index.ts +223 -0
- package/src/embeddings/google.ts +119 -0
- package/src/embeddings/local.ts +118 -0
- package/src/evals/judge.ts +99 -0
- package/src/index.ts +121 -0
- package/src/memory/buffer.ts +222 -0
- package/src/parsers/output.ts +195 -0
- package/src/prompts/registry.ts +205 -0
- package/src/providers/ollama.ts +151 -0
- package/src/providers/openai.ts +320 -0
- package/src/retrievers/index.ts +182 -0
- package/src/router/smartrouter.ts +172 -0
- package/src/text_splitters/index.ts +145 -0
- package/src/tools/decorator.ts +145 -0
- package/src/tools/index.ts +7 -0
- package/src/tools/keiro.ts +92 -0
- package/src/tracing/tracer.ts +178 -0
- package/src/utils/logger.ts +62 -0
- package/src/utils/pricing.ts +133 -0
- package/src/vectorstores/index.ts +338 -0
- package/test-full.mjs +552 -0
- package/test.mjs +74 -0
- 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
|
+
}
|