Skip to content

Commit e577823

Browse files
abrichrclaude
andcommitted
feat: add interactive step correction during recording
After generating suggested steps from the screenshot, the user can now type corrections (e.g., "step 9 formula should reference Sheet1.B2") and the VLM will regenerate with the feedback. Loop continues until the user presses Enter to accept. Also refactors _generate_steps into smaller functions: - _build_setup_desc(): extracts setup description from task config - _vlm_call(): shared OpenAI API call helper - _refine_steps(): sends feedback + screenshot for revised steps - _display_steps(): pretty-prints step box - _interactive_step_review(): correction loop Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 44db6e6 commit e577823

1 file changed

Lines changed: 161 additions & 51 deletions

File tree

scripts/record_waa_demos.py

Lines changed: 161 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,41 @@ def _wait_for_stable_screen(
561561
return prev_png
562562

563563

564+
def _build_setup_desc(task_config: dict) -> str:
565+
"""Build a human-readable description of the task setup actions."""
566+
parts = []
567+
for entry in task_config.get("config", []):
568+
t = entry.get("type", "")
569+
p = entry.get("parameters", {})
570+
if t == "download":
571+
for f in p.get("files", []):
572+
parts.append(f" - File downloaded: {f['path']}")
573+
elif t == "open":
574+
parts.append(f" - File opened: {p.get('path', '?')}")
575+
elif t == "launch":
576+
parts.append(f" - App launched: {p.get('command', '?')}")
577+
return "\n".join(parts) if parts else " (none)"
578+
579+
580+
def _vlm_call(
581+
messages: list[dict],
582+
api_key: str,
583+
model: str = "gpt-4o",
584+
max_tokens: int = 800,
585+
) -> str:
586+
"""Send a chat completion request to OpenAI. Returns the text response."""
587+
import requests as _req
588+
589+
resp = _req.post(
590+
"https://api.openai.com/v1/chat/completions",
591+
headers={"Authorization": f"Bearer {api_key}"},
592+
json={"model": model, "max_tokens": max_tokens, "messages": messages},
593+
timeout=60,
594+
)
595+
resp.raise_for_status()
596+
return resp.json()["choices"][0]["message"]["content"]
597+
598+
564599
def _generate_steps(
565600
screenshot_png: bytes,
566601
instruction: str,
@@ -577,66 +612,143 @@ def _generate_steps(
577612
if not api_key:
578613
return "(No OPENAI_API_KEY set — skipping AI step generation)"
579614

580-
import requests as _req
581-
582615
b64 = base64.b64encode(screenshot_png).decode()
583-
584-
setup_desc = ""
585-
for entry in task_config.get("config", []):
586-
t = entry.get("type", "")
587-
p = entry.get("parameters", {})
588-
if t == "download":
589-
for f in p.get("files", []):
590-
setup_desc += f" - File downloaded: {f['path']}\n"
591-
elif t == "open":
592-
setup_desc += f" - File opened: {p.get('path', '?')}\n"
593-
elif t == "launch":
594-
setup_desc += f" - App launched: {p.get('command', '?')}\n"
616+
setup_desc = _build_setup_desc(task_config)
595617

596618
prompt = f"""You are helping a human perform a task on a Windows desktop via VNC.
597619
598620
TASK: {instruction}
599621
600622
ENVIRONMENT SETUP (already done automatically):
601-
{setup_desc if setup_desc else " (none)"}
623+
{setup_desc}
602624
603625
Look at the screenshot and give step-by-step instructions to complete the task.
604626
Be specific about what to click, what to type, and what menus to use.
605627
If a file should already be open but isn't visible, say how to open it.
606628
Keep each step to one action. Use plain text, numbered list.
607629
Be concise — the user will read this on a phone screen."""
608630

631+
messages = [
632+
{
633+
"role": "user",
634+
"content": [
635+
{"type": "text", "text": prompt},
636+
{
637+
"type": "image_url",
638+
"image_url": {
639+
"url": f"data:image/png;base64,{b64}",
640+
"detail": "high",
641+
},
642+
},
643+
],
644+
}
645+
]
646+
609647
try:
610-
resp = _req.post(
611-
"https://api.openai.com/v1/chat/completions",
612-
headers={"Authorization": f"Bearer {api_key}"},
613-
json={
614-
"model": "gpt-4o",
615-
"max_tokens": 800,
616-
"messages": [
617-
{
618-
"role": "user",
619-
"content": [
620-
{"type": "text", "text": prompt},
621-
{
622-
"type": "image_url",
623-
"image_url": {
624-
"url": f"data:image/png;base64,{b64}",
625-
"detail": "high",
626-
},
627-
},
628-
],
629-
}
630-
],
631-
},
632-
timeout=30,
633-
)
634-
resp.raise_for_status()
635-
return resp.json()["choices"][0]["message"]["content"]
648+
return _vlm_call(messages, api_key)
636649
except Exception as e:
637650
return f"(AI step generation failed: {e})"
638651

639652

653+
def _refine_steps(
654+
screenshot_png: bytes,
655+
instruction: str,
656+
task_config: dict,
657+
current_steps: str,
658+
feedback: str,
659+
) -> str:
660+
"""Refine suggested steps based on user feedback.
661+
662+
Sends the original screenshot, instruction, current steps, and the
663+
user's correction back to the VLM for a revised set of steps.
664+
"""
665+
import base64
666+
import os
667+
668+
api_key = os.environ.get("OPENAI_API_KEY")
669+
if not api_key:
670+
return current_steps
671+
672+
b64 = base64.b64encode(screenshot_png).decode()
673+
setup_desc = _build_setup_desc(task_config)
674+
675+
prompt = f"""You are helping a human perform a task on a Windows desktop via VNC.
676+
677+
TASK: {instruction}
678+
679+
ENVIRONMENT SETUP (already done automatically):
680+
{setup_desc}
681+
682+
You previously suggested these steps:
683+
684+
{current_steps}
685+
686+
The user says this is wrong and provides this feedback:
687+
688+
"{feedback}"
689+
690+
Look at the screenshot again and produce CORRECTED step-by-step instructions
691+
that address the user's feedback. Keep the same format: plain text, numbered list,
692+
one action per step, concise."""
693+
694+
messages = [
695+
{
696+
"role": "user",
697+
"content": [
698+
{"type": "text", "text": prompt},
699+
{
700+
"type": "image_url",
701+
"image_url": {
702+
"url": f"data:image/png;base64,{b64}",
703+
"detail": "high",
704+
},
705+
},
706+
],
707+
}
708+
]
709+
710+
try:
711+
return _vlm_call(messages, api_key)
712+
except Exception as e:
713+
print(f" (Refinement failed: {e})")
714+
return current_steps
715+
716+
717+
def _display_steps(steps_text: str) -> None:
718+
"""Pretty-print suggested steps in a box."""
719+
print()
720+
print(" ┌─ SUGGESTED STEPS ──────────────────────────────")
721+
for line in steps_text.splitlines():
722+
print(f" │ {line}")
723+
print(" └────────────────────────────────────────────────")
724+
print()
725+
726+
727+
def _interactive_step_review(
728+
screenshot_png: bytes,
729+
instruction: str,
730+
task_config: dict,
731+
initial_steps: str,
732+
) -> str:
733+
"""Let the user review and iteratively correct the suggested steps.
734+
735+
The user can press Enter to accept, or type feedback to refine.
736+
Returns the final accepted steps text.
737+
"""
738+
current = initial_steps
739+
while True:
740+
correction = input(
741+
" Press Enter to accept steps, or type correction: "
742+
).strip()
743+
if not correction:
744+
return current
745+
print(" Refining steps...")
746+
current = _refine_steps(
747+
screenshot_png, instruction, task_config, current, correction,
748+
)
749+
_display_steps(current)
750+
751+
640752
def cmd_record_waa(
641753
tasks: str = ",".join(HARDER_TASK_IDS),
642754
server: str = "http://localhost:5001",
@@ -848,11 +960,10 @@ def _hard_reset_task_env() -> bytes:
848960
# Generate AI step-by-step guidance from screenshot
849961
print(" Generating suggested steps...")
850962
suggested = _generate_steps(before_png, instruction, task_config)
851-
print()
852-
print(" ┌─ SUGGESTED STEPS ──────────────────────────────")
853-
for line in suggested.splitlines():
854-
print(f" │ {line}")
855-
print(" └────────────────────────────────────────────────")
963+
_display_steps(suggested)
964+
suggested = _interactive_step_review(
965+
before_png, instruction, task_config, suggested,
966+
)
856967
print()
857968
print(" Perform each action in VNC, then press Enter here.")
858969
print(" Press 'd' when done, 'r' to redo last step,")
@@ -886,11 +997,10 @@ def _hard_reset_task_env() -> bytes:
886997
# Re-generate steps from the new stable screenshot
887998
print(" Generating suggested steps...")
888999
suggested = _generate_steps(before_png, instruction, task_config)
889-
print()
890-
print(" ┌─ SUGGESTED STEPS ──────────────────────────────")
891-
for line in suggested.splitlines():
892-
print(f" │ {line}")
893-
print(" └────────────────────────────────────────────────")
1000+
_display_steps(suggested)
1001+
suggested = _interactive_step_review(
1002+
before_png, instruction, task_config, suggested,
1003+
)
8941004
print()
8951005
print(" Task restarted. Continue recording.\n")
8961006
continue

0 commit comments

Comments
 (0)