@framers/agentos-ext-web-search 1.1.0 → 1.1.1
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/README.md +31 -188
- package/dist/index.d.ts +14 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +52 -50
- package/dist/index.js.map +1 -1
- package/dist/services/searchProvider.d.ts +54 -2
- package/dist/services/searchProvider.d.ts.map +1 -1
- package/dist/services/searchProvider.js +168 -17
- package/dist/services/searchProvider.js.map +1 -1
- package/dist/tools/factCheck.d.ts +46 -88
- package/dist/tools/factCheck.d.ts.map +1 -1
- package/dist/tools/factCheck.js +134 -170
- package/dist/tools/factCheck.js.map +1 -1
- package/dist/tools/researchAggregator.d.ts +37 -91
- package/dist/tools/researchAggregator.d.ts.map +1 -1
- package/dist/tools/researchAggregator.js +134 -175
- package/dist/tools/researchAggregator.js.map +1 -1
- package/dist/tools/webSearch.d.ts +31 -59
- package/dist/tools/webSearch.d.ts.map +1 -1
- package/dist/tools/webSearch.js +69 -87
- package/dist/tools/webSearch.js.map +1 -1
- package/dist/types.js +1 -2
- package/dist/types.js.map +1 -1
- package/manifest.json +11 -6
- package/package.json +2 -2
- package/src/index.ts +64 -46
- package/src/services/searchProvider.ts +231 -25
- package/src/tools/factCheck.ts +206 -224
- package/src/tools/researchAggregator.ts +209 -217
- package/src/tools/webSearch.ts +103 -102
- package/test/integration.spec.ts +240 -39
- package/test/searchProvider.spec.ts +263 -18
- package/test/webSearch.spec.ts +129 -27
- package/tsconfig.json +7 -2
package/src/tools/webSearch.ts
CHANGED
|
@@ -1,128 +1,129 @@
|
|
|
1
|
-
import { ITool } from '@framers/agentos';
|
|
2
|
-
import { SearchProviderService } from '../services/searchProvider';
|
|
3
|
-
|
|
4
1
|
/**
|
|
5
|
-
* Web
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* ```typescript
|
|
12
|
-
* const tool = new WebSearchTool(searchService);
|
|
13
|
-
* const result = await tool.execute({
|
|
14
|
-
* query: 'AgentOS extensions',
|
|
15
|
-
* maxResults: 10,
|
|
16
|
-
* provider: 'serper'
|
|
17
|
-
* });
|
|
18
|
-
* ```
|
|
2
|
+
* Web Search Tool — multi-provider search via SearchProviderService.
|
|
3
|
+
*
|
|
4
|
+
* Updated to conform to AgentOS `ITool` (inputSchema + ToolExecutionContext).
|
|
5
|
+
*
|
|
6
|
+
* @module @framers/agentos-ext-web-search
|
|
19
7
|
*/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
8
|
+
|
|
9
|
+
import type { ITool, JSONSchemaObject, ToolExecutionContext, ToolExecutionResult } from '@framers/agentos';
|
|
10
|
+
import type { ProviderResponse, MultiSearchResponse } from '../services/searchProvider.js';
|
|
11
|
+
import { SearchProviderService } from '../services/searchProvider.js';
|
|
12
|
+
|
|
13
|
+
export interface WebSearchInput {
|
|
14
|
+
query: string;
|
|
15
|
+
maxResults?: number;
|
|
16
|
+
provider?: 'serper' | 'serpapi' | 'brave' | 'searxng' | 'duckduckgo';
|
|
17
|
+
multiSearch?: boolean;
|
|
18
|
+
category?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type WebSearchOutput = ProviderResponse | MultiSearchResponse;
|
|
22
|
+
|
|
23
|
+
export class WebSearchTool implements ITool<WebSearchInput, WebSearchOutput> {
|
|
24
|
+
public readonly id = 'web-search-v1';
|
|
25
|
+
/** Tool call name used by the LLM / ToolExecutor. */
|
|
26
|
+
public readonly name = 'web_search';
|
|
27
|
+
public readonly displayName = 'Web Search';
|
|
28
|
+
public readonly description =
|
|
29
|
+
'Search the web using multiple providers (Serper, SerpAPI, Brave, SearXNG, DuckDuckGo fallback). Set multiSearch=true to query ALL providers in parallel for higher-confidence, deduplicated results.';
|
|
30
|
+
public readonly category = 'research';
|
|
31
|
+
public readonly hasSideEffects = false;
|
|
32
|
+
|
|
33
|
+
public readonly inputSchema: JSONSchemaObject = {
|
|
34
|
+
type: 'object',
|
|
35
|
+
required: ['query'],
|
|
36
|
+
properties: {
|
|
37
|
+
query: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
description: 'The search query',
|
|
40
|
+
},
|
|
41
|
+
maxResults: {
|
|
42
|
+
type: 'integer',
|
|
43
|
+
description: 'Maximum number of results to return',
|
|
44
|
+
default: 10,
|
|
45
|
+
minimum: 1,
|
|
46
|
+
maximum: 50,
|
|
47
|
+
},
|
|
48
|
+
provider: {
|
|
49
|
+
type: 'string',
|
|
50
|
+
description: 'Specific search provider to use (ignored when multiSearch is true)',
|
|
51
|
+
enum: ['serper', 'serpapi', 'brave', 'searxng', 'duckduckgo'],
|
|
52
|
+
},
|
|
53
|
+
multiSearch: {
|
|
54
|
+
type: 'boolean',
|
|
55
|
+
description:
|
|
56
|
+
'When true, searches ALL available providers in parallel and returns merged, deduplicated results ranked by cross-provider agreement. Useful for deep research or fact verification.',
|
|
57
|
+
default: false,
|
|
58
|
+
},
|
|
59
|
+
category: {
|
|
60
|
+
type: 'string',
|
|
61
|
+
description: 'Search category (SearXNG only). Options: general, news, images, videos, it, science, files, social_media',
|
|
62
|
+
enum: ['general', 'news', 'images', 'videos', 'it', 'science', 'files', 'social_media'],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
additionalProperties: false,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
public readonly requiredCapabilities = ['capability:web_search'];
|
|
69
|
+
|
|
70
|
+
constructor(
|
|
71
|
+
private readonly searchService: SearchProviderService,
|
|
72
|
+
private readonly defaultMultiSearch: boolean = false,
|
|
73
|
+
) {}
|
|
74
|
+
|
|
75
|
+
async execute(input: WebSearchInput, _context: ToolExecutionContext): Promise<ToolExecutionResult<WebSearchOutput>> {
|
|
47
76
|
try {
|
|
77
|
+
const useMultiSearch = input.multiSearch ?? this.defaultMultiSearch;
|
|
78
|
+
|
|
79
|
+
if (useMultiSearch && !input.provider) {
|
|
80
|
+
const results = await this.searchService.multiSearch(input.query, {
|
|
81
|
+
maxResults: input.maxResults || 10,
|
|
82
|
+
});
|
|
83
|
+
return { success: true, output: results };
|
|
84
|
+
}
|
|
85
|
+
|
|
48
86
|
const results = await this.searchService.search(input.query, {
|
|
49
87
|
maxResults: input.maxResults || 10,
|
|
50
|
-
provider: input.provider
|
|
88
|
+
provider: input.provider,
|
|
89
|
+
category: input.category,
|
|
51
90
|
});
|
|
52
|
-
|
|
53
|
-
return {
|
|
54
|
-
success: true,
|
|
55
|
-
output: results
|
|
56
|
-
};
|
|
91
|
+
return { success: true, output: results };
|
|
57
92
|
} catch (error: any) {
|
|
58
|
-
return {
|
|
59
|
-
success: false,
|
|
60
|
-
error: error.message
|
|
61
|
-
};
|
|
93
|
+
return { success: false, error: error?.message || String(error) };
|
|
62
94
|
}
|
|
63
95
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
* Validates input parameters for the web search
|
|
67
|
-
*
|
|
68
|
-
* @param {any} input - The input to validate
|
|
69
|
-
* @returns {{valid: boolean, errors: string[]}} Validation result
|
|
70
|
-
*/
|
|
71
|
-
validate(input: any): { valid: boolean; errors: string[] } {
|
|
96
|
+
|
|
97
|
+
validateArgs(input: Record<string, any>): { isValid: boolean; errors?: any[] } {
|
|
72
98
|
const errors: string[] = [];
|
|
73
|
-
|
|
99
|
+
|
|
74
100
|
if (!input.query) {
|
|
75
101
|
errors.push('Query is required');
|
|
76
102
|
} else if (typeof input.query !== 'string') {
|
|
77
103
|
errors.push('Query must be a string');
|
|
78
104
|
}
|
|
79
|
-
|
|
105
|
+
|
|
80
106
|
if (input.maxResults !== undefined) {
|
|
81
107
|
if (typeof input.maxResults !== 'number' || input.maxResults <= 0) {
|
|
82
108
|
errors.push('maxResults must be a positive number');
|
|
83
109
|
}
|
|
84
110
|
}
|
|
85
|
-
|
|
111
|
+
|
|
86
112
|
if (input.provider !== undefined) {
|
|
87
|
-
const validProviders = ['serper', 'serpapi', 'brave', 'duckduckgo'];
|
|
113
|
+
const validProviders = ['serper', 'serpapi', 'brave', 'searxng', 'duckduckgo'];
|
|
88
114
|
if (!validProviders.includes(input.provider)) {
|
|
89
115
|
errors.push('Invalid provider');
|
|
90
116
|
}
|
|
91
117
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
* @returns {Object} JSON Schema object
|
|
103
|
-
*/
|
|
104
|
-
getSchema(): any {
|
|
105
|
-
return {
|
|
106
|
-
type: 'object',
|
|
107
|
-
required: ['query'],
|
|
108
|
-
properties: {
|
|
109
|
-
query: {
|
|
110
|
-
type: 'string',
|
|
111
|
-
description: 'The search query'
|
|
112
|
-
},
|
|
113
|
-
maxResults: {
|
|
114
|
-
type: 'number',
|
|
115
|
-
description: 'Maximum number of results to return',
|
|
116
|
-
default: 10,
|
|
117
|
-
minimum: 1,
|
|
118
|
-
maximum: 50
|
|
119
|
-
},
|
|
120
|
-
provider: {
|
|
121
|
-
type: 'string',
|
|
122
|
-
description: 'Specific search provider to use',
|
|
123
|
-
enum: ['serper', 'serpapi', 'brave', 'duckduckgo']
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
};
|
|
118
|
+
|
|
119
|
+
if (input.multiSearch !== undefined && typeof input.multiSearch !== 'boolean') {
|
|
120
|
+
errors.push('multiSearch must be a boolean');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (input.multiSearch && input.provider) {
|
|
124
|
+
errors.push('Cannot specify both multiSearch and a specific provider');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return errors.length === 0 ? { isValid: true } : { isValid: false, errors };
|
|
127
128
|
}
|
|
128
|
-
}
|
|
129
|
+
}
|
package/test/integration.spec.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
2
|
import { createExtensionPack } from '../src';
|
|
3
3
|
import { ExtensionContext } from '@framers/agentos';
|
|
4
4
|
|
|
@@ -7,6 +7,16 @@ describe('Web Search Extension Integration', () => {
|
|
|
7
7
|
let context: ExtensionContext;
|
|
8
8
|
|
|
9
9
|
beforeEach(() => {
|
|
10
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
11
|
+
ok: true,
|
|
12
|
+
json: async () => ({
|
|
13
|
+
Heading: 'Test',
|
|
14
|
+
AbstractText: 'Test abstract',
|
|
15
|
+
AbstractURL: 'https://example.com',
|
|
16
|
+
RelatedTopics: [],
|
|
17
|
+
}),
|
|
18
|
+
}) as any;
|
|
19
|
+
|
|
10
20
|
context = {
|
|
11
21
|
options: {
|
|
12
22
|
serperApiKey: process.env.SERPER_API_KEY,
|
|
@@ -27,15 +37,15 @@ describe('Web Search Extension Integration', () => {
|
|
|
27
37
|
describe('Extension Pack Creation', () => {
|
|
28
38
|
it('should create extension pack with correct metadata', () => {
|
|
29
39
|
expect(extensionPack.name).toBe('@framers/agentos-ext-web-search');
|
|
30
|
-
expect(extensionPack.version).toBe('1.
|
|
40
|
+
expect(extensionPack.version).toBe('1.1.0');
|
|
31
41
|
expect(extensionPack.descriptors).toHaveLength(3);
|
|
32
42
|
});
|
|
33
43
|
|
|
34
44
|
it('should register all three tools', () => {
|
|
35
45
|
const toolIds = extensionPack.descriptors.map((d: any) => d.id);
|
|
36
|
-
expect(toolIds).toContain('
|
|
37
|
-
expect(toolIds).toContain('
|
|
38
|
-
expect(toolIds).toContain('
|
|
46
|
+
expect(toolIds).toContain('web_search');
|
|
47
|
+
expect(toolIds).toContain('research_aggregate');
|
|
48
|
+
expect(toolIds).toContain('fact_check');
|
|
39
49
|
});
|
|
40
50
|
|
|
41
51
|
it('should set correct tool kinds', () => {
|
|
@@ -48,13 +58,17 @@ describe('Web Search Extension Integration', () => {
|
|
|
48
58
|
describe('Tool Execution', () => {
|
|
49
59
|
it('should execute web search tool', async () => {
|
|
50
60
|
const webSearchDescriptor = extensionPack.descriptors.find(
|
|
51
|
-
(d: any) => d.id === '
|
|
61
|
+
(d: any) => d.id === 'web_search'
|
|
52
62
|
);
|
|
53
63
|
const webSearchTool = webSearchDescriptor.payload;
|
|
54
64
|
|
|
55
65
|
// Test with DuckDuckGo (no API key required)
|
|
56
66
|
const result = await webSearchTool.execute({
|
|
57
67
|
query: 'AgentOS extensions'
|
|
68
|
+
}, {
|
|
69
|
+
gmiId: 'test-gmi',
|
|
70
|
+
personaId: 'test-persona',
|
|
71
|
+
userContext: { userId: 'test-user' },
|
|
58
72
|
});
|
|
59
73
|
|
|
60
74
|
expect(result.success).toBe(true);
|
|
@@ -65,13 +79,17 @@ describe('Web Search Extension Integration', () => {
|
|
|
65
79
|
|
|
66
80
|
it('should execute research aggregator tool', async () => {
|
|
67
81
|
const aggregatorDescriptor = extensionPack.descriptors.find(
|
|
68
|
-
(d: any) => d.id === '
|
|
82
|
+
(d: any) => d.id === 'research_aggregate'
|
|
69
83
|
);
|
|
70
84
|
const aggregatorTool = aggregatorDescriptor.payload;
|
|
71
85
|
|
|
72
86
|
const result = await aggregatorTool.execute({
|
|
73
87
|
topic: 'TypeScript testing',
|
|
74
88
|
sources: 2
|
|
89
|
+
}, {
|
|
90
|
+
gmiId: 'test-gmi',
|
|
91
|
+
personaId: 'test-persona',
|
|
92
|
+
userContext: { userId: 'test-user' },
|
|
75
93
|
});
|
|
76
94
|
|
|
77
95
|
expect(result.success).toBe(true);
|
|
@@ -82,13 +100,17 @@ describe('Web Search Extension Integration', () => {
|
|
|
82
100
|
|
|
83
101
|
it('should execute fact check tool', async () => {
|
|
84
102
|
const factCheckDescriptor = extensionPack.descriptors.find(
|
|
85
|
-
(d: any) => d.id === '
|
|
103
|
+
(d: any) => d.id === 'fact_check'
|
|
86
104
|
);
|
|
87
105
|
const factCheckTool = factCheckDescriptor.payload;
|
|
88
106
|
|
|
89
107
|
const result = await factCheckTool.execute({
|
|
90
108
|
statement: 'TypeScript was created by Microsoft',
|
|
91
109
|
checkSources: true
|
|
110
|
+
}, {
|
|
111
|
+
gmiId: 'test-gmi',
|
|
112
|
+
personaId: 'test-persona',
|
|
113
|
+
userContext: { userId: 'test-user' },
|
|
92
114
|
});
|
|
93
115
|
|
|
94
116
|
expect(result.success).toBe(true);
|
|
@@ -111,6 +133,10 @@ describe('Web Search Extension Integration', () => {
|
|
|
111
133
|
|
|
112
134
|
const result = await webSearch.execute({
|
|
113
135
|
query: 'test query'
|
|
136
|
+
}, {
|
|
137
|
+
gmiId: 'test-gmi',
|
|
138
|
+
personaId: 'test-persona',
|
|
139
|
+
userContext: { userId: 'test-user' },
|
|
114
140
|
});
|
|
115
141
|
|
|
116
142
|
// Should fallback to DuckDuckGo
|
|
@@ -120,54 +146,229 @@ describe('Web Search Extension Integration', () => {
|
|
|
120
146
|
|
|
121
147
|
it('should validate inputs before execution', async () => {
|
|
122
148
|
const webSearchDescriptor = extensionPack.descriptors.find(
|
|
123
|
-
(d: any) => d.id === '
|
|
149
|
+
(d: any) => d.id === 'web_search'
|
|
124
150
|
);
|
|
125
151
|
const webSearchTool = webSearchDescriptor.payload;
|
|
126
152
|
|
|
127
|
-
const validation = webSearchTool.
|
|
153
|
+
const validation = webSearchTool.validateArgs({
|
|
128
154
|
// Missing required 'query' field
|
|
129
155
|
maxResults: 5
|
|
130
156
|
});
|
|
131
157
|
|
|
132
|
-
expect(validation.
|
|
158
|
+
expect(validation.isValid).toBe(false);
|
|
133
159
|
expect(validation.errors).toContain('Query is required');
|
|
134
160
|
});
|
|
135
161
|
});
|
|
136
162
|
|
|
163
|
+
describe('Multi-Search Integration', () => {
|
|
164
|
+
it('should create tools with defaultMultiSearch option', () => {
|
|
165
|
+
const multiContext = {
|
|
166
|
+
options: {
|
|
167
|
+
...context.options,
|
|
168
|
+
defaultMultiSearch: true,
|
|
169
|
+
},
|
|
170
|
+
logger: context.logger,
|
|
171
|
+
} as any;
|
|
172
|
+
|
|
173
|
+
const multiPack = createExtensionPack(multiContext);
|
|
174
|
+
expect(multiPack.descriptors).toHaveLength(3);
|
|
175
|
+
|
|
176
|
+
// Verify tools were created (defaultMultiSearch is internal, just check they still work)
|
|
177
|
+
const webSearch = multiPack.descriptors.find((d: any) => d.id === 'web_search');
|
|
178
|
+
expect(webSearch).toBeDefined();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should execute web_search with multiSearch: true', async () => {
|
|
182
|
+
const webSearchTool = extensionPack.descriptors.find(
|
|
183
|
+
(d: any) => d.id === 'web_search'
|
|
184
|
+
).payload;
|
|
185
|
+
|
|
186
|
+
const result = await webSearchTool.execute({
|
|
187
|
+
query: 'multi search test',
|
|
188
|
+
multiSearch: true,
|
|
189
|
+
}, {
|
|
190
|
+
gmiId: 'test-gmi',
|
|
191
|
+
personaId: 'test-persona',
|
|
192
|
+
userContext: { userId: 'test-user' },
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(result.success).toBe(true);
|
|
196
|
+
// Multi-search returns metadata with provider info
|
|
197
|
+
expect(result.output).toHaveProperty('metadata');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should execute research_aggregate with multiSearch: true', async () => {
|
|
201
|
+
const aggregatorTool = extensionPack.descriptors.find(
|
|
202
|
+
(d: any) => d.id === 'research_aggregate'
|
|
203
|
+
).payload;
|
|
204
|
+
|
|
205
|
+
const result = await aggregatorTool.execute({
|
|
206
|
+
topic: 'test topic',
|
|
207
|
+
sources: 1,
|
|
208
|
+
depth: 'quick',
|
|
209
|
+
multiSearch: true,
|
|
210
|
+
}, {
|
|
211
|
+
gmiId: 'test-gmi',
|
|
212
|
+
personaId: 'test-persona',
|
|
213
|
+
userContext: { userId: 'test-user' },
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
expect(result.success).toBe(true);
|
|
217
|
+
expect(result.output).toHaveProperty('aggregatedResults');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should execute fact_check with multiSearch: true', async () => {
|
|
221
|
+
const factCheckTool = extensionPack.descriptors.find(
|
|
222
|
+
(d: any) => d.id === 'fact_check'
|
|
223
|
+
).payload;
|
|
224
|
+
|
|
225
|
+
const result = await factCheckTool.execute({
|
|
226
|
+
statement: 'The sky is blue because of Rayleigh scattering',
|
|
227
|
+
multiSearch: true,
|
|
228
|
+
}, {
|
|
229
|
+
gmiId: 'test-gmi',
|
|
230
|
+
personaId: 'test-persona',
|
|
231
|
+
userContext: { userId: 'test-user' },
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
expect(result.success).toBe(true);
|
|
235
|
+
expect(result.output).toHaveProperty('verdict');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should reject multiSearch + provider on web_search validation', () => {
|
|
239
|
+
const webSearchTool = extensionPack.descriptors.find(
|
|
240
|
+
(d: any) => d.id === 'web_search'
|
|
241
|
+
).payload;
|
|
242
|
+
|
|
243
|
+
const validation = webSearchTool.validateArgs({
|
|
244
|
+
query: 'test',
|
|
245
|
+
multiSearch: true,
|
|
246
|
+
provider: 'serper',
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
expect(validation.isValid).toBe(false);
|
|
250
|
+
expect(validation.errors).toContain('Cannot specify both multiSearch and a specific provider');
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe('SearXNG Integration', () => {
|
|
255
|
+
it('should create extension pack with searxngUrl option', () => {
|
|
256
|
+
const searxngContext = {
|
|
257
|
+
options: {
|
|
258
|
+
searxngUrl: 'http://searxng:8080',
|
|
259
|
+
},
|
|
260
|
+
logger: context.logger,
|
|
261
|
+
} as any;
|
|
262
|
+
|
|
263
|
+
const pack = createExtensionPack(searxngContext);
|
|
264
|
+
expect(pack.descriptors).toHaveLength(3);
|
|
265
|
+
expect(pack.name).toBe('@framers/agentos-ext-web-search');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should execute web search with searxng provider', async () => {
|
|
269
|
+
const mockFetch = global.fetch as any;
|
|
270
|
+
mockFetch.mockResolvedValue({
|
|
271
|
+
ok: true,
|
|
272
|
+
json: async () => ({
|
|
273
|
+
results: [
|
|
274
|
+
{ title: 'SearXNG Result', url: 'https://example.com/searxng', content: 'A result from SearXNG' },
|
|
275
|
+
],
|
|
276
|
+
}),
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const searxngContext = {
|
|
280
|
+
options: { searxngUrl: 'http://searxng:8080' },
|
|
281
|
+
logger: context.logger,
|
|
282
|
+
} as any;
|
|
283
|
+
|
|
284
|
+
const pack = createExtensionPack(searxngContext);
|
|
285
|
+
const webSearch = pack.descriptors.find((d: any) => d.id === 'web_search')!.payload;
|
|
286
|
+
|
|
287
|
+
const result = await webSearch.execute(
|
|
288
|
+
{ query: 'test', provider: 'searxng' },
|
|
289
|
+
{ gmiId: 'test-gmi', personaId: 'test-persona', userContext: { userId: 'test-user' } },
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
expect(result.success).toBe(true);
|
|
293
|
+
expect(result.output.provider).toBe('searxng');
|
|
294
|
+
expect(result.output.results[0].snippet).toBe('A result from SearXNG');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should pass category through to SearXNG', async () => {
|
|
298
|
+
const mockFetch = global.fetch as any;
|
|
299
|
+
mockFetch.mockResolvedValue({
|
|
300
|
+
ok: true,
|
|
301
|
+
json: async () => ({ results: [] }),
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const searxngContext = {
|
|
305
|
+
options: { searxngUrl: 'http://searxng:8080' },
|
|
306
|
+
logger: context.logger,
|
|
307
|
+
} as any;
|
|
308
|
+
|
|
309
|
+
const pack = createExtensionPack(searxngContext);
|
|
310
|
+
const webSearch = pack.descriptors.find((d: any) => d.id === 'web_search')!.payload;
|
|
311
|
+
|
|
312
|
+
await webSearch.execute(
|
|
313
|
+
{ query: 'AI news', provider: 'searxng', category: 'news' },
|
|
314
|
+
{ gmiId: 'test-gmi', personaId: 'test-persona', userContext: { userId: 'test-user' } },
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const calledUrl = mockFetch.mock.calls[0][0] as string;
|
|
318
|
+
expect(calledUrl).toContain('categories=news');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('should use SearXNG as primary when it is the only configured provider', async () => {
|
|
322
|
+
const mockFetch = global.fetch as any;
|
|
323
|
+
mockFetch.mockResolvedValue({
|
|
324
|
+
ok: true,
|
|
325
|
+
json: async () => ({
|
|
326
|
+
results: [
|
|
327
|
+
{ title: 'SearXNG Primary', url: 'https://example.com/primary', content: 'Primary result' },
|
|
328
|
+
],
|
|
329
|
+
}),
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const searxngOnlyContext = {
|
|
333
|
+
options: { searxngUrl: 'http://searxng:8080' },
|
|
334
|
+
logger: context.logger,
|
|
335
|
+
} as any;
|
|
336
|
+
|
|
337
|
+
const pack = createExtensionPack(searxngOnlyContext);
|
|
338
|
+
const webSearch = pack.descriptors.find((d: any) => d.id === 'web_search')!.payload;
|
|
339
|
+
|
|
340
|
+
const result = await webSearch.execute(
|
|
341
|
+
{ query: 'test' },
|
|
342
|
+
{ gmiId: 'test-gmi', personaId: 'test-persona', userContext: { userId: 'test-user' } },
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
expect(result.success).toBe(true);
|
|
346
|
+
// SearXNG should be used as primary (not falling back to DDG)
|
|
347
|
+
expect(result.output.provider).toBe('searxng');
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
137
351
|
describe('Lifecycle Hooks', () => {
|
|
138
352
|
it('should call onActivate when extension is activated', async () => {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
onActivate
|
|
144
|
-
activated = true;
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
if (customPack.onActivate) {
|
|
149
|
-
await customPack.onActivate();
|
|
353
|
+
const logSpy = vi.fn();
|
|
354
|
+
const lc = { logger: { info: logSpy, warn: vi.fn(), error: vi.fn(), debug: vi.fn() } };
|
|
355
|
+
|
|
356
|
+
if (extensionPack.onActivate) {
|
|
357
|
+
await extensionPack.onActivate(lc);
|
|
150
358
|
}
|
|
151
|
-
|
|
152
|
-
expect(
|
|
359
|
+
|
|
360
|
+
expect(logSpy).toHaveBeenCalledWith('Web Search Extension activated');
|
|
153
361
|
});
|
|
154
|
-
|
|
362
|
+
|
|
155
363
|
it('should call onDeactivate when extension is deactivated', async () => {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
onDeactivate
|
|
161
|
-
deactivated = true;
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
if (customPack.onDeactivate) {
|
|
166
|
-
await customPack.onDeactivate();
|
|
364
|
+
const logSpy = vi.fn();
|
|
365
|
+
const lc = { logger: { info: logSpy, warn: vi.fn(), error: vi.fn(), debug: vi.fn() } };
|
|
366
|
+
|
|
367
|
+
if (extensionPack.onDeactivate) {
|
|
368
|
+
await extensionPack.onDeactivate(lc);
|
|
167
369
|
}
|
|
168
|
-
|
|
169
|
-
expect(
|
|
370
|
+
|
|
371
|
+
expect(logSpy).toHaveBeenCalledWith('Web Search Extension deactivated');
|
|
170
372
|
});
|
|
171
373
|
});
|
|
172
374
|
});
|
|
173
|
-
|