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,27 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { readGuide } from '@/services/guides/guideService';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ name: string }> }
|
||||
) {
|
||||
try {
|
||||
const { name } = await params;
|
||||
const guide = readGuide(name);
|
||||
if (!guide) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: `Guide "${name}" not found` },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
return NextResponse.json({ success: true, data: guide });
|
||||
} catch (error) {
|
||||
console.error('[API /guides/[name]] error:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : 'Failed to read guide' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { listGuides } from '@/services/guides/guideService';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const guides = listGuides();
|
||||
return NextResponse.json({ success: true, data: guides });
|
||||
} catch (error) {
|
||||
console.error('[API /guides] error:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : 'Failed to list guides' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { saveWorkflow, deleteWorkflow, userWorkflowExists } from '@/services/workflows/workflowFileService';
|
||||
import { WorkflowRegistry, BUNDLED_WORKFLOW_KEYS } from '@/services/workflows/registry';
|
||||
|
||||
// PUT /api/workflows/[key] - Create or update a workflow
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ key: string }> }
|
||||
) {
|
||||
try {
|
||||
const { key } = await params;
|
||||
const body = await request.json();
|
||||
|
||||
// Validate required fields
|
||||
if (!body.displayName || typeof body.displayName !== 'string') {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'displayName is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!body.instructions || typeof body.instructions !== 'string') {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'instructions is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Save the workflow
|
||||
saveWorkflow({
|
||||
key,
|
||||
displayName: body.displayName.trim(),
|
||||
description: body.description?.trim() || '',
|
||||
instructions: body.instructions,
|
||||
enabled: body.enabled !== false,
|
||||
requiresFocusedNode: body.requiresFocusedNode !== false,
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error saving workflow:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to save workflow' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/workflows/[key] - Delete a workflow (resets to default if bundled)
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ key: string }> }
|
||||
) {
|
||||
try {
|
||||
const { key } = await params;
|
||||
|
||||
// Check if user file exists
|
||||
if (!userWorkflowExists(key)) {
|
||||
// If it's a bundled workflow, nothing to delete
|
||||
if (BUNDLED_WORKFLOW_KEYS.has(key)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Cannot delete bundled workflow (no user override exists)' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Workflow not found' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const deleted = deleteWorkflow(key);
|
||||
|
||||
if (!deleted) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to delete workflow' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// If this was a bundled workflow, it will now fall back to default
|
||||
const isBundled = BUNDLED_WORKFLOW_KEYS.has(key);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
resetToDefault: isBundled,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error deleting workflow:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to delete workflow' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/workflows/[key] - Get a single workflow
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ key: string }> }
|
||||
) {
|
||||
try {
|
||||
const { key } = await params;
|
||||
const workflow = await WorkflowRegistry.getWorkflowByKey(key);
|
||||
|
||||
if (!workflow) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Workflow not found' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Include metadata about whether it's user-modified
|
||||
const hasUserOverride = userWorkflowExists(key);
|
||||
const isBundled = BUNDLED_WORKFLOW_KEYS.has(key);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
...workflow,
|
||||
isBundled,
|
||||
hasUserOverride,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching workflow:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to fetch workflow' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { WorkflowRegistry } from '@/services/workflows/registry';
|
||||
import { getSQLiteClient } from '@/services/database/sqlite-client';
|
||||
import { WorkflowExecutor } from '@/services/agents/workflowExecutor';
|
||||
import { getAutoContextSummaries } from '@/services/context/autoContext';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { workflowKey, nodeId, userContext } = body;
|
||||
|
||||
// Validate workflowKey
|
||||
if (!workflowKey || typeof workflowKey !== 'string') {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'workflowKey is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Fetch workflow definition
|
||||
const workflow = await WorkflowRegistry.getWorkflowByKey(workflowKey);
|
||||
if (!workflow) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: `Workflow '${workflowKey}' not found` },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!workflow.enabled) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: `Workflow '${workflowKey}' is disabled` },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate node requirement
|
||||
if (workflow.requiresFocusedNode && !nodeId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: `Workflow '${workflowKey}' requires a nodeId` },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 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 NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: `Workflow '${workflowKey}' already ran on node ${nodeId} within the last hour`
|
||||
},
|
||||
{ status: 429 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Build context
|
||||
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 NextResponse.json(
|
||||
{ success: false, error: `Node ${nodeId} not found` },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
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[];
|
||||
}
|
||||
|
||||
if (userContext) {
|
||||
contextLines.push(`User Context: ${userContext}`);
|
||||
}
|
||||
|
||||
// Add auto-context summaries
|
||||
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)'}`;
|
||||
|
||||
const sessionId = `workflow_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
|
||||
try {
|
||||
// Execute workflow synchronously
|
||||
const result = await WorkflowExecutor.execute({
|
||||
sessionId,
|
||||
task,
|
||||
context: contextLines,
|
||||
expectedOutcome: workflow.expectedOutcome,
|
||||
workflowKey,
|
||||
workflowNodeId: nodeId,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
sessionId,
|
||||
workflowKey,
|
||||
nodeId: nodeId || null,
|
||||
status: 'completed',
|
||||
summary: result.summary,
|
||||
message: `Workflow '${workflow.displayName}' completed`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[/api/workflows/execute] Execution failed:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
return NextResponse.json(
|
||||
{ success: false, error: `Workflow execution failed: ${errorMessage}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error executing workflow:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to execute workflow' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { WorkflowRegistry } from '@/services/workflows/registry';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const workflows = await WorkflowRegistry.getAllWorkflows();
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: workflows,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching workflows:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to fetch workflows' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user