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,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,7 @@
1
+ /**
2
+ * Tools - Function calling for LLM agents
3
+ */
4
+
5
+ export { Tool, tool, createToolDecorator, defineTool, calculator, search, weather } from './decorator.js';
6
+ export { KeiroSearch } from './keiro.js';
7
+ export type { ToolOptions } from './decorator.js';
@@ -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
+ }