mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-12 04:37:44 +02:00
[aidan] feat/workflow-suggest: nudge user to convert repeatable chat to workflow
Add SuggestConvertToWorkflow MCP tool that agents call at the end of a task when they've completed something worth repeating (daily report, weekly check, recurring data pull). Frontend detects the tool call and glows the "Convert to workflow" button 3 times to draw the eye. When user clicks it, the suggested cadence (e.g. "every weekday at 9am") is stored in the draft and seeded into the scheduling agent's first prompt, so the agent can act on the suggestion rather than asking the user again. Tool is never auto-called — agents decide when a task is genuinely repeatable (not debugging, creative work, one-off lookup). Tool description emphasizes sparse, high-confidence use only (once per session max). Files changed: - backend/apps/agents/schedule_mcp_server.py: add SuggestConvertToWorkflow tool - frontend/src/shared/mcpToolMeta.ts: add label for new tool - frontend/src/app/pages/Dashboard/cards/AgentCard.tsx: detect suggestion in session messages, show+glow "Convert to workflow" button, pass cadence to draft - frontend/src/shared/state/workflowsSlice.ts: add suggested_cadence field to Workflow interface - frontend/src/app/pages/Workflows/SchedulingView.tsx: seed scheduling agent prompt with suggested cadence hint
This commit is contained in:
@@ -237,6 +237,32 @@ TOOLS = [
|
||||
"required": ["workflow_id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "SuggestConvertToWorkflow",
|
||||
"description": (
|
||||
"Call this at the end of a response when you have just completed a task "
|
||||
"the user is likely to want to repeat on a schedule (e.g. a daily report, "
|
||||
"a weekly digest, a recurring data check, a monitoring ping). Do NOT call "
|
||||
"it for one-off tasks, debugging sessions, creative work, or anything "
|
||||
"where 'repeat it tomorrow' would be odd. Use sparingly — once per session "
|
||||
"maximum, only with high confidence. This nudges the frontend to highlight "
|
||||
"the 'Convert to Workflow' button and suggest a cadence."
|
||||
),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"description": "A brief, user-friendly explanation of why this task is a good candidate for a recurring workflow (e.g. 'This is a daily report that stays the same'). Shown in the tool bubble.",
|
||||
},
|
||||
"suggested_cadence": {
|
||||
"type": "string",
|
||||
"description": "Optional freeform cadence hint (e.g. 'every weekday morning at 9am' or 'weekly on Monday'). Leave blank if uncertain. The frontend will parse it to prefill the schedule.",
|
||||
},
|
||||
},
|
||||
"required": ["reason"],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -507,6 +533,15 @@ def handle_read_test_transcript(args: dict) -> dict:
|
||||
return _ok(f"Test Agent transcript (status: {status}):\n\n{transcript}")
|
||||
|
||||
|
||||
def handle_suggest_convert_to_workflow(args: dict) -> dict:
|
||||
reason = (args.get("reason") or "").strip()
|
||||
if not reason:
|
||||
return _err("reason is required.")
|
||||
cadence = (args.get("suggested_cadence") or "").strip()
|
||||
result = json.dumps({"reason": reason, "cadence": cadence})
|
||||
return {"content": [{"type": "text", "text": result}]}
|
||||
|
||||
|
||||
HANDLERS = {
|
||||
"ScheduleWorkflow": handle_schedule_workflow,
|
||||
"ListScheduledWorkflows": handle_list,
|
||||
@@ -520,6 +555,7 @@ HANDLERS = {
|
||||
"DeleteWorkflowStep": handle_delete_step,
|
||||
"TestWorkflow": handle_test_workflow,
|
||||
"ReadTestTranscript": handle_read_test_transcript,
|
||||
"SuggestConvertToWorkflow": handle_suggest_convert_to_workflow,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -68,6 +68,29 @@ function extractStepsFromSession(session: { messages: Array<{ role: string; cont
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Detect if the session has a completed SuggestConvertToWorkflow tool call. */
|
||||
function findWorkflowSuggestion(session: AgentSession): { reason: string; cadence: string } | null {
|
||||
for (const msg of session.messages || []) {
|
||||
if (msg.role !== 'assistant') continue;
|
||||
const content = Array.isArray(msg.content) ? msg.content : [];
|
||||
for (const block of content) {
|
||||
if (block?.type === 'tool_result' && block?.tool_name === 'SuggestConvertToWorkflow') {
|
||||
const mcpServer = (block as any)?.mcpServer || '';
|
||||
if (!mcpServer.includes('openswarm-schedule')) continue;
|
||||
const text = block?.content?.[0]?.text;
|
||||
if (!text) continue;
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
if (parsed?.reason) return { reason: parsed.reason, cadence: parsed.cadence || '' };
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const GoogleServiceIcon: React.FC<{ service: string; size?: number }> = ({ service, size = 16 }) => {
|
||||
if (service === 'gmail') {
|
||||
return (
|
||||
@@ -259,6 +282,8 @@ const AgentCard: React.FC<Props> = ({
|
||||
const defaultModel = useAppSelector((s) => s.settings.data.default_model);
|
||||
const defaultMode = useAppSelector((s) => s.settings.data.default_mode);
|
||||
const [converting, setConverting] = useState(false);
|
||||
const [suggestAnimationFired, setSuggestAnimationFired] = useState(false);
|
||||
const workflowSuggestion = useMemo(() => findWorkflowSuggestion(session), [session]);
|
||||
// Hide the "Convert to workflow" button when this chat is already
|
||||
// entangled with a workflow (Image #44 note). Two cases:
|
||||
// (a) The session is one of a workflow's runner sessions, OR
|
||||
@@ -318,6 +343,15 @@ const AgentCard: React.FC<Props> = ({
|
||||
}, [session.model, modelsByProvider]);
|
||||
const scrollOverlayRef = useOverlayScrollPassthrough(isSelected);
|
||||
|
||||
const suggestGlowRef = useRef<boolean>(false);
|
||||
useEffect(() => {
|
||||
if (workflowSuggestion && !suggestAnimationFired && !suggestGlowRef.current) {
|
||||
suggestGlowRef.current = true;
|
||||
setSuggestAnimationFired(true);
|
||||
dispatch(fadeGlowingAgentCard(session.id, 3000));
|
||||
}
|
||||
}, [workflowSuggestion, suggestAnimationFired, dispatch, session.id]);
|
||||
|
||||
const cardBoxRef = useRef<HTMLDivElement>(null);
|
||||
// Ref so ResizeObserver sees latest value without re-attaching when active flips.
|
||||
const isDashboardActiveRef = useRef(isDashboardActive);
|
||||
@@ -907,7 +941,7 @@ const AgentCard: React.FC<Props> = ({
|
||||
</Box>
|
||||
</Tooltip>
|
||||
)}
|
||||
{(session.status === 'completed' || session.status === 'stopped') && session.messages.length >= 2 && !isWorkflowRunnerSession && (
|
||||
{((session.status === 'completed' || session.status === 'stopped') && session.messages.length >= 2 && !isWorkflowRunnerSession || !!workflowSuggestion) && (
|
||||
<Tooltip title="Turn this chat into a reusable, schedulable workflow">
|
||||
<Box
|
||||
role="button"
|
||||
@@ -918,9 +952,6 @@ const AgentCard: React.FC<Props> = ({
|
||||
if (steps.length === 0) return;
|
||||
setConverting(true);
|
||||
const draftId = `draft-${session.id}-${Date.now()}`;
|
||||
// The chat card becomes a temporary workflow draft in the
|
||||
// same slot. Nothing is persisted until the user chooses
|
||||
// Save Draft or Schedule Workflow from the draft card.
|
||||
dispatch(addWorkflowCard({ workflowId: draftId, sourceSessionId: session.id, expandedSessionIds }));
|
||||
dispatch(setWorkflowCardPosition({ workflowId: draftId, x: cardX, y: cardY }));
|
||||
dispatch(setWorkflowCardSize({ workflowId: draftId, width: cardWidth, height: cardHeight }));
|
||||
@@ -937,10 +968,12 @@ const AgentCard: React.FC<Props> = ({
|
||||
use_synced_prompt: true,
|
||||
model: defaultModel || session.model,
|
||||
mode: defaultMode || session.mode,
|
||||
suggested_cadence: workflowSuggestion?.cadence || undefined,
|
||||
} as Partial<Workflow>,
|
||||
}));
|
||||
}}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
className={workflowSuggestion && suggestAnimationFired ? 'workflow-suggest-glow' : undefined}
|
||||
sx={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 0.5,
|
||||
color: '#fff',
|
||||
@@ -952,6 +985,20 @@ const AgentCard: React.FC<Props> = ({
|
||||
cursor: converting ? 'wait' : 'pointer',
|
||||
opacity: converting ? 0.7 : 1,
|
||||
'&:hover': { filter: 'brightness(1.05)' },
|
||||
'&.workflow-suggest-glow': {
|
||||
animation: 'workflow-suggest-glow 600ms ease-in-out 3',
|
||||
'@keyframes workflow-suggest-glow': {
|
||||
'0%': {
|
||||
boxShadow: `0 0 0 2px ${c.accent.primary}, 0 0 8px 2px ${c.accent.primary}88`,
|
||||
},
|
||||
'50%': {
|
||||
boxShadow: `0 0 0 4px ${c.accent.primary}66, 0 0 16px 4px ${c.accent.primary}44`,
|
||||
},
|
||||
'100%': {
|
||||
boxShadow: `0 0 0 2px ${c.accent.primary}, 0 0 8px 2px ${c.accent.primary}88`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AutoAwesomeOutlinedIcon sx={{ fontSize: 14 }} />
|
||||
|
||||
@@ -66,11 +66,13 @@ export default function SchedulingView({ workflow, steps }: Props) {
|
||||
(async () => {
|
||||
try {
|
||||
const tok = (() => { try { return getAuthToken(); } catch { return ''; } })();
|
||||
const cadenceHint = workflow.suggested_cadence ? ` I think it should run ${workflow.suggested_cadence}.` : '';
|
||||
const prompt = `Greet me in one short sentence, then ask exactly: "When should this workflow run (e.g. every Wednesday at 1pm)?"${cadenceHint}`;
|
||||
await fetch(`${API_BASE}/agents/sessions/${encodeURIComponent(scheduleSessionId)}/message`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...(tok ? { Authorization: `Bearer ${tok}` } : {}) },
|
||||
body: JSON.stringify({
|
||||
prompt: 'Greet me in one short sentence, then ask exactly: "When should this workflow run (e.g. every Wednesday at 1pm)?"',
|
||||
prompt,
|
||||
hidden: true,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -121,6 +121,7 @@ export function getWorkflowToolLabel(action: string): string | null {
|
||||
if (lower === 'testworkflow') return 'Test workflow';
|
||||
if (lower === 'readtesttranscript') return 'Read test results';
|
||||
if (lower === 'listworkflows') return 'List workflows';
|
||||
if (lower === 'suggestconverttoworkflow') return 'Suggest workflow';
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +101,9 @@ export interface Workflow {
|
||||
* conversion). Compared against the current steps to decide whether to warn
|
||||
* before scheduling. See scheduleUtils.needsScheduleTestWarning. */
|
||||
tested_signature?: string | null;
|
||||
/** Suggested cadence from a SuggestConvertToWorkflow tool call (e.g. "every weekday at 9am").
|
||||
* Used to seed the scheduling agent's prompt. Transient draft field only. */
|
||||
suggested_cadence?: string;
|
||||
}
|
||||
|
||||
export interface WorkflowRun {
|
||||
|
||||
Reference in New Issue
Block a user