diff --git a/backend/apps/workflows/workflows.py b/backend/apps/workflows/workflows.py index 4a4f6de7..f7a54d19 100644 --- a/backend/apps/workflows/workflows.py +++ b/backend/apps/workflows/workflows.py @@ -410,11 +410,10 @@ async def propose_edit(workflow_id: str, body: dict): """Aux-LLM-propose a single-step edit from a natural-language request. Powers the Edit Agent chat (Image #38). Frontend hands us the user's - message, the current draft steps, and optional failure-context (when - we're inside Fix-with-Agent). We respond with a reply string PLUS, - optionally, a `step_idx` + `new_text` that the FE shows as a - proposal card. The user clicks Apply to merge into their local draft; - nothing is persisted until they click Save in the header. + message, the current draft steps, optional failure-context (Fix-with- + Agent), AND the prior turns so the model has multi-turn memory. We + respond with a reply string PLUS, optionally, a `step_idx` + `new_text` + that the FE shows as a proposal card. """ wf = storage.get_workflow(workflow_id) if not wf: @@ -422,6 +421,7 @@ async def propose_edit(workflow_id: str, body: dict): message = (body or {}).get("message", "").strip() steps_in = (body or {}).get("steps") or [] context = (body or {}).get("context") or None + history = (body or {}).get("history") or [] if not message or not isinstance(steps_in, list): raise HTTPException(status_code=400, detail="Missing message or steps") try: @@ -450,6 +450,20 @@ async def propose_edit(workflow_id: str, body: dict): f"The error was: {err}\n" f"Your proposed edit should specifically address that failure if possible." ) + # Build history block so the model remembers prior turns. Each entry + # is {role, text}; we only carry assistant/user pairs (proposals get + # summarised inline so the assistant has context for follow-ups). + history_lines = [] + if isinstance(history, list): + for h in history[-12:]: + if not isinstance(h, dict): + continue + role = str(h.get("role") or "").strip().lower() + text = str(h.get("text") or "").strip() + if role in ("user", "assistant") and text: + history_lines.append(f"{role.capitalize()}: {text}") + history_block = ("\n\nPrior conversation:\n" + "\n".join(history_lines)) if history_lines else "" + prompt = ( "You are an Edit Agent helping the user iterate on a saved automation " "workflow. The workflow's current steps are listed below. The user has " @@ -464,8 +478,9 @@ async def propose_edit(workflow_id: str, body: dict): "- If the user is asking a question or for clarification, set step_idx=null and new_text=null.\n" "- If the user is asking to change a specific step, set step_idx (0-based) and new_text to the FULL replacement prompt for that step.\n" "- `explanation` describes the change in user-facing terms.\n" - "- Never invent new steps. Never remove steps. Only edit existing ones.\n\n" - f"Workflow steps:\n{steps_lines}{fix_context}\n\n" + "- Never invent new steps. Never remove steps. Only edit existing ones.\n" + "- Use prior conversation context to disambiguate follow-ups (e.g. \"yes do that\" should reference the last proposal).\n\n" + f"Workflow steps:\n{steps_lines}{fix_context}{history_block}\n\n" f"User: {message}" ) try: diff --git a/frontend/src/app/pages/Workflows/EditAgentView.tsx b/frontend/src/app/pages/Workflows/EditAgentView.tsx index 35ed65c2..da83a009 100644 --- a/frontend/src/app/pages/Workflows/EditAgentView.tsx +++ b/frontend/src/app/pages/Workflows/EditAgentView.tsx @@ -111,10 +111,30 @@ export default function EditAgentView({ workflow, steps, isFixMode = false }: Pr const text = draft.trim(); if (!text || busy) return; setBusy(true); + // Capture history BEFORE pushing the user turn so the model doesn't + // see the just-submitted message twice (once in history, once as the + // current user message). + const turnsSnapshot = turns; setTurns((t) => [...t, { kind: 'user', text }]); setDraft(''); try { const tok = (() => { try { return getAuthToken(); } catch { return ''; } })(); + // Pull the prior conversation so the model has multi-turn memory. + // Proposals get folded into the history as "Assistant: proposed + // changing step N to ..." so follow-ups like "yes do that" resolve + // correctly. Cap to last 12 entries to keep payload tight. + const historyPayload = turnsSnapshot.flatMap((t) => { + if (t.kind === 'user') return [{ role: 'user', text: t.text }]; + if (t.kind === 'assistant') return [{ role: 'assistant', text: t.text }]; + if (t.kind === 'proposal') { + const verb = t.applied ? 'applied' : 'proposed'; + return [{ role: 'assistant', text: `(${verb}) change step ${t.stepIdx + 1} to: ${t.after.slice(0, 200)}` }]; + } + if (t.kind === 'fix-prefix') { + return [{ role: 'assistant', text: `Fixing step ${t.stepIdx + 1}: ${t.error.slice(0, 200)}` }]; + } + return []; + }); const res = await fetch(`${API_BASE}/workflows/${encodeURIComponent(workflow.id)}/propose-edit`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(tok ? { Authorization: `Bearer ${tok}` } : {}) }, @@ -122,6 +142,7 @@ export default function EditAgentView({ workflow, steps, isFixMode = false }: Pr message: text, steps: draftSteps.map((s) => ({ id: s.id, text: s.text, label: s.label || null })), context: isFixMode && fixSeed ? { failed_step: fixSeed.stepIdx, error: fixSeed.error } : null, + history: historyPayload, }), }); if (!res.ok) {