sync: Workflows P2 features from private repo
- 5 bundled workflows (prep, research, connect, integrate, survey) - quickLink tool for fast edge creation - queryDimensionNodes tool for dimension queries - Workflow orchestration tools (list, get, edit) - Dimension awareness with active dimension tracking - Unified Workflows tab in Agent Panel - MCP server updates for workflow execution Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
3951d9daf4
commit
f9271aeeb4
@@ -11,14 +11,15 @@ import { Node } from '@/types/database';
|
||||
interface AgentsPanelProps {
|
||||
openTabsData: Node[];
|
||||
activeTabId: number | null;
|
||||
activeDimension?: string | null;
|
||||
onNodeClick?: (nodeId: number) => void;
|
||||
onCollapse?: () => void;
|
||||
}
|
||||
|
||||
type ActiveTab = 'ra-h' | string; // 'ra-h' or delegation sessionId
|
||||
type ActiveTab = 'ra-h' | 'workflows' | string; // 'ra-h', 'workflows', or delegation sessionId
|
||||
type Mode = 'quickadd' | 'session';
|
||||
|
||||
export default function AgentsPanel({ openTabsData, activeTabId, onNodeClick, onCollapse }: AgentsPanelProps) {
|
||||
export default function AgentsPanel({ openTabsData, activeTabId, activeDimension, onNodeClick, onCollapse }: AgentsPanelProps) {
|
||||
const [delegationsMap, setDelegationsMap] = useState<Record<string, AgentDelegation>>({});
|
||||
const [activeAgentTab, setActiveAgentTab] = useState<ActiveTab>('ra-h');
|
||||
const [mode, setMode] = useState<Mode>('quickadd');
|
||||
@@ -184,6 +185,7 @@ export default function AgentsPanel({ openTabsData, activeTabId, onNodeClick, on
|
||||
};
|
||||
|
||||
const handleDelegationClick = (sessionId: string) => {
|
||||
setMode('session');
|
||||
setActiveAgentTab(sessionId);
|
||||
};
|
||||
|
||||
@@ -419,76 +421,37 @@ export default function AgentsPanel({ openTabsData, activeTabId, onNodeClick, on
|
||||
onClick={() => setActiveAgentTab('ra-h')}
|
||||
className={`agent-tab ${activeAgentTab === 'ra-h' ? 'active' : ''}`}
|
||||
>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '6px', color: rahMode === 'easy' ? '#22c55e' : '#f97316' }}>
|
||||
{(rahMode === 'easy' ? <Zap size={12} strokeWidth={2.4} /> : <Flame size={12} strokeWidth={2.4} />)}
|
||||
<span>RA-H · {rahMode === 'easy' ? 'Easy' : 'Hard'}</span>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '6px', color: '#22c55e' }}>
|
||||
<Zap size={12} strokeWidth={2.4} />
|
||||
<span>RA-H</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Delegation Tabs */}
|
||||
{orderedDelegations.map((delegation) => {
|
||||
const isActive = activeAgentTab === delegation.sessionId;
|
||||
const isWiseRAH = delegation.agentType === 'wise-rah';
|
||||
|
||||
// Status colors based on agent type
|
||||
const statusColor = isWiseRAH
|
||||
? (delegation.status === 'in_progress' ? '#8b5cf6' :
|
||||
delegation.status === 'completed' ? '#6b6b6b' :
|
||||
delegation.status === 'failed' ? '#ff6b6b' : '#a78bfa')
|
||||
: (delegation.status === 'in_progress' ? '#8bd450' :
|
||||
delegation.status === 'completed' ? '#6b6b6b' :
|
||||
delegation.status === 'failed' ? '#ff6b6b' : '#5c9aff');
|
||||
|
||||
const shortId = delegation.sessionId.split('_').pop() || '';
|
||||
const tabLabel = isWiseRAH
|
||||
? `WISE RA-H · ${shortId.slice(0, 6)}`
|
||||
: `MINI · ${shortId.slice(0, 6)}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={delegation.sessionId}
|
||||
className={`agent-tab-wrapper ${isWiseRAH ? 'wise' : 'mini'} ${isActive ? 'active' : ''}`}
|
||||
>
|
||||
<button
|
||||
onClick={() => setActiveAgentTab(delegation.sessionId)}
|
||||
className="agent-tab"
|
||||
>
|
||||
<span className="status-dot" style={{ background: statusColor }} />
|
||||
<span className="agent-tab-label">{tabLabel}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
console.log(`[AgentsPanel] User clicked close button for delegation ${delegation.sessionId}`);
|
||||
|
||||
// Delete from database
|
||||
try {
|
||||
await fetch(`/api/rah/delegations/${delegation.sessionId}`, { method: 'DELETE' });
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete delegation ${delegation.sessionId}:`, error);
|
||||
}
|
||||
|
||||
// Remove from UI state
|
||||
setDelegationsMap((prev) => {
|
||||
const { [delegation.sessionId]: _ignored, ...rest } = prev;
|
||||
return rest;
|
||||
});
|
||||
setDelegationMessages((prev) => {
|
||||
const { [delegation.sessionId]: _ignored, ...rest } = prev;
|
||||
return rest;
|
||||
});
|
||||
if (activeAgentTab === delegation.sessionId) {
|
||||
setActiveAgentTab('ra-h');
|
||||
}
|
||||
}}
|
||||
className="agent-tab-close"
|
||||
title="Close tab"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{/* Workflows Tab */}
|
||||
{orderedDelegations.length > 0 && (
|
||||
<button
|
||||
onClick={() => setActiveAgentTab('workflows')}
|
||||
className={`agent-tab ${activeAgentTab === 'workflows' || (activeAgentTab !== 'ra-h' && activeAgentTab !== 'workflows') ? 'active' : ''}`}
|
||||
>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '6px' }}>
|
||||
<span className="status-dot" style={{
|
||||
background: orderedDelegations.some(d => d.status === 'in_progress') ? '#22c55e' : '#6b6b6b',
|
||||
animation: orderedDelegations.some(d => d.status === 'in_progress') ? 'pulse 2s infinite' : 'none'
|
||||
}} />
|
||||
<span>Workflows</span>
|
||||
<span style={{
|
||||
background: '#1f1f1f',
|
||||
color: '#a8a8a8',
|
||||
fontSize: '10px',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '10px',
|
||||
fontWeight: 600
|
||||
}}>
|
||||
{orderedDelegations.length}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -509,13 +472,14 @@ export default function AgentsPanel({ openTabsData, activeTabId, onNodeClick, on
|
||||
) : (
|
||||
<>
|
||||
{/* Keep RAHChat always mounted, just hide when not active */}
|
||||
<div style={{
|
||||
height: '100%',
|
||||
display: activeAgentTab === 'ra-h' ? 'block' : 'none'
|
||||
<div style={{
|
||||
height: '100%',
|
||||
display: activeAgentTab === 'ra-h' ? 'block' : 'none'
|
||||
}}>
|
||||
<RAHChat
|
||||
openTabsData={openTabsData}
|
||||
activeTabId={activeTabId}
|
||||
activeDimension={activeDimension}
|
||||
onNodeClick={onNodeClick}
|
||||
delegations={orderedDelegations}
|
||||
messages={rahMessages}
|
||||
@@ -523,23 +487,57 @@ export default function AgentsPanel({ openTabsData, activeTabId, onNodeClick, on
|
||||
mode={rahMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Show delegation chat when delegation tab is active */}
|
||||
{selectedDelegation && activeAgentTab !== 'ra-h' && (
|
||||
|
||||
{/* Workflows list view */}
|
||||
{activeAgentTab === 'workflows' && (
|
||||
<div style={{ height: '100%', display: 'block' }}>
|
||||
<RAHChat
|
||||
openTabsData={openTabsData}
|
||||
activeTabId={activeTabId}
|
||||
onNodeClick={onNodeClick}
|
||||
<WorkflowsListView
|
||||
delegations={orderedDelegations}
|
||||
messages={getDelegationMessages(selectedDelegation.sessionId)}
|
||||
setMessages={setDelegationMessagesFor(selectedDelegation.sessionId)}
|
||||
mode="easy"
|
||||
delegationMode={true}
|
||||
delegationSessionId={selectedDelegation.sessionId}
|
||||
onSelectDelegation={(sessionId) => setActiveAgentTab(sessionId)}
|
||||
onDeleteDelegation={async (sessionId) => {
|
||||
try {
|
||||
await fetch(`/api/rah/delegations/${sessionId}`, { method: 'DELETE' });
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete delegation ${sessionId}:`, error);
|
||||
}
|
||||
setDelegationsMap((prev) => {
|
||||
const { [sessionId]: _ignored, ...rest } = prev;
|
||||
return rest;
|
||||
});
|
||||
setDelegationMessages((prev) => {
|
||||
const { [sessionId]: _ignored, ...rest } = prev;
|
||||
return rest;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show delegation detail when a specific delegation is selected */}
|
||||
{selectedDelegation && activeAgentTab !== 'ra-h' && activeAgentTab !== 'workflows' && (
|
||||
<div style={{ height: '100%', display: 'block' }}>
|
||||
{/* Show summary view if completed/failed with no messages */}
|
||||
{(selectedDelegation.status === 'completed' || selectedDelegation.status === 'failed')
|
||||
&& getDelegationMessages(selectedDelegation.sessionId).length === 0 ? (
|
||||
<DelegationSummaryView
|
||||
delegation={selectedDelegation}
|
||||
onBack={() => setActiveAgentTab('workflows')}
|
||||
/>
|
||||
) : (
|
||||
<DelegationDetailView
|
||||
delegation={selectedDelegation}
|
||||
openTabsData={openTabsData}
|
||||
activeTabId={activeTabId}
|
||||
activeDimension={activeDimension}
|
||||
onNodeClick={onNodeClick}
|
||||
delegations={orderedDelegations}
|
||||
messages={getDelegationMessages(selectedDelegation.sessionId)}
|
||||
setMessages={setDelegationMessagesFor(selectedDelegation.sessionId)}
|
||||
onBack={() => setActiveAgentTab('workflows')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -638,7 +636,468 @@ export default function AgentsPanel({ openTabsData, activeTabId, onNodeClick, on
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Summary view for completed delegations with no messages
|
||||
function DelegationSummaryView({ delegation, onBack }: { delegation: AgentDelegation; onBack?: () => void }) {
|
||||
const isSuccess = delegation.status === 'completed';
|
||||
const statusColor = isSuccess ? '#22c55e' : '#ff6b6b';
|
||||
const statusLabel = isSuccess ? 'Completed' : 'Failed';
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: '#0a0a0a',
|
||||
padding: '24px'
|
||||
}}>
|
||||
{/* Header with back button */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
marginBottom: '24px',
|
||||
paddingBottom: '16px',
|
||||
borderBottom: '1px solid #1a1a1a'
|
||||
}}>
|
||||
{onBack && (
|
||||
<button
|
||||
onClick={onBack}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#666',
|
||||
cursor: 'pointer',
|
||||
padding: '4px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
>
|
||||
←
|
||||
</button>
|
||||
)}
|
||||
<div style={{
|
||||
width: '10px',
|
||||
height: '10px',
|
||||
borderRadius: '50%',
|
||||
background: statusColor
|
||||
}} />
|
||||
<span style={{
|
||||
color: statusColor,
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.08em'
|
||||
}}>
|
||||
{statusLabel}
|
||||
</span>
|
||||
<span style={{
|
||||
color: '#666',
|
||||
fontSize: '11px',
|
||||
marginLeft: 'auto'
|
||||
}}>
|
||||
{new Date(delegation.updatedAt).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Task */}
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<div style={{
|
||||
color: '#666',
|
||||
fontSize: '10px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
marginBottom: '8px'
|
||||
}}>
|
||||
Task
|
||||
</div>
|
||||
<div style={{
|
||||
color: '#e5e5e5',
|
||||
fontSize: '13px',
|
||||
lineHeight: '1.5'
|
||||
}}>
|
||||
{delegation.task}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
{delegation.summary && (
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{
|
||||
color: '#666',
|
||||
fontSize: '10px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
marginBottom: '8px'
|
||||
}}>
|
||||
Result
|
||||
</div>
|
||||
<div style={{
|
||||
color: isSuccess ? '#a8a8a8' : '#fca5a5',
|
||||
fontSize: '13px',
|
||||
lineHeight: '1.6',
|
||||
whiteSpace: 'pre-wrap'
|
||||
}}>
|
||||
{delegation.summary}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* No summary fallback */}
|
||||
{!delegation.summary && (
|
||||
<div style={{
|
||||
color: '#666',
|
||||
fontSize: '12px',
|
||||
fontStyle: 'italic'
|
||||
}}>
|
||||
No details available
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Workflows list view - shows all delegations in a nice list
|
||||
function WorkflowsListView({
|
||||
delegations,
|
||||
onSelectDelegation,
|
||||
onDeleteDelegation
|
||||
}: {
|
||||
delegations: AgentDelegation[];
|
||||
onSelectDelegation: (sessionId: string) => void;
|
||||
onDeleteDelegation: (sessionId: string) => void;
|
||||
}) {
|
||||
const activeDelegations = delegations.filter(d => d.status === 'queued' || d.status === 'in_progress');
|
||||
const completedDelegations = delegations.filter(d => d.status === 'completed' || d.status === 'failed');
|
||||
|
||||
const getStatusInfo = (delegation: AgentDelegation) => {
|
||||
const isWiseRAH = delegation.agentType === 'wise-rah';
|
||||
let color = '#6b6b6b';
|
||||
let label = 'Queued';
|
||||
|
||||
if (delegation.status === 'in_progress') {
|
||||
color = isWiseRAH ? '#8b5cf6' : '#22c55e';
|
||||
label = 'Running';
|
||||
} else if (delegation.status === 'completed') {
|
||||
color = '#22c55e';
|
||||
label = 'Done';
|
||||
} else if (delegation.status === 'failed') {
|
||||
color = '#ff6b6b';
|
||||
label = 'Failed';
|
||||
}
|
||||
|
||||
return { color, label };
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: '#0a0a0a',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{/* Header */}
|
||||
<div style={{
|
||||
padding: '16px 20px',
|
||||
borderBottom: '1px solid #1a1a1a',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<span style={{
|
||||
color: '#e5e5e5',
|
||||
fontSize: '13px',
|
||||
fontWeight: 600,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.08em'
|
||||
}}>
|
||||
Workflows
|
||||
</span>
|
||||
{activeDelegations.length > 0 && (
|
||||
<span style={{
|
||||
color: '#22c55e',
|
||||
fontSize: '11px',
|
||||
fontWeight: 500
|
||||
}}>
|
||||
{activeDelegations.length} running
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* List */}
|
||||
<div style={{
|
||||
flex: 1,
|
||||
overflow: 'auto',
|
||||
padding: '12px'
|
||||
}}>
|
||||
{delegations.length === 0 ? (
|
||||
<div style={{
|
||||
color: '#666',
|
||||
fontSize: '12px',
|
||||
textAlign: 'center',
|
||||
padding: '40px 20px'
|
||||
}}>
|
||||
No workflows yet. Use Quick Capture or ask RA-H to run a workflow.
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
{/* Active workflows first */}
|
||||
{activeDelegations.map((delegation) => {
|
||||
const { color, label } = getStatusInfo(delegation);
|
||||
return (
|
||||
<WorkflowCard
|
||||
key={delegation.sessionId}
|
||||
delegation={delegation}
|
||||
statusColor={color}
|
||||
statusLabel={label}
|
||||
onSelect={() => onSelectDelegation(delegation.sessionId)}
|
||||
onDelete={() => onDeleteDelegation(delegation.sessionId)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Divider if both active and completed exist */}
|
||||
{activeDelegations.length > 0 && completedDelegations.length > 0 && (
|
||||
<div style={{
|
||||
height: '1px',
|
||||
background: '#1f1f1f',
|
||||
margin: '8px 0'
|
||||
}} />
|
||||
)}
|
||||
|
||||
{/* Completed workflows */}
|
||||
{completedDelegations.map((delegation) => {
|
||||
const { color, label } = getStatusInfo(delegation);
|
||||
return (
|
||||
<WorkflowCard
|
||||
key={delegation.sessionId}
|
||||
delegation={delegation}
|
||||
statusColor={color}
|
||||
statusLabel={label}
|
||||
onSelect={() => onSelectDelegation(delegation.sessionId)}
|
||||
onDelete={() => onDeleteDelegation(delegation.sessionId)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Individual workflow card
|
||||
function WorkflowCard({
|
||||
delegation,
|
||||
statusColor,
|
||||
statusLabel,
|
||||
onSelect,
|
||||
onDelete
|
||||
}: {
|
||||
delegation: AgentDelegation;
|
||||
statusColor: string;
|
||||
statusLabel: string;
|
||||
onSelect: () => void;
|
||||
onDelete: () => void;
|
||||
}) {
|
||||
const isActive = delegation.status === 'in_progress' || delegation.status === 'queued';
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onSelect}
|
||||
style={{
|
||||
padding: '14px 16px',
|
||||
background: '#151515',
|
||||
border: '1px solid #1f1f1f',
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s ease',
|
||||
position: 'relative'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#1a1a1a';
|
||||
e.currentTarget.style.borderColor = '#2a2a2a';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = '#151515';
|
||||
e.currentTarget.style.borderColor = '#1f1f1f';
|
||||
}}
|
||||
>
|
||||
{/* Top row: status + time + delete */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
marginBottom: '8px'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
background: statusColor,
|
||||
animation: isActive ? 'pulse 2s infinite' : 'none'
|
||||
}} />
|
||||
<span style={{
|
||||
color: statusColor,
|
||||
fontSize: '11px',
|
||||
fontWeight: 600,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em'
|
||||
}}>
|
||||
{statusLabel}
|
||||
</span>
|
||||
<span style={{
|
||||
color: '#555',
|
||||
fontSize: '11px',
|
||||
marginLeft: 'auto'
|
||||
}}>
|
||||
{new Date(delegation.createdAt).toLocaleTimeString()}
|
||||
</span>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
}}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#444',
|
||||
cursor: 'pointer',
|
||||
padding: '2px 6px',
|
||||
fontSize: '14px',
|
||||
lineHeight: 1
|
||||
}}
|
||||
onMouseEnter={(e) => e.currentTarget.style.color = '#ff6b6b'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.color = '#444'}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Task description */}
|
||||
<div style={{
|
||||
color: '#e5e5e5',
|
||||
fontSize: '12px',
|
||||
lineHeight: '1.4',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical'
|
||||
}}>
|
||||
{delegation.task}
|
||||
</div>
|
||||
|
||||
{/* Summary preview if completed */}
|
||||
{delegation.summary && delegation.status === 'completed' && (
|
||||
<div style={{
|
||||
color: '#666',
|
||||
fontSize: '11px',
|
||||
marginTop: '8px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}}>
|
||||
{delegation.summary}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Delegation detail view with back button
|
||||
|
||||
function DelegationDetailView({
|
||||
delegation,
|
||||
openTabsData,
|
||||
activeTabId,
|
||||
activeDimension,
|
||||
onNodeClick,
|
||||
delegations,
|
||||
messages,
|
||||
setMessages,
|
||||
onBack
|
||||
}: {
|
||||
delegation: AgentDelegation;
|
||||
openTabsData: Node[];
|
||||
activeTabId: number | null;
|
||||
activeDimension?: string | null;
|
||||
onNodeClick?: (nodeId: number) => void;
|
||||
delegations: AgentDelegation[];
|
||||
messages: any[];
|
||||
setMessages: (updater: (prev: any[]) => any[]) => void;
|
||||
onBack: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
{/* Back button header */}
|
||||
<div style={{
|
||||
padding: '8px 12px',
|
||||
borderBottom: '1px solid #1a1a1a',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
background: '#0a0a0a'
|
||||
}}>
|
||||
<button
|
||||
onClick={onBack}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#666',
|
||||
cursor: 'pointer',
|
||||
padding: '4px 8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
onMouseEnter={(e) => e.currentTarget.style.color = '#a8a8a8'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.color = '#666'}
|
||||
>
|
||||
← Workflows
|
||||
</button>
|
||||
<span style={{
|
||||
color: '#555',
|
||||
fontSize: '11px'
|
||||
}}>
|
||||
|
|
||||
</span>
|
||||
<span style={{
|
||||
color: delegation.status === 'in_progress' ? '#22c55e' : '#666',
|
||||
fontSize: '11px',
|
||||
textTransform: 'uppercase'
|
||||
}}>
|
||||
{delegation.status === 'in_progress' ? 'Running' : delegation.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* RAHChat for streaming */}
|
||||
<div style={{ flex: 1 }}>
|
||||
<RAHChat
|
||||
openTabsData={openTabsData}
|
||||
activeTabId={activeTabId}
|
||||
activeDimension={activeDimension}
|
||||
onNodeClick={onNodeClick}
|
||||
delegations={delegations}
|
||||
messages={messages}
|
||||
setMessages={setMessages}
|
||||
mode="easy"
|
||||
delegationMode={true}
|
||||
delegationSessionId={delegation.sessionId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ const createVoiceRequestId = () =>
|
||||
interface RAHChatProps {
|
||||
openTabsData: Node[];
|
||||
activeTabId: number | null;
|
||||
activeDimension?: string | null;
|
||||
onNodeClick?: (nodeId: number) => void;
|
||||
delegations?: AgentDelegation[];
|
||||
messages?: ChatMessage[];
|
||||
@@ -34,10 +35,11 @@ interface RAHChatProps {
|
||||
delegationSessionId?: string;
|
||||
}
|
||||
|
||||
export default function RAHChat({
|
||||
openTabsData,
|
||||
activeTabId,
|
||||
onNodeClick,
|
||||
export default function RAHChat({
|
||||
openTabsData,
|
||||
activeTabId,
|
||||
activeDimension,
|
||||
onNodeClick,
|
||||
delegations = [],
|
||||
messages: externalMessages,
|
||||
setMessages: externalSetMessages,
|
||||
@@ -124,7 +126,6 @@ export default function RAHChat({
|
||||
await handler();
|
||||
}
|
||||
},
|
||||
getApiKeys: () => apiKeyService.getStoredKeys(),
|
||||
});
|
||||
|
||||
useVoiceInterruption({
|
||||
|
||||
@@ -26,6 +26,7 @@ interface SendParams {
|
||||
history: ChatMessage[];
|
||||
openTabs: any[];
|
||||
activeTabId: number | null;
|
||||
activeDimension?: string | null;
|
||||
currentView?: 'nodes' | 'memory';
|
||||
sessionId: string;
|
||||
mode: 'easy' | 'hard';
|
||||
@@ -36,7 +37,6 @@ interface UseSSEChatOptions {
|
||||
beforeRequest?: () => boolean;
|
||||
onRequestError?: (error: unknown, response?: Response) => boolean | void;
|
||||
onStreamComplete?: () => void | Promise<void>;
|
||||
getApiKeys?: () => { openai?: string; anthropic?: string } | undefined;
|
||||
}
|
||||
|
||||
export function useSSEChat(
|
||||
@@ -44,7 +44,7 @@ export function useSSEChat(
|
||||
setMessages: (updater: (prev: ChatMessage[]) => ChatMessage[]) => void,
|
||||
options: UseSSEChatOptions = {}
|
||||
) {
|
||||
const { getAuthToken, beforeRequest, onRequestError, onStreamComplete, getApiKeys } = options;
|
||||
const { getAuthToken, beforeRequest, onRequestError, onStreamComplete } = options;
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
@@ -53,7 +53,7 @@ export function useSSEChat(
|
||||
abortControllerRef.current?.abort();
|
||||
};
|
||||
|
||||
const send = async ({ text, history, openTabs, activeTabId, currentView, sessionId, mode }: SendParams) => {
|
||||
const send = async ({ text, history, openTabs, activeTabId, activeDimension, currentView, sessionId, mode }: SendParams) => {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed || isLoading) return;
|
||||
if (beforeRequest && !beforeRequest()) return;
|
||||
@@ -97,10 +97,10 @@ export function useSSEChat(
|
||||
.map((m) => ({ role: m.role, parts: [{ type: 'text', text: m.content }] })),
|
||||
openTabs,
|
||||
activeTabId,
|
||||
activeDimension,
|
||||
currentView,
|
||||
sessionId,
|
||||
mode,
|
||||
apiKeys: getApiKeys?.(),
|
||||
mode
|
||||
}),
|
||||
signal: abortControllerRef.current.signal
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ Mission:
|
||||
|
||||
Operating principles:
|
||||
- Handle analysis, planning, and writes yourself; do not delegate.
|
||||
- Use quickLink to instantly find and link related nodes (fast, no reasoning needed).
|
||||
- Use createNode, updateNode, createEdge, and updateEdge when the change is unambiguous.
|
||||
- When creating nodes derived from existing content (ideas, insights, summaries), do NOT include the 'link' field. The 'link' field is ONLY for nodes that directly represent external content (YouTube videos, websites, PDFs). Derived idea nodes should not have links.
|
||||
- When referencing stored content, quote verbatim text in quotes and include the node citation.
|
||||
|
||||
@@ -11,6 +11,7 @@ When to ask the user:
|
||||
|
||||
Execution approach:
|
||||
- Handle planning, analysis, and writes directly—do not delegate to mini ra-h during normal conversations.
|
||||
- Use quickLink to instantly find and link related nodes (fast, no AI reasoning needed—just database search).
|
||||
- Call createNode, updateNode, createEdge, updateEdge, and extraction tools yourself when the change is clear.
|
||||
- When creating nodes derived from existing content (ideas, insights, summaries), do NOT include the 'link' field. The 'link' field is ONLY for nodes that directly represent external content (YouTube videos, websites, PDFs).
|
||||
- Treat "this conversation/paper/video" as the active focused node.
|
||||
|
||||
@@ -15,6 +15,8 @@ Available tools:
|
||||
- webSearch — external research when necessary
|
||||
- think — internal planning/reflection (use once per workflow unless plan changes)
|
||||
- updateNode — append content to nodes (tool handles appending automatically)
|
||||
- createEdge — create connections between nodes
|
||||
- quickLink — instantly find and link related nodes (fast database search, no reasoning)
|
||||
- Dimension management: createDimension, updateDimension, lockDimension, unlockDimension, deleteDimension — manage dimensions to organize knowledge base structure
|
||||
</tools>
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
export const CONNECT_WORKFLOW_INSTRUCTIONS = `You are executing the CONNECT workflow for the currently focused node.
|
||||
|
||||
MISSION
|
||||
Quick link: find explicitly related nodes and create edges. Fast text search only - no slow embedding search.
|
||||
|
||||
WORKFLOW STEPS
|
||||
|
||||
1. READ NODE
|
||||
Call getNodesById for the focused node. Note the title, type, and key names/entities mentioned.
|
||||
|
||||
2. QUICK SEARCH
|
||||
Call queryNodes ONCE with the most specific entity (person name, project name, company, tool).
|
||||
- Use search parameter with the exact name
|
||||
- Set limit: 10
|
||||
|
||||
DO NOT call searchContentEmbeddings - use queryNodes only for speed.
|
||||
|
||||
3. CREATE EDGES
|
||||
From the search results, pick 2-4 nodes that are clearly related.
|
||||
Call createEdge for each:
|
||||
- from_node_id: focused node ID
|
||||
- to_node_id: related node ID
|
||||
- context: { explanation: "brief reason" }
|
||||
|
||||
4. DONE
|
||||
Reply: "Linked [title] → [list of connected node titles]"
|
||||
|
||||
RULES
|
||||
- Total tool calls ≤ 5 (1 read + 1 search + up to 3 edges)
|
||||
- Use queryNodes only - NO searchContentEmbeddings
|
||||
- Only link nodes with clear, explicit relationships
|
||||
- Skip if no good matches found`;
|
||||
@@ -1,71 +1,63 @@
|
||||
export const INTEGRATE_WORKFLOW_INSTRUCTIONS = `You are executing the INTEGRATE workflow for the currently focused node.
|
||||
|
||||
MISSION
|
||||
Find meaningful connections across the user's knowledge graph and append an Integration Analysis to the node.
|
||||
Find meaningful connections across the user's knowledge graph, create edges, and append an Integration Analysis.
|
||||
|
||||
YOU HAVE DIRECT WRITE ACCESS via updateNode. The tool automatically appends – you cannot overwrite existing content.
|
||||
YOU HAVE DIRECT WRITE ACCESS via updateNode (appends only) and createEdge (creates graph connections).
|
||||
|
||||
WORKFLOW STEPS
|
||||
|
||||
0. PLAN (MANDATORY)
|
||||
- Call think to outline your approach for steps 1–4
|
||||
- Focus on what entities/concepts to extract and how to search for them
|
||||
|
||||
1. RETRIEVE & GROUND THE NODE
|
||||
1. RETRIEVE & UNDERSTAND
|
||||
- Call getNodesById for the focused node
|
||||
- Identify what type of thing this is (person, project, paper, idea, video, tweet, technique, etc.)
|
||||
- Extract key entities: specific names, projects, concepts, techniques mentioned
|
||||
- Summarize the core insight in one sentence
|
||||
- Identify: what type of thing is this? (person, project, paper, idea, video, etc.)
|
||||
- Extract key entities: names, projects, concepts, techniques
|
||||
- Note the core insight in one sentence
|
||||
|
||||
2. SEARCH FOR CONNECTIONS
|
||||
Search the database using entities from step 1:
|
||||
|
||||
a) Structural connections:
|
||||
- Names mentioned → queryNodes to find nodes about those people
|
||||
- Projects/tools mentioned → queryNodes to find those nodes
|
||||
|
||||
2. SEARCH THE DATABASE FOR CONNECTIONS
|
||||
Search the ENTIRE database:
|
||||
|
||||
a) Obvious structural connections:
|
||||
- If names mentioned → queryNodes to find existing nodes about those people
|
||||
- If projects mentioned → queryNodes to find those project nodes
|
||||
- If specific techniques/tools mentioned → search for those exact terms
|
||||
|
||||
b) Thematic connections:
|
||||
- Use searchContentEmbeddings with key concepts from step 1
|
||||
- Use searchContentEmbeddings with key concepts
|
||||
- Look for shared themes, complementary ideas, contradictions
|
||||
- Prefer nodes with high-signal relevance over weak matches
|
||||
|
||||
- Aim for 3–8 strong connections, not 20 weak ones
|
||||
- Check existing edges with queryEdge to avoid duplicating connections
|
||||
|
||||
3. CONTEXTUALIZE WITH TOP NODES
|
||||
Review the BACKGROUND CONTEXT (top nodes by edge count):
|
||||
- Why might this node matter given the user's focus areas?
|
||||
- Does it advance any themes visible in their most connected nodes?
|
||||
- Keep this brief – 1–2 sentences maximum
|
||||
Target: 3-5 strong connections (quality over quantity)
|
||||
|
||||
3. CREATE EDGES
|
||||
For each connection found, call createEdge:
|
||||
- from_node_id: the focused node ID
|
||||
- to_node_id: the connected node ID
|
||||
- context: { explanation: "why this connection matters" }
|
||||
|
||||
The tool handles duplicates gracefully - if edge exists, it returns an error and you continue.
|
||||
|
||||
Create 3-5 edges total.
|
||||
|
||||
4. DOCUMENT IN CONTENT
|
||||
Call updateNode ONCE with ONLY this new section:
|
||||
|
||||
4. APPEND INTEGRATION ANALYSIS
|
||||
Call updateNode ONCE with ONLY the new section (do NOT include existing content):
|
||||
|
||||
---
|
||||
## Integration Analysis
|
||||
|
||||
[2–3 sentences: what this node is, why it matters, core insight]
|
||||
|
||||
**Database Connections:**
|
||||
- [NODE:123:"Title"] — [why: authorship/shared concept/dependency/contradiction]
|
||||
- [NODE:456:"Title"] — [why: ...]
|
||||
- [continue for 3–8 connections found in step 2]
|
||||
|
||||
**Relevance:** [1–2 sentences connecting to user's top nodes/focus areas]
|
||||
|
||||
CRITICAL: Send ONLY this new section. The tool will automatically append it to existing content.
|
||||
|
||||
After ONE successful updateNode call, IMMEDIATELY move to step 5. Do NOT call updateNode again.
|
||||
|
||||
[2-3 sentences: what this is, why it matters, core insight]
|
||||
|
||||
**Connections:**
|
||||
- [NODE:123:"Title"] — [why connected]
|
||||
- [NODE:456:"Title"] — [why connected]
|
||||
[list all edges created in step 3]
|
||||
|
||||
CRITICAL: Send ONLY the new section. The tool appends automatically.
|
||||
|
||||
5. RETURN SUMMARY
|
||||
Reply with: Task / Actions / Result / Nodes / Follow-up (≤120 words)
|
||||
Reply with: Task / Actions / Result / Nodes / Follow-up (≤100 words)
|
||||
|
||||
CRITICAL RULES
|
||||
- Search the FULL database, not just top nodes
|
||||
- Use entities from step 1 to guide searches in step 2
|
||||
- Call updateNode EXACTLY ONCE - after success, move to step 5 immediately
|
||||
- Keep total tool calls ≤ 18 (be efficient)
|
||||
- Adapt to any node type – don't assume it's always a paper or video
|
||||
RULES
|
||||
- Keep total tool calls ≤ 12
|
||||
- Create edges BEFORE documenting (step 3 before step 4)
|
||||
- Call updateNode exactly once
|
||||
- Adapt to any node type
|
||||
|
||||
The goal: integrate this node into the knowledge graph through meaningful, database-wide connections.`;
|
||||
OPTIONAL: Call think at any point if you need to plan your approach.`;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
export const PREP_WORKFLOW_INSTRUCTIONS = `You are executing the PREP workflow for the currently focused node.
|
||||
|
||||
MISSION
|
||||
Quick summary to help the user decide if this content is worth deeper engagement.
|
||||
|
||||
WORKFLOW STEPS
|
||||
|
||||
1. READ THE NODE
|
||||
- Call getNodesById for the focused node
|
||||
- Understand what this is and extract the core message
|
||||
|
||||
2. APPEND BRIEF
|
||||
Call updateNode ONCE with ONLY this section:
|
||||
|
||||
---
|
||||
## Brief
|
||||
|
||||
**What:** [One sentence - what is this?]
|
||||
|
||||
**Gist:** [2-3 sentences - the core message or takeaway]
|
||||
|
||||
**Why it matters:** [1-2 sentences - relevance or implications]
|
||||
|
||||
CRITICAL: Send ONLY the new section. The tool appends automatically.
|
||||
|
||||
3. RETURN SUMMARY
|
||||
Reply with a one-line confirmation: "Prepped [title] - [gist in <10 words]"
|
||||
|
||||
RULES
|
||||
- Keep total tool calls ≤ 3
|
||||
- Call updateNode exactly once
|
||||
- Be concise - this is a quick prep, not deep analysis`;
|
||||
@@ -0,0 +1,41 @@
|
||||
export const RESEARCH_WORKFLOW_INSTRUCTIONS = `You are executing the RESEARCH workflow for the currently focused node.
|
||||
|
||||
MISSION
|
||||
Conduct background research on the topic/person/concept and append findings to the node.
|
||||
|
||||
WORKFLOW STEPS
|
||||
|
||||
1. READ & IDENTIFY
|
||||
- Call getNodesById for the focused node
|
||||
- Identify: what needs researching? (person's background, concept origins, recent developments, etc.)
|
||||
|
||||
2. WEB RESEARCH
|
||||
- Call webSearch with targeted queries (1-2 searches)
|
||||
- Focus on: background context, recent news, authoritative sources
|
||||
- Extract the most relevant findings
|
||||
|
||||
3. APPEND RESEARCH
|
||||
Call updateNode ONCE with ONLY this section:
|
||||
|
||||
---
|
||||
## Research Notes
|
||||
|
||||
**Background:** [2-3 sentences of context]
|
||||
|
||||
**Key Findings:**
|
||||
- [Finding 1]
|
||||
- [Finding 2]
|
||||
- [Finding 3]
|
||||
|
||||
**Sources:** [Brief attribution]
|
||||
|
||||
CRITICAL: Send ONLY the new section. The tool appends automatically.
|
||||
|
||||
4. RETURN SUMMARY
|
||||
Reply with: "Researched [topic] - [key insight in <15 words]"
|
||||
|
||||
RULES
|
||||
- Keep total tool calls ≤ 5
|
||||
- Call updateNode exactly once
|
||||
- Focus on factual background, not opinion
|
||||
- Cite sources when possible`;
|
||||
@@ -0,0 +1,47 @@
|
||||
export const SURVEY_WORKFLOW_INSTRUCTIONS = `You are executing the SURVEY workflow for the currently active dimension.
|
||||
|
||||
MISSION
|
||||
Analyze the active dimension to identify patterns, themes, gaps, and insights across all nodes within it.
|
||||
|
||||
PREREQUISITE: This workflow requires an active dimension. Check ACTIVE DIMENSION section in context.
|
||||
|
||||
WORKFLOW STEPS
|
||||
|
||||
1. RETRIEVE DIMENSION NODES
|
||||
- Call queryDimensionNodes with the active dimension name
|
||||
- Set limit to 50 to get a comprehensive view
|
||||
- Note the total count and most connected nodes
|
||||
|
||||
2. ANALYZE THEMES
|
||||
- Call searchContentEmbeddings with key concepts from the dimension
|
||||
- Identify recurring themes, patterns, and relationships
|
||||
- Note any gaps or underrepresented areas
|
||||
|
||||
3. DOCUMENT FINDINGS
|
||||
Call updateDimension to update the dimension description with:
|
||||
|
||||
**Survey Summary** (append to existing description)
|
||||
|
||||
**Node Count:** [X nodes]
|
||||
**Top Hubs:** [List 3-5 most connected nodes with [NODE:id:"title"]]
|
||||
|
||||
**Themes:**
|
||||
- [Theme 1 - brief description]
|
||||
- [Theme 2 - brief description]
|
||||
|
||||
**Gaps/Opportunities:**
|
||||
- [Identified gap or area for expansion]
|
||||
|
||||
**Connections to Other Dimensions:**
|
||||
- [Any cross-dimension patterns observed]
|
||||
|
||||
4. RETURN SUMMARY
|
||||
Reply with: "Surveyed [dimension] — [X nodes, key insight in <15 words]"
|
||||
|
||||
RULES
|
||||
- Keep total tool calls ≤ 5
|
||||
- Focus on patterns across the dimension, not individual node details
|
||||
- Identify both strengths (dense areas) and gaps (sparse areas)
|
||||
- Reference specific nodes using [NODE:id:"title"] format
|
||||
|
||||
If no active dimension is set, inform the user to open a dimension folder first.`;
|
||||
@@ -8,6 +8,14 @@ import { buildAutoContextBlock } from '@/services/context/autoContext';
|
||||
export interface NodeContext {
|
||||
nodes: Node[];
|
||||
activeNodeId: number | null;
|
||||
activeDimension?: string | null;
|
||||
}
|
||||
|
||||
export interface DimensionContext {
|
||||
name: string;
|
||||
description: string | null;
|
||||
isPriority: boolean;
|
||||
nodeCount: number;
|
||||
}
|
||||
|
||||
export interface ContextBuilderOptions {
|
||||
@@ -151,6 +159,40 @@ export function buildFocusedNodesBlock(
|
||||
return contextString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches dimension context from API or database
|
||||
*/
|
||||
async function fetchDimensionContext(dimensionName: string): Promise<DimensionContext | null> {
|
||||
try {
|
||||
// In server context, we can call the API internally or query directly
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/dimensions/${encodeURIComponent(dimensionName)}/context`);
|
||||
if (!response.ok) return null;
|
||||
const data = await response.json();
|
||||
return data.success ? data.data : null;
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch dimension context:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the dimension context block for the system prompt
|
||||
*/
|
||||
function buildDimensionContextBlock(dimension: DimensionContext | null): string {
|
||||
if (!dimension) return '';
|
||||
|
||||
return `
|
||||
=== ACTIVE DIMENSION ===
|
||||
Dimension: "${dimension.name}"
|
||||
Description: ${dimension.description || 'No description'}
|
||||
Node Count: ${dimension.nodeCount} nodes
|
||||
Priority: ${dimension.isPriority ? 'Yes (locked)' : 'No'}
|
||||
|
||||
Note: Use queryDimensionNodes tool to retrieve actual nodes in this dimension.
|
||||
===================================
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds system prompt as cacheable blocks (Anthropic prompt caching)
|
||||
*/
|
||||
@@ -212,6 +254,15 @@ export async function buildSystemPromptBlocks(
|
||||
});
|
||||
}
|
||||
|
||||
// Add dimension context if an active dimension is provided
|
||||
if (nodeContext.activeDimension) {
|
||||
const dimensionContext = await fetchDimensionContext(nodeContext.activeDimension);
|
||||
const dimensionBlock = buildDimensionContextBlock(dimensionContext);
|
||||
if (dimensionBlock.trim().length > 0) {
|
||||
blocks.push({ type: 'text', text: dimensionBlock });
|
||||
}
|
||||
}
|
||||
|
||||
const focusBlock = buildFocusedNodesBlock(nodeContext, options);
|
||||
blocks.push({ type: 'text', text: focusBlock });
|
||||
|
||||
|
||||
@@ -1,20 +1,68 @@
|
||||
import { INTEGRATE_WORKFLOW_INSTRUCTIONS } from '@/config/workflows/integrate';
|
||||
import { PREP_WORKFLOW_INSTRUCTIONS } from '@/config/workflows/prep';
|
||||
import { RESEARCH_WORKFLOW_INSTRUCTIONS } from '@/config/workflows/research';
|
||||
import { CONNECT_WORKFLOW_INSTRUCTIONS } from '@/config/workflows/connect';
|
||||
import { SURVEY_WORKFLOW_INSTRUCTIONS } from '@/config/workflows/survey';
|
||||
import type { WorkflowDefinition } from './types';
|
||||
import { listUserWorkflows, loadUserWorkflow } from './workflowFileService';
|
||||
|
||||
// Bundled default workflows (always available as fallback)
|
||||
const BUNDLED_WORKFLOWS: Record<string, WorkflowDefinition> = {
|
||||
'integrate': {
|
||||
'prep': {
|
||||
id: 1,
|
||||
key: 'prep',
|
||||
displayName: 'Prep',
|
||||
description: 'Quick summary to decide if content is worth deeper engagement',
|
||||
instructions: PREP_WORKFLOW_INSTRUCTIONS,
|
||||
enabled: true,
|
||||
requiresFocusedNode: true,
|
||||
primaryActor: 'oracle',
|
||||
expectedOutcome: 'Brief section appended with what/gist/why it matters',
|
||||
},
|
||||
'research': {
|
||||
id: 2,
|
||||
key: 'research',
|
||||
displayName: 'Research',
|
||||
description: 'Background research on topic, person, or concept',
|
||||
instructions: RESEARCH_WORKFLOW_INSTRUCTIONS,
|
||||
enabled: true,
|
||||
requiresFocusedNode: true,
|
||||
primaryActor: 'oracle',
|
||||
expectedOutcome: 'Research notes appended with background and key findings',
|
||||
},
|
||||
'connect': {
|
||||
id: 3,
|
||||
key: 'connect',
|
||||
displayName: 'Connect',
|
||||
description: 'Find and create edges to related nodes',
|
||||
instructions: CONNECT_WORKFLOW_INSTRUCTIONS,
|
||||
enabled: true,
|
||||
requiresFocusedNode: true,
|
||||
primaryActor: 'oracle',
|
||||
expectedOutcome: '3-5 edges created to related nodes',
|
||||
},
|
||||
'integrate': {
|
||||
id: 4,
|
||||
key: 'integrate',
|
||||
displayName: 'Integrate',
|
||||
description: 'Deep analysis and connection-building for focused node',
|
||||
description: 'Full analysis, connection discovery, and documentation',
|
||||
instructions: INTEGRATE_WORKFLOW_INSTRUCTIONS,
|
||||
enabled: true,
|
||||
requiresFocusedNode: true,
|
||||
primaryActor: 'oracle',
|
||||
expectedOutcome: 'Focused node updated with insights; 3-5 high-value edges created',
|
||||
}
|
||||
expectedOutcome: 'Integration analysis appended; 3-5 edges created',
|
||||
},
|
||||
'survey': {
|
||||
id: 5,
|
||||
key: 'survey',
|
||||
displayName: 'Survey',
|
||||
description: 'Analyze dimension patterns, themes, and gaps',
|
||||
instructions: SURVEY_WORKFLOW_INSTRUCTIONS,
|
||||
enabled: true,
|
||||
requiresFocusedNode: false, // Requires active dimension, not focused node
|
||||
primaryActor: 'oracle',
|
||||
expectedOutcome: 'Dimension description updated with survey findings',
|
||||
},
|
||||
};
|
||||
|
||||
// Set of bundled workflow keys (for UI to know which can be "reset to default")
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { nodeService } from '@/services/database/nodes';
|
||||
import { formatNodeForChat } from '../infrastructure/nodeFormatter';
|
||||
import type { Node } from '@/types/database';
|
||||
|
||||
export const queryDimensionNodesTool = tool({
|
||||
description: 'Query all nodes within a specific dimension. Returns nodes sorted by edge count (most connected first).',
|
||||
inputSchema: z.object({
|
||||
dimension: z.string().describe('The dimension name to query nodes from'),
|
||||
limit: z.number().optional().default(20).describe('Maximum number of nodes to return (default: 20)'),
|
||||
offset: z.number().optional().default(0).describe('Number of nodes to skip for pagination'),
|
||||
includeContent: z.boolean().optional().default(false).describe('Include truncated content preview (default: false)'),
|
||||
}),
|
||||
execute: async ({ dimension, limit = 20, offset = 0, includeContent = false }) => {
|
||||
try {
|
||||
// Query nodes with this dimension
|
||||
const nodes = await nodeService.getNodes({
|
||||
dimensions: [dimension],
|
||||
limit,
|
||||
offset,
|
||||
sortBy: 'edges',
|
||||
});
|
||||
|
||||
if (!nodes || nodes.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
dimension,
|
||||
nodes: [],
|
||||
total: 0,
|
||||
message: `No nodes found in dimension "${dimension}"`,
|
||||
};
|
||||
}
|
||||
|
||||
const formattedNodes = nodes.map((node: Node) => {
|
||||
const formatted: Record<string, unknown> = {
|
||||
id: node.id,
|
||||
title: node.title,
|
||||
label: formatNodeForChat({
|
||||
id: node.id,
|
||||
title: node.title,
|
||||
dimensions: node.dimensions || [],
|
||||
}),
|
||||
edgeCount: node.edge_count || 0,
|
||||
dimensions: node.dimensions || [],
|
||||
};
|
||||
|
||||
if (includeContent && node.content) {
|
||||
// Truncate to ~100 chars
|
||||
formatted.contentPreview = node.content.length > 100
|
||||
? node.content.substring(0, 100) + '...'
|
||||
: node.content;
|
||||
}
|
||||
|
||||
return formatted;
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
dimension,
|
||||
nodes: formattedNodes,
|
||||
total: nodes.length,
|
||||
hasMore: nodes.length >= limit,
|
||||
message: `Found ${nodes.length} nodes in dimension "${dimension}"`,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('queryDimensionNodes error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to query dimension nodes',
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,159 @@
|
||||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { nodeService } from '@/services/database/nodes';
|
||||
import { edgeService } from '@/services/database/edges';
|
||||
|
||||
/**
|
||||
* Quick Link Tool - Creates edges to related nodes in ONE operation
|
||||
* No agent loop, no LLM reasoning - just fast database operations
|
||||
*/
|
||||
export const quickLinkTool = tool({
|
||||
description: 'Instantly find and link related nodes based on title/content matches. Fast - no AI reasoning, just database search and edge creation.',
|
||||
inputSchema: z.object({
|
||||
node_id: z.number().describe('The node ID to find connections for'),
|
||||
max_edges: z.number().optional().default(3).describe('Maximum edges to create (default 3)'),
|
||||
}),
|
||||
execute: async (params) => {
|
||||
const { node_id, max_edges = 3 } = params;
|
||||
try {
|
||||
// 1. Get the source node
|
||||
const sourceNode = await nodeService.getNodeById(node_id);
|
||||
if (!sourceNode) {
|
||||
return { success: false, error: `Node ${node_id} not found`, edgesCreated: 0 };
|
||||
}
|
||||
|
||||
// 2. Extract search terms from title (split on common separators)
|
||||
const title = sourceNode.title || '';
|
||||
const searchTerms = extractSearchTerms(title);
|
||||
|
||||
if (searchTerms.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
message: 'No searchable terms found in title',
|
||||
edgesCreated: 0,
|
||||
searchTerms: []
|
||||
};
|
||||
}
|
||||
|
||||
// 3. Search for related nodes using each term
|
||||
const foundNodeIds = new Set<number>();
|
||||
const matchDetails: Array<{ nodeId: number; title: string; matchedTerm: string }> = [];
|
||||
|
||||
for (const term of searchTerms.slice(0, 3)) { // Max 3 search terms
|
||||
const results = await nodeService.getNodes({
|
||||
search: term,
|
||||
limit: 10
|
||||
});
|
||||
|
||||
for (const node of results) {
|
||||
// Skip self and already found
|
||||
if (node.id === node_id || foundNodeIds.has(node.id)) continue;
|
||||
|
||||
foundNodeIds.add(node.id);
|
||||
matchDetails.push({
|
||||
nodeId: node.id,
|
||||
title: node.title || `Node ${node.id}`,
|
||||
matchedTerm: term
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Get existing edges to avoid duplicates
|
||||
const existingConnections = await edgeService.getNodeConnections(node_id);
|
||||
const existingTargets = new Set(existingConnections.map(c => c.connected_node.id));
|
||||
|
||||
// 5. Create edges for top matches (excluding existing)
|
||||
const edgesToCreate = matchDetails
|
||||
.filter(m => !existingTargets.has(m.nodeId))
|
||||
.slice(0, max_edges);
|
||||
|
||||
const createdEdges: Array<{ toNodeId: number; toTitle: string }> = [];
|
||||
|
||||
for (const match of edgesToCreate) {
|
||||
try {
|
||||
await edgeService.createEdge({
|
||||
from_node_id: node_id,
|
||||
to_node_id: match.nodeId,
|
||||
source: 'helper_name',
|
||||
context: { quickLink: true, matchedTerm: match.matchedTerm }
|
||||
});
|
||||
createdEdges.push({ toNodeId: match.nodeId, toTitle: match.title });
|
||||
} catch (err) {
|
||||
// Edge might already exist, continue
|
||||
console.warn(`[quickLink] Failed to create edge to ${match.nodeId}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Return summary
|
||||
const skippedCount = matchDetails.filter(m => existingTargets.has(m.nodeId)).length;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
sourceNode: { id: node_id, title: sourceNode.title },
|
||||
searchTerms,
|
||||
edgesCreated: createdEdges.length,
|
||||
edges: createdEdges,
|
||||
skippedExisting: skippedCount,
|
||||
totalMatches: matchDetails.length,
|
||||
message: createdEdges.length > 0
|
||||
? `Linked to ${createdEdges.length} nodes: ${createdEdges.map(e => e.toTitle).join(', ')}`
|
||||
: skippedCount > 0
|
||||
? `Found ${skippedCount} matches but edges already exist`
|
||||
: `No related nodes found for: ${searchTerms.join(', ')}`
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Quick link failed',
|
||||
edgesCreated: 0
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Extract meaningful search terms from a title
|
||||
* Looks for: names, quoted phrases, capitalized words, etc.
|
||||
*/
|
||||
function extractSearchTerms(title: string): string[] {
|
||||
const terms: string[] = [];
|
||||
|
||||
// 1. Extract quoted phrases
|
||||
const quotedMatches = title.match(/["']([^"']+)["']/g);
|
||||
if (quotedMatches) {
|
||||
terms.push(...quotedMatches.map(m => m.replace(/["']/g, '').trim()));
|
||||
}
|
||||
|
||||
// 2. Extract potential names (2-3 capitalized words in sequence)
|
||||
const namePattern = /\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+){1,2})\b/g;
|
||||
let match;
|
||||
while ((match = namePattern.exec(title)) !== null) {
|
||||
const name = match[1];
|
||||
// Skip common non-name phrases
|
||||
if (!isCommonPhrase(name)) {
|
||||
terms.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Extract standalone capitalized words (potential company/project names)
|
||||
const words = title.split(/\s+/);
|
||||
for (const word of words) {
|
||||
const cleaned = word.replace(/[^a-zA-Z0-9]/g, '');
|
||||
if (cleaned.length > 2 && /^[A-Z]/.test(cleaned) && !terms.includes(cleaned)) {
|
||||
terms.push(cleaned);
|
||||
}
|
||||
}
|
||||
|
||||
// Dedupe and limit
|
||||
const unique = [...new Set(terms)].filter(t => t.length > 2);
|
||||
return unique.slice(0, 5);
|
||||
}
|
||||
|
||||
function isCommonPhrase(phrase: string): boolean {
|
||||
const common = [
|
||||
'The', 'How', 'What', 'Why', 'When', 'Where', 'Which',
|
||||
'New', 'First', 'Last', 'Next', 'This', 'That',
|
||||
'Part', 'Chapter', 'Section', 'Episode'
|
||||
];
|
||||
return common.some(c => phrase.startsWith(c + ' ') || phrase === c);
|
||||
}
|
||||
@@ -37,6 +37,9 @@ export const TOOL_GROUP_ASSIGNMENTS: Record<string, string> = {
|
||||
queryNodes: 'core',
|
||||
getNodesById: 'core',
|
||||
queryEdge: 'core',
|
||||
queryDimensions: 'core',
|
||||
getDimension: 'core',
|
||||
queryDimensionNodes: 'core',
|
||||
searchContentEmbeddings: 'core',
|
||||
|
||||
// Orchestration: Delegation and reasoning (orchestrator only)
|
||||
@@ -47,12 +50,16 @@ export const TOOL_GROUP_ASSIGNMENTS: Record<string, string> = {
|
||||
delegateNodeComparison: 'orchestration',
|
||||
delegateToWiseRAH: 'orchestration',
|
||||
executeWorkflow: 'orchestration',
|
||||
listWorkflows: 'orchestration',
|
||||
getWorkflow: 'orchestration',
|
||||
editWorkflow: 'orchestration',
|
||||
|
||||
// Execution: Write operations and extraction (workers only)
|
||||
createNode: 'execution',
|
||||
updateNode: 'execution',
|
||||
createEdge: 'execution',
|
||||
updateEdge: 'execution',
|
||||
quickLink: 'execution',
|
||||
embedContent: 'execution',
|
||||
youtubeExtract: 'execution',
|
||||
websiteExtract: 'execution',
|
||||
|
||||
@@ -6,6 +6,7 @@ import { updateNodeTool } from '../database/updateNode';
|
||||
import { createEdgeTool } from '../database/createEdge';
|
||||
import { queryEdgeTool } from '../database/queryEdge';
|
||||
import { updateEdgeTool } from '../database/updateEdge';
|
||||
import { quickLinkTool } from '../database/quickLink';
|
||||
import { createDimensionTool } from '../database/createDimension';
|
||||
import { updateDimensionTool } from '../database/updateDimension';
|
||||
import { lockDimensionTool } from '../database/lockDimension';
|
||||
@@ -13,6 +14,7 @@ import { unlockDimensionTool } from '../database/unlockDimension';
|
||||
import { deleteDimensionTool } from '../database/deleteDimension';
|
||||
import { queryDimensionsTool } from '../database/queryDimensions';
|
||||
import { getDimensionTool } from '../database/getDimension';
|
||||
import { queryDimensionNodesTool } from '../database/queryDimensionNodes';
|
||||
import { searchContentEmbeddingsTool } from '../other/searchContentEmbeddings';
|
||||
import { webSearchTool } from '../other/webSearch';
|
||||
import { thinkTool } from '../other/think';
|
||||
@@ -20,6 +22,9 @@ import { delegateToMiniRAHTool } from '../orchestration/delegateToMiniRAH';
|
||||
import { delegateNodeQuotesTool, delegateNodeComparisonTool } from '../orchestration/delegationHelpers';
|
||||
import { delegateToWiseRAHTool } from '../orchestration/delegateToWiseRAH';
|
||||
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';
|
||||
@@ -32,6 +37,7 @@ const CORE_TOOLS: Record<string, any> = {
|
||||
queryEdge: queryEdgeTool,
|
||||
queryDimensions: queryDimensionsTool,
|
||||
getDimension: getDimensionTool,
|
||||
queryDimensionNodes: queryDimensionNodesTool,
|
||||
searchContentEmbeddings: searchContentEmbeddingsTool,
|
||||
};
|
||||
|
||||
@@ -43,6 +49,9 @@ const ORCHESTRATION_TOOLS: Record<string, any> = {
|
||||
delegateNodeComparison: delegateNodeComparisonTool,
|
||||
delegateToWiseRAH: delegateToWiseRAHTool,
|
||||
executeWorkflow: executeWorkflowTool,
|
||||
listWorkflows: listWorkflowsTool,
|
||||
getWorkflow: getWorkflowTool,
|
||||
editWorkflow: editWorkflowTool,
|
||||
};
|
||||
|
||||
// Execution tools for worker agents (includes write operations)
|
||||
@@ -51,6 +60,7 @@ const EXECUTION_TOOLS: Record<string, any> = {
|
||||
updateNode: updateNodeTool,
|
||||
createEdge: createEdgeTool,
|
||||
updateEdge: updateEdgeTool,
|
||||
quickLink: quickLinkTool,
|
||||
createDimension: createDimensionTool,
|
||||
updateDimension: updateDimensionTool,
|
||||
lockDimension: lockDimensionTool,
|
||||
@@ -78,10 +88,14 @@ const ORCHESTRATOR_TOOL_NAMES = Array.from(new Set([
|
||||
'webSearch',
|
||||
'think',
|
||||
'executeWorkflow',
|
||||
'listWorkflows',
|
||||
'getWorkflow',
|
||||
'editWorkflow',
|
||||
'createNode',
|
||||
'updateNode',
|
||||
'createEdge',
|
||||
'updateEdge',
|
||||
'quickLink',
|
||||
'createDimension',
|
||||
'updateDimension',
|
||||
'lockDimension',
|
||||
@@ -110,7 +124,9 @@ const PLANNER_TOOL_NAMES = [
|
||||
'think',
|
||||
'delegateToMiniRAH',
|
||||
'updateNode', // For workflow execution (integrate workflow needs direct write access)
|
||||
'createEdge', // For Quick Link workflow
|
||||
'createEdge', // For edge creation in workflows
|
||||
'quickLink', // Fast edge creation via text search
|
||||
'updateDimension', // For survey workflow (dimension analysis)
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
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',
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
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',
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
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