sync: editable workflows from private repo
- Users can edit/create workflows from Settings → Workflows tab - File-based storage for custom workflows - Bundled workflows serve as fallback when no user file exists - Added createEdge tool access for workflows (Quick Link) - Cleaned up stale terminology in workflow prompts Files synced: - src/services/workflows/workflowFileService.ts (NEW) - app/api/workflows/[key]/route.ts (NEW) - src/services/workflows/registry.ts - src/components/settings/WorkflowsViewer.tsx - src/tools/infrastructure/registry.ts - src/services/agents/wiseRAHExecutor.ts - src/config/workflows/integrate.ts - src/config/prompts/wise-rah.ts
This commit is contained in:
@@ -163,21 +163,24 @@ export class WiseRAHExecutor {
|
||||
);
|
||||
|
||||
// Enforce read-only constraint - remove write tools EXCEPT for workflows
|
||||
// Workflows (like integrate) need updateNode for direct content updates
|
||||
const writeToolsToRemove = isWorkflow
|
||||
? ['createNode', 'createEdge', 'updateEdge', 'embedContent', 'youtubeExtract', 'websiteExtract', 'paperExtract', 'delegateToWiseRAH']
|
||||
// Workflows need updateNode for content updates and createEdge for Quick Link workflow
|
||||
const writeToolsToRemove = isWorkflow
|
||||
? ['createNode', 'updateEdge', 'embedContent', 'youtubeExtract', 'websiteExtract', 'paperExtract', 'delegateToWiseRAH']
|
||||
: ['createNode', 'updateNode', 'createEdge', 'updateEdge', 'embedContent', 'youtubeExtract', 'websiteExtract', 'paperExtract', 'delegateToWiseRAH'];
|
||||
|
||||
|
||||
writeToolsToRemove.forEach(toolName => {
|
||||
if (toolName in wrappedTools) {
|
||||
console.warn(`WiseRAHExecutor: ${toolName} detected in planner toolset. Removing to enforce read-only constraint.`);
|
||||
delete wrappedTools[toolName];
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (isWorkflow && 'updateNode' in wrappedTools) {
|
||||
console.log('✅ [WiseRAHExecutor] updateNode preserved for workflow execution');
|
||||
}
|
||||
if (isWorkflow && 'createEdge' in wrappedTools) {
|
||||
console.log('✅ [WiseRAHExecutor] createEdge preserved for workflow execution');
|
||||
}
|
||||
console.log('🔒 [WiseRAHExecutor] Final tools after read-only enforcement:', Object.keys(wrappedTools));
|
||||
|
||||
console.log('📝 [WiseRAHExecutor] Starting manual agentic loop...');
|
||||
|
||||
@@ -1,30 +1,79 @@
|
||||
import { INTEGRATE_WORKFLOW_INSTRUCTIONS } from '@/config/workflows/integrate';
|
||||
import type { WorkflowDefinition } from './types';
|
||||
import { listUserWorkflows, loadUserWorkflow } from './workflowFileService';
|
||||
|
||||
// Bundled default workflows (always available as fallback)
|
||||
const BUNDLED_WORKFLOWS: Record<string, WorkflowDefinition> = {
|
||||
'integrate': {
|
||||
id: 1,
|
||||
key: 'integrate',
|
||||
displayName: 'Integrate',
|
||||
description: 'Deep analysis and connection-building for focused node',
|
||||
instructions: INTEGRATE_WORKFLOW_INSTRUCTIONS,
|
||||
enabled: true,
|
||||
requiresFocusedNode: true,
|
||||
primaryActor: 'oracle',
|
||||
expectedOutcome: 'Focused node updated with insights; 3-5 high-value edges created',
|
||||
}
|
||||
};
|
||||
|
||||
// Set of bundled workflow keys (for UI to know which can be "reset to default")
|
||||
export const BUNDLED_WORKFLOW_KEYS = new Set(Object.keys(BUNDLED_WORKFLOWS));
|
||||
|
||||
function userWorkflowToDefinition(uw: ReturnType<typeof loadUserWorkflow>, id: number): WorkflowDefinition {
|
||||
if (!uw) throw new Error('Cannot convert null workflow');
|
||||
return {
|
||||
id,
|
||||
key: uw.key,
|
||||
displayName: uw.displayName,
|
||||
description: uw.description,
|
||||
instructions: uw.instructions,
|
||||
enabled: uw.enabled,
|
||||
requiresFocusedNode: uw.requiresFocusedNode,
|
||||
primaryActor: 'oracle',
|
||||
expectedOutcome: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export class WorkflowRegistry {
|
||||
private static readonly WORKFLOWS: Record<string, WorkflowDefinition> = {
|
||||
'integrate': {
|
||||
id: 1,
|
||||
key: 'integrate',
|
||||
displayName: 'Integrate',
|
||||
description: 'Deep analysis and connection-building for focused node',
|
||||
instructions: INTEGRATE_WORKFLOW_INSTRUCTIONS,
|
||||
enabled: true,
|
||||
requiresFocusedNode: true,
|
||||
primaryActor: 'oracle',
|
||||
expectedOutcome: 'Focused node updated with insights; 3-5 high-value edges created',
|
||||
}
|
||||
};
|
||||
|
||||
static async getWorkflowByKey(key: string): Promise<WorkflowDefinition | null> {
|
||||
return this.WORKFLOWS[key] || null;
|
||||
// Try user file first
|
||||
const userWorkflow = loadUserWorkflow(key);
|
||||
if (userWorkflow) {
|
||||
return userWorkflowToDefinition(userWorkflow, BUNDLED_WORKFLOWS[key]?.id || 100);
|
||||
}
|
||||
|
||||
// Fall back to bundled
|
||||
return BUNDLED_WORKFLOWS[key] || null;
|
||||
}
|
||||
|
||||
static async getEnabledWorkflows(): Promise<WorkflowDefinition[]> {
|
||||
return Object.values(this.WORKFLOWS).filter(w => w.enabled);
|
||||
const all = await this.getAllWorkflows();
|
||||
return all.filter(w => w.enabled);
|
||||
}
|
||||
|
||||
static async getAllWorkflows(): Promise<WorkflowDefinition[]> {
|
||||
return Object.values(this.WORKFLOWS);
|
||||
// Start with bundled defaults
|
||||
const result: Record<string, WorkflowDefinition> = {};
|
||||
|
||||
for (const [key, workflow] of Object.entries(BUNDLED_WORKFLOWS)) {
|
||||
result[key] = { ...workflow };
|
||||
}
|
||||
|
||||
// Load user workflows (overwrite bundled if same key, add new ones)
|
||||
const userWorkflows = listUserWorkflows();
|
||||
let nextId = 100;
|
||||
|
||||
for (const uw of userWorkflows) {
|
||||
const existingId = result[uw.key]?.id || nextId++;
|
||||
result[uw.key] = userWorkflowToDefinition(uw, existingId);
|
||||
}
|
||||
|
||||
return Object.values(result);
|
||||
}
|
||||
|
||||
// Check if a workflow key is a bundled default
|
||||
static isBundledWorkflow(key: string): boolean {
|
||||
return BUNDLED_WORKFLOW_KEYS.has(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
export interface UserWorkflow {
|
||||
key: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
instructions: string;
|
||||
enabled: boolean;
|
||||
requiresFocusedNode: boolean;
|
||||
}
|
||||
|
||||
function resolveBaseConfigDir(): string {
|
||||
const override = process.env.RAH_CONFIG_DIR;
|
||||
if (override && override.trim().length > 0) {
|
||||
return override;
|
||||
}
|
||||
|
||||
const home = os.homedir();
|
||||
if (process.platform === 'darwin') {
|
||||
return path.join(home, 'Library', 'Application Support', 'RA-H');
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
const roaming = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
||||
return path.join(roaming, 'RA-H');
|
||||
}
|
||||
|
||||
const xdgConfig = process.env.XDG_CONFIG_HOME || path.join(home, '.config');
|
||||
return path.join(xdgConfig, 'ra-h');
|
||||
}
|
||||
|
||||
export function getWorkflowsDir(): string {
|
||||
return path.join(resolveBaseConfigDir(), 'workflows');
|
||||
}
|
||||
|
||||
function ensureWorkflowsDirExists(): void {
|
||||
const dir = getWorkflowsDir();
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
export function listUserWorkflows(): UserWorkflow[] {
|
||||
const dir = getWorkflowsDir();
|
||||
if (!fs.existsSync(dir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const workflows: UserWorkflow[] = [];
|
||||
const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
const filePath = path.join(dir, file);
|
||||
const raw = fs.readFileSync(filePath, 'utf-8');
|
||||
const parsed = JSON.parse(raw);
|
||||
|
||||
// Validate required fields
|
||||
if (parsed.key && parsed.displayName && parsed.instructions) {
|
||||
workflows.push({
|
||||
key: parsed.key,
|
||||
displayName: parsed.displayName,
|
||||
description: parsed.description || '',
|
||||
instructions: parsed.instructions,
|
||||
enabled: parsed.enabled !== false,
|
||||
requiresFocusedNode: parsed.requiresFocusedNode !== false,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to load workflow file ${file}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return workflows;
|
||||
}
|
||||
|
||||
export function loadUserWorkflow(key: string): UserWorkflow | null {
|
||||
const filePath = path.join(getWorkflowsDir(), `${key}.json`);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = fs.readFileSync(filePath, 'utf-8');
|
||||
const parsed = JSON.parse(raw);
|
||||
|
||||
if (!parsed.key || !parsed.displayName || !parsed.instructions) {
|
||||
console.warn(`Invalid workflow file for key ${key}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
key: parsed.key,
|
||||
displayName: parsed.displayName,
|
||||
description: parsed.description || '',
|
||||
instructions: parsed.instructions,
|
||||
enabled: parsed.enabled !== false,
|
||||
requiresFocusedNode: parsed.requiresFocusedNode !== false,
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn(`Failed to load workflow ${key}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveWorkflow(workflow: UserWorkflow): void {
|
||||
ensureWorkflowsDirExists();
|
||||
|
||||
const filePath = path.join(getWorkflowsDir(), `${workflow.key}.json`);
|
||||
const data = {
|
||||
key: workflow.key,
|
||||
displayName: workflow.displayName,
|
||||
description: workflow.description,
|
||||
instructions: workflow.instructions,
|
||||
enabled: workflow.enabled,
|
||||
requiresFocusedNode: workflow.requiresFocusedNode,
|
||||
};
|
||||
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
export function deleteWorkflow(key: string): boolean {
|
||||
const filePath = path.join(getWorkflowsDir(), `${key}.json`);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
fs.unlinkSync(filePath);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn(`Failed to delete workflow ${key}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function userWorkflowExists(key: string): boolean {
|
||||
const filePath = path.join(getWorkflowsDir(), `${key}.json`);
|
||||
return fs.existsSync(filePath);
|
||||
}
|
||||
Reference in New Issue
Block a user