illuma-agents 1.0.9 → 1.0.11
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/LICENSE +1 -1
- package/dist/cjs/agents/AgentContext.cjs +228 -27
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/enum.cjs +2 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/events.cjs +3 -0
- package/dist/cjs/events.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +29 -19
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/instrumentation.cjs +1 -1
- package/dist/cjs/instrumentation.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/index.cjs +1 -1
- package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +122 -7
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/llm/google/index.cjs +1 -1
- package/dist/cjs/llm/google/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +108 -6
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/utils/index.cjs +87 -1
- package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs +176 -2
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +18 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/cache.cjs +149 -54
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/messages/tools.cjs +85 -0
- package/dist/cjs/messages/tools.cjs.map +1 -0
- package/dist/cjs/stream.cjs +20 -0
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +4 -0
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +438 -0
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -0
- package/dist/cjs/tools/ToolNode.cjs +54 -6
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/ToolSearchRegex.cjs +455 -0
- package/dist/cjs/tools/ToolSearchRegex.cjs.map +1 -0
- package/dist/cjs/tools/search/tool.cjs +21 -1
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/cjs/utils/run.cjs +5 -1
- package/dist/cjs/utils/run.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +228 -27
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/enum.mjs +2 -0
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/events.mjs +4 -1
- package/dist/esm/events.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +29 -19
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/instrumentation.mjs +1 -1
- package/dist/esm/instrumentation.mjs.map +1 -1
- package/dist/esm/llm/anthropic/index.mjs +1 -1
- package/dist/esm/llm/anthropic/index.mjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +122 -7
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/llm/google/index.mjs +1 -1
- package/dist/esm/llm/google/index.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +109 -7
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openai/utils/index.mjs +88 -2
- package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs +176 -2
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/main.mjs +3 -0
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/messages/cache.mjs +149 -54
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/messages/tools.mjs +82 -0
- package/dist/esm/messages/tools.mjs.map +1 -0
- package/dist/esm/stream.mjs +20 -0
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +4 -0
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +430 -0
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -0
- package/dist/esm/tools/ToolNode.mjs +54 -6
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/ToolSearchRegex.mjs +448 -0
- package/dist/esm/tools/ToolSearchRegex.mjs.map +1 -0
- package/dist/esm/tools/search/tool.mjs +21 -1
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/esm/utils/run.mjs +5 -1
- package/dist/esm/utils/run.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +65 -5
- package/dist/types/common/enum.d.ts +2 -0
- package/dist/types/graphs/Graph.d.ts +3 -2
- package/dist/types/index.d.ts +2 -0
- package/dist/types/llm/anthropic/index.d.ts +1 -1
- package/dist/types/llm/bedrock/index.d.ts +31 -4
- package/dist/types/llm/google/index.d.ts +1 -1
- package/dist/types/llm/openai/index.d.ts +4 -3
- package/dist/types/llm/openai/utils/index.d.ts +10 -1
- package/dist/types/llm/openrouter/index.d.ts +5 -2
- package/dist/types/messages/cache.d.ts +23 -8
- package/dist/types/messages/index.d.ts +1 -0
- package/dist/types/messages/tools.d.ts +17 -0
- package/dist/types/test/mockTools.d.ts +28 -0
- package/dist/types/tools/ProgrammaticToolCalling.d.ts +91 -0
- package/dist/types/tools/ToolNode.d.ts +10 -2
- package/dist/types/tools/ToolSearchRegex.d.ts +80 -0
- package/dist/types/types/graph.d.ts +7 -1
- package/dist/types/types/tools.d.ts +138 -0
- package/package.json +8 -3
- package/src/agents/AgentContext.ts +267 -27
- package/src/agents/__tests__/AgentContext.test.ts +805 -0
- package/src/common/enum.ts +2 -0
- package/src/events.ts +5 -1
- package/src/graphs/Graph.ts +35 -20
- package/src/index.ts +2 -0
- package/src/instrumentation.ts +1 -1
- package/src/llm/anthropic/index.ts +2 -2
- package/src/llm/bedrock/__tests__/bedrock-caching.test.ts +473 -0
- package/src/llm/bedrock/index.ts +150 -13
- package/src/llm/google/index.ts +2 -2
- package/src/llm/google/llm.spec.ts +3 -1
- package/src/llm/openai/index.ts +135 -9
- package/src/llm/openai/utils/index.ts +116 -1
- package/src/llm/openrouter/index.ts +224 -3
- package/src/messages/__tests__/tools.test.ts +473 -0
- package/src/messages/cache.ts +163 -61
- package/src/messages/index.ts +1 -0
- package/src/messages/tools.ts +99 -0
- package/src/scripts/code_exec_ptc.ts +334 -0
- package/src/scripts/programmatic_exec.ts +396 -0
- package/src/scripts/programmatic_exec_agent.ts +231 -0
- package/src/scripts/tool_search_regex.ts +162 -0
- package/src/specs/thinking-prune.test.ts +52 -118
- package/src/stream.ts +26 -0
- package/src/test/mockTools.ts +366 -0
- package/src/tools/CodeExecutor.ts +4 -0
- package/src/tools/ProgrammaticToolCalling.ts +558 -0
- package/src/tools/ToolNode.ts +60 -7
- package/src/tools/ToolSearchRegex.ts +535 -0
- package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.ts +318 -0
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +853 -0
- package/src/tools/__tests__/ToolSearchRegex.integration.test.ts +161 -0
- package/src/tools/__tests__/ToolSearchRegex.test.ts +232 -0
- package/src/tools/search/jina-reranker.test.ts +16 -16
- package/src/tools/search/tool.ts +23 -1
- package/src/types/graph.ts +7 -1
- package/src/types/tools.ts +166 -0
- package/src/utils/llmConfig.ts +8 -2
- package/src/utils/run.ts +5 -1
- package/src/tools/search/direct-url.test.ts +0 -530
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// src/tools/__tests__/ToolSearchRegex.integration.test.ts
|
|
2
|
+
/**
|
|
3
|
+
* Integration tests for Tool Search Regex.
|
|
4
|
+
* These tests hit the LIVE Code API and verify end-to-end search functionality.
|
|
5
|
+
*
|
|
6
|
+
* Run with: npm test -- ToolSearchRegex.integration.test.ts
|
|
7
|
+
*/
|
|
8
|
+
import { config as dotenvConfig } from 'dotenv';
|
|
9
|
+
dotenvConfig();
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, beforeAll } from '@jest/globals';
|
|
12
|
+
import { createToolSearchRegexTool } from '../ToolSearchRegex';
|
|
13
|
+
import { createToolSearchToolRegistry } from '@/test/mockTools';
|
|
14
|
+
|
|
15
|
+
describe('ToolSearchRegex - Live API Integration', () => {
|
|
16
|
+
let searchTool: ReturnType<typeof createToolSearchRegexTool>;
|
|
17
|
+
const toolRegistry = createToolSearchToolRegistry();
|
|
18
|
+
|
|
19
|
+
beforeAll(() => {
|
|
20
|
+
const apiKey = process.env.LIBRECHAT_CODE_API_KEY;
|
|
21
|
+
if (apiKey == null || apiKey === '') {
|
|
22
|
+
throw new Error(
|
|
23
|
+
'LIBRECHAT_CODE_API_KEY not set. Required for integration tests.'
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
searchTool = createToolSearchRegexTool({ apiKey, toolRegistry });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('searches for expense-related tools', async () => {
|
|
31
|
+
const output = await searchTool.invoke({ query: 'expense' });
|
|
32
|
+
|
|
33
|
+
expect(typeof output).toBe('string');
|
|
34
|
+
expect(output).toContain('Found');
|
|
35
|
+
expect(output).toContain('matching tools');
|
|
36
|
+
expect(output).toContain('get_expenses');
|
|
37
|
+
expect(output).toContain('calculate_expense_totals');
|
|
38
|
+
expect(output).toContain('score: 0.95');
|
|
39
|
+
}, 10000);
|
|
40
|
+
|
|
41
|
+
it('searches for weather tools with OR pattern', async () => {
|
|
42
|
+
const output = await searchTool.invoke({ query: 'weather|forecast' });
|
|
43
|
+
|
|
44
|
+
expect(output).toContain('Found');
|
|
45
|
+
expect(output).toContain('get_weather');
|
|
46
|
+
expect(output).toContain('get_forecast');
|
|
47
|
+
}, 10000);
|
|
48
|
+
|
|
49
|
+
it('performs case-insensitive search', async () => {
|
|
50
|
+
const output = await searchTool.invoke({ query: 'EMAIL' });
|
|
51
|
+
|
|
52
|
+
expect(output).toContain('send_email');
|
|
53
|
+
}, 10000);
|
|
54
|
+
|
|
55
|
+
it('searches descriptions only', async () => {
|
|
56
|
+
const output = await searchTool.invoke({
|
|
57
|
+
query: 'database',
|
|
58
|
+
fields: ['description'],
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(output).toContain('Found');
|
|
62
|
+
expect(output).toContain('Matched in: description');
|
|
63
|
+
expect(output).toContain('run_database_query');
|
|
64
|
+
}, 10000);
|
|
65
|
+
|
|
66
|
+
it('searches parameter names', async () => {
|
|
67
|
+
const output = await searchTool.invoke({
|
|
68
|
+
query: 'query',
|
|
69
|
+
fields: ['parameters'],
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(output).toContain('Found');
|
|
73
|
+
expect(output).toContain('Matched in: parameters');
|
|
74
|
+
}, 10000);
|
|
75
|
+
|
|
76
|
+
it('limits results to specified count', async () => {
|
|
77
|
+
const output = await searchTool.invoke({
|
|
78
|
+
query: 'get',
|
|
79
|
+
max_results: 2,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
expect(output).toContain('Found 2 matching tools');
|
|
83
|
+
const toolMatches = output.match(/- \w+ \(score:/g);
|
|
84
|
+
expect(toolMatches?.length).toBe(2);
|
|
85
|
+
}, 10000);
|
|
86
|
+
|
|
87
|
+
it('returns no matches for nonsense pattern', async () => {
|
|
88
|
+
const output = await searchTool.invoke({
|
|
89
|
+
query: 'xyznonexistent999',
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(output).toContain('No tools matched');
|
|
93
|
+
expect(output).toContain('Total tools searched:');
|
|
94
|
+
}, 10000);
|
|
95
|
+
|
|
96
|
+
it('uses regex character classes', async () => {
|
|
97
|
+
const output = await searchTool.invoke({ query: 'get_[a-z]+' });
|
|
98
|
+
|
|
99
|
+
expect(output).toContain('Found');
|
|
100
|
+
expect(output).toContain('matching tools');
|
|
101
|
+
}, 10000);
|
|
102
|
+
|
|
103
|
+
it('sanitizes dangerous patterns with warning', async () => {
|
|
104
|
+
const output = await searchTool.invoke({ query: '(a+)+' });
|
|
105
|
+
|
|
106
|
+
expect(output).toContain(
|
|
107
|
+
'Note: The provided pattern was converted to a literal search for safety'
|
|
108
|
+
);
|
|
109
|
+
// After sanitization, pattern is shown in the output
|
|
110
|
+
expect(output).toContain('Total tools searched:');
|
|
111
|
+
expect(output).toContain('No tools matched');
|
|
112
|
+
}, 10000);
|
|
113
|
+
|
|
114
|
+
it('searches across all fields', async () => {
|
|
115
|
+
const output = await searchTool.invoke({
|
|
116
|
+
query: 'text',
|
|
117
|
+
fields: ['name', 'description', 'parameters'],
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(output).toContain('Found');
|
|
121
|
+
expect(output).toContain('translate_text');
|
|
122
|
+
}, 10000);
|
|
123
|
+
|
|
124
|
+
it('handles complex regex patterns', async () => {
|
|
125
|
+
const output = await searchTool.invoke({
|
|
126
|
+
query: '^(get|create)_.*',
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(output).toContain('Found');
|
|
130
|
+
expect(output).toContain('matching tools');
|
|
131
|
+
}, 10000);
|
|
132
|
+
|
|
133
|
+
it('prioritizes name matches over description matches', async () => {
|
|
134
|
+
const output = await searchTool.invoke({ query: 'generate' });
|
|
135
|
+
|
|
136
|
+
expect(output).toContain('generate_report');
|
|
137
|
+
expect(output).toContain('score:');
|
|
138
|
+
|
|
139
|
+
const lines = output.split('\n');
|
|
140
|
+
const generateLine = lines.find((l: string) =>
|
|
141
|
+
l.includes('generate_report')
|
|
142
|
+
);
|
|
143
|
+
expect(generateLine).toContain('score: 0.95'); // Name match = 0.95
|
|
144
|
+
}, 10000);
|
|
145
|
+
|
|
146
|
+
it('finds tools by partial name match', async () => {
|
|
147
|
+
const output = await searchTool.invoke({ query: 'budget' });
|
|
148
|
+
|
|
149
|
+
expect(output).toContain('create_budget');
|
|
150
|
+
}, 10000);
|
|
151
|
+
|
|
152
|
+
it('handles very specific patterns', async () => {
|
|
153
|
+
const output = await searchTool.invoke({
|
|
154
|
+
query: 'send_email',
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
expect(output).toContain('Found');
|
|
158
|
+
expect(output).toContain('send_email');
|
|
159
|
+
expect(output).toContain('score: 0.95');
|
|
160
|
+
}, 10000);
|
|
161
|
+
});
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// src/tools/__tests__/ToolSearchRegex.test.ts
|
|
2
|
+
/**
|
|
3
|
+
* Unit tests for Tool Search Regex.
|
|
4
|
+
* Tests helper functions and sanitization logic without hitting the API.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect } from '@jest/globals';
|
|
7
|
+
import {
|
|
8
|
+
sanitizeRegex,
|
|
9
|
+
escapeRegexSpecialChars,
|
|
10
|
+
isDangerousPattern,
|
|
11
|
+
countNestedGroups,
|
|
12
|
+
hasNestedQuantifiers,
|
|
13
|
+
} from '../ToolSearchRegex';
|
|
14
|
+
|
|
15
|
+
describe('ToolSearchRegex', () => {
|
|
16
|
+
describe('escapeRegexSpecialChars', () => {
|
|
17
|
+
it('escapes special regex characters', () => {
|
|
18
|
+
expect(escapeRegexSpecialChars('hello.world')).toBe('hello\\.world');
|
|
19
|
+
expect(escapeRegexSpecialChars('test*pattern')).toBe('test\\*pattern');
|
|
20
|
+
expect(escapeRegexSpecialChars('query+result')).toBe('query\\+result');
|
|
21
|
+
expect(escapeRegexSpecialChars('a?b')).toBe('a\\?b');
|
|
22
|
+
expect(escapeRegexSpecialChars('(group)')).toBe('\\(group\\)');
|
|
23
|
+
expect(escapeRegexSpecialChars('[abc]')).toBe('\\[abc\\]');
|
|
24
|
+
expect(escapeRegexSpecialChars('a|b')).toBe('a\\|b');
|
|
25
|
+
expect(escapeRegexSpecialChars('a^b$c')).toBe('a\\^b\\$c');
|
|
26
|
+
expect(escapeRegexSpecialChars('a{2,3}')).toBe('a\\{2,3\\}');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('handles empty string', () => {
|
|
30
|
+
expect(escapeRegexSpecialChars('')).toBe('');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('handles string with no special chars', () => {
|
|
34
|
+
expect(escapeRegexSpecialChars('hello_world')).toBe('hello_world');
|
|
35
|
+
expect(escapeRegexSpecialChars('test123')).toBe('test123');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('handles multiple consecutive special chars', () => {
|
|
39
|
+
expect(escapeRegexSpecialChars('...')).toBe('\\.\\.\\.');
|
|
40
|
+
expect(escapeRegexSpecialChars('***')).toBe('\\*\\*\\*');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('countNestedGroups', () => {
|
|
45
|
+
it('counts simple nesting', () => {
|
|
46
|
+
expect(countNestedGroups('(a)')).toBe(1);
|
|
47
|
+
expect(countNestedGroups('((a))')).toBe(2);
|
|
48
|
+
expect(countNestedGroups('(((a)))')).toBe(3);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('counts maximum depth with multiple groups', () => {
|
|
52
|
+
expect(countNestedGroups('(a)(b)(c)')).toBe(1);
|
|
53
|
+
expect(countNestedGroups('(a(b)c)')).toBe(2);
|
|
54
|
+
expect(countNestedGroups('(a(b(c)))')).toBe(3);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('handles mixed nesting levels', () => {
|
|
58
|
+
expect(countNestedGroups('(a)((b)(c))')).toBe(2);
|
|
59
|
+
expect(countNestedGroups('((a)(b))((c))')).toBe(2);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('ignores escaped parentheses', () => {
|
|
63
|
+
expect(countNestedGroups('\\(not a group\\)')).toBe(0);
|
|
64
|
+
expect(countNestedGroups('(a\\(b\\)c)')).toBe(1);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('handles no groups', () => {
|
|
68
|
+
expect(countNestedGroups('abc')).toBe(0);
|
|
69
|
+
expect(countNestedGroups('test.*pattern')).toBe(0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('handles unbalanced groups', () => {
|
|
73
|
+
expect(countNestedGroups('((a)')).toBe(2);
|
|
74
|
+
expect(countNestedGroups('(a))')).toBe(1);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('hasNestedQuantifiers', () => {
|
|
79
|
+
it('detects nested quantifiers', () => {
|
|
80
|
+
expect(hasNestedQuantifiers('(a+)+')).toBe(true);
|
|
81
|
+
expect(hasNestedQuantifiers('(a*)*')).toBe(true);
|
|
82
|
+
expect(hasNestedQuantifiers('(a+)*')).toBe(true);
|
|
83
|
+
expect(hasNestedQuantifiers('(a*)?')).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('allows safe quantifiers', () => {
|
|
87
|
+
expect(hasNestedQuantifiers('a+')).toBe(false);
|
|
88
|
+
expect(hasNestedQuantifiers('(abc)+')).toBe(false);
|
|
89
|
+
expect(hasNestedQuantifiers('a+b*c?')).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('handles complex patterns', () => {
|
|
93
|
+
expect(hasNestedQuantifiers('(a|b)+')).toBe(false);
|
|
94
|
+
// Note: This pattern might not be detected by the simple regex check
|
|
95
|
+
const complexPattern = '((a|b)+)+';
|
|
96
|
+
const result = hasNestedQuantifiers(complexPattern);
|
|
97
|
+
// Just verify it doesn't crash - detection may vary
|
|
98
|
+
expect(typeof result).toBe('boolean');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('isDangerousPattern', () => {
|
|
103
|
+
it('detects nested quantifiers', () => {
|
|
104
|
+
expect(isDangerousPattern('(a+)+')).toBe(true);
|
|
105
|
+
expect(isDangerousPattern('(a*)*')).toBe(true);
|
|
106
|
+
expect(isDangerousPattern('(.+)+')).toBe(true);
|
|
107
|
+
expect(isDangerousPattern('(.*)*')).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('detects excessive nesting', () => {
|
|
111
|
+
expect(isDangerousPattern('((((((a))))))')).toBe(true); // Depth > 5
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('detects excessive wildcards', () => {
|
|
115
|
+
const pattern = '.{1000,}';
|
|
116
|
+
expect(isDangerousPattern(pattern)).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('allows safe patterns', () => {
|
|
120
|
+
expect(isDangerousPattern('weather')).toBe(false);
|
|
121
|
+
expect(isDangerousPattern('get_.*_data')).toBe(false);
|
|
122
|
+
expect(isDangerousPattern('(a|b|c)')).toBe(false);
|
|
123
|
+
expect(isDangerousPattern('test\\d+')).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('detects various dangerous patterns', () => {
|
|
127
|
+
expect(isDangerousPattern('(.*)+')).toBe(true);
|
|
128
|
+
expect(isDangerousPattern('(.+)*')).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('sanitizeRegex', () => {
|
|
133
|
+
it('returns safe pattern unchanged', () => {
|
|
134
|
+
const result = sanitizeRegex('weather');
|
|
135
|
+
expect(result.safe).toBe('weather');
|
|
136
|
+
expect(result.wasEscaped).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('escapes dangerous patterns', () => {
|
|
140
|
+
const result = sanitizeRegex('(a+)+');
|
|
141
|
+
expect(result.safe).toBe('\\(a\\+\\)\\+');
|
|
142
|
+
expect(result.wasEscaped).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('escapes invalid regex', () => {
|
|
146
|
+
const result = sanitizeRegex('(unclosed');
|
|
147
|
+
expect(result.wasEscaped).toBe(true);
|
|
148
|
+
expect(result.safe).toContain('\\(');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('allows complex but safe patterns', () => {
|
|
152
|
+
const result = sanitizeRegex('get_[a-z]+_data');
|
|
153
|
+
expect(result.safe).toBe('get_[a-z]+_data');
|
|
154
|
+
expect(result.wasEscaped).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('handles alternation patterns', () => {
|
|
158
|
+
const result = sanitizeRegex('weather|forecast');
|
|
159
|
+
expect(result.safe).toBe('weather|forecast');
|
|
160
|
+
expect(result.wasEscaped).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('Pattern Validation Edge Cases', () => {
|
|
165
|
+
it('handles empty pattern', () => {
|
|
166
|
+
expect(countNestedGroups('')).toBe(0);
|
|
167
|
+
expect(hasNestedQuantifiers('')).toBe(false);
|
|
168
|
+
expect(isDangerousPattern('')).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('handles pattern with only quantifiers', () => {
|
|
172
|
+
expect(hasNestedQuantifiers('+++')).toBe(false);
|
|
173
|
+
expect(hasNestedQuantifiers('***')).toBe(false);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('handles escaped special sequences', () => {
|
|
177
|
+
const result = sanitizeRegex('\\d+\\w*\\s?');
|
|
178
|
+
expect(result.wasEscaped).toBe(false);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('sanitizes exponential backtracking patterns', () => {
|
|
182
|
+
// These can cause catastrophic backtracking
|
|
183
|
+
expect(isDangerousPattern('(a+)+')).toBe(true);
|
|
184
|
+
expect(isDangerousPattern('(a*)*')).toBe(true);
|
|
185
|
+
expect(isDangerousPattern('(.*)*')).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('Real-World Pattern Examples', () => {
|
|
190
|
+
it('handles common search patterns safely', () => {
|
|
191
|
+
const safePatterns = [
|
|
192
|
+
'expense',
|
|
193
|
+
'weather|forecast',
|
|
194
|
+
'data.*query',
|
|
195
|
+
'_tool$',
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
for (const pattern of safePatterns) {
|
|
199
|
+
const result = sanitizeRegex(pattern);
|
|
200
|
+
expect(result.wasEscaped).toBe(false);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('escapes clearly dangerous patterns', () => {
|
|
205
|
+
const dangerousPatterns = ['(a+)+', '(.*)+', '(.+)*'];
|
|
206
|
+
|
|
207
|
+
for (const pattern of dangerousPatterns) {
|
|
208
|
+
const result = sanitizeRegex(pattern);
|
|
209
|
+
expect(result.wasEscaped).toBe(true);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('handles patterns that may or may not be escaped', () => {
|
|
214
|
+
// These patterns might be escaped depending on validation logic
|
|
215
|
+
const edgeCasePatterns = [
|
|
216
|
+
'(?i)email',
|
|
217
|
+
'^create_',
|
|
218
|
+
'get_[a-z]+_info',
|
|
219
|
+
'get_.*',
|
|
220
|
+
'((((((a))))))',
|
|
221
|
+
'(a|a)*',
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
for (const pattern of edgeCasePatterns) {
|
|
225
|
+
const result = sanitizeRegex(pattern);
|
|
226
|
+
// Just verify it returns a result without crashing
|
|
227
|
+
expect(typeof result.safe).toBe('string');
|
|
228
|
+
expect(typeof result.wasEscaped).toBe('boolean');
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
});
|
|
@@ -3,14 +3,14 @@ import { createDefaultLogger } from './utils';
|
|
|
3
3
|
|
|
4
4
|
describe('JinaReranker', () => {
|
|
5
5
|
const mockLogger = createDefaultLogger();
|
|
6
|
-
|
|
6
|
+
|
|
7
7
|
describe('constructor', () => {
|
|
8
8
|
it('should use default API URL when no apiUrl is provided', () => {
|
|
9
9
|
const reranker = new JinaReranker({
|
|
10
10
|
apiKey: 'test-key',
|
|
11
11
|
logger: mockLogger,
|
|
12
12
|
});
|
|
13
|
-
|
|
13
|
+
|
|
14
14
|
// Access private property for testing
|
|
15
15
|
const apiUrl = (reranker as any).apiUrl;
|
|
16
16
|
expect(apiUrl).toBe('https://api.jina.ai/v1/rerank');
|
|
@@ -23,7 +23,7 @@ describe('JinaReranker', () => {
|
|
|
23
23
|
apiUrl: customUrl,
|
|
24
24
|
logger: mockLogger,
|
|
25
25
|
});
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
const apiUrl = (reranker as any).apiUrl;
|
|
28
28
|
expect(apiUrl).toBe(customUrl);
|
|
29
29
|
});
|
|
@@ -31,15 +31,15 @@ describe('JinaReranker', () => {
|
|
|
31
31
|
it('should use environment variable JINA_API_URL when available', () => {
|
|
32
32
|
const originalEnv = process.env.JINA_API_URL;
|
|
33
33
|
process.env.JINA_API_URL = 'https://env-jina-endpoint.com/v1/rerank';
|
|
34
|
-
|
|
34
|
+
|
|
35
35
|
const reranker = new JinaReranker({
|
|
36
36
|
apiKey: 'test-key',
|
|
37
37
|
logger: mockLogger,
|
|
38
38
|
});
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
const apiUrl = (reranker as any).apiUrl;
|
|
41
41
|
expect(apiUrl).toBe('https://env-jina-endpoint.com/v1/rerank');
|
|
42
|
-
|
|
42
|
+
|
|
43
43
|
// Restore original environment
|
|
44
44
|
if (originalEnv) {
|
|
45
45
|
process.env.JINA_API_URL = originalEnv;
|
|
@@ -51,17 +51,17 @@ describe('JinaReranker', () => {
|
|
|
51
51
|
it('should prioritize explicit apiUrl over environment variable', () => {
|
|
52
52
|
const originalEnv = process.env.JINA_API_URL;
|
|
53
53
|
process.env.JINA_API_URL = 'https://env-jina-endpoint.com/v1/rerank';
|
|
54
|
-
|
|
54
|
+
|
|
55
55
|
const customUrl = 'https://explicit-jina-endpoint.com/v1/rerank';
|
|
56
56
|
const reranker = new JinaReranker({
|
|
57
57
|
apiKey: 'test-key',
|
|
58
58
|
apiUrl: customUrl,
|
|
59
59
|
logger: mockLogger,
|
|
60
60
|
});
|
|
61
|
-
|
|
61
|
+
|
|
62
62
|
const apiUrl = (reranker as any).apiUrl;
|
|
63
63
|
expect(apiUrl).toBe(customUrl);
|
|
64
|
-
|
|
64
|
+
|
|
65
65
|
// Restore original environment
|
|
66
66
|
if (originalEnv) {
|
|
67
67
|
process.env.JINA_API_URL = originalEnv;
|
|
@@ -79,19 +79,19 @@ describe('JinaReranker', () => {
|
|
|
79
79
|
apiUrl: customUrl,
|
|
80
80
|
logger: mockLogger,
|
|
81
81
|
});
|
|
82
|
-
|
|
82
|
+
|
|
83
83
|
const logSpy = jest.spyOn(mockLogger, 'debug');
|
|
84
|
-
|
|
84
|
+
|
|
85
85
|
try {
|
|
86
86
|
await reranker.rerank('test query', ['document1', 'document2'], 2);
|
|
87
87
|
} catch (error) {
|
|
88
88
|
// Expected to fail due to missing API key, but we can check the log
|
|
89
89
|
}
|
|
90
|
-
|
|
90
|
+
|
|
91
91
|
expect(logSpy).toHaveBeenCalledWith(
|
|
92
92
|
expect.stringContaining(`Reranking 2 chunks with Jina using API URL: ${customUrl}`)
|
|
93
93
|
);
|
|
94
|
-
|
|
94
|
+
|
|
95
95
|
logSpy.mockRestore();
|
|
96
96
|
});
|
|
97
97
|
});
|
|
@@ -99,7 +99,7 @@ describe('JinaReranker', () => {
|
|
|
99
99
|
|
|
100
100
|
describe('createReranker', () => {
|
|
101
101
|
const { createReranker } = require('./rerankers');
|
|
102
|
-
|
|
102
|
+
|
|
103
103
|
it('should create JinaReranker with jinaApiUrl when provided', () => {
|
|
104
104
|
const customUrl = 'https://custom-jina-endpoint.com/v1/rerank';
|
|
105
105
|
const reranker = createReranker({
|
|
@@ -107,7 +107,7 @@ describe('createReranker', () => {
|
|
|
107
107
|
jinaApiKey: 'test-key',
|
|
108
108
|
jinaApiUrl: customUrl,
|
|
109
109
|
});
|
|
110
|
-
|
|
110
|
+
|
|
111
111
|
expect(reranker).toBeInstanceOf(JinaReranker);
|
|
112
112
|
const apiUrl = (reranker as any).apiUrl;
|
|
113
113
|
expect(apiUrl).toBe(customUrl);
|
|
@@ -118,7 +118,7 @@ describe('createReranker', () => {
|
|
|
118
118
|
rerankerType: 'jina',
|
|
119
119
|
jinaApiKey: 'test-key',
|
|
120
120
|
});
|
|
121
|
-
|
|
121
|
+
|
|
122
122
|
expect(reranker).toBeInstanceOf(JinaReranker);
|
|
123
123
|
const apiUrl = (reranker as any).apiUrl;
|
|
124
124
|
expect(apiUrl).toBe('https://api.jina.ai/v1/rerank');
|
package/src/tools/search/tool.ts
CHANGED
|
@@ -424,6 +424,15 @@ function createTool({
|
|
|
424
424
|
async (params, runnableConfig) => {
|
|
425
425
|
const { query, date, country: _c, images, videos, news } = params;
|
|
426
426
|
const country = typeof _c === 'string' && _c ? _c : undefined;
|
|
427
|
+
|
|
428
|
+
// Log the incoming query for debugging URL detection
|
|
429
|
+
const toolLogger = createDefaultLogger();
|
|
430
|
+
toolLogger.debug(`[web_search] Received query: "${query}"`);
|
|
431
|
+
const detectedUrls = extractUrlsFromQuery(query);
|
|
432
|
+
if (detectedUrls.length > 0) {
|
|
433
|
+
toolLogger.debug(`[web_search] Detected URLs in query: ${detectedUrls.join(', ')}`);
|
|
434
|
+
}
|
|
435
|
+
|
|
427
436
|
const searchResult = await search({
|
|
428
437
|
query,
|
|
429
438
|
date,
|
|
@@ -443,7 +452,20 @@ function createTool({
|
|
|
443
452
|
},
|
|
444
453
|
{
|
|
445
454
|
name: Constants.WEB_SEARCH,
|
|
446
|
-
description: `Real-time search. Results have required citation anchors.
|
|
455
|
+
description: `Real-time web search and direct URL content extraction. Results have required citation anchors.
|
|
456
|
+
|
|
457
|
+
**CAPABILITIES:**
|
|
458
|
+
- Search: Query the web for information on any topic
|
|
459
|
+
- Direct URL: Fetch and extract content from a specific URL for summarization or analysis
|
|
460
|
+
|
|
461
|
+
**CRITICAL - URL HANDLING:**
|
|
462
|
+
When user provides a URL (e.g., "summarize https://example.com/article"), you MUST include the FULL URL in the query parameter.
|
|
463
|
+
- CORRECT: query = "https://example.com/article" or "summarize https://example.com/article"
|
|
464
|
+
- WRONG: query = "example article summary" (do NOT convert URLs to search terms)
|
|
465
|
+
|
|
466
|
+
**USAGE:**
|
|
467
|
+
- For search: Use concise search terms as query
|
|
468
|
+
- For URL extraction: Pass the complete URL in the query field
|
|
447
469
|
|
|
448
470
|
Note: Use ONCE per reply unless instructed otherwise.
|
|
449
471
|
|
package/src/types/graph.ts
CHANGED
|
@@ -18,7 +18,7 @@ import type {
|
|
|
18
18
|
import type { RunnableConfig, Runnable } from '@langchain/core/runnables';
|
|
19
19
|
import type { ChatGenerationChunk } from '@langchain/core/outputs';
|
|
20
20
|
import type { GoogleAIToolType } from '@langchain/google-common';
|
|
21
|
-
import type { ToolMap, ToolEndEvent, GenericTool } from '@/types/tools';
|
|
21
|
+
import type { ToolMap, ToolEndEvent, GenericTool, LCTool } from '@/types/tools';
|
|
22
22
|
import type { Providers, Callback, GraphNodeKeys } from '@/common';
|
|
23
23
|
import type { StandardGraph, MultiAgentGraph } from '@/graphs';
|
|
24
24
|
import type { ClientOptions } from '@/types/llm';
|
|
@@ -369,4 +369,10 @@ export interface AgentInputs {
|
|
|
369
369
|
reasoningKey?: 'reasoning_content' | 'reasoning';
|
|
370
370
|
/** Format content blocks as strings (for legacy compatibility i.e. Ollama/Azure Serverless) */
|
|
371
371
|
useLegacyContent?: boolean;
|
|
372
|
+
/**
|
|
373
|
+
* Tool definitions for all tools, including deferred and programmatic.
|
|
374
|
+
* Used for tool search and programmatic tool calling.
|
|
375
|
+
* Maps tool name to LCTool definition.
|
|
376
|
+
*/
|
|
377
|
+
toolRegistry?: Map<string, LCTool>;
|
|
372
378
|
}
|