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,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracing - Built-in observability without SaaS
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { logger } from '../utils/logger.js';
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Span
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
export interface SpanData {
|
|
12
|
+
name: string;
|
|
13
|
+
type: string;
|
|
14
|
+
durationMs: number;
|
|
15
|
+
tokensIn: number;
|
|
16
|
+
tokensOut: number;
|
|
17
|
+
costUsd: number;
|
|
18
|
+
error?: string;
|
|
19
|
+
metadata?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class Span {
|
|
23
|
+
name: string;
|
|
24
|
+
type: string;
|
|
25
|
+
startTime: number;
|
|
26
|
+
endTime?: number;
|
|
27
|
+
tokensIn = 0;
|
|
28
|
+
tokensOut = 0;
|
|
29
|
+
metadata: Record<string, unknown> = {};
|
|
30
|
+
error?: string;
|
|
31
|
+
|
|
32
|
+
constructor(name: string, type: string = 'llm') {
|
|
33
|
+
this.name = name;
|
|
34
|
+
this.type = type;
|
|
35
|
+
this.startTime = Date.now();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
finish(): void {
|
|
39
|
+
this.endTime = Date.now();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get durationMs(): number {
|
|
43
|
+
if (!this.endTime) return 0;
|
|
44
|
+
return this.endTime - this.startTime;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get costUsd(): number {
|
|
48
|
+
const rate = this.type === 'llm' ? 0.002 : 0;
|
|
49
|
+
return ((this.tokensIn + this.tokensOut) / 1000) * rate;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
toDict(): SpanData {
|
|
53
|
+
return {
|
|
54
|
+
name: this.name,
|
|
55
|
+
type: this.type,
|
|
56
|
+
durationMs: this.durationMs,
|
|
57
|
+
tokensIn: this.tokensIn,
|
|
58
|
+
tokensOut: this.tokensOut,
|
|
59
|
+
costUsd: this.costUsd,
|
|
60
|
+
error: this.error,
|
|
61
|
+
metadata: this.metadata,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ============================================================================
|
|
67
|
+
// Tracer
|
|
68
|
+
// ============================================================================
|
|
69
|
+
|
|
70
|
+
interface Run {
|
|
71
|
+
name: string;
|
|
72
|
+
correlationId: string;
|
|
73
|
+
totalDurationMs: number;
|
|
74
|
+
totalTokens: number;
|
|
75
|
+
totalCostUsd: number;
|
|
76
|
+
spans: SpanData[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class Tracer {
|
|
80
|
+
private runs: Run[] = [];
|
|
81
|
+
private currentRunName?: string;
|
|
82
|
+
private currentSpans: Span[] = [];
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Start a trace context
|
|
86
|
+
*/
|
|
87
|
+
trace(runName: string): { tracer: Tracer; span: (name: string, type?: string) => Span; end: () => void } {
|
|
88
|
+
this.currentRunName = runName;
|
|
89
|
+
this.currentSpans = [];
|
|
90
|
+
const runStart = Date.now();
|
|
91
|
+
|
|
92
|
+
logger.info(`[Tracer] Run '${runName}' started.`);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
tracer: this,
|
|
96
|
+
span: (name: string, type: string = 'llm') => {
|
|
97
|
+
const s = new Span(name, type);
|
|
98
|
+
this.currentSpans.push(s);
|
|
99
|
+
return s;
|
|
100
|
+
},
|
|
101
|
+
end: () => {
|
|
102
|
+
const runEnd = Date.now();
|
|
103
|
+
const runDuration = runEnd - runStart;
|
|
104
|
+
const totalCost = this.currentSpans.reduce((sum, s) => sum + s.costUsd, 0);
|
|
105
|
+
const totalTokens = this.currentSpans.reduce((sum, s) => sum + s.tokensIn + s.tokensOut, 0);
|
|
106
|
+
|
|
107
|
+
this.runs.push({
|
|
108
|
+
name: runName,
|
|
109
|
+
correlationId: Math.random().toString(36).slice(2, 10),
|
|
110
|
+
totalDurationMs: runDuration,
|
|
111
|
+
totalTokens,
|
|
112
|
+
totalCostUsd: totalCost,
|
|
113
|
+
spans: this.currentSpans.map(s => s.toDict()),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
logger.info(`[Tracer] Run '${runName}' finished in ${runDuration}ms. Est. cost: $${totalCost.toFixed(5)}`);
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Record an event/span
|
|
123
|
+
*/
|
|
124
|
+
event(
|
|
125
|
+
name: string,
|
|
126
|
+
type: string = 'llm',
|
|
127
|
+
tokensIn: number = 0,
|
|
128
|
+
tokensOut: number = 0,
|
|
129
|
+
metadata?: Record<string, unknown>
|
|
130
|
+
): void {
|
|
131
|
+
const span = new Span(name, type);
|
|
132
|
+
span.tokensIn = tokensIn;
|
|
133
|
+
span.tokensOut = tokensOut;
|
|
134
|
+
span.metadata = metadata ?? {};
|
|
135
|
+
span.finish();
|
|
136
|
+
this.currentSpans.push(span);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Print trace summary
|
|
141
|
+
*/
|
|
142
|
+
printSummary(): void {
|
|
143
|
+
if (this.runs.length === 0) {
|
|
144
|
+
console.log('No traces recorded yet.');
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const run of this.runs) {
|
|
149
|
+
console.log(`\n${'='.repeat(70)}`);
|
|
150
|
+
console.log(` Lazlo Trace: ${run.name}`);
|
|
151
|
+
console.log(` Total: ${run.totalDurationMs}ms | ~${run.totalTokens.toLocaleString()} tokens | Est. $${run.totalCostUsd.toFixed(5)}`);
|
|
152
|
+
console.log(`${'='.repeat(70)}`);
|
|
153
|
+
console.log(` ${'Step'.padEnd(28)} ${'Type'.padEnd(12)} ${'ms'.padStart(8)} ${'Tok-In'.padStart(8)} ${'Tok-Out'.padStart(8)} ${'Cost'.padStart(10)}`);
|
|
154
|
+
console.log(` ${'-'.repeat(68)}`);
|
|
155
|
+
|
|
156
|
+
for (const s of run.spans) {
|
|
157
|
+
const status = s.error ? '❌' : '✓';
|
|
158
|
+
console.log(
|
|
159
|
+
` ${status} ${s.name.slice(0, 26).padEnd(28)} ${s.type.padEnd(12)} ` +
|
|
160
|
+
`${s.durationMs.toString().padStart(7)}ms ${s.tokensIn.toString().padStart(8)} ` +
|
|
161
|
+
`${s.tokensOut.toString().padStart(8)} $${s.costUsd.toFixed(5).padStart(9)}`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
console.log(`${'='.repeat(70)}\n`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Clear all traces
|
|
170
|
+
*/
|
|
171
|
+
clear(): void {
|
|
172
|
+
this.runs = [];
|
|
173
|
+
this.currentSpans = [];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Singleton instance
|
|
178
|
+
export const tracer = new Tracer();
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazlo Logger - Simple, structured logging
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export enum LogLevel {
|
|
6
|
+
DEBUG = 0,
|
|
7
|
+
INFO = 1,
|
|
8
|
+
WARN = 2,
|
|
9
|
+
ERROR = 3,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const LEVEL_NAMES = ['DEBUG', 'INFO', 'WARN', 'ERROR'];
|
|
13
|
+
|
|
14
|
+
class Logger {
|
|
15
|
+
private level: LogLevel = LogLevel.INFO;
|
|
16
|
+
private prefix: string = '';
|
|
17
|
+
|
|
18
|
+
constructor(prefix: string = '') {
|
|
19
|
+
this.prefix = prefix;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
setLevel(level: LogLevel): void {
|
|
23
|
+
this.level = level;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
setPrefix(prefix: string): void {
|
|
27
|
+
this.prefix = prefix;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private log(level: LogLevel, ...args: unknown[]): void {
|
|
31
|
+
if (level < this.level) return;
|
|
32
|
+
|
|
33
|
+
const timestamp = new Date().toISOString();
|
|
34
|
+
const levelName = LEVEL_NAMES[level];
|
|
35
|
+
const prefix = this.prefix ? `[${this.prefix}] ` : '';
|
|
36
|
+
|
|
37
|
+
console.log(`\x1b[90m${timestamp}\x1b[0m ${levelName} ${prefix}`, ...args);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
debug(...args: unknown[]): void {
|
|
41
|
+
this.log(LogLevel.DEBUG, ...args);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
info(...args: unknown[]): void {
|
|
45
|
+
this.log(LogLevel.INFO, ...args);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
warn(...args: unknown[]): void {
|
|
49
|
+
this.log(LogLevel.WARN, ...args);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
error(...args: unknown[]): void {
|
|
53
|
+
this.log(LogLevel.ERROR, ...args);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const logger = new Logger('Lazlo');
|
|
58
|
+
|
|
59
|
+
// Factory for child loggers
|
|
60
|
+
export function createLogger(prefix: string): Logger {
|
|
61
|
+
return new Logger(prefix);
|
|
62
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazlo Pricing - Model pricing lookup and cost calculation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ModelPricing } from '../core/types.js';
|
|
6
|
+
|
|
7
|
+
// Default pricing for common models (per 1M tokens)
|
|
8
|
+
const DEFAULT_PRICING: Record<string, ModelPricing> = {
|
|
9
|
+
// OpenAI
|
|
10
|
+
'gpt-4o': { input: 2.50, output: 10.00 },
|
|
11
|
+
'gpt-4o-mini': { input: 0.15, output: 0.60 },
|
|
12
|
+
'gpt-4-turbo': { input: 10.00, output: 30.00 },
|
|
13
|
+
'gpt-3.5-turbo': { input: 0.50, output: 1.50 },
|
|
14
|
+
'gpt-4': { input: 30.00, output: 60.00 },
|
|
15
|
+
|
|
16
|
+
// Anthropic
|
|
17
|
+
'claude-3-5-sonnet': { input: 3.00, output: 15.00 },
|
|
18
|
+
'claude-3-5-sonnet-20241022': { input: 3.00, output: 15.00 },
|
|
19
|
+
'claude-3-opus': { input: 15.00, output: 75.00 },
|
|
20
|
+
'claude-3-sonnet': { input: 3.00, output: 15.00 },
|
|
21
|
+
'claude-3-haiku': { input: 0.25, output: 1.25 },
|
|
22
|
+
|
|
23
|
+
// Google
|
|
24
|
+
'gemini-1.5-flash': { input: 0.075, output: 0.30 },
|
|
25
|
+
'gemini-1.5-pro': { input: 1.25, output: 5.00 },
|
|
26
|
+
'gemini-2.0-flash': { input: 0.00, output: 0.00 },
|
|
27
|
+
'gemini-2.0-flash-001': { input: 0.00, output: 0.00 },
|
|
28
|
+
'gemini-2.0-pro': { input: 1.25, output: 5.00 },
|
|
29
|
+
|
|
30
|
+
// DeepSeek
|
|
31
|
+
'deepseek-chat': { input: 0.14, output: 0.28 },
|
|
32
|
+
'deepseek-coder': { input: 0.14, output: 0.28 },
|
|
33
|
+
|
|
34
|
+
// Meta Llama
|
|
35
|
+
'llama-3.1-70b': { input: 0.00, output: 0.00 },
|
|
36
|
+
'llama-3.1-8b': { input: 0.00, output: 0.00 },
|
|
37
|
+
'llama-3-70b': { input: 0.00, output: 0.00 },
|
|
38
|
+
'llama-3-8b': { input: 0.00, output: 0.00 },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Custom pricing set by user
|
|
42
|
+
const customPricing: Map<string, ModelPricing> = new Map();
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get pricing for a model
|
|
46
|
+
*/
|
|
47
|
+
export function getPricing(model: string): ModelPricing {
|
|
48
|
+
// Check custom pricing first
|
|
49
|
+
if (customPricing.has(model)) {
|
|
50
|
+
return customPricing.get(model)!;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Try exact match
|
|
54
|
+
if (DEFAULT_PRICING[model]) {
|
|
55
|
+
return DEFAULT_PRICING[model];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Try partial match
|
|
59
|
+
const modelLower = model.toLowerCase();
|
|
60
|
+
for (const [key, pricing] of Object.entries(DEFAULT_PRICING)) {
|
|
61
|
+
if (modelLower.includes(key.toLowerCase()) || key.toLowerCase().includes(modelLower)) {
|
|
62
|
+
return pricing;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Return placeholder - user needs to set pricing
|
|
67
|
+
return { input: 0, output: 0 };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Set custom pricing for a model
|
|
72
|
+
*/
|
|
73
|
+
export function setPricing(model: string, input: number, output: number): void {
|
|
74
|
+
customPricing.set(model, { input, output });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Calculate cost in USD
|
|
79
|
+
*/
|
|
80
|
+
export function calculateCost(
|
|
81
|
+
model: string,
|
|
82
|
+
tokensIn: number,
|
|
83
|
+
tokensOut: number
|
|
84
|
+
): number {
|
|
85
|
+
const pricing = getPricing(model);
|
|
86
|
+
return (tokensIn / 1_000_000 * pricing.input) + (tokensOut / 1_000_000 * pricing.output);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Estimate tokens from text (rough approximation)
|
|
91
|
+
*/
|
|
92
|
+
export function estimateTokens(text: string): number {
|
|
93
|
+
// Rough estimate: ~4 characters per token on average
|
|
94
|
+
return Math.max(1, Math.ceil(text.length / 4));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Estimate cost from text
|
|
99
|
+
*/
|
|
100
|
+
export function estimateCost(
|
|
101
|
+
model: string,
|
|
102
|
+
textIn: string,
|
|
103
|
+
textOut: string
|
|
104
|
+
): number {
|
|
105
|
+
const tokensIn = estimateTokens(textIn);
|
|
106
|
+
const tokensOut = estimateTokens(textOut);
|
|
107
|
+
return calculateCost(model, tokensIn, tokensOut);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Load pricing from environment variables
|
|
112
|
+
* Call this after setting process.env if needed
|
|
113
|
+
* Format: LAZLO_PRICING_model_name=input,output
|
|
114
|
+
*/
|
|
115
|
+
export function loadEnvPricing(env?: Record<string, string | undefined>): void {
|
|
116
|
+
const environment = env ?? (typeof globalThis !== 'undefined' ? (globalThis as any).process?.env : {});
|
|
117
|
+
|
|
118
|
+
for (const [key, value] of Object.entries(environment)) {
|
|
119
|
+
if (key.startsWith('LAZLO_PRICING_')) {
|
|
120
|
+
const modelName = key.replace('LAZLO_PRICING_', '').toLowerCase();
|
|
121
|
+
if (typeof value === 'string') {
|
|
122
|
+
const parts = value.split(',');
|
|
123
|
+
if (parts.length === 2) {
|
|
124
|
+
const input = parseFloat(parts[0]);
|
|
125
|
+
const output = parseFloat(parts[1]);
|
|
126
|
+
if (!isNaN(input) && !isNaN(output)) {
|
|
127
|
+
setPricing(modelName, input, output);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vector Stores
|
|
3
|
+
*
|
|
4
|
+
* In-Memory, Chroma, and SQLite-Vector stores for document retrieval
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { logger } from '../utils/logger.js';
|
|
8
|
+
|
|
9
|
+
export interface Document {
|
|
10
|
+
pageContent: string;
|
|
11
|
+
metadata: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface BaseEmbeddings {
|
|
15
|
+
embedQuery(text: string): Promise<number[]>;
|
|
16
|
+
embedDocuments(texts: string[]): Promise<number[][]>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Cosine Similarity
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
function cosineSimilarity(a: number[], b: number[]): number {
|
|
24
|
+
const dot = a.reduce((sum, v, i) => sum + v * b[i], 0);
|
|
25
|
+
const normA = Math.sqrt(a.reduce((sum, v) => sum + v * v, 0));
|
|
26
|
+
const normB = Math.sqrt(b.reduce((sum, v) => sum + v * v, 0));
|
|
27
|
+
|
|
28
|
+
if (normA === 0 || normB === 0) return 0;
|
|
29
|
+
return dot / (normA * normB);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// In-Memory Vector Store
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
interface StoredDoc {
|
|
37
|
+
pageContent: string;
|
|
38
|
+
metadata: Record<string, unknown>;
|
|
39
|
+
embedding: number[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class InMemoryVectorStore {
|
|
43
|
+
private embeddingModel?: BaseEmbeddings;
|
|
44
|
+
private store: StoredDoc[] = [];
|
|
45
|
+
|
|
46
|
+
constructor(embeddingModel?: BaseEmbeddings) {
|
|
47
|
+
this.embeddingModel = embeddingModel;
|
|
48
|
+
logger.info('[Lazlo] InMemoryVectorStore ready.');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async addDocuments(documents: Document[]): Promise<void> {
|
|
52
|
+
if (!this.embeddingModel) {
|
|
53
|
+
throw new Error('No embedding model provided');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const texts = documents.map(d => d.pageContent);
|
|
57
|
+
const metadatas = documents.map(d => d.metadata);
|
|
58
|
+
const embeddings = await this.embeddingModel.embedDocuments(texts);
|
|
59
|
+
|
|
60
|
+
documents.forEach((doc, i) => {
|
|
61
|
+
this.store.push({
|
|
62
|
+
pageContent: doc.pageContent,
|
|
63
|
+
metadata: doc.metadata,
|
|
64
|
+
embedding: embeddings[i],
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async similaritySearch(query: string, k: number = 4): Promise<Document[]> {
|
|
70
|
+
if (!this.embeddingModel) {
|
|
71
|
+
throw new Error('No embedding model provided');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const queryEmbedding = await this.embeddingModel.embedQuery(query);
|
|
75
|
+
|
|
76
|
+
// Score all documents
|
|
77
|
+
const scored = this.store.map(doc => ({
|
|
78
|
+
doc,
|
|
79
|
+
score: cosineSimilarity(queryEmbedding, doc.embedding),
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
// Sort by score descending
|
|
83
|
+
scored.sort((a, b) => b.score - a.score);
|
|
84
|
+
|
|
85
|
+
// Return top k
|
|
86
|
+
return scored.slice(0, k).map(s => ({
|
|
87
|
+
pageContent: s.doc.pageContent,
|
|
88
|
+
metadata: s.doc.metadata,
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async similaritySearchWithScore(query: string, k: number = 4): Promise<[Document, number][]> {
|
|
93
|
+
if (!this.embeddingModel) {
|
|
94
|
+
throw new Error('No embedding model provided');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const queryEmbedding = await this.embeddingModel.embedQuery(query);
|
|
98
|
+
|
|
99
|
+
const scored = this.store.map(doc => ({
|
|
100
|
+
doc,
|
|
101
|
+
score: cosineSimilarity(queryEmbedding, doc.embedding),
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
scored.sort((a, b) => b.score - a.score);
|
|
105
|
+
|
|
106
|
+
return scored.slice(0, k).map(s => [
|
|
107
|
+
{ pageContent: s.doc.pageContent, metadata: s.doc.metadata },
|
|
108
|
+
s.score,
|
|
109
|
+
]);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
delete(): void {
|
|
113
|
+
this.store = [];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ============================================================================
|
|
118
|
+
// Chroma Vector Store (Client-side implementation)
|
|
119
|
+
// ============================================================================
|
|
120
|
+
|
|
121
|
+
export class ChromaVectorStore {
|
|
122
|
+
private embeddingModel?: BaseEmbeddings;
|
|
123
|
+
private collectionName: string;
|
|
124
|
+
private documents: Map<string, StoredDoc> = new Map();
|
|
125
|
+
|
|
126
|
+
constructor(options: {
|
|
127
|
+
embeddingModel?: BaseEmbeddings;
|
|
128
|
+
collectionName?: string;
|
|
129
|
+
} = {}) {
|
|
130
|
+
this.embeddingModel = options.embeddingModel;
|
|
131
|
+
this.collectionName = options.collectionName || 'lazlo-collection';
|
|
132
|
+
logger.info(`[Lazlo] ChromaVectorStore ready. Collection: ${this.collectionName}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async addDocuments(documents: Document[]): Promise<string[]> {
|
|
136
|
+
if (!this.embeddingModel) {
|
|
137
|
+
throw new Error('No embedding model provided');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const texts = documents.map(d => d.pageContent);
|
|
141
|
+
const embeddings = await this.embeddingModel.embedDocuments(texts);
|
|
142
|
+
const ids: string[] = [];
|
|
143
|
+
|
|
144
|
+
documents.forEach((doc, i) => {
|
|
145
|
+
const id = crypto.randomUUID();
|
|
146
|
+
this.documents.set(id, {
|
|
147
|
+
pageContent: doc.pageContent,
|
|
148
|
+
metadata: doc.metadata,
|
|
149
|
+
embedding: embeddings[i],
|
|
150
|
+
});
|
|
151
|
+
ids.push(id);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return ids;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async similaritySearch(query: string, k: number = 4): Promise<Document[]> {
|
|
158
|
+
if (!this.embeddingModel) {
|
|
159
|
+
throw new Error('No embedding model provided');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const queryEmbedding = await this.embeddingModel.embedQuery(query);
|
|
163
|
+
|
|
164
|
+
const scored = Array.from(this.documents.entries()).map(([id, doc]) => ({
|
|
165
|
+
id,
|
|
166
|
+
doc,
|
|
167
|
+
score: cosineSimilarity(queryEmbedding, doc.embedding),
|
|
168
|
+
}));
|
|
169
|
+
|
|
170
|
+
scored.sort((a, b) => b.score - a.score);
|
|
171
|
+
|
|
172
|
+
return scored.slice(0, k).map(s => ({
|
|
173
|
+
pageContent: s.doc.pageContent,
|
|
174
|
+
metadata: s.doc.metadata,
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async similaritySearchWithScore(query: string, k: number = 4): Promise<[Document, number][]> {
|
|
179
|
+
if (!this.embeddingModel) {
|
|
180
|
+
throw new Error('No embedding model provided');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const queryEmbedding = await this.embeddingModel.embedQuery(query);
|
|
184
|
+
|
|
185
|
+
const scored = Array.from(this.documents.entries()).map(([id, doc]) => ({
|
|
186
|
+
id,
|
|
187
|
+
doc,
|
|
188
|
+
score: cosineSimilarity(queryEmbedding, doc.embedding),
|
|
189
|
+
}));
|
|
190
|
+
|
|
191
|
+
scored.sort((a, b) => b.score - a.score);
|
|
192
|
+
|
|
193
|
+
return scored.slice(0, k).map(s => [
|
|
194
|
+
{ pageContent: s.doc.pageContent, metadata: s.doc.metadata },
|
|
195
|
+
s.score,
|
|
196
|
+
]);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async delete(ids: string[]): Promise<void> {
|
|
200
|
+
for (const id of ids) {
|
|
201
|
+
this.documents.delete(id);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async get(ids: string[]): Promise<Document[]> {
|
|
206
|
+
return ids
|
|
207
|
+
.map(id => this.documents.get(id))
|
|
208
|
+
.filter((d): d is StoredDoc => d !== undefined)
|
|
209
|
+
.map(d => ({ pageContent: d.pageContent, metadata: d.metadata }));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ============================================================================
|
|
214
|
+
// SQLite-Vector Store (using better-sqlite3 if available)
|
|
215
|
+
// ============================================================================
|
|
216
|
+
|
|
217
|
+
export class SQLiteVectorStore {
|
|
218
|
+
private embeddingModel?: BaseEmbeddings;
|
|
219
|
+
private dbPath: string;
|
|
220
|
+
private documents: Map<string, StoredDoc> = new Map();
|
|
221
|
+
private db: any = null;
|
|
222
|
+
|
|
223
|
+
constructor(options: {
|
|
224
|
+
embeddingModel?: BaseEmbeddings;
|
|
225
|
+
dbPath?: string;
|
|
226
|
+
} = {}) {
|
|
227
|
+
this.embeddingModel = options.embeddingModel;
|
|
228
|
+
this.dbPath = options.dbPath || ':memory:';
|
|
229
|
+
logger.info(`[Lazlo] SQLiteVectorStore ready. Path: ${this.dbPath}`);
|
|
230
|
+
|
|
231
|
+
// Initialize in-memory storage (full SQLite-Vector requires native module)
|
|
232
|
+
this.initialize();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private initialize(): void {
|
|
236
|
+
// In-memory storage as fallback (actual sqlite-vec requires native module)
|
|
237
|
+
logger.debug('[SQLiteVectorStore] Using in-memory storage (install better-sqlite3 for full SQLite-Vector support)');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async addDocuments(documents: Document[]): Promise<string[]> {
|
|
241
|
+
if (!this.embeddingModel) {
|
|
242
|
+
throw new Error('No embedding model provided');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const texts = documents.map(d => d.pageContent);
|
|
246
|
+
const embeddings = await this.embeddingModel.embedDocuments(texts);
|
|
247
|
+
const ids: string[] = [];
|
|
248
|
+
|
|
249
|
+
documents.forEach((doc, i) => {
|
|
250
|
+
const id = crypto.randomUUID();
|
|
251
|
+
this.documents.set(id, {
|
|
252
|
+
pageContent: doc.pageContent,
|
|
253
|
+
metadata: doc.metadata,
|
|
254
|
+
embedding: embeddings[i],
|
|
255
|
+
});
|
|
256
|
+
ids.push(id);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
return ids;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async similaritySearch(query: string, k: number = 4): Promise<Document[]> {
|
|
263
|
+
if (!this.embeddingModel) {
|
|
264
|
+
throw new Error('No embedding model provided');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const queryEmbedding = await this.embeddingModel.embedQuery(query);
|
|
268
|
+
|
|
269
|
+
const scored = Array.from(this.documents.entries()).map(([id, doc]) => ({
|
|
270
|
+
id,
|
|
271
|
+
doc,
|
|
272
|
+
score: cosineSimilarity(queryEmbedding, doc.embedding),
|
|
273
|
+
}));
|
|
274
|
+
|
|
275
|
+
scored.sort((a, b) => b.score - a.score);
|
|
276
|
+
|
|
277
|
+
return scored.slice(0, k).map(s => ({
|
|
278
|
+
pageContent: s.doc.pageContent,
|
|
279
|
+
metadata: s.doc.metadata,
|
|
280
|
+
}));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async similaritySearchWithScore(query: string, k: number = 4): Promise<[Document, number][]> {
|
|
284
|
+
if (!this.embeddingModel) {
|
|
285
|
+
throw new Error('No embedding model provided');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const queryEmbedding = await this.embeddingModel.embedQuery(query);
|
|
289
|
+
|
|
290
|
+
const scored = Array.from(this.documents.entries()).map(([id, doc]) => ({
|
|
291
|
+
id,
|
|
292
|
+
doc,
|
|
293
|
+
score: cosineSimilarity(queryEmbedding, doc.embedding),
|
|
294
|
+
}));
|
|
295
|
+
|
|
296
|
+
scored.sort((a, b) => b.score - a.score);
|
|
297
|
+
|
|
298
|
+
return scored.slice(0, k).map(s => [
|
|
299
|
+
{ pageContent: s.doc.pageContent, metadata: s.doc.metadata },
|
|
300
|
+
s.score,
|
|
301
|
+
]);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async delete(ids: string[]): Promise<void> {
|
|
305
|
+
for (const id of ids) {
|
|
306
|
+
this.documents.delete(id);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async get(ids: string[]): Promise<Document[]> {
|
|
311
|
+
return ids
|
|
312
|
+
.map(id => this.documents.get(id))
|
|
313
|
+
.filter((d): d is StoredDoc => d !== undefined)
|
|
314
|
+
.map(d => ({ pageContent: d.pageContent, metadata: d.metadata }));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ============================================================================
|
|
319
|
+
// Factory
|
|
320
|
+
// ============================================================================
|
|
321
|
+
|
|
322
|
+
export function createVectorStore(embeddingModel?: BaseEmbeddings): InMemoryVectorStore {
|
|
323
|
+
return new InMemoryVectorStore(embeddingModel);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function createChromaVectorStore(options?: {
|
|
327
|
+
embeddingModel?: BaseEmbeddings;
|
|
328
|
+
collectionName?: string;
|
|
329
|
+
}): ChromaVectorStore {
|
|
330
|
+
return new ChromaVectorStore(options);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function createSQLiteVectorStore(options?: {
|
|
334
|
+
embeddingModel?: BaseEmbeddings;
|
|
335
|
+
dbPath?: string;
|
|
336
|
+
}): SQLiteVectorStore {
|
|
337
|
+
return new SQLiteVectorStore(options);
|
|
338
|
+
}
|