web-agent-bridge 2.0.0 → 2.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.
- package/README.ar.md +127 -0
- package/README.md +190 -1
- package/bin/agent-runner.js +465 -0
- package/bin/cli.js +58 -0
- package/examples/cross-site-agent.js +91 -0
- package/package.json +4 -2
- package/public/index.html +97 -0
- package/public/script/wab.min.js +173 -2
- package/public/sovereign.html +660 -0
- package/script/ai-agent-bridge.js +1 -1
- package/sdk/README.md +44 -0
- package/sdk/index.d.ts +93 -0
- package/sdk/index.js +3 -1
- package/sdk/multi-agent.js +318 -0
- package/sdk/package.json +2 -2
- package/server/index.js +2 -0
- package/server/routes/sovereign.js +307 -0
- package/server/services/negotiation.js +439 -0
- package/server/services/reputation.js +465 -0
- package/server/services/verification.js +481 -0
- package/templates/artisan-marketplace.yaml +104 -0
- package/templates/book-price-scout.yaml +98 -0
- package/templates/electronics-price-tracker.yaml +108 -0
- package/templates/flight-deal-hunter.yaml +113 -0
- package/templates/freelancer-direct.yaml +116 -0
- package/templates/grocery-price-compare.yaml +93 -0
- package/templates/hotel-direct-booking.yaml +113 -0
- package/templates/local-services.yaml +98 -0
- package/templates/olive-oil-tunisia.yaml +88 -0
- package/templates/organic-farm-fresh.yaml +101 -0
- package/templates/restaurant-direct.yaml +97 -0
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anti-Hallucination Shield (Cross-Verification Engine)
|
|
3
|
+
* ════════════════════════════════════════════════════════════════════════
|
|
4
|
+
* Prevents AI agent "hallucinations" by cross-verifying data read from
|
|
5
|
+
* the DOM against visual analysis (screenshots). If the agent reads a
|
|
6
|
+
* price as "$10" but the screenshot shows "$100", the shield catches the
|
|
7
|
+
* discrepancy and halts execution, requesting human confirmation.
|
|
8
|
+
*
|
|
9
|
+
* Verification layers:
|
|
10
|
+
* 1. DOM vs Vision: Compare text extracted from DOM with OCR/vision output
|
|
11
|
+
* 2. Price Sanity: Flag unrealistically low prices vs market averages
|
|
12
|
+
* 3. Temporal Consistency: Compare with previously cached values
|
|
13
|
+
* 4. Multi-Source: Cross-check across multiple page elements
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { db } = require('../models/db');
|
|
17
|
+
const crypto = require('crypto');
|
|
18
|
+
|
|
19
|
+
// ─── Schema ──────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
db.exec(`
|
|
22
|
+
CREATE TABLE IF NOT EXISTS verification_results (
|
|
23
|
+
id TEXT PRIMARY KEY,
|
|
24
|
+
site_id TEXT NOT NULL,
|
|
25
|
+
agent_id TEXT,
|
|
26
|
+
url TEXT,
|
|
27
|
+
verification_type TEXT NOT NULL CHECK(verification_type IN (
|
|
28
|
+
'price','text','element_presence','form_data','navigation','action_result'
|
|
29
|
+
)),
|
|
30
|
+
dom_value TEXT,
|
|
31
|
+
vision_value TEXT,
|
|
32
|
+
cached_value TEXT,
|
|
33
|
+
match_score REAL DEFAULT 0,
|
|
34
|
+
discrepancy_type TEXT CHECK(discrepancy_type IN (
|
|
35
|
+
'none','minor','major','critical','fraud_suspected'
|
|
36
|
+
)),
|
|
37
|
+
discrepancy_details TEXT DEFAULT '{}',
|
|
38
|
+
action_taken TEXT DEFAULT 'none' CHECK(action_taken IN (
|
|
39
|
+
'none','warn','halt','confirm_human','auto_correct','block'
|
|
40
|
+
)),
|
|
41
|
+
human_confirmed INTEGER DEFAULT 0,
|
|
42
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
CREATE TABLE IF NOT EXISTS price_benchmarks (
|
|
46
|
+
id TEXT PRIMARY KEY,
|
|
47
|
+
category TEXT NOT NULL,
|
|
48
|
+
item_pattern TEXT NOT NULL,
|
|
49
|
+
avg_price REAL NOT NULL,
|
|
50
|
+
min_price REAL NOT NULL,
|
|
51
|
+
max_price REAL NOT NULL,
|
|
52
|
+
currency TEXT DEFAULT 'USD',
|
|
53
|
+
sample_count INTEGER DEFAULT 1,
|
|
54
|
+
last_updated TEXT DEFAULT (datetime('now'))
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
CREATE TABLE IF NOT EXISTS verification_cache (
|
|
58
|
+
id TEXT PRIMARY KEY,
|
|
59
|
+
site_id TEXT NOT NULL,
|
|
60
|
+
url TEXT NOT NULL,
|
|
61
|
+
field_name TEXT NOT NULL,
|
|
62
|
+
field_value TEXT NOT NULL,
|
|
63
|
+
value_hash TEXT NOT NULL,
|
|
64
|
+
source TEXT DEFAULT 'dom' CHECK(source IN ('dom','vision','agent')),
|
|
65
|
+
captured_at TEXT DEFAULT (datetime('now')),
|
|
66
|
+
expires_at TEXT
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_verif_results_site ON verification_results(site_id);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_verif_results_type ON verification_results(discrepancy_type);
|
|
71
|
+
CREATE INDEX IF NOT EXISTS idx_verif_cache_site ON verification_cache(site_id);
|
|
72
|
+
CREATE INDEX IF NOT EXISTS idx_verif_cache_url ON verification_cache(url);
|
|
73
|
+
CREATE INDEX IF NOT EXISTS idx_price_bench_cat ON price_benchmarks(category);
|
|
74
|
+
`);
|
|
75
|
+
|
|
76
|
+
// ─── Constants ───────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
const THRESHOLDS = {
|
|
79
|
+
priceMismatchMinor: 0.05, // 5% difference
|
|
80
|
+
priceMismatchMajor: 0.15, // 15%
|
|
81
|
+
priceMismatchCritical: 0.50, // 50%
|
|
82
|
+
priceAnomalyLow: 0.3, // 70% below market average
|
|
83
|
+
priceAnomalyHigh: 3.0, // 300% above market average
|
|
84
|
+
textSimilarityOk: 0.85, // 85% text match is acceptable
|
|
85
|
+
textSimilarityWarn: 0.60, // Below 60% is a warning
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// ─── Text Similarity (Levenshtein-based) ─────────────────────────────
|
|
89
|
+
|
|
90
|
+
function levenshteinDistance(a, b) {
|
|
91
|
+
const m = a.length, n = b.length;
|
|
92
|
+
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
93
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
94
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
95
|
+
for (let i = 1; i <= m; i++) {
|
|
96
|
+
for (let j = 1; j <= n; j++) {
|
|
97
|
+
dp[i][j] = a[i - 1] === b[j - 1]
|
|
98
|
+
? dp[i - 1][j - 1]
|
|
99
|
+
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return dp[m][n];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function textSimilarity(a, b) {
|
|
106
|
+
if (!a || !b) return 0;
|
|
107
|
+
const cleanA = a.toString().trim().toLowerCase();
|
|
108
|
+
const cleanB = b.toString().trim().toLowerCase();
|
|
109
|
+
if (cleanA === cleanB) return 1.0;
|
|
110
|
+
const maxLen = Math.max(cleanA.length, cleanB.length);
|
|
111
|
+
if (maxLen === 0) return 1.0;
|
|
112
|
+
return 1 - levenshteinDistance(cleanA, cleanB) / maxLen;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── Price Extraction ────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
function extractPrice(text) {
|
|
118
|
+
if (typeof text !== 'string') return null;
|
|
119
|
+
// Match common price formats: $100, $1,000.00, 100.00$, EUR 50, etc.
|
|
120
|
+
const patterns = [
|
|
121
|
+
/[\$€£¥]\s*([\d,]+\.?\d*)/,
|
|
122
|
+
/([\d,]+\.?\d*)\s*[\$€£¥]/,
|
|
123
|
+
/(?:USD|EUR|GBP|SAR|AED)\s*([\d,]+\.?\d*)/i,
|
|
124
|
+
/([\d,]+\.?\d*)\s*(?:USD|EUR|GBP|SAR|AED)/i,
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
for (const pattern of patterns) {
|
|
128
|
+
const match = text.match(pattern);
|
|
129
|
+
if (match) {
|
|
130
|
+
return parseFloat(match[1].replace(/,/g, ''));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ─── Core Verification ───────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
function verifyPrice({ siteId, agentId, url, domValue, visionValue, category, itemName }) {
|
|
139
|
+
const id = crypto.randomBytes(12).toString('hex');
|
|
140
|
+
|
|
141
|
+
const domPrice = extractPrice(domValue);
|
|
142
|
+
const visionPrice = extractPrice(visionValue);
|
|
143
|
+
|
|
144
|
+
const result = {
|
|
145
|
+
id,
|
|
146
|
+
siteId,
|
|
147
|
+
verificationType: 'price',
|
|
148
|
+
domValue,
|
|
149
|
+
visionValue,
|
|
150
|
+
domPrice,
|
|
151
|
+
visionPrice,
|
|
152
|
+
matchScore: 1.0,
|
|
153
|
+
discrepancyType: 'none',
|
|
154
|
+
discrepancyDetails: {},
|
|
155
|
+
actionTaken: 'none',
|
|
156
|
+
checks: []
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Check 1: DOM vs Vision price match
|
|
160
|
+
if (domPrice !== null && visionPrice !== null) {
|
|
161
|
+
const priceDiff = Math.abs(domPrice - visionPrice);
|
|
162
|
+
const relativeDiff = domPrice > 0 ? priceDiff / domPrice : 0;
|
|
163
|
+
|
|
164
|
+
result.matchScore = 1 - relativeDiff;
|
|
165
|
+
|
|
166
|
+
if (relativeDiff > THRESHOLDS.priceMismatchCritical) {
|
|
167
|
+
result.discrepancyType = 'critical';
|
|
168
|
+
result.actionTaken = 'halt';
|
|
169
|
+
result.discrepancyDetails.domVsVision = {
|
|
170
|
+
difference: priceDiff,
|
|
171
|
+
relativeDifference: Math.round(relativeDiff * 100) + '%',
|
|
172
|
+
message: `CRITICAL: DOM shows $${domPrice} but visual shows $${visionPrice}. ` +
|
|
173
|
+
`Possible deceptive pricing or site error. Operation halted.`
|
|
174
|
+
};
|
|
175
|
+
result.checks.push({ check: 'dom_vs_vision', status: 'FAIL', severity: 'critical' });
|
|
176
|
+
} else if (relativeDiff > THRESHOLDS.priceMismatchMajor) {
|
|
177
|
+
result.discrepancyType = 'major';
|
|
178
|
+
result.actionTaken = 'confirm_human';
|
|
179
|
+
result.discrepancyDetails.domVsVision = {
|
|
180
|
+
difference: priceDiff,
|
|
181
|
+
relativeDifference: Math.round(relativeDiff * 100) + '%',
|
|
182
|
+
message: `WARNING: Price mismatch detected. DOM: $${domPrice}, Visual: $${visionPrice}. ` +
|
|
183
|
+
`Requesting human confirmation before proceeding.`
|
|
184
|
+
};
|
|
185
|
+
result.checks.push({ check: 'dom_vs_vision', status: 'WARN', severity: 'major' });
|
|
186
|
+
} else if (relativeDiff > THRESHOLDS.priceMismatchMinor) {
|
|
187
|
+
result.discrepancyType = 'minor';
|
|
188
|
+
result.actionTaken = 'warn';
|
|
189
|
+
result.discrepancyDetails.domVsVision = {
|
|
190
|
+
difference: priceDiff,
|
|
191
|
+
message: `Minor price variance: DOM $${domPrice} vs Visual $${visionPrice}. May be rounding.`
|
|
192
|
+
};
|
|
193
|
+
result.checks.push({ check: 'dom_vs_vision', status: 'OK', severity: 'minor' });
|
|
194
|
+
} else {
|
|
195
|
+
result.checks.push({ check: 'dom_vs_vision', status: 'PASS', severity: 'none' });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Check 2: Price sanity vs market benchmarks
|
|
200
|
+
const effectivePrice = domPrice || visionPrice;
|
|
201
|
+
if (effectivePrice !== null && category) {
|
|
202
|
+
const benchmark = db.prepare(`
|
|
203
|
+
SELECT * FROM price_benchmarks
|
|
204
|
+
WHERE category = ? AND item_pattern LIKE ?
|
|
205
|
+
ORDER BY sample_count DESC LIMIT 1
|
|
206
|
+
`).get(category, `%${(itemName || '').slice(0, 20)}%`);
|
|
207
|
+
|
|
208
|
+
if (benchmark) {
|
|
209
|
+
const ratio = effectivePrice / benchmark.avg_price;
|
|
210
|
+
|
|
211
|
+
if (ratio < THRESHOLDS.priceAnomalyLow) {
|
|
212
|
+
result.discrepancyType = result.discrepancyType === 'none' ? 'major' : result.discrepancyType;
|
|
213
|
+
result.actionTaken = result.actionTaken === 'none' ? 'confirm_human' : result.actionTaken;
|
|
214
|
+
result.discrepancyDetails.marketAnomaly = {
|
|
215
|
+
price: effectivePrice,
|
|
216
|
+
marketAvg: benchmark.avg_price,
|
|
217
|
+
ratio: Math.round(ratio * 100) / 100,
|
|
218
|
+
message: `Suspiciously low price ($${effectivePrice}) vs market average ($${benchmark.avg_price}). ` +
|
|
219
|
+
`Could be a scam, error, or genuine deal. Verify manually.`
|
|
220
|
+
};
|
|
221
|
+
result.checks.push({ check: 'market_benchmark', status: 'WARN', severity: 'major' });
|
|
222
|
+
} else if (ratio > THRESHOLDS.priceAnomalyHigh) {
|
|
223
|
+
result.discrepancyDetails.marketAnomaly = {
|
|
224
|
+
price: effectivePrice,
|
|
225
|
+
marketAvg: benchmark.avg_price,
|
|
226
|
+
ratio: Math.round(ratio * 100) / 100,
|
|
227
|
+
message: `Price ($${effectivePrice}) is ${Math.round(ratio)}x market average ($${benchmark.avg_price}).`
|
|
228
|
+
};
|
|
229
|
+
result.checks.push({ check: 'market_benchmark', status: 'WARN', severity: 'minor' });
|
|
230
|
+
} else {
|
|
231
|
+
result.checks.push({ check: 'market_benchmark', status: 'PASS', severity: 'none' });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Check 3: Temporal consistency (compare with cached values)
|
|
237
|
+
if (effectivePrice !== null && url) {
|
|
238
|
+
const cached = db.prepare(`
|
|
239
|
+
SELECT * FROM verification_cache
|
|
240
|
+
WHERE site_id = ? AND url = ? AND field_name = 'price'
|
|
241
|
+
ORDER BY captured_at DESC LIMIT 1
|
|
242
|
+
`).get(siteId, url);
|
|
243
|
+
|
|
244
|
+
if (cached) {
|
|
245
|
+
const cachedPrice = extractPrice(cached.field_value);
|
|
246
|
+
if (cachedPrice !== null) {
|
|
247
|
+
const temporalDiff = Math.abs(effectivePrice - cachedPrice) / cachedPrice;
|
|
248
|
+
if (temporalDiff > 0.5) {
|
|
249
|
+
result.discrepancyDetails.temporalChange = {
|
|
250
|
+
currentPrice: effectivePrice,
|
|
251
|
+
previousPrice: cachedPrice,
|
|
252
|
+
previousDate: cached.captured_at,
|
|
253
|
+
changePct: Math.round(temporalDiff * 100) + '%',
|
|
254
|
+
message: `Price changed ${Math.round(temporalDiff * 100)}% since last check. ` +
|
|
255
|
+
`Was $${cachedPrice}, now $${effectivePrice}.`
|
|
256
|
+
};
|
|
257
|
+
result.checks.push({ check: 'temporal_consistency', status: 'WARN', severity: 'minor' });
|
|
258
|
+
} else {
|
|
259
|
+
result.checks.push({ check: 'temporal_consistency', status: 'PASS', severity: 'none' });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Cache current value
|
|
265
|
+
const cacheId = crypto.randomBytes(12).toString('hex');
|
|
266
|
+
const cacheExpiry = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
|
|
267
|
+
const valueHash = crypto.createHash('sha256').update(String(effectivePrice)).digest('hex');
|
|
268
|
+
db.prepare(`
|
|
269
|
+
INSERT OR REPLACE INTO verification_cache
|
|
270
|
+
(id, site_id, url, field_name, field_value, value_hash, source, expires_at)
|
|
271
|
+
VALUES (?, ?, ?, 'price', ?, ?, 'dom', ?)
|
|
272
|
+
`).run(cacheId, siteId, url, String(effectivePrice), valueHash, cacheExpiry);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Persist result
|
|
276
|
+
db.prepare(`
|
|
277
|
+
INSERT INTO verification_results
|
|
278
|
+
(id, site_id, agent_id, url, verification_type, dom_value, vision_value,
|
|
279
|
+
match_score, discrepancy_type, discrepancy_details, action_taken)
|
|
280
|
+
VALUES (?, ?, ?, ?, 'price', ?, ?, ?, ?, ?, ?)
|
|
281
|
+
`).run(
|
|
282
|
+
id, siteId, agentId || null, url || null,
|
|
283
|
+
domValue, visionValue,
|
|
284
|
+
Math.round(result.matchScore * 1000) / 1000,
|
|
285
|
+
result.discrepancyType,
|
|
286
|
+
JSON.stringify(result.discrepancyDetails),
|
|
287
|
+
result.actionTaken
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ─── Text Verification ───────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
function verifyText({ siteId, agentId, url, domValue, visionValue, fieldName }) {
|
|
296
|
+
const id = crypto.randomBytes(12).toString('hex');
|
|
297
|
+
|
|
298
|
+
const similarity = textSimilarity(domValue, visionValue);
|
|
299
|
+
|
|
300
|
+
let discrepancyType = 'none';
|
|
301
|
+
let actionTaken = 'none';
|
|
302
|
+
let message = '';
|
|
303
|
+
|
|
304
|
+
if (similarity < THRESHOLDS.textSimilarityWarn) {
|
|
305
|
+
discrepancyType = 'major';
|
|
306
|
+
actionTaken = 'confirm_human';
|
|
307
|
+
message = `Text mismatch: DOM reads "${domValue}" but visual shows "${visionValue}" ` +
|
|
308
|
+
`(${Math.round(similarity * 100)}% match). Human confirmation required.`;
|
|
309
|
+
} else if (similarity < THRESHOLDS.textSimilarityOk) {
|
|
310
|
+
discrepancyType = 'minor';
|
|
311
|
+
actionTaken = 'warn';
|
|
312
|
+
message = `Slight text variance: "${domValue}" vs "${visionValue}" ` +
|
|
313
|
+
`(${Math.round(similarity * 100)}% match).`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
db.prepare(`
|
|
317
|
+
INSERT INTO verification_results
|
|
318
|
+
(id, site_id, agent_id, url, verification_type, dom_value, vision_value,
|
|
319
|
+
match_score, discrepancy_type, discrepancy_details, action_taken)
|
|
320
|
+
VALUES (?, ?, ?, ?, 'text', ?, ?, ?, ?, ?, ?)
|
|
321
|
+
`).run(
|
|
322
|
+
id, siteId, agentId || null, url || null,
|
|
323
|
+
domValue, visionValue,
|
|
324
|
+
Math.round(similarity * 1000) / 1000,
|
|
325
|
+
discrepancyType,
|
|
326
|
+
JSON.stringify({ message, similarity }),
|
|
327
|
+
actionTaken
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
id,
|
|
332
|
+
matchScore: Math.round(similarity * 100),
|
|
333
|
+
discrepancyType,
|
|
334
|
+
actionTaken,
|
|
335
|
+
message: message || 'Text verified successfully.',
|
|
336
|
+
verified: discrepancyType === 'none'
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ─── Full Page Verification ──────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
function verifyPage({ siteId, agentId, url, domData, visionData }) {
|
|
343
|
+
const results = { checks: [], overallScore: 100, actionRequired: 'none', details: [] };
|
|
344
|
+
|
|
345
|
+
// Verify prices
|
|
346
|
+
if (domData.prices && visionData.prices) {
|
|
347
|
+
for (let i = 0; i < Math.min(domData.prices.length, visionData.prices.length); i++) {
|
|
348
|
+
const priceCheck = verifyPrice({
|
|
349
|
+
siteId, agentId, url,
|
|
350
|
+
domValue: domData.prices[i],
|
|
351
|
+
visionValue: visionData.prices[i],
|
|
352
|
+
category: domData.category,
|
|
353
|
+
itemName: domData.itemNames?.[i]
|
|
354
|
+
});
|
|
355
|
+
results.checks.push(priceCheck);
|
|
356
|
+
if (priceCheck.discrepancyType !== 'none') {
|
|
357
|
+
results.overallScore -= priceCheck.discrepancyType === 'critical' ? 40 : priceCheck.discrepancyType === 'major' ? 20 : 5;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Verify key text elements
|
|
363
|
+
if (domData.texts && visionData.texts) {
|
|
364
|
+
for (let i = 0; i < Math.min(domData.texts.length, visionData.texts.length); i++) {
|
|
365
|
+
const textCheck = verifyText({
|
|
366
|
+
siteId, agentId, url,
|
|
367
|
+
domValue: domData.texts[i],
|
|
368
|
+
visionValue: visionData.texts[i],
|
|
369
|
+
fieldName: `text_${i}`
|
|
370
|
+
});
|
|
371
|
+
results.checks.push(textCheck);
|
|
372
|
+
if (!textCheck.verified) results.overallScore -= 10;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
results.overallScore = Math.max(0, results.overallScore);
|
|
377
|
+
|
|
378
|
+
if (results.overallScore < 30) results.actionRequired = 'block';
|
|
379
|
+
else if (results.overallScore < 60) results.actionRequired = 'confirm_human';
|
|
380
|
+
else if (results.overallScore < 80) results.actionRequired = 'warn';
|
|
381
|
+
|
|
382
|
+
return results;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ─── Market Benchmark Management ─────────────────────────────────────
|
|
386
|
+
|
|
387
|
+
function updateBenchmark(category, itemPattern, price) {
|
|
388
|
+
const existing = db.prepare(`
|
|
389
|
+
SELECT * FROM price_benchmarks WHERE category = ? AND item_pattern = ?
|
|
390
|
+
`).get(category, itemPattern);
|
|
391
|
+
|
|
392
|
+
if (existing) {
|
|
393
|
+
const newCount = existing.sample_count + 1;
|
|
394
|
+
const newAvg = (existing.avg_price * existing.sample_count + price) / newCount;
|
|
395
|
+
const newMin = Math.min(existing.min_price, price);
|
|
396
|
+
const newMax = Math.max(existing.max_price, price);
|
|
397
|
+
|
|
398
|
+
db.prepare(`
|
|
399
|
+
UPDATE price_benchmarks
|
|
400
|
+
SET avg_price = ?, min_price = ?, max_price = ?,
|
|
401
|
+
sample_count = ?, last_updated = datetime('now')
|
|
402
|
+
WHERE id = ?
|
|
403
|
+
`).run(newAvg, newMin, newMax, newCount, existing.id);
|
|
404
|
+
} else {
|
|
405
|
+
const id = crypto.randomBytes(12).toString('hex');
|
|
406
|
+
db.prepare(`
|
|
407
|
+
INSERT INTO price_benchmarks (id, category, item_pattern, avg_price, min_price, max_price, sample_count)
|
|
408
|
+
VALUES (?, ?, ?, ?, ?, ?, 1)
|
|
409
|
+
`).run(id, category, itemPattern, price, price, price);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ─── Human Confirmation ──────────────────────────────────────────────
|
|
414
|
+
|
|
415
|
+
function confirmVerification(verificationId, humanApproved) {
|
|
416
|
+
db.prepare(`
|
|
417
|
+
UPDATE verification_results
|
|
418
|
+
SET human_confirmed = ?, action_taken = CASE WHEN ? = 1 THEN 'none' ELSE 'block' END
|
|
419
|
+
WHERE id = ?
|
|
420
|
+
`).run(humanApproved ? 1 : 0, humanApproved ? 1 : 0, verificationId);
|
|
421
|
+
|
|
422
|
+
return { confirmed: true, approved: humanApproved };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ─── Shield Stats ────────────────────────────────────────────────────
|
|
426
|
+
|
|
427
|
+
function getShieldStats(siteId) {
|
|
428
|
+
const stats = db.prepare(`
|
|
429
|
+
SELECT
|
|
430
|
+
COUNT(*) as total_checks,
|
|
431
|
+
SUM(CASE WHEN discrepancy_type = 'none' THEN 1 ELSE 0 END) as passed,
|
|
432
|
+
SUM(CASE WHEN discrepancy_type = 'minor' THEN 1 ELSE 0 END) as minor_issues,
|
|
433
|
+
SUM(CASE WHEN discrepancy_type = 'major' THEN 1 ELSE 0 END) as major_issues,
|
|
434
|
+
SUM(CASE WHEN discrepancy_type = 'critical' THEN 1 ELSE 0 END) as critical_issues,
|
|
435
|
+
SUM(CASE WHEN discrepancy_type = 'fraud_suspected' THEN 1 ELSE 0 END) as fraud_suspected,
|
|
436
|
+
SUM(CASE WHEN action_taken = 'halt' THEN 1 ELSE 0 END) as halted_operations,
|
|
437
|
+
SUM(CASE WHEN action_taken = 'block' THEN 1 ELSE 0 END) as blocked_operations,
|
|
438
|
+
AVG(match_score) as avg_match_score
|
|
439
|
+
FROM verification_results
|
|
440
|
+
WHERE site_id = ?
|
|
441
|
+
`).get(siteId);
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
...stats,
|
|
445
|
+
avg_match_score: stats.avg_match_score ? Math.round(stats.avg_match_score * 100) : 100,
|
|
446
|
+
integrity_rating: stats.total_checks > 0
|
|
447
|
+
? Math.round((stats.passed / stats.total_checks) * 100)
|
|
448
|
+
: 100
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function getGlobalShieldStats() {
|
|
453
|
+
return db.prepare(`
|
|
454
|
+
SELECT
|
|
455
|
+
COUNT(*) as total_checks,
|
|
456
|
+
SUM(CASE WHEN discrepancy_type = 'none' THEN 1 ELSE 0 END) as passed,
|
|
457
|
+
SUM(CASE WHEN action_taken = 'halt' THEN 1 ELSE 0 END) as threats_blocked,
|
|
458
|
+
SUM(CASE WHEN action_taken = 'confirm_human' THEN 1 ELSE 0 END) as human_reviews,
|
|
459
|
+
COUNT(DISTINCT site_id) as sites_verified
|
|
460
|
+
FROM verification_results
|
|
461
|
+
`).get();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ─── Cleanup ─────────────────────────────────────────────────────────
|
|
465
|
+
|
|
466
|
+
function cleanupExpiredCache() {
|
|
467
|
+
return db.prepare("DELETE FROM verification_cache WHERE expires_at < datetime('now')").run();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
module.exports = {
|
|
471
|
+
verifyPrice,
|
|
472
|
+
verifyText,
|
|
473
|
+
verifyPage,
|
|
474
|
+
updateBenchmark,
|
|
475
|
+
confirmVerification,
|
|
476
|
+
getShieldStats,
|
|
477
|
+
getGlobalShieldStats,
|
|
478
|
+
cleanupExpiredCache,
|
|
479
|
+
extractPrice,
|
|
480
|
+
textSimilarity
|
|
481
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# WAB Agent Template — Artisan Marketplace Scout
|
|
2
|
+
# Discovers handmade products from small artisans worldwide
|
|
3
|
+
# Run: npx wab-agent run artisan-marketplace.yaml
|
|
4
|
+
|
|
5
|
+
name: artisan-marketplace
|
|
6
|
+
version: 1.0.0
|
|
7
|
+
description: Discover and buy handmade products directly from artisans, not intermediaries
|
|
8
|
+
author: WAB Community
|
|
9
|
+
tags: [artisan, handmade, fair-trade, crafts, direct-buy]
|
|
10
|
+
|
|
11
|
+
goal: >
|
|
12
|
+
Find WAB-enabled artisan shops, evaluate product authenticity and craftsmanship,
|
|
13
|
+
compare prices against mass-produced alternatives, negotiate fair prices that
|
|
14
|
+
support artisans, and verify product descriptions are accurate.
|
|
15
|
+
|
|
16
|
+
target_sites:
|
|
17
|
+
discovery_method: wab-registry
|
|
18
|
+
category: artisan-crafts
|
|
19
|
+
region: global
|
|
20
|
+
fallback_urls: []
|
|
21
|
+
|
|
22
|
+
parameters:
|
|
23
|
+
- name: product_type
|
|
24
|
+
type: string
|
|
25
|
+
required: true
|
|
26
|
+
description: "Type of handmade product (e.g. pottery, textiles, jewelry, leather)"
|
|
27
|
+
- name: origin_country
|
|
28
|
+
type: string
|
|
29
|
+
description: Preferred country of origin
|
|
30
|
+
- name: max_price
|
|
31
|
+
type: number
|
|
32
|
+
default: 500
|
|
33
|
+
|
|
34
|
+
actions:
|
|
35
|
+
- name: discover
|
|
36
|
+
description: Find artisan shops selling the target product type
|
|
37
|
+
wab_action: discover
|
|
38
|
+
|
|
39
|
+
- name: browse_products
|
|
40
|
+
description: Browse available handmade products
|
|
41
|
+
wab_action: getProducts
|
|
42
|
+
params:
|
|
43
|
+
category: "{{product_type}}"
|
|
44
|
+
handmade: true
|
|
45
|
+
|
|
46
|
+
- name: verify_authenticity
|
|
47
|
+
description: Check product descriptions against photos
|
|
48
|
+
wab_action: verifyText
|
|
49
|
+
params:
|
|
50
|
+
fields: [material, origin, technique, dimensions]
|
|
51
|
+
|
|
52
|
+
- name: check_artisan_profile
|
|
53
|
+
description: Verify the artisan is a real person/workshop
|
|
54
|
+
wab_action: getArtisanProfile
|
|
55
|
+
|
|
56
|
+
- name: compare_prices
|
|
57
|
+
description: Compare with mass-produced alternatives for fair pricing
|
|
58
|
+
wab_action: getPrice
|
|
59
|
+
collect: true
|
|
60
|
+
|
|
61
|
+
- name: negotiate
|
|
62
|
+
description: Negotiate a fair price supporting the artisan
|
|
63
|
+
wab_action: negotiate
|
|
64
|
+
strategy: first_time
|
|
65
|
+
conditions:
|
|
66
|
+
proposed_discount: 5
|
|
67
|
+
fallback_strategy: instant_payment
|
|
68
|
+
|
|
69
|
+
- name: purchase
|
|
70
|
+
description: Purchase at agreed price
|
|
71
|
+
wab_action: buy
|
|
72
|
+
requires: [verify_authenticity]
|
|
73
|
+
|
|
74
|
+
fairness_rules:
|
|
75
|
+
prefer_local: false
|
|
76
|
+
prefer_small_business: true
|
|
77
|
+
max_price: "{{max_price}}"
|
|
78
|
+
currency: USD
|
|
79
|
+
min_reputation_score: 30
|
|
80
|
+
avoid_monopolies: true
|
|
81
|
+
support_artisans: true
|
|
82
|
+
|
|
83
|
+
negotiation:
|
|
84
|
+
enabled: true
|
|
85
|
+
max_rounds: 2
|
|
86
|
+
strategies:
|
|
87
|
+
- first_time
|
|
88
|
+
- instant_payment
|
|
89
|
+
- bulk_order
|
|
90
|
+
walk_away_threshold: 3
|
|
91
|
+
respect_artisan_floor: true
|
|
92
|
+
|
|
93
|
+
verification:
|
|
94
|
+
anti_hallucination: true
|
|
95
|
+
cross_check_prices: true
|
|
96
|
+
verify_descriptions: true
|
|
97
|
+
require_vision_match: true
|
|
98
|
+
max_price_variance: 0.15
|
|
99
|
+
|
|
100
|
+
output:
|
|
101
|
+
format: json
|
|
102
|
+
include: [artisan_name, product, material, origin, price, negotiated_price, authenticity_score, reputation]
|
|
103
|
+
sort_by: authenticity_score
|
|
104
|
+
limit: 15
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# WAB Agent Template — Book Price Scout
|
|
2
|
+
# Finds the cheapest books directly from independent bookstores
|
|
3
|
+
# Run: npx wab-agent run book-price-scout.yaml --title "Clean Code"
|
|
4
|
+
|
|
5
|
+
name: book-price-scout
|
|
6
|
+
version: 1.0.0
|
|
7
|
+
description: Find books at the best price from independent bookstores, not just Amazon
|
|
8
|
+
author: WAB Community
|
|
9
|
+
tags: [books, price-comparison, indie-bookstore, shopping]
|
|
10
|
+
|
|
11
|
+
goal: >
|
|
12
|
+
Search WAB-enabled independent bookstores for a specific book,
|
|
13
|
+
compare prices with Amazon, negotiate direct purchase discounts,
|
|
14
|
+
and verify book details are accurate.
|
|
15
|
+
|
|
16
|
+
target_sites:
|
|
17
|
+
discovery_method: wab-registry
|
|
18
|
+
category: books
|
|
19
|
+
region: global
|
|
20
|
+
fallback_urls: []
|
|
21
|
+
|
|
22
|
+
parameters:
|
|
23
|
+
- name: title
|
|
24
|
+
type: string
|
|
25
|
+
required: true
|
|
26
|
+
- name: author
|
|
27
|
+
type: string
|
|
28
|
+
- name: isbn
|
|
29
|
+
type: string
|
|
30
|
+
- name: format
|
|
31
|
+
type: string
|
|
32
|
+
default: paperback
|
|
33
|
+
enum: [paperback, hardcover, ebook]
|
|
34
|
+
|
|
35
|
+
actions:
|
|
36
|
+
- name: discover
|
|
37
|
+
description: Find independent bookstores
|
|
38
|
+
wab_action: discover
|
|
39
|
+
|
|
40
|
+
- name: search_book
|
|
41
|
+
description: Search for the book
|
|
42
|
+
wab_action: searchProducts
|
|
43
|
+
params:
|
|
44
|
+
title: "{{title}}"
|
|
45
|
+
author: "{{author}}"
|
|
46
|
+
isbn: "{{isbn}}"
|
|
47
|
+
collect: true
|
|
48
|
+
|
|
49
|
+
- name: verify_details
|
|
50
|
+
description: Verify book title, author, edition match
|
|
51
|
+
wab_action: verifyText
|
|
52
|
+
require_pass: true
|
|
53
|
+
|
|
54
|
+
- name: verify_prices
|
|
55
|
+
description: Cross-check displayed prices
|
|
56
|
+
wab_action: verifyPrice
|
|
57
|
+
require_pass: true
|
|
58
|
+
|
|
59
|
+
- name: negotiate
|
|
60
|
+
description: Negotiate direct purchase discount
|
|
61
|
+
wab_action: negotiate
|
|
62
|
+
strategy: instant_payment
|
|
63
|
+
conditions:
|
|
64
|
+
proposed_discount: 8
|
|
65
|
+
fallback_strategy: first_time
|
|
66
|
+
|
|
67
|
+
- name: purchase
|
|
68
|
+
description: Buy the book
|
|
69
|
+
wab_action: buy
|
|
70
|
+
requires: [verify_details, verify_prices]
|
|
71
|
+
|
|
72
|
+
fairness_rules:
|
|
73
|
+
prefer_local: true
|
|
74
|
+
prefer_small_business: true
|
|
75
|
+
currency: USD
|
|
76
|
+
min_reputation_score: 25
|
|
77
|
+
avoid_monopolies: true
|
|
78
|
+
|
|
79
|
+
negotiation:
|
|
80
|
+
enabled: true
|
|
81
|
+
max_rounds: 2
|
|
82
|
+
strategies:
|
|
83
|
+
- instant_payment
|
|
84
|
+
- first_time
|
|
85
|
+
- bulk_order
|
|
86
|
+
walk_away_threshold: 3
|
|
87
|
+
|
|
88
|
+
verification:
|
|
89
|
+
anti_hallucination: true
|
|
90
|
+
cross_check_prices: true
|
|
91
|
+
verify_descriptions: true
|
|
92
|
+
max_price_variance: 0.05
|
|
93
|
+
|
|
94
|
+
output:
|
|
95
|
+
format: table
|
|
96
|
+
include: [bookstore, title, author, format, price, negotiated_price, shipping, total, reputation]
|
|
97
|
+
sort_by: total
|
|
98
|
+
limit: 10
|