[eric] workflows: EditAgent multi-turn memory via history payload

This commit is contained in:
ciregenz
2026-05-22 02:18:11 -07:00
parent 56cc953a5e
commit 72f3e7bc5f
2 changed files with 43 additions and 7 deletions
+22 -7
View File
@@ -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:
@@ -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) {