Initial commit: RA-H Open Source Edition

Local-first knowledge management system with BYO API keys.

Features:
- 3-panel UI (Nodes | Focus | Helpers)
- SQLite + sqlite-vec for vector search
- Agent system (Easy/Hard mode orchestrators)
- Content extraction (YouTube, PDF, web)
- Integrate workflow for connection discovery
- Dimension system with auto-assignment

Tech stack:
- Next.js 15 + TypeScript + Tailwind CSS
- Anthropic (Claude) + OpenAI (GPT) via Vercel AI SDK

Setup:
  npm install && npm rebuild better-sqlite3
  scripts/dev/bootstrap-local.sh
  npm run dev

MIT License
This commit is contained in:
“BeeRad”
2025-12-15 16:14:28 +11:00
commit 733d1c3407
226 changed files with 46231 additions and 0 deletions
@@ -0,0 +1,221 @@
import { tool } from 'ai';
import { z } from 'zod';
import { AgentDelegationService } from '@/services/agents/delegation';
import { MiniRAHExecutor } from '@/services/agents/executor';
import { RequestContext } from '@/services/context/requestContext';
import type { Node } from '@/types/database';
import { nodeService } from '@/services/database/nodes';
type CapsuleNodeRole = 'primary' | 'secondary' | 'referenced';
interface CapsuleNodeSnapshot {
id: number;
title: string | null;
link: string | null;
dimensions: string[];
chunkStatus: string;
hasChunks: boolean;
excerpt: string | null;
role: CapsuleNodeRole;
}
interface DelegationCapsule {
version: number;
generatedAt: string;
primary: CapsuleNodeSnapshot | null;
secondary: CapsuleNodeSnapshot[];
referenced: CapsuleNodeSnapshot[];
focusCount: number;
}
function truncateWords(text: string, limit: number): string {
if (!text) return '';
const words = text.trim().split(/\s+/);
if (words.length <= limit) return text.trim();
return `${words.slice(0, limit).join(' ')}`;
}
function describeChunk(node: Node): { status: string; hasChunks: boolean } {
const status = node.chunk_status || 'unknown';
const hasChunks = status === 'chunked' || (typeof node.chunk === 'string' && node.chunk.length > 0);
return { status, hasChunks };
}
function buildSnapshot(node: Node, role: CapsuleNodeRole): CapsuleNodeSnapshot {
const primaryText = (node.content || node.description || '').trim();
const linkFallback = node.link || '';
const excerptSource = primaryText.length > 0 ? primaryText : linkFallback;
const { status, hasChunks } = describeChunk(node);
return {
id: node.id,
title: node.title || null,
link: node.link || null,
dimensions: node.dimensions || [],
chunkStatus: status,
hasChunks,
excerpt: excerptSource ? truncateWords(excerptSource, 80) : null,
role,
};
}
function formatCapsuleForLLM(capsule: DelegationCapsule): string {
const lines: string[] = ['=== DELEGATION CAPSULE ==='];
if (capsule.primary) {
lines.push('Primary focus:', formatSnapshotLine(capsule.primary));
} else {
lines.push('Primary focus: None');
}
if (capsule.secondary.length > 0) {
lines.push('Secondary focus nodes:');
capsule.secondary.forEach(snapshot => {
lines.push(`- ${formatSnapshotLine(snapshot)}`);
});
} else {
lines.push('Secondary focus nodes: None');
}
if (capsule.referenced.length > 0) {
lines.push('Referenced nodes provided in this task:');
capsule.referenced.forEach(snapshot => {
lines.push(`- ${formatSnapshotLine(snapshot)}`);
});
}
lines.push('Instructions:',
'- Use the capsule snapshots as ground truth for node IDs and titles.',
'- Call getNodesById if you need the full record beyond the provided excerpt.',
'- List the node IDs you used in the "Context sources used" line of your final summary.',
'=== END CAPSULE ===');
return lines.join('\n');
}
function formatSnapshotLine(snapshot: CapsuleNodeSnapshot): string {
const dimensionLabel = snapshot.dimensions.length > 0 ? snapshot.dimensions.join(', ') : 'none';
const excerpt = snapshot.excerpt ? `Excerpt: ${snapshot.excerpt}` : 'Excerpt: (no stored content; hydrate via getNodesById if required)';
return `[NODE:${snapshot.id}:"${snapshot.title || 'Untitled'}"] | role=${snapshot.role} | dimensions=${dimensionLabel} | chunk_status=${snapshot.chunkStatus} | ${excerpt}`;
}
function buildSourceBlock(snapshot: CapsuleNodeSnapshot): string {
return [
`=== SOURCE: NODE ${snapshot.id} ===`,
`Title: "${snapshot.title || 'Untitled'}"`,
`Role: ${snapshot.role}`,
`Chunk status: ${snapshot.chunkStatus} (has_chunks=${snapshot.hasChunks})`,
snapshot.link ? `Link: ${snapshot.link}` : 'Link: None',
snapshot.dimensions.length > 0 ? `Dimensions: ${snapshot.dimensions.join(', ')}` : 'Dimensions: None',
snapshot.excerpt ? `Excerpt: ${snapshot.excerpt}` : 'Excerpt: (not available) — call getNodesById if you need more context.',
'====================='
].join('\n');
}
async function hydrateReferencedNodes(nodeIds: number[]): Promise<Node[]> {
const nodes: Node[] = [];
for (const id of nodeIds) {
try {
const node = await nodeService.getNodeById(id);
if (node) {
nodes.push(node);
}
} catch (error) {
console.warn(`delegateToMiniRAH: failed to load node ${id}`, error);
}
}
return nodes;
}
export const delegateToMiniRAHTool = tool({
description: 'Delegate task to mini worker',
inputSchema: z.object({
task: z.string().describe('Clear, actionable description of what the mini ra-h should do'),
context: z.array(z.string()).max(16).default([]).describe('Optional context: URLs, node IDs, or key information the worker needs'),
expectedOutcome: z.string().optional().describe('Optional: what format or structure you expect in the summary'),
}),
execute: async ({ task, context = [], expectedOutcome }) => {
const requestContext = RequestContext.get();
const openTabs = (requestContext.openTabs ?? []) as Node[];
const activeTabId = requestContext.activeTabId ?? null;
const providedEntries = Array.isArray(context) ? context.filter(entry => typeof entry === 'string') as string[] : [];
const referencedNodeIds = new Set<number>();
const passthroughEntries: string[] = [];
providedEntries.forEach(entry => {
const trimmed = entry.trim();
const numericMatch = trimmed.match(/^(?:node_id:)?(\d+)$/i);
if (numericMatch) {
const id = Number(numericMatch[1]);
if (Number.isFinite(id) && id > 0) {
referencedNodeIds.add(id);
}
return;
}
passthroughEntries.push(entry);
});
const focusMap = new Map<number, Node>();
openTabs.forEach(node => {
if (node && typeof node.id === 'number') {
focusMap.set(node.id, node);
}
});
const additionalIds = Array.from(referencedNodeIds).filter(id => !focusMap.has(id));
const referencedNodes = await hydrateReferencedNodes(additionalIds);
referencedNodes.forEach(node => focusMap.set(node.id, node));
const focusNodes = Array.from(focusMap.values());
const primaryNode = focusNodes.find(node => node.id === activeTabId) || focusNodes[0] || null;
const secondaryNodes = focusNodes.filter(node => primaryNode && node.id !== primaryNode.id && openTabs.some(tab => tab.id === node.id));
const referencedOnlyNodes = focusNodes.filter(node => !openTabs.some(tab => tab.id === node.id));
const capsule: DelegationCapsule = {
version: 1,
generatedAt: new Date().toISOString(),
primary: primaryNode ? buildSnapshot(primaryNode, 'primary') : null,
secondary: secondaryNodes.map(node => buildSnapshot(node, 'secondary')),
referenced: referencedOnlyNodes.map(node => buildSnapshot(node, 'referenced')),
focusCount: openTabs.length,
};
const sourceBlocks: string[] = [];
const snapshots: CapsuleNodeSnapshot[] = [];
if (capsule.primary) snapshots.push(capsule.primary);
snapshots.push(...capsule.secondary, ...capsule.referenced);
snapshots.forEach(snapshot => {
sourceBlocks.push(buildSourceBlock(snapshot));
});
const enrichedContext: string[] = [
`CAPSULE_JSON::${JSON.stringify(capsule)}`,
formatCapsuleForLLM(capsule),
...sourceBlocks,
...passthroughEntries,
].slice(0, 16);
const delegation = AgentDelegationService.createDelegation({
task,
context: enrichedContext,
expectedOutcome,
supabaseToken: null,
});
const execution = await MiniRAHExecutor.execute({
sessionId: delegation.sessionId,
task,
context: enrichedContext,
expectedOutcome,
traceId: requestContext.traceId,
parentChatId: requestContext.parentChatId,
workflowKey: requestContext.workflowKey,
workflowNodeId: requestContext.workflowNodeId,
});
const summary = execution?.summary || 'Task delegated but no summary returned.';
const status = execution?.status || 'completed';
const sessionSuffix = delegation.sessionId.split('_').pop();
const statusLabel = status === 'completed' ? 'completed the task' : 'flagged an issue';
return `Mini ra-h (session ${sessionSuffix}) ${statusLabel}:\n\n${summary}`;
},
});
@@ -0,0 +1,43 @@
import { tool } from 'ai';
import { z } from 'zod';
import { AgentDelegationService } from '@/services/agents/delegation';
import { WiseRAHExecutor } from '@/services/agents/wiseRAHExecutor';
import { RequestContext } from '@/services/context/requestContext';
export const delegateToWiseRAHTool = tool({
description: 'Delegate complex workflows to wise ra-h (GPT-5)',
inputSchema: z.object({
task: z.string().describe('Complex workflow description: what needs to be planned and executed'),
context: z.array(z.string()).max(8).default([]).describe('Optional context: node IDs, URLs, or key information the planner needs'),
expectedOutcome: z.string().optional().describe('Optional: what final result or format you expect in the summary'),
workflowKey: z.string().optional().describe('Optional: workflow key if invoked via executeWorkflow'),
workflowNodeId: z.number().optional().describe('Optional: target node ID for workflow'),
}),
execute: async ({ task, context = [], expectedOutcome, workflowKey, workflowNodeId }) => {
const requestContext = RequestContext.get();
console.log('[delegateToWiseRAH] Current traceId:', requestContext.traceId);
const delegation = AgentDelegationService.createDelegation({
task,
context,
expectedOutcome,
agentType: 'wise-rah',
supabaseToken: null,
});
const execution = await WiseRAHExecutor.execute({
sessionId: delegation.sessionId,
task,
context,
expectedOutcome,
traceId: requestContext.traceId,
parentChatId: requestContext.parentChatId,
workflowKey,
workflowNodeId,
});
// Return a simple string that Claude can directly use in conversation
const summary = execution?.summary || 'Wise ra-h delegated but no summary returned.';
return `Wise ra-h (session ${delegation.sessionId.split('_').pop()}) completed the workflow:\n\n${summary}`;
},
});
@@ -0,0 +1,60 @@
import { tool } from 'ai';
import { z } from 'zod';
import { delegateToMiniRAHTool } from './delegateToMiniRAH';
export const delegateNodeQuotesTool = tool({
description: 'Extract quotes from single node',
inputSchema: z.object({
nodeId: z.number().int().positive().describe('The node to read from'),
question: z.string().min(3).describe('The question or prompt the quotes should answer'),
maxQuotes: z.number().int().min(1).max(6).default(3).describe('Maximum number of quotes to return'),
requireSummary: z.boolean().default(false).describe('Whether to include a short synthesis after the quotes'),
}),
execute: async ({ nodeId, question, maxQuotes, requireSummary }) => {
const task = `Extract up to ${maxQuotes} ready-to-use verbatim quotes (include speaker/source if present) from [NODE:${nodeId}] that answer: "${question}". Provide each quote with a one-line takeaway.`;
const context = [String(nodeId), `REQUIRE_SYNTHESIS:${requireSummary ? 'yes' : 'no'}`];
const expectedOutcome = requireSummary
? 'Use the required Task/Actions/Result/Node/Context sources used/Follow-up template. In the Result line, list the quotes (prefixed with takeaways) first, then add a 2-3 sentence comparison summary. Always finish with "Context sources used" listing every NODE ID referenced.'
: 'Use the required Task/Actions/Result/Node/Context sources used/Follow-up template. In the Result line, list only the quotes (each prefixed with a takeaway) and end the summary with "Context sources used" covering every NODE ID referenced.';
if (typeof delegateToMiniRAHTool.execute !== 'function') {
throw new Error('delegateToMiniRAH tool is unavailable.');
}
return delegateToMiniRAHTool.execute({
task,
context,
expectedOutcome,
}, undefined as any);
}
});
export const delegateNodeComparisonTool = tool({
description: 'Create synthesis from gathered evidence',
inputSchema: z.object({
title: z.string().min(3).describe('Title for the synthesis node'),
comparisonPrompt: z.string().min(5).describe('Short description of what to compare or conclude'),
sourceNodeIds: z.array(z.number().int().positive()).min(1).max(8).describe('Source nodes that must be cited'),
includeOutline: z.boolean().default(false).describe('If true, worker should draft with numbered sections matching current outline in context'),
}),
execute: async ({ title, comparisonPrompt, sourceNodeIds, includeOutline }) => {
const task = `Using the gathered evidence, draft the final synthesis titled "${title}". Compare or conclude on: ${comparisonPrompt}.`;
const outlineHint = includeOutline
? 'Follow the outline provided in the context exactly (use the same headings).'
: 'Structure the answer with clear sections (Intro, Comparison, Takeaways).';
const contextEntries = sourceNodeIds.map(id => String(id));
contextEntries.push(outlineHint);
const expectedOutcome = 'Use the required Task/Actions/Result/Node/Context sources used/Follow-up template. In the Result line, deliver polished prose ready for createNode/updateNode, cite specific quotes inline where relevant, and finish with "Context sources used" listing every NODE ID you relied on.';
if (typeof delegateToMiniRAHTool.execute !== 'function') {
throw new Error('delegateToMiniRAH tool is unavailable.');
}
return delegateToMiniRAHTool.execute({
task,
context: contextEntries,
expectedOutcome,
}, undefined as any);
}
});
+130
View File
@@ -0,0 +1,130 @@
import { tool } from 'ai';
import { z } from 'zod';
import { WorkflowRegistry } from '@/services/workflows/registry';
import { getSQLiteClient } from '@/services/database/sqlite-client';
import { AgentDelegationService } from '@/services/agents/delegation';
import { WiseRAHExecutor } from '@/services/agents/wiseRAHExecutor';
import { RequestContext } from '@/services/context/requestContext';
import { getAutoContextSummaries } from '@/services/context/autoContext';
export const executeWorkflowTool = tool({
description: 'Execute predefined workflow via wise ra-h',
inputSchema: z.object({
workflowKey: z.string().describe('Key of workflow to execute (e.g., "integrate")'),
nodeId: z.number().describe('ID of node to run workflow on (usually focused node)'),
userContext: z.string().optional().describe('Optional: Additional context or instructions from user'),
}),
execute: async ({ workflowKey, nodeId, userContext }) => {
// 1. Fetch workflow definition
const workflow = await WorkflowRegistry.getWorkflowByKey(workflowKey);
if (!workflow) {
return { success: false, error: `Workflow '${workflowKey}' not found` };
}
if (!workflow.enabled) {
return { success: false, error: `Workflow '${workflowKey}' is disabled` };
}
// 2. Validate node requirement
if (workflow.requiresFocusedNode && !nodeId) {
return { success: false, error: `Workflow '${workflowKey}' requires a focused node` };
}
// 2.5. Prevent re-running same workflow on same node within 1 hour
if (nodeId) {
const db = getSQLiteClient();
const recentRuns = db.query<{ id: number }>(
`SELECT id FROM chats
WHERE json_extract(metadata, '$.workflow_key') = ?
AND json_extract(metadata, '$.workflow_node_id') = ?
AND datetime(created_at) > datetime('now', '-1 hour')
LIMIT 1`,
[workflowKey, nodeId]
).rows;
if (recentRuns.length > 0) {
return {
success: false,
error: `Workflow '${workflowKey}' already ran on node ${nodeId} within the last hour. Check the node content - the Integration Analysis section should already be there.`
};
}
}
// 3. Validate node exists and fetch full context (if node provided)
let contextLines: string[] = [];
if (nodeId) {
const db = getSQLiteClient();
const stmt = db.prepare('SELECT * FROM nodes WHERE id = ?');
const node = stmt.get(nodeId) as any;
if (!node) {
return { success: false, error: `Node ${nodeId} not found` };
}
contextLines = [
`Focused Node: [NODE:${node.id}:"${node.title}"]`,
node.description ? `Description: ${node.description}` : null,
node.content ? `Content: ${node.content}` : null,
node.link ? `Link: ${node.link}` : null,
].filter(Boolean) as string[];
}
// Removed conflicting guardrail - wise-rah has updateNode access and should use it
if (userContext) {
contextLines.push(`User Context: ${userContext}`);
}
// 4. Build task with workflow instructions
RequestContext.set({ workflowKey, workflowNodeId: nodeId });
const autoContextSummaries = getAutoContextSummaries(6);
if (autoContextSummaries.length > 0) {
contextLines.push('Background Context (Top Hubs):');
autoContextSummaries.forEach((summary) => {
contextLines.push(
`[NODE:${summary.id}:"${summary.title}"] (${summary.edgeCount} edges)`
);
});
}
const task = `Execute workflow: ${workflow.displayName}
${workflow.instructions}
${nodeId ? `Target Node ID: ${nodeId}` : 'No specific node targeted (general workflow)'}`;
// 5. Delegate to wise ra-h oracle with workflow metadata
const requestContext = RequestContext.get();
console.log('[executeWorkflowTool] Current traceId:', requestContext.traceId);
const delegation = AgentDelegationService.createDelegation({
task,
context: contextLines,
expectedOutcome: workflow.expectedOutcome,
agentType: 'wise-rah',
supabaseToken: null,
});
// Fire-and-forget execution so main ra-h stays responsive while workflow runs
void WiseRAHExecutor.execute({
sessionId: delegation.sessionId,
task,
context: contextLines,
expectedOutcome: workflow.expectedOutcome,
traceId: requestContext.traceId,
parentChatId: requestContext.parentChatId,
workflowKey,
workflowNodeId: nodeId,
}).catch((error) => {
console.error('[executeWorkflowTool] Wise ra-h delegation failed', error);
});
RequestContext.set({ workflowKey: undefined, workflowNodeId: undefined });
const shortSessionId = delegation.sessionId.split('_').pop();
const workflowLabel = workflow.displayName || workflowKey;
return [
`Delegated **${workflowLabel}** to wise ra-h (session ${shortSessionId}).`,
'Keep working here—wise ra-h will stream every step inside its tab and post the final summary when complete.',
].join('\n');
},
});