mirror of
https://github.com/ruvnet/RuVector.git
synced 2026-05-25 15:03:46 +00:00
Phase 1 (Fact Verifier): verified 2 memories with grounding sources Phase 2 (Relation Generator): found 1 'contradicts' relation Phase 3 (Cross-Domain Explorer): framework working, needs JSON parse fix Phase 4 (Research Director): framework working, needs drift data Scripts: gemini-agents.js, deploy-gemini-agents.sh Cloud Run Job + 4 scheduler entries deploying. Brain grew: 1,809 → 1,812 (+3 from initial run) Co-Authored-By: claude-flow <ruv@ruv.net>
431 lines
16 KiB
JavaScript
431 lines
16 KiB
JavaScript
#!/usr/bin/env node
|
|
// rvAgent Gemini Grounding Agents (ADR-122)
|
|
// Implements 4 phases that use Gemini with Google Search grounding
|
|
// to verify, relate, explore, and research brain knowledge.
|
|
//
|
|
// Usage:
|
|
// node scripts/gemini-agents.js --phase fact-verify
|
|
// node scripts/gemini-agents.js --phase relate
|
|
// node scripts/gemini-agents.js --phase cross-domain
|
|
// node scripts/gemini-agents.js --phase research
|
|
// node scripts/gemini-agents.js --phase all
|
|
|
|
'use strict';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Configuration (from env vars)
|
|
// ---------------------------------------------------------------------------
|
|
const BRAIN_URL = process.env.BRAIN_URL || 'https://pi.ruv.io';
|
|
const BRAIN_AUTH = process.env.BRAIN_AUTH || 'Bearer ruvector-crawl-2026';
|
|
const GEMINI_API_KEY = process.env.GEMINI_API_KEY || '';
|
|
const GEMINI_MODEL = process.env.GEMINI_MODEL || 'gemini-2.5-flash';
|
|
const MAX_MEMORIES = parseInt(process.env.MAX_MEMORIES || '20', 10);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** Call Gemini with Google Search grounding enabled. */
|
|
async function callGemini(prompt) {
|
|
const url =
|
|
`https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent?key=${GEMINI_API_KEY}`;
|
|
|
|
const res = await fetch(url, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
contents: [{ role: 'user', parts: [{ text: prompt }] }],
|
|
tools: [{ google_search: {} }],
|
|
generationConfig: { maxOutputTokens: 1024, temperature: 0.3 },
|
|
}),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const body = await res.text();
|
|
throw new Error(`Gemini ${res.status}: ${body.slice(0, 300)}`);
|
|
}
|
|
|
|
const json = await res.json();
|
|
const text = json?.candidates?.[0]?.content?.parts?.[0]?.text || '';
|
|
const grounding = json?.candidates?.[0]?.groundingMetadata || null;
|
|
return { text, grounding };
|
|
}
|
|
|
|
/** Search brain memories. */
|
|
async function brainSearch(query, limit = 5) {
|
|
const res = await fetch(
|
|
`${BRAIN_URL}/v1/memories/search?q=${encodeURIComponent(query)}&limit=${limit}`,
|
|
{ headers: { Authorization: BRAIN_AUTH } },
|
|
);
|
|
if (!res.ok) {
|
|
console.warn(` brainSearch error ${res.status}`);
|
|
return [];
|
|
}
|
|
const data = await res.json();
|
|
return Array.isArray(data) ? data : data.memories || [];
|
|
}
|
|
|
|
/** List brain memories by category. */
|
|
async function brainList(category, limit = 5) {
|
|
const res = await fetch(
|
|
`${BRAIN_URL}/v1/memories/list?category=${encodeURIComponent(category)}&limit=${limit}`,
|
|
{ headers: { Authorization: BRAIN_AUTH } },
|
|
);
|
|
if (!res.ok) {
|
|
console.warn(` brainList error ${res.status}`);
|
|
return [];
|
|
}
|
|
const data = await res.json();
|
|
return Array.isArray(data) ? data : data.memories || [];
|
|
}
|
|
|
|
/** Inject a memory into the brain pipeline. */
|
|
async function brainShare(item) {
|
|
const res = await fetch(`${BRAIN_URL}/v1/pipeline/inject`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', Authorization: BRAIN_AUTH },
|
|
body: JSON.stringify({ source: 'gemini-agent', ...item }),
|
|
});
|
|
if (!res.ok) {
|
|
console.warn(` brainShare error ${res.status}`);
|
|
return null;
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
/** Ground a symbolic proposition in the brain. */
|
|
async function groundProposition(subject, predicate, object, confidence) {
|
|
try {
|
|
await fetch(`${BRAIN_URL}/v1/ground`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', Authorization: BRAIN_AUTH },
|
|
body: JSON.stringify({ subject, predicate, object, confidence, source: 'gemini-grounding' }),
|
|
});
|
|
} catch (err) {
|
|
console.warn(` groundProposition error: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
/** Strip PHI (emails, phone numbers, SSNs, date patterns). */
|
|
function stripPHI(text) {
|
|
return text
|
|
.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, '[EMAIL]')
|
|
.replace(/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g, '[PHONE]')
|
|
.replace(/\b\d{3}-\d{2}-\d{4}\b/g, '[SSN]')
|
|
.replace(/\b\d{1,2}\/\d{1,2}\/\d{2,4}\b/g, '[DATE]');
|
|
}
|
|
|
|
/** Extract first JSON object from text. */
|
|
function parseJSON(text) {
|
|
try {
|
|
const match = text.match(/\{[\s\S]*\}/);
|
|
if (match) return JSON.parse(match[0]);
|
|
} catch { /* ignore parse errors */ }
|
|
return null;
|
|
}
|
|
|
|
/** Sleep for ms milliseconds. */
|
|
function sleep(ms) {
|
|
return new Promise((r) => setTimeout(r, ms));
|
|
}
|
|
|
|
/** Retry-aware wrapper for callGemini with exponential backoff. */
|
|
async function callGeminiRetry(prompt, maxRetries = 3) {
|
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
try {
|
|
return await callGemini(prompt);
|
|
} catch (err) {
|
|
const is429 = err.message.includes('429');
|
|
const is500 = err.message.includes('500');
|
|
if (attempt === maxRetries - 1) throw err;
|
|
if (is429 || is500) {
|
|
const delay = Math.pow(2, attempt) * 1000;
|
|
console.warn(` Gemini ${is429 ? '429' : '500'}, retrying in ${delay}ms...`);
|
|
await sleep(delay);
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Phase 1: Fact Verifier
|
|
// ---------------------------------------------------------------------------
|
|
async function factVerify() {
|
|
console.log('\n--- Phase 1: Fact Verifier ---');
|
|
const memories = await brainSearch('*', MAX_MEMORIES);
|
|
console.log(` Fetched ${memories.length} memories to verify`);
|
|
|
|
let verified = 0;
|
|
let contradicted = 0;
|
|
let skipped = 0;
|
|
|
|
for (const mem of memories) {
|
|
const claim = (mem.title || '') + ': ' + (mem.content || '').slice(0, 500);
|
|
const sanitized = stripPHI(claim);
|
|
|
|
try {
|
|
const result = await callGeminiRetry(
|
|
`Verify this claim. Is it factually accurate based on current evidence? ` +
|
|
`Respond with JSON: {"verified": true/false, "confidence": 0.0-1.0, "correction": "..." or null, "sources": ["url1", "url2"]}` +
|
|
`\n\nClaim: ${sanitized}`,
|
|
);
|
|
|
|
const verification = parseJSON(result.text);
|
|
if (verification) {
|
|
const status = verification.verified ? 'verified' : 'contradicted';
|
|
await brainShare({
|
|
title: `Fact Check: ${(mem.title || '').slice(0, 80)}`,
|
|
content:
|
|
`Verification: ${verification.verified ? 'SUPPORTED' : 'CONTRADICTED'} ` +
|
|
`(confidence: ${verification.confidence}). ` +
|
|
`${verification.correction || 'No correction needed.'}`,
|
|
tags: ['fact-check', status, 'gemini-grounding', 'phase-1'],
|
|
category: 'pattern',
|
|
});
|
|
|
|
if (verification.verified) verified++;
|
|
else contradicted++;
|
|
|
|
if (result.grounding) {
|
|
const srcCount = result.grounding.groundingChunks?.length ||
|
|
result.grounding.sources || 0;
|
|
const supCount = result.grounding.groundingSupports?.length ||
|
|
result.grounding.supports || 0;
|
|
console.log(` [${status.toUpperCase()}] ${(mem.title || '').slice(0, 60)} — ${srcCount} sources, ${supCount} supports`);
|
|
} else {
|
|
console.log(` [${status.toUpperCase()}] ${(mem.title || '').slice(0, 60)}`);
|
|
}
|
|
} else {
|
|
skipped++;
|
|
console.log(` [SKIP] ${(mem.title || '').slice(0, 60)} — could not parse Gemini response`);
|
|
}
|
|
} catch (err) {
|
|
skipped++;
|
|
console.warn(` [ERROR] ${(mem.title || '').slice(0, 60)}: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
console.log(` Phase 1 complete: ${verified} verified, ${contradicted} contradicted, ${skipped} skipped`);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Phase 2: Relation Generator
|
|
// ---------------------------------------------------------------------------
|
|
async function generateRelations() {
|
|
console.log('\n--- Phase 2: Relation Generator ---');
|
|
const categories = ['architecture', 'solution', 'pattern', 'security'];
|
|
const allMems = [];
|
|
|
|
for (const cat of categories) {
|
|
const mems = await brainList(cat, 5);
|
|
allMems.push(...mems);
|
|
}
|
|
console.log(` Fetched ${allMems.length} memories across ${categories.length} categories`);
|
|
|
|
let relationsFound = 0;
|
|
let pairsEvaluated = 0;
|
|
|
|
for (let i = 0; i < allMems.length; i++) {
|
|
for (let j = i + 1; j < Math.min(allMems.length, i + 5); j++) {
|
|
const a = allMems[i];
|
|
const b = allMems[j];
|
|
pairsEvaluated++;
|
|
|
|
try {
|
|
const result = await callGeminiRetry(
|
|
`What is the relationship between these two knowledge items? ` +
|
|
`Respond with JSON: {"predicate": "causes|implies|requires|contradicts|enables|similar_to|no_relationship", "confidence": 0.0-1.0, "explanation": "..."}` +
|
|
`\n\nItem A: ${(a.title || '')}: ${(a.content || '').slice(0, 300)}` +
|
|
`\n\nItem B: ${(b.title || '')}: ${(b.content || '').slice(0, 300)}`,
|
|
);
|
|
|
|
const rel = parseJSON(result.text);
|
|
if (rel && rel.confidence > 0.6 && rel.predicate !== 'no_relationship') {
|
|
await brainShare({
|
|
title: `Relation: ${(a.title || '').slice(0, 30)} ${rel.predicate} ${(b.title || '').slice(0, 30)}`,
|
|
content: `${rel.predicate}: ${rel.explanation}. Source A: ${a.id || 'unknown'}, Source B: ${b.id || 'unknown'}`,
|
|
tags: ['relation', rel.predicate, 'gemini-grounding', 'horn-clause', 'phase-2'],
|
|
category: 'pattern',
|
|
});
|
|
|
|
await groundProposition(a.title || '', rel.predicate, b.title || '', rel.confidence);
|
|
relationsFound++;
|
|
console.log(` [REL] ${(a.title || '').slice(0, 25)} --${rel.predicate}--> ${(b.title || '').slice(0, 25)} (${rel.confidence})`);
|
|
}
|
|
} catch (err) {
|
|
console.warn(` [ERROR] pair ${i},${j}: ${err.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(` Phase 2 complete: ${relationsFound} relations from ${pairsEvaluated} pairs`);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Phase 3: Cross-Domain Explorer
|
|
// ---------------------------------------------------------------------------
|
|
async function crossDomainExplore() {
|
|
console.log('\n--- Phase 3: Cross-Domain Explorer ---');
|
|
const domains = [
|
|
{ query: 'melanoma skin cancer treatment', domain: 'medical' },
|
|
{ query: 'neural network deep learning', domain: 'cs' },
|
|
{ query: 'quantum mechanics dark matter', domain: 'physics' },
|
|
];
|
|
|
|
const domainMems = {};
|
|
for (const d of domains) {
|
|
domainMems[d.domain] = await brainSearch(d.query, 3);
|
|
console.log(` ${d.domain}: ${domainMems[d.domain].length} memories`);
|
|
}
|
|
|
|
let discoveries = 0;
|
|
const domainKeys = Object.keys(domainMems);
|
|
|
|
for (let i = 0; i < domainKeys.length; i++) {
|
|
for (let j = i + 1; j < domainKeys.length; j++) {
|
|
const dA = domainKeys[i];
|
|
const dB = domainKeys[j];
|
|
const memA = domainMems[dA][0];
|
|
const memB = domainMems[dB][0];
|
|
if (!memA || !memB) continue;
|
|
|
|
try {
|
|
const result = await callGeminiRetry(
|
|
`These two items are from different fields (${dA} and ${dB}). ` +
|
|
`Find a non-obvious connection between them. ` +
|
|
`Respond with JSON: {"connection": "...", "strength": 0.0-1.0, "novelty": 0.0-1.0, "bridge_concept": "..."}` +
|
|
`\n\nField ${dA}: ${(memA.title || '')}: ${(memA.content || '').slice(0, 300)}` +
|
|
`\n\nField ${dB}: ${(memB.title || '')}: ${(memB.content || '').slice(0, 300)}`,
|
|
);
|
|
|
|
const conn = parseJSON(result.text);
|
|
if (conn && conn.strength > 0.4) {
|
|
await brainShare({
|
|
title: `Cross-Domain: ${dA}<->${dB} via "${conn.bridge_concept}"`,
|
|
content: conn.connection,
|
|
tags: ['cross-domain', dA, dB, conn.bridge_concept || 'bridge', 'gemini-grounding', 'discovery', 'phase-3'],
|
|
category: 'pattern',
|
|
});
|
|
|
|
discoveries++;
|
|
console.log(` [BRIDGE] ${dA}<->${dB}: "${conn.bridge_concept}" (strength: ${conn.strength}, novelty: ${conn.novelty})`);
|
|
}
|
|
} catch (err) {
|
|
console.warn(` [ERROR] ${dA}<->${dB}: ${err.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(` Phase 3 complete: ${discoveries} cross-domain discoveries`);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Phase 4: Research Director
|
|
// ---------------------------------------------------------------------------
|
|
async function researchDirector() {
|
|
console.log('\n--- Phase 4: Research Director ---');
|
|
|
|
// 1. Check drift status
|
|
let drift = null;
|
|
try {
|
|
const driftRes = await fetch(`${BRAIN_URL}/v1/drift`, {
|
|
headers: { Authorization: BRAIN_AUTH },
|
|
});
|
|
if (driftRes.ok) drift = await driftRes.json();
|
|
} catch (err) {
|
|
console.warn(` Drift endpoint unavailable: ${err.message}`);
|
|
}
|
|
|
|
if (drift) {
|
|
console.log(` Drift status: ${JSON.stringify(drift).slice(0, 200)}`);
|
|
}
|
|
|
|
// 2. Formulate research questions about recent topics
|
|
const recentMems = await brainSearch('2025 2026 latest recent', 5);
|
|
console.log(` Found ${recentMems.length} recent memories for research`);
|
|
|
|
let findings = 0;
|
|
|
|
for (const mem of recentMems.slice(0, 3)) {
|
|
try {
|
|
const result = await callGeminiRetry(
|
|
`Based on this knowledge, what are 2 important questions that need answering? ` +
|
|
`Research these questions using current information. ` +
|
|
`Respond with JSON: {"questions": ["q1", "q2"], "answers": [{"question": "q1", "answer": "...", "confidence": 0.0-1.0}]}` +
|
|
`\n\nTopic: ${(mem.title || '')}: ${(mem.content || '').slice(0, 400)}`,
|
|
);
|
|
|
|
const research = parseJSON(result.text);
|
|
if (research && research.answers) {
|
|
for (const ans of research.answers) {
|
|
if (ans.confidence > 0.5) {
|
|
await brainShare({
|
|
title: `Research: ${(ans.question || '').slice(0, 80)}`,
|
|
content: ans.answer,
|
|
tags: ['research', 'gemini-grounding', 'auto-research', 'discovery', 'phase-4'],
|
|
category: 'solution',
|
|
});
|
|
findings++;
|
|
console.log(` [RESEARCH] ${(ans.question || '').slice(0, 70)} (conf: ${ans.confidence})`);
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.warn(` [ERROR] research on "${(mem.title || '').slice(0, 40)}": ${err.message}`);
|
|
}
|
|
}
|
|
|
|
console.log(` Phase 4 complete: ${findings} research findings injected`);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Main
|
|
// ---------------------------------------------------------------------------
|
|
const phase =
|
|
process.argv.find((a) => a.startsWith('--phase='))?.split('=')[1] ||
|
|
process.argv[process.argv.indexOf('--phase') + 1] ||
|
|
'all';
|
|
|
|
async function main() {
|
|
console.log(`=== Gemini Grounding Agents — Phase: ${phase} ===`);
|
|
console.log(`Brain: ${BRAIN_URL}, Model: ${GEMINI_MODEL}`);
|
|
|
|
if (!GEMINI_API_KEY) {
|
|
console.error('ERROR: GEMINI_API_KEY not set');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Get brain status before
|
|
let beforeCount = 0;
|
|
try {
|
|
const before = await fetch(`${BRAIN_URL}/v1/status`).then((r) => r.json());
|
|
beforeCount = before.total_memories || 0;
|
|
console.log(`Brain before: ${beforeCount} memories`);
|
|
} catch (err) {
|
|
console.warn(`Could not fetch brain status: ${err.message}`);
|
|
}
|
|
|
|
// Execute requested phase(s)
|
|
if (phase === 'fact-verify' || phase === 'all') await factVerify();
|
|
if (phase === 'relate' || phase === 'all') await generateRelations();
|
|
if (phase === 'cross-domain' || phase === 'all') await crossDomainExplore();
|
|
if (phase === 'research' || phase === 'all') await researchDirector();
|
|
|
|
// Get brain status after
|
|
try {
|
|
const after = await fetch(`${BRAIN_URL}/v1/status`).then((r) => r.json());
|
|
const afterCount = after.total_memories || 0;
|
|
console.log(`\nBrain after: ${afterCount} memories (+${afterCount - beforeCount})`);
|
|
} catch (err) {
|
|
console.warn(`Could not fetch brain status: ${err.message}`);
|
|
}
|
|
|
|
console.log('=== Done ===');
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error('Fatal error:', err);
|
|
process.exit(1);
|
|
});
|