@framers/agentos-ext-web-search 1.1.1 → 1.2.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.
@@ -1,300 +0,0 @@
1
- /**
2
- * Fact Check Tool — verify statements against web sources.
3
- *
4
- * Updated to conform to AgentOS `ITool` (inputSchema + ToolExecutionContext).
5
- *
6
- * @module @framers/agentos-ext-web-search
7
- */
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 FactCheckInput {
14
- statement: string;
15
- checkSources?: boolean;
16
- confidence?: 'low' | 'medium' | 'high';
17
- multiSearch?: boolean;
18
- }
19
-
20
- export interface FactCheckOutput {
21
- statement: string;
22
- verdict: 'TRUE' | 'FALSE' | 'UNVERIFIED' | 'PARTIALLY TRUE';
23
- confidence: number;
24
- confidenceLevel: 'low' | 'medium' | 'high';
25
- explanation: string;
26
- sources: Array<{ title: string; url: string; snippet: string; provider: string; query: string }>;
27
- contradictingSources: string[];
28
- supportingSources: string[];
29
- meetsRequiredConfidence: boolean;
30
- timestamp: string;
31
- }
32
-
33
- export class FactCheckTool implements ITool<FactCheckInput, FactCheckOutput> {
34
- public readonly id = 'fact-check-v1';
35
- /** Tool call name used by the LLM / ToolExecutor. */
36
- public readonly name = 'fact_check';
37
- public readonly displayName = 'Fact Check';
38
- public readonly description = 'Verify a statement against web sources and return a verdict with confidence.';
39
- public readonly category = 'research';
40
- public readonly hasSideEffects = false;
41
-
42
- public readonly inputSchema: JSONSchemaObject = {
43
- type: 'object',
44
- required: ['statement'],
45
- properties: {
46
- statement: {
47
- type: 'string',
48
- description: 'The statement to fact-check',
49
- minLength: 5,
50
- },
51
- checkSources: {
52
- type: 'boolean',
53
- description: 'Whether to include source citations',
54
- default: true,
55
- },
56
- confidence: {
57
- type: 'string',
58
- description: 'Required confidence level for the verdict',
59
- enum: ['low', 'medium', 'high'],
60
- default: 'medium',
61
- },
62
- multiSearch: {
63
- type: 'boolean',
64
- description:
65
- 'When true, each verification query fans out to ALL available providers in parallel for higher-confidence cross-provider consensus.',
66
- default: false,
67
- },
68
- },
69
- additionalProperties: false,
70
- };
71
-
72
- public readonly requiredCapabilities = ['capability:web_search'];
73
-
74
- constructor(
75
- private readonly searchService: SearchProviderService,
76
- private readonly defaultMultiSearch: boolean = false,
77
- ) {}
78
-
79
- async execute(input: FactCheckInput, _context: ToolExecutionContext): Promise<ToolExecutionResult<FactCheckOutput>> {
80
- try {
81
- const checkSources = input.checkSources !== false;
82
- const requiredConfidence = input.confidence || 'medium';
83
- const useMultiSearch = input.multiSearch ?? this.defaultMultiSearch;
84
-
85
- const queries = this.generateFactCheckQueries(input.statement);
86
-
87
- let searchResults: ProviderResponse[];
88
-
89
- if (useMultiSearch) {
90
- const multiPromises = queries.map((query) =>
91
- this.searchService
92
- .multiSearch(query, { maxResults: 5 })
93
- .then((multi): ProviderResponse => ({
94
- provider: 'multi',
95
- results: multi.results,
96
- metadata: { query, timestamp: multi.metadata.timestamp },
97
- }))
98
- .catch((err) => ({
99
- provider: 'error',
100
- results: [],
101
- metadata: { query, error: err?.message || String(err), timestamp: new Date().toISOString() },
102
- }))
103
- );
104
- searchResults = (await Promise.all(multiPromises)) as ProviderResponse[];
105
- } else {
106
- const searchPromises = queries.map((query) =>
107
- this.searchService
108
- .search(query, { maxResults: 5 })
109
- .catch((err) => ({
110
- provider: 'error',
111
- results: [],
112
- metadata: { query, error: err?.message || String(err), timestamp: new Date().toISOString() },
113
- }))
114
- );
115
- searchResults = (await Promise.all(searchPromises)) as ProviderResponse[];
116
- }
117
-
118
- const analysis = this.analyzeFactCheckResults(input.statement, searchResults, requiredConfidence);
119
- const sources = checkSources ? this.compileSources(searchResults) : [];
120
-
121
- return {
122
- success: true,
123
- output: {
124
- statement: input.statement,
125
- verdict: analysis.verdict,
126
- confidence: analysis.confidence,
127
- confidenceLevel: analysis.confidenceLevel,
128
- explanation: analysis.explanation,
129
- sources,
130
- contradictingSources: analysis.contradictingSources,
131
- supportingSources: analysis.supportingSources,
132
- meetsRequiredConfidence: analysis.meetsRequiredConfidence,
133
- timestamp: new Date().toISOString(),
134
- },
135
- };
136
- } catch (error: any) {
137
- return { success: false, error: error?.message || String(error) };
138
- }
139
- }
140
-
141
- validateArgs(input: Record<string, any>): { isValid: boolean; errors?: any[] } {
142
- const errors: string[] = [];
143
-
144
- if (!input.statement) {
145
- errors.push('Statement is required');
146
- } else if (typeof input.statement !== 'string') {
147
- errors.push('Statement must be a string');
148
- } else if (input.statement.length < 5) {
149
- errors.push('Statement must be at least 5 characters long');
150
- }
151
-
152
- if (input.confidence !== undefined) {
153
- if (!['low', 'medium', 'high'].includes(input.confidence)) {
154
- errors.push('Confidence must be low, medium, or high');
155
- }
156
- }
157
-
158
- if (input.checkSources !== undefined && typeof input.checkSources !== 'boolean') {
159
- errors.push('checkSources must be a boolean');
160
- }
161
-
162
- if (input.multiSearch !== undefined && typeof input.multiSearch !== 'boolean') {
163
- errors.push('multiSearch must be a boolean');
164
- }
165
-
166
- return errors.length === 0 ? { isValid: true } : { isValid: false, errors };
167
- }
168
-
169
- // ============================================================================
170
- // Internals (ported from the previous implementation)
171
- // ============================================================================
172
-
173
- private generateFactCheckQueries(statement: string): string[] {
174
- return [
175
- statement,
176
- `"${statement}" fact check`,
177
- `${statement} true or false`,
178
- `${statement} debunked myths`,
179
- `is it true that ${statement}`,
180
- ];
181
- }
182
-
183
- private analyzeFactCheckResults(statement: string, searchResults: any[], requiredConfidence: string): any {
184
- let supportingCount = 0;
185
- let contradictingCount = 0;
186
- let neutralCount = 0;
187
- const supportingSources: string[] = [];
188
- const contradictingSources: string[] = [];
189
-
190
- const statementLower = statement.toLowerCase();
191
- void statementLower; // retained for future use
192
-
193
- const negativeIndicators = ['false', 'myth', 'debunked', 'incorrect', 'wrong', 'not true', 'fake'];
194
- const positiveIndicators = ['true', 'correct', 'accurate', 'confirmed', 'verified', 'fact'];
195
-
196
- for (const searchResult of searchResults) {
197
- if (!searchResult.results) continue;
198
-
199
- for (const result of searchResult.results) {
200
- const contentLower = (String(result.title || '') + ' ' + String(result.snippet || '')).toLowerCase();
201
-
202
- const isFactCheckSite =
203
- String(result.url || '').includes('snopes') ||
204
- String(result.url || '').includes('factcheck') ||
205
- String(result.url || '').includes('politifact');
206
-
207
- // Multi-search results with cross-provider agreement get extra weight
208
- const agreementBonus = typeof result.agreementCount === 'number' ? Math.max(0, result.agreementCount - 1) : 0;
209
- const weight = (isFactCheckSite ? 2 : 1) + agreementBonus;
210
-
211
- const hasNegative = negativeIndicators.some((ind) => contentLower.includes(ind));
212
- const hasPositive = positiveIndicators.some((ind) => contentLower.includes(ind));
213
-
214
- if (hasNegative && !hasPositive) {
215
- contradictingCount += weight;
216
- if (result.url) contradictingSources.push(String(result.url));
217
- } else if (hasPositive && !hasNegative) {
218
- supportingCount += weight;
219
- if (result.url) supportingSources.push(String(result.url));
220
- } else {
221
- neutralCount += 1;
222
- }
223
- }
224
- }
225
-
226
- const total = supportingCount + contradictingCount + neutralCount;
227
- let confidence: number;
228
- let verdict: FactCheckOutput['verdict'];
229
- let explanation: string;
230
-
231
- if (total === 0) {
232
- verdict = 'UNVERIFIED';
233
- confidence = 0;
234
- explanation = 'No relevant sources found to verify this statement.';
235
- } else {
236
- const supportRatio = supportingCount / total;
237
- const contradictRatio = contradictingCount / total;
238
-
239
- if (supportRatio > 0.7) {
240
- verdict = 'TRUE';
241
- confidence = supportRatio * 100;
242
- explanation = `Strong evidence supports this statement (${supportingCount} supporting sources).`;
243
- } else if (contradictRatio > 0.7) {
244
- verdict = 'FALSE';
245
- confidence = contradictRatio * 100;
246
- explanation = `Strong evidence contradicts this statement (${contradictingCount} contradicting sources).`;
247
- } else if (supportRatio > 0.4 && contradictRatio > 0.4) {
248
- verdict = 'PARTIALLY TRUE';
249
- confidence = 50;
250
- explanation = 'Mixed evidence with both supporting and contradicting sources.';
251
- } else {
252
- verdict = 'UNVERIFIED';
253
- confidence = Math.max(supportRatio, contradictRatio) * 100;
254
- explanation = 'Insufficient clear evidence to verify this statement.';
255
- }
256
- }
257
-
258
- const confidenceLevel: 'low' | 'medium' | 'high' = confidence > 80 ? 'high' : confidence > 50 ? 'medium' : 'low';
259
-
260
- return {
261
- verdict,
262
- confidence: Math.round(confidence),
263
- confidenceLevel,
264
- explanation,
265
- supportingSources: supportingSources.slice(0, 5),
266
- contradictingSources: contradictingSources.slice(0, 5),
267
- meetsRequiredConfidence: this.meetsConfidenceRequirement(confidenceLevel, requiredConfidence),
268
- };
269
- }
270
-
271
- private meetsConfidenceRequirement(actual: string, required: string): boolean {
272
- const levels = { low: 1, medium: 2, high: 3 };
273
- return levels[actual as keyof typeof levels] >= levels[required as keyof typeof levels];
274
- }
275
-
276
- private compileSources(searchResults: any[]): Array<{ title: string; url: string; snippet: string; provider: string; query: string }> {
277
- const sources: Array<{ title: string; url: string; snippet: string; provider: string; query: string }> = [];
278
- const seenUrls = new Set<string>();
279
-
280
- for (const searchResult of searchResults) {
281
- if (!searchResult.results) continue;
282
-
283
- for (const result of searchResult.results) {
284
- if (!result.url) continue;
285
- if (seenUrls.has(result.url)) continue;
286
- seenUrls.add(result.url);
287
-
288
- sources.push({
289
- title: String(result.title || ''),
290
- url: String(result.url),
291
- snippet: String(result.snippet || ''),
292
- provider: String(searchResult.provider || 'unknown'),
293
- query: String(searchResult.metadata?.query || ''),
294
- });
295
- }
296
- }
297
-
298
- return sources.slice(0, 10);
299
- }
300
- }
@@ -1,289 +0,0 @@
1
- /**
2
- * Research Aggregator Tool — runs multiple searches on a topic and aggregates results.
3
- *
4
- * Updated to conform to AgentOS `ITool` (inputSchema + ToolExecutionContext).
5
- *
6
- * @module @framers/agentos-ext-web-search
7
- */
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 ResearchAggregatorInput {
14
- topic: string;
15
- sources?: number;
16
- depth?: 'quick' | 'moderate' | 'comprehensive';
17
- multiSearch?: boolean;
18
- }
19
-
20
- export interface ResearchAggregatorOutput {
21
- topic: string;
22
- sources: number;
23
- depth: 'quick' | 'moderate' | 'comprehensive';
24
- queries: string[];
25
- aggregatedResults: any;
26
- timestamp: string;
27
- }
28
-
29
- export class ResearchAggregatorTool implements ITool<ResearchAggregatorInput, ResearchAggregatorOutput> {
30
- public readonly id = 'research-aggregator-v1';
31
- /** Tool call name used by the LLM / ToolExecutor. */
32
- public readonly name = 'research_aggregate';
33
- public readonly displayName = 'Research Aggregator';
34
- public readonly description = 'Aggregate research from multiple web searches on a topic (deduplicated + categorized).';
35
- public readonly category = 'research';
36
- public readonly hasSideEffects = false;
37
-
38
- public readonly inputSchema: JSONSchemaObject = {
39
- type: 'object',
40
- required: ['topic'],
41
- properties: {
42
- topic: {
43
- type: 'string',
44
- description: 'The research topic to aggregate information about',
45
- },
46
- sources: {
47
- type: 'integer',
48
- description: 'Number of different search angles to explore',
49
- default: 3,
50
- minimum: 1,
51
- maximum: 10,
52
- },
53
- depth: {
54
- type: 'string',
55
- description: 'Research depth level',
56
- enum: ['quick', 'moderate', 'comprehensive'],
57
- default: 'moderate',
58
- },
59
- multiSearch: {
60
- type: 'boolean',
61
- description:
62
- 'When true, each search query fans out to ALL available providers in parallel, returning merged and deduplicated results ranked by cross-provider agreement.',
63
- default: false,
64
- },
65
- },
66
- additionalProperties: false,
67
- };
68
-
69
- public readonly requiredCapabilities = ['capability:web_search'];
70
-
71
- constructor(
72
- private readonly searchService: SearchProviderService,
73
- private readonly defaultMultiSearch: boolean = false,
74
- ) {}
75
-
76
- async execute(
77
- input: ResearchAggregatorInput,
78
- _context: ToolExecutionContext,
79
- ): Promise<ToolExecutionResult<ResearchAggregatorOutput>> {
80
- try {
81
- const sources = input.sources || 3;
82
- const depth = input.depth || 'moderate';
83
- const useMultiSearch = input.multiSearch ?? this.defaultMultiSearch;
84
-
85
- const queries = this.generateSearchQueries(input.topic, sources, depth);
86
-
87
- const perQueryMaxResults = depth === 'quick' ? 5 : depth === 'moderate' ? 10 : 15;
88
-
89
- let searchResults: ProviderResponse[];
90
-
91
- if (useMultiSearch) {
92
- // Fan out each query to ALL providers in parallel, then normalize into ProviderResponse shape
93
- const multiPromises = queries.map((query) =>
94
- this.searchService
95
- .multiSearch(query, { maxResults: perQueryMaxResults })
96
- .then((multi): ProviderResponse => ({
97
- provider: 'multi',
98
- results: multi.results,
99
- metadata: { query, timestamp: multi.metadata.timestamp },
100
- }))
101
- .catch((err) => ({
102
- provider: 'error',
103
- results: [],
104
- metadata: { query, error: err?.message || String(err), timestamp: new Date().toISOString() },
105
- }))
106
- );
107
- searchResults = (await Promise.all(multiPromises)) as ProviderResponse[];
108
- } else {
109
- const searchPromises = queries.map((query) =>
110
- this.searchService
111
- .search(query, { maxResults: perQueryMaxResults })
112
- .catch((err) => ({
113
- provider: 'error',
114
- results: [],
115
- metadata: { query, error: err?.message || String(err), timestamp: new Date().toISOString() },
116
- }))
117
- );
118
- searchResults = (await Promise.all(searchPromises)) as ProviderResponse[];
119
- }
120
-
121
- const aggregatedResults = this.aggregateResults(searchResults, input.topic);
122
-
123
- return {
124
- success: true,
125
- output: {
126
- topic: input.topic,
127
- sources,
128
- depth,
129
- queries,
130
- aggregatedResults,
131
- timestamp: new Date().toISOString(),
132
- },
133
- };
134
- } catch (error: any) {
135
- return { success: false, error: error?.message || String(error) };
136
- }
137
- }
138
-
139
- validateArgs(input: Record<string, any>): { isValid: boolean; errors?: any[] } {
140
- const errors: string[] = [];
141
-
142
- if (!input.topic) {
143
- errors.push('Topic is required');
144
- } else if (typeof input.topic !== 'string') {
145
- errors.push('Topic must be a string');
146
- }
147
-
148
- if (input.sources !== undefined) {
149
- if (typeof input.sources !== 'number' || input.sources < 1 || input.sources > 10) {
150
- errors.push('Sources must be a number between 1 and 10');
151
- }
152
- }
153
-
154
- if (input.depth !== undefined) {
155
- if (!['quick', 'moderate', 'comprehensive'].includes(input.depth)) {
156
- errors.push('Depth must be quick, moderate, or comprehensive');
157
- }
158
- }
159
-
160
- if (input.multiSearch !== undefined && typeof input.multiSearch !== 'boolean') {
161
- errors.push('multiSearch must be a boolean');
162
- }
163
-
164
- return errors.length === 0 ? { isValid: true } : { isValid: false, errors };
165
- }
166
-
167
- // ============================================================================
168
- // Internals (ported from the previous implementation)
169
- // ============================================================================
170
-
171
- private generateSearchQueries(topic: string, count: number, depth: string): string[] {
172
- const queries: string[] = [topic];
173
-
174
- const modifiers = {
175
- quick: ['overview', 'summary'],
176
- moderate: ['overview', 'recent developments', 'key concepts', 'applications'],
177
- comprehensive: [
178
- 'overview',
179
- 'recent research',
180
- 'state of the art',
181
- 'challenges',
182
- 'future directions',
183
- 'industry applications',
184
- 'academic papers',
185
- 'expert opinions',
186
- ],
187
- };
188
-
189
- const selectedModifiers = modifiers[depth as keyof typeof modifiers];
190
-
191
- for (let i = 1; i < Math.min(count, selectedModifiers.length + 1); i++) {
192
- if (selectedModifiers[i - 1]) {
193
- queries.push(`${topic} ${selectedModifiers[i - 1]}`);
194
- }
195
- }
196
-
197
- return queries;
198
- }
199
-
200
- private aggregateResults(searchResults: any[], topic: string): any {
201
- const seenUrls = new Set<string>();
202
- const categories = {
203
- overview: [] as any[],
204
- research: [] as any[],
205
- applications: [] as any[],
206
- resources: [] as any[],
207
- };
208
-
209
- for (const searchResult of searchResults) {
210
- if (!searchResult.results) continue;
211
-
212
- for (const result of searchResult.results) {
213
- if (seenUrls.has(result.url)) continue;
214
- seenUrls.add(result.url);
215
-
216
- const lowerTitle = String(result.title || '').toLowerCase();
217
-
218
- if (lowerTitle.includes('overview') || lowerTitle.includes('introduction') || lowerTitle.includes('what is')) {
219
- categories.overview.push(result);
220
- } else if (
221
- lowerTitle.includes('research') ||
222
- lowerTitle.includes('study') ||
223
- lowerTitle.includes('paper') ||
224
- lowerTitle.includes('journal')
225
- ) {
226
- categories.research.push(result);
227
- } else if (
228
- lowerTitle.includes('application') ||
229
- lowerTitle.includes('use case') ||
230
- lowerTitle.includes('implementation')
231
- ) {
232
- categories.applications.push(result);
233
- } else {
234
- categories.resources.push(result);
235
- }
236
- }
237
- }
238
-
239
- const scoredCategories = Object.entries(categories).reduce((acc, [key, items]) => {
240
- acc[key] = items
241
- .map((item: any) => ({
242
- ...item,
243
- relevanceScore: this.calculateRelevance(item, topic),
244
- }))
245
- .sort((a: any, b: any) => b.relevanceScore - a.relevanceScore);
246
- return acc;
247
- }, {} as any);
248
-
249
- return {
250
- categories: scoredCategories,
251
- totalResults: seenUrls.size,
252
- topResults: this.getTopResults(scoredCategories, 10),
253
- };
254
- }
255
-
256
- private calculateRelevance(result: any, topic: string): number {
257
- let score = 50;
258
-
259
- const topicWords = topic.toLowerCase().split(/\s+/);
260
- const titleWords = String(result.title || '').toLowerCase();
261
- const snippetWords = String(result.snippet || '').toLowerCase();
262
-
263
- for (const word of topicWords) {
264
- if (titleWords.includes(word)) score += 15;
265
- if (snippetWords.includes(word)) score += 5;
266
- }
267
-
268
- if (titleWords.includes('official')) score += 10;
269
- if (titleWords.includes('guide')) score += 5;
270
- if (titleWords.includes('tutorial')) score += 5;
271
-
272
- // Bonus for multi-search results with cross-provider agreement
273
- if (typeof result.confidenceScore === 'number') {
274
- score += (result.confidenceScore / 100) * 20;
275
- }
276
-
277
- return Math.min(100, score);
278
- }
279
-
280
- private getTopResults(categories: any, count: number): any[] {
281
- const allResults: any[] = [];
282
-
283
- for (const items of Object.values(categories)) {
284
- allResults.push(...(items as any[]));
285
- }
286
-
287
- return allResults.sort((a, b) => b.relevanceScore - a.relevanceScore).slice(0, count);
288
- }
289
- }