prism-mcp-server 1.5.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/.gitmodules +3 -0
- package/Dockerfile +30 -0
- package/LICENSE +21 -0
- package/README.md +970 -0
- package/benchmark.ts +172 -0
- package/call_chrome_mcp.py +96 -0
- package/docker-compose.yml +67 -0
- package/execute_via_chrome_mcp.py +133 -0
- package/gmail_auth_test.py +29 -0
- package/gmail_list_latest_5.py +27 -0
- package/index.ts +34 -0
- package/list_chrome_tools.py +70 -0
- package/package.json +64 -0
- package/patch_cgc_mcp.py +90 -0
- package/repomix-output.xml +9 -0
- package/run_server.sh +9 -0
- package/server.json +78 -0
- package/src/config.ts +85 -0
- package/src/server.ts +627 -0
- package/src/tools/compactionHandler.ts +313 -0
- package/src/tools/definitions.ts +367 -0
- package/src/tools/handlers.ts +261 -0
- package/src/tools/index.ts +38 -0
- package/src/tools/sessionMemoryDefinitions.ts +437 -0
- package/src/tools/sessionMemoryHandlers.ts +774 -0
- package/src/utils/braveApi.ts +375 -0
- package/src/utils/embeddingApi.ts +97 -0
- package/src/utils/executor.ts +105 -0
- package/src/utils/googleAi.ts +107 -0
- package/src/utils/keywordExtractor.ts +207 -0
- package/src/utils/supabaseApi.ts +194 -0
- package/supabase/migrations/015_session_memory.sql +145 -0
- package/supabase/migrations/016_knowledge_accumulation.sql +315 -0
- package/supabase/migrations/017_ledger_compaction.sql +74 -0
- package/supabase/migrations/018_semantic_search.sql +110 -0
- package/supabase/migrations/019_concurrency_control.sql +320 -0
- package/supabase/migrations/020_multi_tenant_rls.sql +459 -0
- package/test_cross_mcp.js +393 -0
- package/test_mcp_schema.js +83 -0
- package/tests/test_knowledge_system.js +319 -0
- package/tsconfig.json +16 -0
- package/vertex-ai/test_claude_vertex.py +78 -0
- package/vertex-ai/test_gemini_vertex.py +39 -0
- package/vertex-ai/test_hybrid_search_pipeline.ts +296 -0
- package/vertex-ai/test_pipeline_benchmark.ts +251 -0
- package/vertex-ai/test_realworld_comparison.ts +290 -0
- package/vertex-ai/verify_discovery_engine.ts +72 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
#!/usr/bin/env npx ts-node
|
|
2
|
+
/**
|
|
3
|
+
* Hybrid Search Pipeline Test
|
|
4
|
+
*
|
|
5
|
+
* Validates the combined MCP + Vertex AI Discovery Engine search pipeline:
|
|
6
|
+
* 1. Brave Search (real-time web) via MCP server
|
|
7
|
+
* 2. Discovery Engine (curated enterprise index) via Vertex AI
|
|
8
|
+
* 3. code_mode_transform (context reduction via sandboxed JS)
|
|
9
|
+
* 4. Gemini analysis (LLM post-processing of merged results)
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* npx ts-node vertex-ai/test_hybrid_search_pipeline.ts
|
|
13
|
+
*
|
|
14
|
+
* Prerequisites:
|
|
15
|
+
* - BRAVE_API_KEY environment variable
|
|
16
|
+
* - GCP ADC configured (gcloud auth application-default login)
|
|
17
|
+
* - DISCOVERY_ENGINE_* environment variables set
|
|
18
|
+
* - GEMINI_API_KEY or GOOGLE_API_KEY environment variable
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { SearchServiceClient } from '@google-cloud/discoveryengine';
|
|
22
|
+
import { GoogleGenerativeAI } from '@google/generative-ai';
|
|
23
|
+
|
|
24
|
+
// ─── Configuration ───────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
const BRAVE_API_KEY = process.env.BRAVE_API_KEY || '';
|
|
27
|
+
const GEMINI_API_KEY = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || '';
|
|
28
|
+
|
|
29
|
+
const DE_PROJECT_ID = process.env.DISCOVERY_ENGINE_PROJECT_ID || process.env.GCP_PROJECT_ID || '';
|
|
30
|
+
const DE_LOCATION = process.env.DISCOVERY_ENGINE_LOCATION || 'global';
|
|
31
|
+
const DE_COLLECTION = process.env.DISCOVERY_ENGINE_COLLECTION || 'default_collection';
|
|
32
|
+
const DE_ENGINE_ID = process.env.DISCOVERY_ENGINE_ENGINE_ID || '';
|
|
33
|
+
const DE_SERVING_CONFIG = process.env.DISCOVERY_ENGINE_SERVING_CONFIG || 'default_serving_config';
|
|
34
|
+
|
|
35
|
+
const TEST_QUERY = 'machine learning model optimization techniques';
|
|
36
|
+
|
|
37
|
+
// ─── Interfaces ──────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
interface SearchResult {
|
|
40
|
+
source: 'brave' | 'discovery_engine';
|
|
41
|
+
title: string;
|
|
42
|
+
url: string;
|
|
43
|
+
snippet: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface PipelineMetrics {
|
|
47
|
+
stage: string;
|
|
48
|
+
latencyMs: number;
|
|
49
|
+
inputSizeKB: number;
|
|
50
|
+
outputSizeKB: number;
|
|
51
|
+
reductionPct: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── Stage 1: Brave Web Search ───────────────────────────────────
|
|
55
|
+
|
|
56
|
+
async function braveWebSearch(query: string, count: number = 5): Promise<{ results: SearchResult[]; rawSize: number; latencyMs: number }> {
|
|
57
|
+
if (!BRAVE_API_KEY) {
|
|
58
|
+
console.log(' ⚠️ BRAVE_API_KEY not set — skipping Brave Search stage');
|
|
59
|
+
return { results: [], rawSize: 0, latencyMs: 0 };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const start = Date.now();
|
|
63
|
+
const url = `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${count}`;
|
|
64
|
+
|
|
65
|
+
const response = await fetch(url, {
|
|
66
|
+
headers: {
|
|
67
|
+
'Accept': 'application/json',
|
|
68
|
+
'Accept-Encoding': 'gzip',
|
|
69
|
+
'X-Subscription-Token': BRAVE_API_KEY,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const data = await response.json();
|
|
74
|
+
const latencyMs = Date.now() - start;
|
|
75
|
+
const rawJson = JSON.stringify(data);
|
|
76
|
+
const rawSize = Buffer.byteLength(rawJson, 'utf8');
|
|
77
|
+
|
|
78
|
+
const results: SearchResult[] = (data.web?.results || []).map((r: any) => ({
|
|
79
|
+
source: 'brave' as const,
|
|
80
|
+
title: r.title || '',
|
|
81
|
+
url: r.url || '',
|
|
82
|
+
snippet: (r.description || '').substring(0, 200),
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
return { results, rawSize, latencyMs };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─── Stage 2: Discovery Engine Search ────────────────────────────
|
|
89
|
+
|
|
90
|
+
async function discoveryEngineSearch(query: string, pageSize: number = 5): Promise<{ results: SearchResult[]; rawSize: number; latencyMs: number }> {
|
|
91
|
+
if (!DE_PROJECT_ID || !DE_ENGINE_ID) {
|
|
92
|
+
console.log(' ⚠️ Discovery Engine env vars not set — skipping DE stage');
|
|
93
|
+
return { results: [], rawSize: 0, latencyMs: 0 };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const start = Date.now();
|
|
97
|
+
const client = new SearchServiceClient();
|
|
98
|
+
|
|
99
|
+
const servingConfig = `projects/${DE_PROJECT_ID}/locations/${DE_LOCATION}/collections/${DE_COLLECTION}/engines/${DE_ENGINE_ID}/servingConfigs/${DE_SERVING_CONFIG}`;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const [response] = await client.search({
|
|
103
|
+
servingConfig,
|
|
104
|
+
query,
|
|
105
|
+
pageSize,
|
|
106
|
+
}, { autoPaginate: false });
|
|
107
|
+
|
|
108
|
+
const latencyMs = Date.now() - start;
|
|
109
|
+
|
|
110
|
+
// With autoPaginate: false, response is an array of ISearchResult
|
|
111
|
+
const resultItems = Array.isArray(response) ? response : (response as any).results || [];
|
|
112
|
+
const rawJson = JSON.stringify(resultItems);
|
|
113
|
+
const rawSize = Buffer.byteLength(rawJson, 'utf8');
|
|
114
|
+
|
|
115
|
+
// Helper to extract string from protobuf Value
|
|
116
|
+
const getField = (structData: any, field: string): string => {
|
|
117
|
+
if (!structData) return '';
|
|
118
|
+
// Direct access (already decoded)
|
|
119
|
+
if (typeof structData[field] === 'string') return structData[field];
|
|
120
|
+
// Protobuf fields format
|
|
121
|
+
if (structData.fields?.[field]?.stringValue) return structData.fields[field].stringValue;
|
|
122
|
+
return '';
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const results: SearchResult[] = resultItems.map((r: any) => ({
|
|
126
|
+
source: 'discovery_engine' as const,
|
|
127
|
+
title: getField(r.document?.derivedStructData, 'title') ||
|
|
128
|
+
getField(r.document?.derivedStructData, 'displayLink') ||
|
|
129
|
+
r.document?.name?.split('/').pop() || 'Untitled',
|
|
130
|
+
url: getField(r.document?.derivedStructData, 'link') ||
|
|
131
|
+
getField(r.document?.derivedStructData, 'htmlFormattedUrl') || '',
|
|
132
|
+
snippet: getField(r.document?.derivedStructData, 'snippet') ||
|
|
133
|
+
getField(r.document?.derivedStructData, 'htmlSnippet') || '',
|
|
134
|
+
}));
|
|
135
|
+
|
|
136
|
+
return { results, rawSize, latencyMs };
|
|
137
|
+
} catch (error: any) {
|
|
138
|
+
console.log(` ⚠️ Discovery Engine error: ${error.message}`);
|
|
139
|
+
return { results: [], rawSize: 0, latencyMs: Date.now() - start };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ─── Stage 3: Context Reduction (simulates code_mode_transform) ──
|
|
144
|
+
|
|
145
|
+
function contextReduction(braveResults: SearchResult[], deResults: SearchResult[]): { merged: SearchResult[]; metrics: PipelineMetrics } {
|
|
146
|
+
const start = Date.now();
|
|
147
|
+
|
|
148
|
+
// Merge and deduplicate by URL
|
|
149
|
+
const allResults = [...braveResults, ...deResults];
|
|
150
|
+
const seen = new Set<string>();
|
|
151
|
+
const merged: SearchResult[] = [];
|
|
152
|
+
|
|
153
|
+
for (const result of allResults) {
|
|
154
|
+
const key = result.url.toLowerCase().replace(/\/$/, '');
|
|
155
|
+
if (!seen.has(key) && result.url) {
|
|
156
|
+
seen.add(key);
|
|
157
|
+
merged.push(result);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const inputJson = JSON.stringify(allResults);
|
|
162
|
+
const outputJson = JSON.stringify(merged.map(r => ({
|
|
163
|
+
source: r.source,
|
|
164
|
+
title: r.title,
|
|
165
|
+
url: r.url,
|
|
166
|
+
})));
|
|
167
|
+
|
|
168
|
+
const inputSizeKB = Buffer.byteLength(inputJson, 'utf8') / 1024;
|
|
169
|
+
const outputSizeKB = Buffer.byteLength(outputJson, 'utf8') / 1024;
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
merged,
|
|
173
|
+
metrics: {
|
|
174
|
+
stage: 'context_reduction',
|
|
175
|
+
latencyMs: Date.now() - start,
|
|
176
|
+
inputSizeKB,
|
|
177
|
+
outputSizeKB,
|
|
178
|
+
reductionPct: inputSizeKB > 0 ? Number((100 - (outputSizeKB / inputSizeKB) * 100).toFixed(1)) : 0,
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ─── Stage 4: Gemini Analysis ────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
async function geminiAnalysis(mergedResults: SearchResult[], query: string): Promise<{ analysis: string; latencyMs: number }> {
|
|
186
|
+
if (!GEMINI_API_KEY) {
|
|
187
|
+
console.log(' ⚠️ GEMINI_API_KEY not set — skipping Gemini analysis stage');
|
|
188
|
+
return { analysis: '[Skipped - no API key]', latencyMs: 0 };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const start = Date.now();
|
|
192
|
+
const genAI = new GoogleGenerativeAI(GEMINI_API_KEY);
|
|
193
|
+
const model = genAI.getGenerativeModel({ model: 'gemini-2.5-flash' });
|
|
194
|
+
|
|
195
|
+
const sourceSummary = mergedResults.map((r, i) =>
|
|
196
|
+
`[${i + 1}] (${r.source}) ${r.title}\n URL: ${r.url}`
|
|
197
|
+
).join('\n');
|
|
198
|
+
|
|
199
|
+
const prompt = `Given the following search results from a hybrid pipeline (web search + enterprise Discovery Engine) for the query "${query}", provide a brief analytical summary highlighting the most relevant findings and how the two sources complement each other:\n\n${sourceSummary}\n\nKeep the summary concise (3-5 sentences).`;
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const result = await model.generateContent(prompt);
|
|
203
|
+
const text = result.response.text();
|
|
204
|
+
return { analysis: text, latencyMs: Date.now() - start };
|
|
205
|
+
} catch (error: any) {
|
|
206
|
+
return { analysis: `[Error: ${error.message}]`, latencyMs: Date.now() - start };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ─── Main Pipeline ───────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
async function runHybridPipeline() {
|
|
213
|
+
console.log('╔══════════════════════════════════════════════════════════╗');
|
|
214
|
+
console.log('║ Hybrid Search Pipeline Test: MCP + Vertex AI ║');
|
|
215
|
+
console.log('╚══════════════════════════════════════════════════════════╝\n');
|
|
216
|
+
|
|
217
|
+
console.log(`Query: "${TEST_QUERY}"\n`);
|
|
218
|
+
|
|
219
|
+
const metrics: PipelineMetrics[] = [];
|
|
220
|
+
|
|
221
|
+
// Stage 1: Brave Search
|
|
222
|
+
console.log('── Stage 1: Brave Web Search (MCP) ──');
|
|
223
|
+
const brave = await braveWebSearch(TEST_QUERY);
|
|
224
|
+
console.log(` Results: ${brave.results.length} | Latency: ${brave.latencyMs}ms | Raw: ${(brave.rawSize / 1024).toFixed(1)}KB`);
|
|
225
|
+
metrics.push({
|
|
226
|
+
stage: 'brave_web_search',
|
|
227
|
+
latencyMs: brave.latencyMs,
|
|
228
|
+
inputSizeKB: 0,
|
|
229
|
+
outputSizeKB: brave.rawSize / 1024,
|
|
230
|
+
reductionPct: 0,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Stage 2: Discovery Engine
|
|
234
|
+
console.log('\n── Stage 2: Vertex AI Discovery Engine ──');
|
|
235
|
+
const de = await discoveryEngineSearch(TEST_QUERY);
|
|
236
|
+
console.log(` Results: ${de.results.length} | Latency: ${de.latencyMs}ms | Raw: ${(de.rawSize / 1024).toFixed(1)}KB`);
|
|
237
|
+
metrics.push({
|
|
238
|
+
stage: 'discovery_engine',
|
|
239
|
+
latencyMs: de.latencyMs,
|
|
240
|
+
inputSizeKB: 0,
|
|
241
|
+
outputSizeKB: de.rawSize / 1024,
|
|
242
|
+
reductionPct: 0,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Stage 3: Context Reduction
|
|
246
|
+
console.log('\n── Stage 3: Context Reduction (code_mode_transform) ──');
|
|
247
|
+
const { merged, metrics: reductionMetrics } = contextReduction(brave.results, de.results);
|
|
248
|
+
console.log(` Merged: ${merged.length} unique results (from ${brave.results.length + de.results.length} total)`);
|
|
249
|
+
console.log(` Reduction: ${reductionMetrics.inputSizeKB.toFixed(1)}KB → ${reductionMetrics.outputSizeKB.toFixed(1)}KB (${reductionMetrics.reductionPct}%)`);
|
|
250
|
+
metrics.push(reductionMetrics);
|
|
251
|
+
|
|
252
|
+
// Stage 4: Gemini Analysis
|
|
253
|
+
console.log('\n── Stage 4: Gemini LLM Analysis ──');
|
|
254
|
+
const analysis = await geminiAnalysis(merged, TEST_QUERY);
|
|
255
|
+
console.log(` Latency: ${analysis.latencyMs}ms`);
|
|
256
|
+
console.log(` Analysis:\n${analysis.analysis.split('\n').map(l => ` ${l}`).join('\n')}`);
|
|
257
|
+
metrics.push({
|
|
258
|
+
stage: 'gemini_analysis',
|
|
259
|
+
latencyMs: analysis.latencyMs,
|
|
260
|
+
inputSizeKB: 0,
|
|
261
|
+
outputSizeKB: Buffer.byteLength(analysis.analysis, 'utf8') / 1024,
|
|
262
|
+
reductionPct: 0,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Summary
|
|
266
|
+
const totalLatency = metrics.reduce((sum, m) => sum + m.latencyMs, 0);
|
|
267
|
+
|
|
268
|
+
console.log('\n╔══════════════════════════════════════════════════════════╗');
|
|
269
|
+
console.log('║ PIPELINE SUMMARY ║');
|
|
270
|
+
console.log('╠══════════════════════════════════════════════════════════╣');
|
|
271
|
+
console.log(`║ Total latency: ${totalLatency}ms `);
|
|
272
|
+
console.log(`║ Sources queried: ${[brave.results.length > 0 ? 'Brave' : null, de.results.length > 0 ? 'Discovery Engine' : null].filter(Boolean).join(' + ') || 'None'}`);
|
|
273
|
+
console.log(`║ Unique results: ${merged.length} `);
|
|
274
|
+
console.log(`║ Context reduction: ${reductionMetrics.reductionPct}% `);
|
|
275
|
+
console.log('╚══════════════════════════════════════════════════════════╝');
|
|
276
|
+
|
|
277
|
+
// Detailed results table
|
|
278
|
+
console.log('\n── Merged Results ──');
|
|
279
|
+
merged.forEach((r, i) => {
|
|
280
|
+
console.log(` [${i + 1}] (${r.source === 'brave' ? '🌐 Brave' : '🔍 DE'}) ${r.title}`);
|
|
281
|
+
console.log(` ${r.url}`);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Pass/fail
|
|
285
|
+
const passed = brave.results.length > 0 || de.results.length > 0;
|
|
286
|
+
console.log(`\n${passed ? '✅ PIPELINE TEST PASSED' : '⚠️ No results from either source — check API keys and env vars'}`);
|
|
287
|
+
|
|
288
|
+
return passed;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
runHybridPipeline()
|
|
292
|
+
.then(passed => process.exit(passed ? 0 : 1))
|
|
293
|
+
.catch(err => {
|
|
294
|
+
console.error('Pipeline error:', err);
|
|
295
|
+
process.exit(1);
|
|
296
|
+
});
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
#!/usr/bin/env npx ts-node
|
|
2
|
+
/**
|
|
3
|
+
* Pipeline Performance Benchmark
|
|
4
|
+
*
|
|
5
|
+
* Measures and compares performance characteristics of the hybrid search pipeline
|
|
6
|
+
* across different configurations:
|
|
7
|
+
* - MCP-only (Brave Search + code_mode_transform)
|
|
8
|
+
* - Discovery Engine-only (Vertex AI Search)
|
|
9
|
+
* - Hybrid (both sources merged + LLM analysis)
|
|
10
|
+
*
|
|
11
|
+
* Metrics collected:
|
|
12
|
+
* - Query latency (ms)
|
|
13
|
+
* - Payload size (KB before/after context reduction)
|
|
14
|
+
* - Token estimation (chars / 4 ≈ tokens)
|
|
15
|
+
* - Context window efficiency (% reduction)
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* npx ts-node vertex-ai/test_pipeline_benchmark.ts
|
|
19
|
+
*
|
|
20
|
+
* Prerequisites:
|
|
21
|
+
* - BRAVE_API_KEY environment variable
|
|
22
|
+
* - GCP ADC configured for Discovery Engine
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { SearchServiceClient } from '@google-cloud/discoveryengine';
|
|
26
|
+
|
|
27
|
+
// ─── Configuration ───────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
const BRAVE_API_KEY = process.env.BRAVE_API_KEY || '';
|
|
30
|
+
const DE_PROJECT_ID = process.env.DISCOVERY_ENGINE_PROJECT_ID || process.env.GCP_PROJECT_ID || '';
|
|
31
|
+
const DE_ENGINE_ID = process.env.DISCOVERY_ENGINE_ENGINE_ID || '';
|
|
32
|
+
const DE_LOCATION = process.env.DISCOVERY_ENGINE_LOCATION || 'global';
|
|
33
|
+
const DE_COLLECTION = process.env.DISCOVERY_ENGINE_COLLECTION || 'default_collection';
|
|
34
|
+
const DE_SERVING_CONFIG = process.env.DISCOVERY_ENGINE_SERVING_CONFIG || 'default_serving_config';
|
|
35
|
+
|
|
36
|
+
const BENCHMARK_QUERIES = [
|
|
37
|
+
'transformer architecture attention mechanism',
|
|
38
|
+
'kubernetes pod autoscaling best practices',
|
|
39
|
+
'python async await concurrency patterns',
|
|
40
|
+
'vertex ai discovery engine structured search',
|
|
41
|
+
'mcp model context protocol integration',
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const ITERATIONS = 3; // Number of runs per query for averaging
|
|
45
|
+
|
|
46
|
+
// ─── Types ───────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
interface BenchmarkResult {
|
|
49
|
+
source: string;
|
|
50
|
+
query: string;
|
|
51
|
+
avgLatencyMs: number;
|
|
52
|
+
avgRawSizeKB: number;
|
|
53
|
+
avgReducedSizeKB: number;
|
|
54
|
+
avgReductionPct: number;
|
|
55
|
+
estimatedTokensBefore: number;
|
|
56
|
+
estimatedTokensAfter: number;
|
|
57
|
+
successRate: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─── Brave Search Benchmark ─────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
async function benchmarkBrave(query: string): Promise<{ latencyMs: number; rawSizeKB: number; reducedSizeKB: number; success: boolean }> {
|
|
63
|
+
if (!BRAVE_API_KEY) return { latencyMs: 0, rawSizeKB: 0, reducedSizeKB: 0, success: false };
|
|
64
|
+
|
|
65
|
+
const start = Date.now();
|
|
66
|
+
try {
|
|
67
|
+
const url = `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=10`;
|
|
68
|
+
const response = await fetch(url, {
|
|
69
|
+
headers: {
|
|
70
|
+
'Accept': 'application/json',
|
|
71
|
+
'Accept-Encoding': 'gzip',
|
|
72
|
+
'X-Subscription-Token': BRAVE_API_KEY,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const data = await response.json();
|
|
77
|
+
const latencyMs = Date.now() - start;
|
|
78
|
+
|
|
79
|
+
const rawJson = JSON.stringify(data);
|
|
80
|
+
const rawSizeKB = Buffer.byteLength(rawJson, 'utf8') / 1024;
|
|
81
|
+
|
|
82
|
+
// Simulate code_mode_transform: extract only title + url + description
|
|
83
|
+
const reduced = (data.web?.results || []).map((r: any) => ({
|
|
84
|
+
title: r.title,
|
|
85
|
+
url: r.url,
|
|
86
|
+
desc: (r.description || '').substring(0, 150),
|
|
87
|
+
}));
|
|
88
|
+
const reducedJson = JSON.stringify(reduced);
|
|
89
|
+
const reducedSizeKB = Buffer.byteLength(reducedJson, 'utf8') / 1024;
|
|
90
|
+
|
|
91
|
+
return { latencyMs, rawSizeKB, reducedSizeKB, success: true };
|
|
92
|
+
} catch {
|
|
93
|
+
return { latencyMs: Date.now() - start, rawSizeKB: 0, reducedSizeKB: 0, success: false };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─── Discovery Engine Benchmark ─────────────────────────────────
|
|
98
|
+
|
|
99
|
+
async function benchmarkDiscoveryEngine(query: string): Promise<{ latencyMs: number; rawSizeKB: number; reducedSizeKB: number; success: boolean }> {
|
|
100
|
+
if (!DE_PROJECT_ID || !DE_ENGINE_ID) return { latencyMs: 0, rawSizeKB: 0, reducedSizeKB: 0, success: false };
|
|
101
|
+
|
|
102
|
+
const start = Date.now();
|
|
103
|
+
try {
|
|
104
|
+
const client = new SearchServiceClient();
|
|
105
|
+
const servingConfig = `projects/${DE_PROJECT_ID}/locations/${DE_LOCATION}/collections/${DE_COLLECTION}/engines/${DE_ENGINE_ID}/servingConfigs/${DE_SERVING_CONFIG}`;
|
|
106
|
+
|
|
107
|
+
const [response] = await client.search({ servingConfig, query, pageSize: 10 }, { autoPaginate: false });
|
|
108
|
+
const latencyMs = Date.now() - start;
|
|
109
|
+
|
|
110
|
+
const resultItems = Array.isArray(response) ? response : (response as any).results || [];
|
|
111
|
+
const rawJson = JSON.stringify(resultItems);
|
|
112
|
+
const rawSizeKB = Buffer.byteLength(rawJson, 'utf8') / 1024;
|
|
113
|
+
|
|
114
|
+
// Helper for protobuf Value extraction
|
|
115
|
+
const getField = (sd: any, f: string): string => {
|
|
116
|
+
if (!sd) return '';
|
|
117
|
+
if (typeof sd[f] === 'string') return sd[f];
|
|
118
|
+
if (sd.fields?.[f]?.stringValue) return sd.fields[f].stringValue;
|
|
119
|
+
return '';
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const reduced = resultItems.map((r: any) => ({
|
|
123
|
+
title: getField(r.document?.derivedStructData, 'title') || getField(r.document?.derivedStructData, 'displayLink') || '',
|
|
124
|
+
url: getField(r.document?.derivedStructData, 'link') || getField(r.document?.derivedStructData, 'htmlFormattedUrl') || '',
|
|
125
|
+
}));
|
|
126
|
+
const reducedJson = JSON.stringify(reduced);
|
|
127
|
+
const reducedSizeKB = Buffer.byteLength(reducedJson, 'utf8') / 1024;
|
|
128
|
+
|
|
129
|
+
return { latencyMs, rawSizeKB, reducedSizeKB, success: true };
|
|
130
|
+
} catch {
|
|
131
|
+
return { latencyMs: Date.now() - start, rawSizeKB: 0, reducedSizeKB: 0, success: false };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── Run Benchmark ───────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
async function runBenchmarks() {
|
|
138
|
+
console.log('╔══════════════════════════════════════════════════════════════╗');
|
|
139
|
+
console.log('║ Pipeline Performance Benchmark: MCP vs Vertex AI ║');
|
|
140
|
+
console.log('╚══════════════════════════════════════════════════════════════╝\n');
|
|
141
|
+
|
|
142
|
+
const braveResults: BenchmarkResult[] = [];
|
|
143
|
+
const deResults: BenchmarkResult[] = [];
|
|
144
|
+
|
|
145
|
+
for (const query of BENCHMARK_QUERIES) {
|
|
146
|
+
console.log(`\n── Query: "${query}" ──`);
|
|
147
|
+
|
|
148
|
+
// Benchmark Brave Search
|
|
149
|
+
const braveRuns: Awaited<ReturnType<typeof benchmarkBrave>>[] = [];
|
|
150
|
+
for (let i = 0; i < ITERATIONS; i++) {
|
|
151
|
+
braveRuns.push(await benchmarkBrave(query));
|
|
152
|
+
if (i < ITERATIONS - 1) await sleep(500); // Rate limit courtesy
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const braveSuccessful = braveRuns.filter(r => r.success);
|
|
156
|
+
if (braveSuccessful.length > 0) {
|
|
157
|
+
const result: BenchmarkResult = {
|
|
158
|
+
source: 'Brave Search',
|
|
159
|
+
query,
|
|
160
|
+
avgLatencyMs: avg(braveSuccessful.map(r => r.latencyMs)),
|
|
161
|
+
avgRawSizeKB: avg(braveSuccessful.map(r => r.rawSizeKB)),
|
|
162
|
+
avgReducedSizeKB: avg(braveSuccessful.map(r => r.reducedSizeKB)),
|
|
163
|
+
avgReductionPct: avg(braveSuccessful.map(r => r.rawSizeKB > 0 ? (1 - r.reducedSizeKB / r.rawSizeKB) * 100 : 0)),
|
|
164
|
+
estimatedTokensBefore: Math.round(avg(braveSuccessful.map(r => r.rawSizeKB * 1024 / 4))),
|
|
165
|
+
estimatedTokensAfter: Math.round(avg(braveSuccessful.map(r => r.reducedSizeKB * 1024 / 4))),
|
|
166
|
+
successRate: braveSuccessful.length / ITERATIONS,
|
|
167
|
+
};
|
|
168
|
+
braveResults.push(result);
|
|
169
|
+
console.log(` 🌐 Brave: ${result.avgLatencyMs.toFixed(0)}ms | ${result.avgRawSizeKB.toFixed(1)}KB → ${result.avgReducedSizeKB.toFixed(1)}KB (${result.avgReductionPct.toFixed(0)}% reduction)`);
|
|
170
|
+
} else {
|
|
171
|
+
console.log(' 🌐 Brave: skipped (no API key)');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Benchmark Discovery Engine
|
|
175
|
+
const deRuns: Awaited<ReturnType<typeof benchmarkDiscoveryEngine>>[] = [];
|
|
176
|
+
for (let i = 0; i < ITERATIONS; i++) {
|
|
177
|
+
deRuns.push(await benchmarkDiscoveryEngine(query));
|
|
178
|
+
if (i < ITERATIONS - 1) await sleep(300);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const deSuccessful = deRuns.filter(r => r.success);
|
|
182
|
+
if (deSuccessful.length > 0) {
|
|
183
|
+
const result: BenchmarkResult = {
|
|
184
|
+
source: 'Discovery Engine',
|
|
185
|
+
query,
|
|
186
|
+
avgLatencyMs: avg(deSuccessful.map(r => r.latencyMs)),
|
|
187
|
+
avgRawSizeKB: avg(deSuccessful.map(r => r.rawSizeKB)),
|
|
188
|
+
avgReducedSizeKB: avg(deSuccessful.map(r => r.reducedSizeKB)),
|
|
189
|
+
avgReductionPct: avg(deSuccessful.map(r => r.rawSizeKB > 0 ? (1 - r.reducedSizeKB / r.rawSizeKB) * 100 : 0)),
|
|
190
|
+
estimatedTokensBefore: Math.round(avg(deSuccessful.map(r => r.rawSizeKB * 1024 / 4))),
|
|
191
|
+
estimatedTokensAfter: Math.round(avg(deSuccessful.map(r => r.reducedSizeKB * 1024 / 4))),
|
|
192
|
+
successRate: deSuccessful.length / ITERATIONS,
|
|
193
|
+
};
|
|
194
|
+
deResults.push(result);
|
|
195
|
+
console.log(` 🔍 DE: ${result.avgLatencyMs.toFixed(0)}ms | ${result.avgRawSizeKB.toFixed(1)}KB → ${result.avgReducedSizeKB.toFixed(1)}KB (${result.avgReductionPct.toFixed(0)}% reduction)`);
|
|
196
|
+
} else {
|
|
197
|
+
console.log(' 🔍 DE: skipped (env vars not set)');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Summary
|
|
202
|
+
console.log('\n╔══════════════════════════════════════════════════════════════╗');
|
|
203
|
+
console.log('║ AGGREGATE RESULTS ║');
|
|
204
|
+
console.log('╠══════════════════════════════════════════════════════════════╣');
|
|
205
|
+
|
|
206
|
+
if (braveResults.length > 0) {
|
|
207
|
+
console.log('║ 🌐 Brave Search (MCP) ║');
|
|
208
|
+
console.log(`║ Avg latency: ${avg(braveResults.map(r => r.avgLatencyMs)).toFixed(0)}ms`);
|
|
209
|
+
console.log(`║ Avg raw payload: ${avg(braveResults.map(r => r.avgRawSizeKB)).toFixed(1)}KB`);
|
|
210
|
+
console.log(`║ Avg reduced: ${avg(braveResults.map(r => r.avgReducedSizeKB)).toFixed(1)}KB`);
|
|
211
|
+
console.log(`║ Avg reduction: ${avg(braveResults.map(r => r.avgReductionPct)).toFixed(0)}%`);
|
|
212
|
+
console.log(`║ Token savings: ~${avg(braveResults.map(r => r.estimatedTokensBefore - r.estimatedTokensAfter)).toFixed(0)} tokens/query`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (deResults.length > 0) {
|
|
216
|
+
console.log('║ ║');
|
|
217
|
+
console.log('║ 🔍 Discovery Engine (Vertex AI) ║');
|
|
218
|
+
console.log(`║ Avg latency: ${avg(deResults.map(r => r.avgLatencyMs)).toFixed(0)}ms`);
|
|
219
|
+
console.log(`║ Avg raw payload: ${avg(deResults.map(r => r.avgRawSizeKB)).toFixed(1)}KB`);
|
|
220
|
+
console.log(`║ Avg reduced: ${avg(deResults.map(r => r.avgReducedSizeKB)).toFixed(1)}KB`);
|
|
221
|
+
console.log(`║ Avg reduction: ${avg(deResults.map(r => r.avgReductionPct)).toFixed(0)}%`);
|
|
222
|
+
console.log(`║ Token savings: ~${avg(deResults.map(r => r.estimatedTokensBefore - r.estimatedTokensAfter)).toFixed(0)} tokens/query`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (braveResults.length > 0 && deResults.length > 0) {
|
|
226
|
+
const braveLatency = avg(braveResults.map(r => r.avgLatencyMs));
|
|
227
|
+
const deLatency = avg(deResults.map(r => r.avgLatencyMs));
|
|
228
|
+
const latencyDiff = ((braveLatency - deLatency) / braveLatency * 100).toFixed(0);
|
|
229
|
+
console.log('║ ║');
|
|
230
|
+
console.log(`║ ⚡ DE is ${latencyDiff}% ${Number(latencyDiff) > 0 ? 'faster' : 'slower'} than Brave (pre-indexed vs live search)`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
console.log('╚══════════════════════════════════════════════════════════════╝');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ─── Utilities ───────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
function avg(nums: number[]): number {
|
|
239
|
+
return nums.length > 0 ? nums.reduce((a, b) => a + b, 0) / nums.length : 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function sleep(ms: number): Promise<void> {
|
|
243
|
+
return new Promise(r => setTimeout(r, ms));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ─── Entry ───────────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
runBenchmarks().catch(err => {
|
|
249
|
+
console.error('Benchmark error:', err);
|
|
250
|
+
process.exit(1);
|
|
251
|
+
});
|