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,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart Router - Route prompts to fast or smart models based on complexity
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { BaseChatModel, Message } from '../core/types.js';
|
|
6
|
+
import { logger } from '../utils/logger.js';
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Complexity Signals
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
const COMPLEX_SIGNALS = [
|
|
13
|
+
'explain', 'compare', 'analyze', 'design', 'implement', 'prove',
|
|
14
|
+
'write code', 'refactor', 'debug', 'architecture', 'difference between',
|
|
15
|
+
'step by step', 'tradeoffs', 'evaluate', 'research', 'synthesize',
|
|
16
|
+
'multi-step', 'complex', 'detailed', 'comprehensive', 'why does',
|
|
17
|
+
'algorithm', 'system', 'distributed', 'optimize', 'refactor',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const SIMPLE_SIGNALS = [
|
|
21
|
+
'what is', 'who is', 'when did', 'where is', 'define',
|
|
22
|
+
'list', 'how many', 'translate', 'convert', 'yes or no',
|
|
23
|
+
'true or false', 'capital', 'president', 'date of',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// Heuristic Complexity Calculation
|
|
28
|
+
// ============================================================================
|
|
29
|
+
|
|
30
|
+
function heuristicComplexity(prompt: string): number {
|
|
31
|
+
const text = prompt.toLowerCase();
|
|
32
|
+
let score = 0.0;
|
|
33
|
+
|
|
34
|
+
// Word count factor
|
|
35
|
+
const wordCount = text.split(/\s+/).length;
|
|
36
|
+
if (wordCount > 100) {
|
|
37
|
+
score += 0.3;
|
|
38
|
+
} else if (wordCount > 40) {
|
|
39
|
+
score += 0.15;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Question count
|
|
43
|
+
const questionCount = (text.match(/\?/g) || []).length;
|
|
44
|
+
score += Math.min(questionCount * 0.1, 0.2);
|
|
45
|
+
|
|
46
|
+
// Complex signals
|
|
47
|
+
for (const signal of COMPLEX_SIGNALS) {
|
|
48
|
+
if (text.includes(signal)) {
|
|
49
|
+
score += 0.15;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Simple signals (reduce complexity)
|
|
55
|
+
for (const signal of SIMPLE_SIGNALS) {
|
|
56
|
+
if (text.includes(signal)) {
|
|
57
|
+
score -= 0.15;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Code detection
|
|
63
|
+
if (/```|def |function |class |import |const |let |var /.test(text)) {
|
|
64
|
+
score += 0.25;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return Math.max(0.0, Math.min(1.0, score));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// Smart Router
|
|
72
|
+
// ============================================================================
|
|
73
|
+
|
|
74
|
+
export interface RouterOptions {
|
|
75
|
+
fast: BaseChatModel;
|
|
76
|
+
smart: BaseChatModel;
|
|
77
|
+
threshold?: number;
|
|
78
|
+
classifierLLM?: BaseChatModel;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export class SmartRouter {
|
|
82
|
+
private fast: BaseChatModel;
|
|
83
|
+
private smart: BaseChatModel;
|
|
84
|
+
private threshold: number;
|
|
85
|
+
private classifierLLM?: BaseChatModel;
|
|
86
|
+
private fastCalls = 0;
|
|
87
|
+
private smartCalls = 0;
|
|
88
|
+
|
|
89
|
+
constructor(options: RouterOptions) {
|
|
90
|
+
this.fast = options.fast;
|
|
91
|
+
this.smart = options.smart;
|
|
92
|
+
this.threshold = options.threshold ?? 0.5;
|
|
93
|
+
this.classifierLLM = options.classifierLLM;
|
|
94
|
+
|
|
95
|
+
logger.info(`SmartRouter ready. Threshold: ${this.threshold}. Classifier: ${this.classifierLLM ? 'LLM' : 'heuristic'}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private async classify(prompt: string): Promise<number> {
|
|
99
|
+
if (!this.classifierLLM) {
|
|
100
|
+
return heuristicComplexity(prompt);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Use LLM classifier
|
|
104
|
+
const classificationPrompt = `Rate the complexity of the following task on a scale of 0.0 to 1.0.
|
|
105
|
+
0.0 = trivial fact lookup. 1.0 = requires deep reasoning, code writing, or multi-step analysis.
|
|
106
|
+
Output ONLY a single decimal number, nothing else.
|
|
107
|
+
|
|
108
|
+
Task: ${prompt}`;
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const response = await this.classifierLLM.invoke([
|
|
112
|
+
{ role: 'user', content: classificationPrompt }
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
const match = response.content.match(/[0-9]*\.?[0-9]+/);
|
|
116
|
+
if (match) {
|
|
117
|
+
return parseFloat(match[0]);
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
logger.warn(`SmartRouter classifier failed: ${error}. Using heuristic.`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return heuristicComplexity(prompt);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async invoke(prompt: string, options?: {
|
|
127
|
+
model?: string; // Override for specific model
|
|
128
|
+
temperature?: number;
|
|
129
|
+
}): Promise<{ content: string; model: string }> {
|
|
130
|
+
const score = await this.classify(prompt);
|
|
131
|
+
|
|
132
|
+
if (score >= this.threshold) {
|
|
133
|
+
logger.info(`[SmartRouter] Complexity=${score.toFixed(2)} → SMART model`);
|
|
134
|
+
this.smartCalls++;
|
|
135
|
+
const response = await this.smart.invoke([{ role: 'user', content: prompt }], options);
|
|
136
|
+
return { content: response.content, model: (this.smart as any).defaultModel ?? 'smart' };
|
|
137
|
+
} else {
|
|
138
|
+
logger.info(`[SmartRouter] Complexity=${score.toFixed(2)} → FAST model`);
|
|
139
|
+
this.fastCalls++;
|
|
140
|
+
const response = await this.fast.invoke([{ role: 'user', content: prompt }], options);
|
|
141
|
+
return { content: response.content, model: (this.fast as any).defaultModel ?? 'fast' };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async ainvoke(prompt: string, options?: {
|
|
146
|
+
model?: string;
|
|
147
|
+
temperature?: number;
|
|
148
|
+
}): Promise<{ content: string; model: string }> {
|
|
149
|
+
return this.invoke(prompt, options);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
printStats(): void {
|
|
153
|
+
const total = this.fastCalls + this.smartCalls;
|
|
154
|
+
if (total === 0) {
|
|
155
|
+
console.log('No calls routed yet.');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const fastPct = (this.fastCalls / total * 100).toFixed(1);
|
|
160
|
+
const smartPct = (this.smartCalls / total * 100).toFixed(1);
|
|
161
|
+
|
|
162
|
+
console.log(`
|
|
163
|
+
${'='.repeat(50)}
|
|
164
|
+
Lazlo SmartRouter Stats
|
|
165
|
+
${'='.repeat(50)}
|
|
166
|
+
Total calls : ${total}
|
|
167
|
+
Fast model : ${this.fastCalls} (${fastPct}%)
|
|
168
|
+
Smart model : ${this.smartCalls} (${smartPct}%)
|
|
169
|
+
${'='.repeat(50)}
|
|
170
|
+
`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text Splitters - Split documents into chunks
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Document } from '../core/types.js';
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Character Text Splitter
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
export interface TextSplitterOptions {
|
|
12
|
+
separator?: string;
|
|
13
|
+
chunkSize?: number;
|
|
14
|
+
chunkOverlap?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class CharacterTextSplitter {
|
|
18
|
+
private separator: string;
|
|
19
|
+
private chunkSize: number;
|
|
20
|
+
private chunkOverlap: number;
|
|
21
|
+
|
|
22
|
+
constructor(options: TextSplitterOptions = {}) {
|
|
23
|
+
this.separator = options.separator ?? '\n\n';
|
|
24
|
+
this.chunkSize = options.chunkSize ?? 1000;
|
|
25
|
+
this.chunkOverlap = options.chunkOverlap ?? 200;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
splitText(text: string): string[] {
|
|
29
|
+
if (this.chunkSize === 0 || this.chunkSize >= text.length) {
|
|
30
|
+
return [text];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const splits = text.split(this.separator);
|
|
34
|
+
const chunks: string[] = [];
|
|
35
|
+
let currentChunk: string[] = [];
|
|
36
|
+
let currentLength = 0;
|
|
37
|
+
|
|
38
|
+
for (const split of splits) {
|
|
39
|
+
const splitLen = split.length;
|
|
40
|
+
|
|
41
|
+
if (currentLength + splitLen > this.chunkSize && currentChunk.length > 0) {
|
|
42
|
+
chunks.push(currentChunk.join(this.separator));
|
|
43
|
+
|
|
44
|
+
// Remove from front until under overlap threshold
|
|
45
|
+
while (currentLength > this.chunkOverlap && currentChunk.length > 0) {
|
|
46
|
+
const removed = currentChunk.shift();
|
|
47
|
+
currentLength -= (removed?.length ?? 0) + this.separator.length;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
currentChunk.push(split);
|
|
52
|
+
currentLength += splitLen + (currentChunk.length > 1 ? this.separator.length : 0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (currentChunk.length > 0) {
|
|
56
|
+
chunks.push(currentChunk.join(this.separator));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return chunks;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
splitDocuments(documents: Document[]): Document[] {
|
|
63
|
+
const newDocs: Document[] = [];
|
|
64
|
+
|
|
65
|
+
for (const doc of documents) {
|
|
66
|
+
const chunks = this.splitText(doc.pageContent);
|
|
67
|
+
|
|
68
|
+
chunks.forEach((chunk, i) => {
|
|
69
|
+
newDocs.push({
|
|
70
|
+
pageContent: chunk,
|
|
71
|
+
metadata: { ...doc.metadata, chunk_id: i },
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return newDocs;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// Recursive Character Text Splitter
|
|
82
|
+
// ============================================================================
|
|
83
|
+
|
|
84
|
+
export class RecursiveCharacterTextSplitter extends CharacterTextSplitter {
|
|
85
|
+
private separators: string[];
|
|
86
|
+
|
|
87
|
+
constructor(options: TextSplitterOptions & { separators?: string[] } = {}) {
|
|
88
|
+
super(options);
|
|
89
|
+
this.separators = options.separators ?? ['\n\n', '\n', ' ', ''];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
splitText(text: string): string[] {
|
|
93
|
+
const finalChunks: string[] = [];
|
|
94
|
+
|
|
95
|
+
// If text is small enough, return as-is
|
|
96
|
+
if (text.length <= (this as any).chunkSize) {
|
|
97
|
+
return [text];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Try each separator in order
|
|
101
|
+
const separator = this.separators[0];
|
|
102
|
+
const splits = text.split(separator);
|
|
103
|
+
|
|
104
|
+
for (const split of splits) {
|
|
105
|
+
if (!split.trim()) continue;
|
|
106
|
+
|
|
107
|
+
if (split.length <= (this as any).chunkSize) {
|
|
108
|
+
finalChunks.push(split);
|
|
109
|
+
} else if (this.separators.length > 1) {
|
|
110
|
+
// Recursively split with next separator
|
|
111
|
+
const nextSplitter = new RecursiveCharacterTextSplitter({
|
|
112
|
+
separators: this.separators.slice(1),
|
|
113
|
+
chunkSize: (this as any).chunkSize,
|
|
114
|
+
chunkOverlap: (this as any).chunkOverlap,
|
|
115
|
+
});
|
|
116
|
+
finalChunks.push(...nextSplitter.splitText(split));
|
|
117
|
+
} else {
|
|
118
|
+
// Last resort - force split
|
|
119
|
+
finalChunks.push(...this.forceSplit(split));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return finalChunks;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private forceSplit(text: string): string[] {
|
|
127
|
+
const chunks: string[] = [];
|
|
128
|
+
const chunkSize = (this as any).chunkSize;
|
|
129
|
+
|
|
130
|
+
for (let i = 0; i < text.length; i += chunkSize) {
|
|
131
|
+
chunks.push(text.slice(i, i + chunkSize));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return chunks;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Factory helpers
|
|
139
|
+
export function createCharacterSplitter(options?: TextSplitterOptions): CharacterTextSplitter {
|
|
140
|
+
return new CharacterTextSplitter(options);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function createRecursiveSplitter(options?: TextSplitterOptions): RecursiveCharacterTextSplitter {
|
|
144
|
+
return new RecursiveCharacterTextSplitter(options);
|
|
145
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Decorator - Turn functions into Lazlo tools
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { BaseTool } from '../core/types.js';
|
|
6
|
+
import { logger } from '../utils/logger.js';
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Tool Class
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
export interface ToolOptions {
|
|
13
|
+
name?: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
argsSchema?: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class Tool implements BaseTool {
|
|
19
|
+
name: string;
|
|
20
|
+
description: string;
|
|
21
|
+
argsSchema?: Record<string, unknown>;
|
|
22
|
+
private func: Function;
|
|
23
|
+
|
|
24
|
+
constructor(func: Function, options: ToolOptions = {}) {
|
|
25
|
+
this.func = func;
|
|
26
|
+
this.name = options.name ?? func.name ?? 'unnamed_tool';
|
|
27
|
+
this.description = options.description ?? func.toString().slice(0, 100) ?? '';
|
|
28
|
+
this.argsSchema = options.argsSchema;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async invoke(input: string | Record<string, unknown>): Promise<string> {
|
|
32
|
+
try {
|
|
33
|
+
let args: Record<string, unknown>;
|
|
34
|
+
|
|
35
|
+
if (typeof input === 'string') {
|
|
36
|
+
try {
|
|
37
|
+
args = JSON.parse(input);
|
|
38
|
+
} catch {
|
|
39
|
+
args = { input };
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
args = input;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const result = await this.func(args);
|
|
46
|
+
return typeof result === 'string' ? result : JSON.stringify(result);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
logger.error(`Tool ${this.name} failed:`, error);
|
|
49
|
+
return `Error: ${error}`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
toJSON(): Record<string, unknown> {
|
|
54
|
+
return {
|
|
55
|
+
name: this.name,
|
|
56
|
+
description: this.description,
|
|
57
|
+
parameters: this.argsSchema ?? {
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {},
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ============================================================================
|
|
66
|
+
// Tool Decorator Factory
|
|
67
|
+
// ============================================================================
|
|
68
|
+
|
|
69
|
+
export function createToolDecorator(options?: ToolOptions) {
|
|
70
|
+
return function(func: Function): Tool {
|
|
71
|
+
return new Tool(func, options);
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Shorthand: @tool
|
|
76
|
+
export const tool = createToolDecorator();
|
|
77
|
+
|
|
78
|
+
// With options: @tool({ name: 'calc', description: '...' })
|
|
79
|
+
export const toolWithOptions = createToolDecorator;
|
|
80
|
+
|
|
81
|
+
// ============================================================================
|
|
82
|
+
// Create Tool Helper
|
|
83
|
+
// ============================================================================
|
|
84
|
+
|
|
85
|
+
export function defineTool(
|
|
86
|
+
func: Function,
|
|
87
|
+
name: string,
|
|
88
|
+
description: string,
|
|
89
|
+
argsSchema?: Record<string, unknown>
|
|
90
|
+
): Tool {
|
|
91
|
+
return new Tool(func, { name, description, argsSchema });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ============================================================================
|
|
95
|
+
// Example Tools
|
|
96
|
+
// ============================================================================
|
|
97
|
+
|
|
98
|
+
// Calculator
|
|
99
|
+
export const calculator = defineTool(
|
|
100
|
+
(args: { expression: string }) => {
|
|
101
|
+
try {
|
|
102
|
+
const result = Function(`"use strict"; return (${args.expression})`)();
|
|
103
|
+
return String(result);
|
|
104
|
+
} catch (e) {
|
|
105
|
+
return `Error: ${e}`;
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
'calculator',
|
|
109
|
+
'Perform a mathematical calculation',
|
|
110
|
+
{
|
|
111
|
+
type: 'object',
|
|
112
|
+
properties: {
|
|
113
|
+
expression: { type: 'string', description: 'Mathematical expression' },
|
|
114
|
+
},
|
|
115
|
+
required: ['expression'],
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// Search
|
|
120
|
+
export const search = defineTool(
|
|
121
|
+
(args: { query: string }) => `Search results for "${args.query}"`,
|
|
122
|
+
'search',
|
|
123
|
+
'Search the web',
|
|
124
|
+
{
|
|
125
|
+
type: 'object',
|
|
126
|
+
properties: {
|
|
127
|
+
query: { type: 'string', description: 'Search query' },
|
|
128
|
+
},
|
|
129
|
+
required: ['query'],
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// Weather
|
|
134
|
+
export const weather = defineTool(
|
|
135
|
+
(args: { location: string }) => `Weather for ${args.location}: Sunny, 72°F`,
|
|
136
|
+
'weather',
|
|
137
|
+
'Get weather info',
|
|
138
|
+
{
|
|
139
|
+
type: 'object',
|
|
140
|
+
properties: {
|
|
141
|
+
location: { type: 'string', description: 'City name' },
|
|
142
|
+
},
|
|
143
|
+
required: ['location'],
|
|
144
|
+
}
|
|
145
|
+
);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keiro Search - Web search tool for LLM agents
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import axios, { AxiosInstance } from 'axios';
|
|
6
|
+
import { BaseTool } from '../core/types.js';
|
|
7
|
+
import { logger } from '../utils/logger.js';
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Keiro Search Tool
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
export interface KeiroSearchOptions {
|
|
14
|
+
apiKey?: string;
|
|
15
|
+
cacheSearch?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class KeiroSearch implements BaseTool {
|
|
19
|
+
name = 'keiro_search';
|
|
20
|
+
description = 'A powerful web search engine. Use this to find current events, up-to-date information, or if you do not know the answer. Input should be a specific search query string.';
|
|
21
|
+
|
|
22
|
+
private client: AxiosInstance;
|
|
23
|
+
private apiKey?: string;
|
|
24
|
+
private cacheSearch: boolean;
|
|
25
|
+
|
|
26
|
+
constructor(options: KeiroSearchOptions = {}) {
|
|
27
|
+
this.apiKey = options.apiKey ?? process.env.KEIRO_API_KEY;
|
|
28
|
+
this.cacheSearch = options.cacheSearch ?? true;
|
|
29
|
+
|
|
30
|
+
this.client = axios.create({
|
|
31
|
+
baseURL: 'https://kierolabs.space/api',
|
|
32
|
+
timeout: 30000,
|
|
33
|
+
headers: {
|
|
34
|
+
'Content-Type': 'application/json',
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
logger.info('KeiroSearch tool initialized.');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private parseInput(input: string | Record<string, unknown>): string {
|
|
42
|
+
if (typeof input === 'string') {
|
|
43
|
+
return input;
|
|
44
|
+
}
|
|
45
|
+
// If it's an object with a query field, use that
|
|
46
|
+
if ('query' in input && typeof input.query === 'string') {
|
|
47
|
+
return input.query;
|
|
48
|
+
}
|
|
49
|
+
// Otherwise stringify
|
|
50
|
+
return JSON.stringify(input);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async invoke(input: string | Record<string, unknown>): Promise<string> {
|
|
54
|
+
const query = this.parseInput(input);
|
|
55
|
+
|
|
56
|
+
logger.debug(`KeiroSearch: "${query}"`);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const response = await this.client.post('/search-pro', {
|
|
60
|
+
apiKey: this.apiKey,
|
|
61
|
+
query,
|
|
62
|
+
cache_search: this.cacheSearch,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const results = response.data.results ?? [];
|
|
66
|
+
const answer = response.data.answer;
|
|
67
|
+
|
|
68
|
+
if (answer) {
|
|
69
|
+
return answer;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (results.length === 0) {
|
|
73
|
+
return 'No search results found.';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Format results
|
|
77
|
+
const formatted = results.slice(0, 5).map((r: any, i: number) => {
|
|
78
|
+
return `${i + 1}. ${r.title}\n ${r.url}\n ${r.content?.slice(0, 200) ?? ''}`;
|
|
79
|
+
}).join('\n\n');
|
|
80
|
+
|
|
81
|
+
return `Search results for "${query}":\n\n${formatted}`;
|
|
82
|
+
|
|
83
|
+
} catch (error: any) {
|
|
84
|
+
logger.error(`KeiroSearch error: ${error.message}`);
|
|
85
|
+
return `Error searching the web: ${error.message}`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async ainvoke(input: string | Record<string, unknown>): Promise<string> {
|
|
90
|
+
return this.invoke(input);
|
|
91
|
+
}
|
|
92
|
+
}
|