feat(rah-light): replace workflows with guides system
- Remove entire workflow system (API routes, services, tools, components) - Add guides system from main ra-h repo (for external agent integration) - Update pane types: 'workflows' → 'guides' throughout - Update LeftToolbar to show guides icon instead of workflows - Update SettingsModal with GuidesViewer tab - Add GuidesPane for browsing guides in main UI - Clean up prompts to remove workflow references - Update tool registry to remove workflow tools - Add gray-matter package for frontmatter parsing Guides are essential for RA-H Light as they help external agents (via MCP) understand the knowledge base context and usage patterns. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
f383d770ca
commit
b31441b35f
@@ -0,0 +1,25 @@
|
||||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { listGuides } from '@/services/guides/guideService';
|
||||
|
||||
export const listGuidesTool = tool({
|
||||
description: 'List all available guides with their names and descriptions.',
|
||||
inputSchema: z.object({}),
|
||||
execute: async () => {
|
||||
try {
|
||||
const guides = listGuides();
|
||||
return {
|
||||
success: true,
|
||||
data: guides,
|
||||
message: `Found ${guides.length} guides`,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[listGuides] error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to list guides',
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { readGuide } from '@/services/guides/guideService';
|
||||
|
||||
export const readGuideTool = tool({
|
||||
description: 'Read a guide by name. Returns the full markdown content with instructions.',
|
||||
inputSchema: z.object({
|
||||
name: z.string().describe('The name of the guide to read'),
|
||||
}),
|
||||
execute: async ({ name }) => {
|
||||
try {
|
||||
const guide = readGuide(name);
|
||||
if (!guide) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Guide "${name}" not found`,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: guide,
|
||||
message: `Loaded guide: ${guide.name}`,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[readGuide] error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to read guide',
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { writeGuide } from '@/services/guides/guideService';
|
||||
import { eventBroadcaster } from '@/services/events';
|
||||
|
||||
export const writeGuideTool = tool({
|
||||
description: 'Write or update a guide. Content should be full markdown with YAML frontmatter (name, description).',
|
||||
inputSchema: z.object({
|
||||
name: z.string().describe('The name of the guide to write'),
|
||||
content: z.string().describe('Full markdown content including YAML frontmatter'),
|
||||
}),
|
||||
execute: async ({ name, content }) => {
|
||||
try {
|
||||
writeGuide(name, content);
|
||||
eventBroadcaster.broadcast({ type: 'GUIDE_UPDATED', data: { name } });
|
||||
return {
|
||||
success: true,
|
||||
data: { name },
|
||||
message: `Guide "${name}" saved`,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[writeGuide] error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to write guide',
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -18,7 +18,7 @@ export const TOOL_GROUPS: Record<string, ToolGroup> = {
|
||||
orchestration: {
|
||||
id: 'orchestration',
|
||||
name: 'Orchestration',
|
||||
description: 'Workflows, web search, and reasoning (orchestrator only)',
|
||||
description: 'Web search and reasoning tools',
|
||||
icon: '●',
|
||||
color: '#8b5cf6'
|
||||
},
|
||||
@@ -42,13 +42,9 @@ export const TOOL_GROUP_ASSIGNMENTS: Record<string, string> = {
|
||||
queryDimensionNodes: 'core',
|
||||
searchContentEmbeddings: 'core',
|
||||
|
||||
// Orchestration: Workflows and reasoning (orchestrator only)
|
||||
// Orchestration: Web search and reasoning
|
||||
webSearch: 'orchestration',
|
||||
think: 'orchestration',
|
||||
executeWorkflow: 'orchestration',
|
||||
listWorkflows: 'orchestration',
|
||||
getWorkflow: 'orchestration',
|
||||
editWorkflow: 'orchestration',
|
||||
|
||||
// Execution: Write operations and extraction (workers only)
|
||||
createNode: 'execution',
|
||||
|
||||
@@ -16,10 +16,6 @@ import { queryDimensionNodesTool } from '../database/queryDimensionNodes';
|
||||
import { searchContentEmbeddingsTool } from '../other/searchContentEmbeddings';
|
||||
import { webSearchTool } from '../other/webSearch';
|
||||
import { thinkTool } from '../other/think';
|
||||
import { executeWorkflowTool } from '../orchestration/executeWorkflow';
|
||||
import { listWorkflowsTool } from '../orchestration/listWorkflows';
|
||||
import { getWorkflowTool } from '../orchestration/getWorkflow';
|
||||
import { editWorkflowTool } from '../orchestration/editWorkflow';
|
||||
import { youtubeExtractTool } from '../other/youtubeExtract';
|
||||
import { websiteExtractTool } from '../other/websiteExtract';
|
||||
import { paperExtractTool } from '../other/paperExtract';
|
||||
@@ -41,10 +37,6 @@ const CORE_TOOLS: Record<string, any> = {
|
||||
const ORCHESTRATION_TOOLS: Record<string, any> = {
|
||||
webSearch: webSearchTool,
|
||||
think: thinkTool,
|
||||
executeWorkflow: executeWorkflowTool,
|
||||
listWorkflows: listWorkflowsTool,
|
||||
getWorkflow: getWorkflowTool,
|
||||
editWorkflow: editWorkflowTool,
|
||||
};
|
||||
|
||||
// Execution tools for worker agents (includes write operations)
|
||||
@@ -77,10 +69,6 @@ const ORCHESTRATOR_TOOL_NAMES = Array.from(new Set([
|
||||
...Object.keys(CORE_TOOLS),
|
||||
'webSearch',
|
||||
'think',
|
||||
'executeWorkflow',
|
||||
'listWorkflows',
|
||||
'getWorkflow',
|
||||
'editWorkflow',
|
||||
'createNode',
|
||||
'updateNode',
|
||||
'createEdge',
|
||||
@@ -95,9 +83,7 @@ const ORCHESTRATOR_TOOL_NAMES = Array.from(new Set([
|
||||
|
||||
const EXECUTOR_TOOL_NAMES = [
|
||||
...Object.keys(CORE_TOOLS),
|
||||
...Object.keys(ORCHESTRATION_TOOLS).filter(name =>
|
||||
name !== 'executeWorkflow'
|
||||
),
|
||||
...Object.keys(ORCHESTRATION_TOOLS),
|
||||
...Object.keys(EXECUTION_TOOLS),
|
||||
];
|
||||
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { WorkflowRegistry } from '@/services/workflows/registry';
|
||||
import { saveWorkflow, loadUserWorkflow } from '@/services/workflows/workflowFileService';
|
||||
|
||||
export const editWorkflowTool = tool({
|
||||
description: 'Update a workflow definition. Use to refine instructions, update description, or toggle enabled state.',
|
||||
inputSchema: z.object({
|
||||
workflowKey: z.string().describe('The key of the workflow to edit'),
|
||||
updates: z.object({
|
||||
displayName: z.string().optional().describe('New display name'),
|
||||
description: z.string().optional().describe('New description'),
|
||||
instructions: z.string().optional().describe('New instructions (full replacement)'),
|
||||
enabled: z.boolean().optional().describe('Enable or disable the workflow'),
|
||||
requiresFocusedNode: z.boolean().optional().describe('Whether workflow requires a focused node'),
|
||||
}).describe('Fields to update'),
|
||||
}),
|
||||
execute: async ({ workflowKey, updates }) => {
|
||||
try {
|
||||
// Get existing workflow (from user file or bundled default)
|
||||
const existing = await WorkflowRegistry.getWorkflowByKey(workflowKey);
|
||||
|
||||
if (!existing) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Workflow '${workflowKey}' not found`,
|
||||
};
|
||||
}
|
||||
|
||||
// Merge updates with existing values
|
||||
const updated = {
|
||||
key: workflowKey,
|
||||
displayName: updates.displayName ?? existing.displayName,
|
||||
description: updates.description ?? existing.description,
|
||||
instructions: updates.instructions ?? existing.instructions,
|
||||
enabled: updates.enabled ?? existing.enabled,
|
||||
requiresFocusedNode: updates.requiresFocusedNode ?? existing.requiresFocusedNode,
|
||||
};
|
||||
|
||||
// Save to user workflow file (this will override bundled if same key)
|
||||
saveWorkflow(updated);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Workflow '${workflowKey}' updated`,
|
||||
workflow: {
|
||||
key: updated.key,
|
||||
displayName: updated.displayName,
|
||||
description: updated.description,
|
||||
enabled: updated.enabled,
|
||||
requiresFocusedNode: updated.requiresFocusedNode,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('editWorkflow error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to edit workflow',
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,127 +0,0 @@
|
||||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { WorkflowRegistry } from '@/services/workflows/registry';
|
||||
import { getSQLiteClient } from '@/services/database/sqlite-client';
|
||||
import { WorkflowExecutor } from '@/services/agents/workflowExecutor';
|
||||
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. Execute workflow directly
|
||||
const requestContext = RequestContext.get();
|
||||
console.log('[executeWorkflowTool] Current traceId:', requestContext.traceId);
|
||||
|
||||
const sessionId = `workflow_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
|
||||
try {
|
||||
const result = await WorkflowExecutor.execute({
|
||||
sessionId,
|
||||
task,
|
||||
context: contextLines,
|
||||
expectedOutcome: workflow.expectedOutcome,
|
||||
traceId: requestContext.traceId,
|
||||
parentChatId: requestContext.parentChatId,
|
||||
workflowKey,
|
||||
workflowNodeId: nodeId,
|
||||
});
|
||||
|
||||
RequestContext.set({ workflowKey: undefined, workflowNodeId: undefined });
|
||||
|
||||
const workflowLabel = workflow.displayName || workflowKey;
|
||||
return {
|
||||
success: true,
|
||||
message: `Workflow **${workflowLabel}** completed.`,
|
||||
summary: result.summary,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[executeWorkflowTool] Workflow execution failed', error);
|
||||
RequestContext.set({ workflowKey: undefined, workflowNodeId: undefined });
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
return { success: false, error: `Workflow execution failed: ${errorMessage}` };
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,43 +0,0 @@
|
||||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { WorkflowRegistry, BUNDLED_WORKFLOW_KEYS } from '@/services/workflows/registry';
|
||||
import { userWorkflowExists } from '@/services/workflows/workflowFileService';
|
||||
|
||||
export const getWorkflowTool = tool({
|
||||
description: 'Retrieve a workflow definition including its full instructions',
|
||||
inputSchema: z.object({
|
||||
workflowKey: z.string().describe('The key of the workflow to retrieve (e.g., "integrate", "prep")'),
|
||||
}),
|
||||
execute: async ({ workflowKey }) => {
|
||||
try {
|
||||
const workflow = await WorkflowRegistry.getWorkflowByKey(workflowKey);
|
||||
|
||||
if (!workflow) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Workflow '${workflowKey}' not found`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
workflow: {
|
||||
key: workflow.key,
|
||||
displayName: workflow.displayName,
|
||||
description: workflow.description,
|
||||
instructions: workflow.instructions,
|
||||
enabled: workflow.enabled,
|
||||
requiresFocusedNode: workflow.requiresFocusedNode,
|
||||
isBundled: BUNDLED_WORKFLOW_KEYS.has(workflow.key),
|
||||
hasUserOverride: userWorkflowExists(workflow.key),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('getWorkflow error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to get workflow',
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { WorkflowRegistry, BUNDLED_WORKFLOW_KEYS } from '@/services/workflows/registry';
|
||||
|
||||
export const listWorkflowsTool = tool({
|
||||
description: 'List all available workflows',
|
||||
inputSchema: z.object({}),
|
||||
execute: async () => {
|
||||
try {
|
||||
const workflows = await WorkflowRegistry.getAllWorkflows();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
workflows: workflows.map(w => ({
|
||||
key: w.key,
|
||||
displayName: w.displayName,
|
||||
description: w.description,
|
||||
enabled: w.enabled,
|
||||
requiresFocusedNode: w.requiresFocusedNode,
|
||||
isBundled: BUNDLED_WORKFLOW_KEYS.has(w.key),
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('listWorkflows error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to list workflows',
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user