@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.
@@ -1,173 +1,233 @@
1
- import { ITool } from '@framers/agentos';
2
- import { SearchProviderService } from '../services/searchProvider';
3
-
4
1
  /**
5
- * Fact checking tool that verifies statements against web sources
6
- *
7
- * @class FactCheckTool
8
- * @implements {ITool}
9
- *
10
- * @example
11
- * ```typescript
12
- * const tool = new FactCheckTool(searchService);
13
- * const result = await tool.execute({
14
- * statement: 'The Earth orbits the Sun',
15
- * checkSources: true,
16
- * confidence: 'high'
17
- * });
18
- * ```
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
19
7
  */
20
- export class FactCheckTool implements ITool {
21
- public readonly id = 'factCheck';
22
- public readonly name = 'Fact Check';
23
- public readonly description = 'Verify facts and statements against web sources';
24
-
25
- /**
26
- * Creates an instance of FactCheckTool
27
- * @param {SearchProviderService} searchService - The search provider service
28
- */
29
- constructor(private searchService: SearchProviderService) {}
30
-
31
- /**
32
- * Executes fact checking on a statement
33
- *
34
- * @param {Object} input - The fact check parameters
35
- * @param {string} input.statement - The statement to fact-check
36
- * @param {boolean} [input.checkSources=true] - Whether to include source citations
37
- * @param {string} [input.confidence='medium'] - Required confidence level
38
- * @returns {Promise<{success: boolean, output?: any, error?: string}>} Fact check results
39
- */
40
- async execute(input: {
41
- statement: string;
42
- checkSources?: boolean;
43
- confidence?: 'low' | 'medium' | 'high';
44
- }): Promise<{ success: boolean; output?: any; error?: string }> {
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>> {
45
80
  try {
46
81
  const checkSources = input.checkSources !== false;
47
82
  const requiredConfidence = input.confidence || 'medium';
48
-
49
- // Search for information about the statement
83
+ const useMultiSearch = input.multiSearch ?? this.defaultMultiSearch;
84
+
50
85
  const queries = this.generateFactCheckQueries(input.statement);
51
-
52
- // Perform searches in parallel
53
- const searchPromises = queries.map(query =>
54
- this.searchService.search(query, { maxResults: 5 })
55
- .catch(err => ({
56
- provider: 'error',
57
- results: [],
58
- metadata: { query, error: err.message, timestamp: new Date().toISOString() }
59
- }))
60
- );
61
-
62
- const searchResults = await Promise.all(searchPromises);
63
-
64
- // Analyze results for fact verification
65
- const analysis = this.analyzeFactCheckResults(
66
- input.statement,
67
- searchResults,
68
- requiredConfidence
69
- );
70
-
71
- // Compile sources if requested
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);
72
119
  const sources = checkSources ? this.compileSources(searchResults) : [];
73
-
120
+
74
121
  return {
75
122
  success: true,
76
123
  output: {
77
124
  statement: input.statement,
78
125
  verdict: analysis.verdict,
79
126
  confidence: analysis.confidence,
127
+ confidenceLevel: analysis.confidenceLevel,
80
128
  explanation: analysis.explanation,
81
- sources: sources,
129
+ sources,
82
130
  contradictingSources: analysis.contradictingSources,
83
131
  supportingSources: analysis.supportingSources,
84
- timestamp: new Date().toISOString()
85
- }
132
+ meetsRequiredConfidence: analysis.meetsRequiredConfidence,
133
+ timestamp: new Date().toISOString(),
134
+ },
86
135
  };
87
136
  } catch (error: any) {
88
- return {
89
- success: false,
90
- error: error.message
91
- };
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');
92
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 };
93
167
  }
94
-
95
- /**
96
- * Generates queries for fact-checking a statement
97
- *
98
- * @private
99
- * @param {string} statement - The statement to check
100
- * @returns {string[]} Array of search queries
101
- */
168
+
169
+ // ============================================================================
170
+ // Internals (ported from the previous implementation)
171
+ // ============================================================================
172
+
102
173
  private generateFactCheckQueries(statement: string): string[] {
103
174
  return [
104
- statement, // Original statement
105
- `"${statement}" fact check`, // Exact match with fact check
106
- `${statement} true or false`, // Truth verification
107
- `${statement} debunked myths`, // Check for debunking
108
- `is it true that ${statement}` // Question format
175
+ statement,
176
+ `"${statement}" fact check`,
177
+ `${statement} true or false`,
178
+ `${statement} debunked myths`,
179
+ `is it true that ${statement}`,
109
180
  ];
110
181
  }
111
-
112
- /**
113
- * Analyzes search results to determine fact validity
114
- *
115
- * @private
116
- * @param {string} statement - Original statement
117
- * @param {any[]} searchResults - Search results to analyze
118
- * @param {string} requiredConfidence - Required confidence level
119
- * @returns {Object} Analysis results with verdict and confidence
120
- */
121
- private analyzeFactCheckResults(
122
- statement: string,
123
- searchResults: any[],
124
- requiredConfidence: string
125
- ): any {
182
+
183
+ private analyzeFactCheckResults(statement: string, searchResults: any[], requiredConfidence: string): any {
126
184
  let supportingCount = 0;
127
185
  let contradictingCount = 0;
128
186
  let neutralCount = 0;
129
187
  const supportingSources: string[] = [];
130
188
  const contradictingSources: string[] = [];
131
-
189
+
132
190
  const statementLower = statement.toLowerCase();
191
+ void statementLower; // retained for future use
192
+
133
193
  const negativeIndicators = ['false', 'myth', 'debunked', 'incorrect', 'wrong', 'not true', 'fake'];
134
194
  const positiveIndicators = ['true', 'correct', 'accurate', 'confirmed', 'verified', 'fact'];
135
-
195
+
136
196
  for (const searchResult of searchResults) {
137
197
  if (!searchResult.results) continue;
138
-
198
+
139
199
  for (const result of searchResult.results) {
140
- const contentLower = (result.title + ' ' + result.snippet).toLowerCase();
141
-
142
- // Check for fact-checking sites
143
- const isFactCheckSite = result.url.includes('snopes') ||
144
- result.url.includes('factcheck') ||
145
- result.url.includes('politifact');
146
-
147
- const weight = isFactCheckSite ? 2 : 1; // Give more weight to fact-checking sites
148
-
149
- // Analyze sentiment toward the statement
150
- let hasNegative = negativeIndicators.some(ind => contentLower.includes(ind));
151
- let hasPositive = positiveIndicators.some(ind => contentLower.includes(ind));
152
-
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
+
153
214
  if (hasNegative && !hasPositive) {
154
215
  contradictingCount += weight;
155
- contradictingSources.push(result.url);
216
+ if (result.url) contradictingSources.push(String(result.url));
156
217
  } else if (hasPositive && !hasNegative) {
157
218
  supportingCount += weight;
158
- supportingSources.push(result.url);
219
+ if (result.url) supportingSources.push(String(result.url));
159
220
  } else {
160
221
  neutralCount += 1;
161
222
  }
162
223
  }
163
224
  }
164
-
165
- // Calculate confidence and verdict
225
+
166
226
  const total = supportingCount + contradictingCount + neutralCount;
167
227
  let confidence: number;
168
- let verdict: 'TRUE' | 'FALSE' | 'UNVERIFIED' | 'PARTIALLY TRUE';
228
+ let verdict: FactCheckOutput['verdict'];
169
229
  let explanation: string;
170
-
230
+
171
231
  if (total === 0) {
172
232
  verdict = 'UNVERIFIED';
173
233
  confidence = 0;
@@ -175,7 +235,7 @@ export class FactCheckTool implements ITool {
175
235
  } else {
176
236
  const supportRatio = supportingCount / total;
177
237
  const contradictRatio = contradictingCount / total;
178
-
238
+
179
239
  if (supportRatio > 0.7) {
180
240
  verdict = 'TRUE';
181
241
  confidence = supportRatio * 100;
@@ -187,17 +247,16 @@ export class FactCheckTool implements ITool {
187
247
  } else if (supportRatio > 0.4 && contradictRatio > 0.4) {
188
248
  verdict = 'PARTIALLY TRUE';
189
249
  confidence = 50;
190
- explanation = `Mixed evidence with both supporting and contradicting sources.`;
250
+ explanation = 'Mixed evidence with both supporting and contradicting sources.';
191
251
  } else {
192
252
  verdict = 'UNVERIFIED';
193
253
  confidence = Math.max(supportRatio, contradictRatio) * 100;
194
- explanation = `Insufficient clear evidence to verify this statement.`;
254
+ explanation = 'Insufficient clear evidence to verify this statement.';
195
255
  }
196
256
  }
197
-
198
- // Adjust confidence based on required level
199
- const confidenceLevel = confidence > 80 ? 'high' : confidence > 50 ? 'medium' : 'low';
200
-
257
+
258
+ const confidenceLevel: 'low' | 'medium' | 'high' = confidence > 80 ? 'high' : confidence > 50 ? 'medium' : 'low';
259
+
201
260
  return {
202
261
  verdict,
203
262
  confidence: Math.round(confidence),
@@ -205,114 +264,37 @@ export class FactCheckTool implements ITool {
205
264
  explanation,
206
265
  supportingSources: supportingSources.slice(0, 5),
207
266
  contradictingSources: contradictingSources.slice(0, 5),
208
- meetsRequiredConfidence: this.meetsConfidenceRequirement(confidenceLevel, requiredConfidence)
267
+ meetsRequiredConfidence: this.meetsConfidenceRequirement(confidenceLevel, requiredConfidence),
209
268
  };
210
269
  }
211
-
212
- /**
213
- * Checks if confidence level meets requirement
214
- *
215
- * @private
216
- * @param {string} actual - Actual confidence level
217
- * @param {string} required - Required confidence level
218
- * @returns {boolean} True if requirement is met
219
- */
270
+
220
271
  private meetsConfidenceRequirement(actual: string, required: string): boolean {
221
272
  const levels = { low: 1, medium: 2, high: 3 };
222
273
  return levels[actual as keyof typeof levels] >= levels[required as keyof typeof levels];
223
274
  }
224
-
225
- /**
226
- * Compiles relevant sources from search results
227
- *
228
- * @private
229
- * @param {any[]} searchResults - Search results to compile from
230
- * @returns {any[]} Compiled sources with metadata
231
- */
232
- private compileSources(searchResults: any[]): any[] {
233
- const sources: any[] = [];
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 }> = [];
234
278
  const seenUrls = new Set<string>();
235
-
279
+
236
280
  for (const searchResult of searchResults) {
237
281
  if (!searchResult.results) continue;
238
-
282
+
239
283
  for (const result of searchResult.results) {
284
+ if (!result.url) continue;
240
285
  if (seenUrls.has(result.url)) continue;
241
286
  seenUrls.add(result.url);
242
-
287
+
243
288
  sources.push({
244
- title: result.title,
245
- url: result.url,
246
- snippet: result.snippet,
247
- provider: searchResult.provider,
248
- query: searchResult.metadata.query
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 || ''),
249
294
  });
250
295
  }
251
296
  }
252
-
253
- return sources.slice(0, 10); // Limit to top 10 sources
254
- }
255
-
256
- /**
257
- * Validates input parameters
258
- *
259
- * @param {any} input - Input to validate
260
- * @returns {{valid: boolean, errors: string[]}} Validation result
261
- */
262
- validate(input: any): { valid: boolean; errors: string[] } {
263
- const errors: string[] = [];
264
-
265
- if (!input.statement) {
266
- errors.push('Statement is required');
267
- } else if (typeof input.statement !== 'string') {
268
- errors.push('Statement must be a string');
269
- } else if (input.statement.length < 5) {
270
- errors.push('Statement must be at least 5 characters long');
271
- }
272
-
273
- if (input.confidence !== undefined) {
274
- if (!['low', 'medium', 'high'].includes(input.confidence)) {
275
- errors.push('Confidence must be low, medium, or high');
276
- }
277
- }
278
-
279
- if (input.checkSources !== undefined && typeof input.checkSources !== 'boolean') {
280
- errors.push('checkSources must be a boolean');
281
- }
282
-
283
- return {
284
- valid: errors.length === 0,
285
- errors
286
- };
287
- }
288
-
289
- /**
290
- * Returns the JSON schema for the tool's input
291
- *
292
- * @returns {Object} JSON Schema object
293
- */
294
- getSchema(): any {
295
- return {
296
- type: 'object',
297
- required: ['statement'],
298
- properties: {
299
- statement: {
300
- type: 'string',
301
- description: 'The statement to fact-check',
302
- minLength: 5
303
- },
304
- checkSources: {
305
- type: 'boolean',
306
- description: 'Whether to include source citations',
307
- default: true
308
- },
309
- confidence: {
310
- type: 'string',
311
- description: 'Required confidence level for the verdict',
312
- enum: ['low', 'medium', 'high'],
313
- default: 'medium'
314
- }
315
- }
316
- };
297
+
298
+ return sources.slice(0, 10);
317
299
  }
318
- }
300
+ }