Mayank2027 commited on
Commit
af81535
·
verified ·
1 Parent(s): cec3c3f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +252 -106
app.py CHANGED
@@ -12,7 +12,6 @@ from playwright.async_api import async_playwright, Browser, Page
12
  from groq import AsyncGroq
13
 
14
  app = FastAPI()
15
-
16
  app.add_middleware(
17
  CORSMiddleware,
18
  allow_origins=["*"],
@@ -21,7 +20,7 @@ app.add_middleware(
21
  allow_headers=["*"],
22
  )
23
 
24
- # ---------- Groq client ----------
25
  GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
26
  if not GROQ_API_KEY:
27
  raise RuntimeError("GROQ_API_KEY not set.")
@@ -29,18 +28,34 @@ client = AsyncGroq(api_key=GROQ_API_KEY)
29
 
30
  # ---------- System prompt ----------
31
  SYSTEM_PROMPT = """
32
- You are an autonomous web agent. You have access to a browser via Playwright.
33
- Your goal is to complete the user's task by issuing a sequence of actions.
 
 
 
34
 
35
- Available actions (return a JSON object with exactly one of these):
36
  - {"action": "navigate", "url": "https://..."}
37
  - {"action": "click", "selector": "css_selector"}
38
  - {"action": "fill", "selector": "css_selector", "value": "text"}
 
 
 
 
 
39
  - {"action": "wait", "seconds": 2}
40
- - {"action": "screenshot"} (captures current page)
41
- - {"action": "done", "result": "final answer or error message"}
 
 
 
 
 
 
 
 
 
42
 
43
- You will receive the current page URL and a simplified DOM text (visible text only).
44
  Think step by step. Only output the JSON action, nothing else.
45
  """
46
 
@@ -53,6 +68,8 @@ class SessionState:
53
  self.steps = 0
54
  self.last_result = ""
55
  self.done = False
 
 
56
 
57
  sessions = {}
58
 
@@ -64,18 +81,40 @@ async def get_screenshot_b64(page):
64
  screenshot_bytes = await page.screenshot(full_page=False)
65
  return base64.b64encode(screenshot_bytes).decode("utf-8")
66
 
67
- async def ask_llm_for_action(task: str, url: str, page_text: str):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  if len(page_text) > 4000:
69
  page_text = page_text[:4000] + "\n... (truncated)"
 
 
 
70
  user_prompt = f"""
71
- Task: {task}
72
 
73
  Current URL: {url}
74
 
75
- Page content (visible text):
 
 
 
76
  {page_text}
77
 
78
- What is your next action? Output a JSON action.
 
 
79
  """
80
  completion = await client.chat.completions.create(
81
  model="llama-3.3-70b-versatile",
@@ -97,101 +136,179 @@ What is your next action? Output a JSON action.
97
  action = {"action": "done", "result": f"Invalid LLM output: {raw}"}
98
  return action
99
 
100
- async def execute_one_action(session: SessionState, user_instruction: str = None):
101
- """Execute exactly one action. If user_instruction is given, override the LLM action."""
 
 
 
 
 
 
102
  page = session.page
103
- # If user gave an instruction, try to parse it as a JSON action; otherwise treat as a new task.
104
- if user_instruction and user_instruction.lower() != "auto":
105
- # Attempt to parse as a JSON action
106
  try:
107
- action = json.loads(user_instruction)
108
- except:
109
- # If not JSON, treat as a new task description – we'll update the session task
110
- session.task = user_instruction
111
- # Then get LLM action for the new task
112
- action = await ask_llm_for_action(
113
- session.task,
114
- page.url,
115
- await get_page_text(page)
116
- )
117
  else:
118
  # Auto – ask LLM
119
- action = await ask_llm_for_action(
120
- session.task,
121
- page.url,
122
- await get_page_text(page)
123
- )
124
 
125
  act = action.get("action")
126
  result_text = ""
127
  screenshot_b64 = ""
128
 
129
- if act == "done":
130
- session.done = True
131
- session.last_result = action.get("result", "Task completed.")
132
- result_text = session.last_result
133
- screenshot_b64 = await get_screenshot_b64(page)
134
- return {"done": True, "result": result_text, "screenshot": screenshot_b64, "action": action}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
- elif act == "navigate":
137
- url = action.get("url")
138
- if url:
139
- await page.goto(url, timeout=30000)
140
  else:
141
- result_text = "Error: missing 'url' for navigate"
142
- elif act == "click":
143
- sel = action.get("selector")
144
- if sel:
145
- try:
146
- await page.click(sel, timeout=5000)
147
- except Exception as e:
148
- result_text = f"Click failed: {str(e)}"
149
- else:
150
- result_text = "Error: missing 'selector' for click"
151
- elif act == "fill":
152
- sel = action.get("selector")
153
- val = action.get("value")
154
- if sel and val is not None:
155
- try:
156
- await page.fill(sel, val, timeout=5000)
157
- except Exception as e:
158
- result_text = f"Fill failed: {str(e)}"
159
- else:
160
- result_text = "Error: missing 'selector' or 'value' for fill"
161
- elif act == "wait":
162
- seconds = action.get("seconds", 1)
163
- await asyncio.sleep(seconds)
164
- elif act == "screenshot":
165
- # just capture, will return later
166
- pass
167
- else:
168
- result_text = f"Unknown action: {act}"
169
 
 
 
 
 
 
170
  session.steps += 1
171
- if result_text:
172
- session.last_result = result_text
173
 
174
  # Capture screenshot after the action
175
  screenshot_b64 = await get_screenshot_b64(page)
176
 
177
  # Ask LLM for the next action (to show the user)
178
- next_action = await ask_llm_for_action(
179
- session.task,
180
- page.url,
181
- await get_page_text(page)
182
- )
183
 
184
  return {
185
  "done": session.done,
186
- "result": session.last_result or "Action executed.",
187
  "screenshot": screenshot_b64,
188
  "steps": session.steps,
189
  "proposed_next_action": next_action,
190
  "current_url": page.url,
191
- "action_taken": action
 
192
  }
193
 
194
- # ---------- Endpoints ----------
195
  class StartRequest(BaseModel):
196
  task: str
197
  headless: bool = True
@@ -219,9 +336,8 @@ async def start_session(req: StartRequest):
219
  session = SessionState(req.task, browser, page)
220
  sessions[session_id] = session
221
 
222
- # Get initial screenshot and proposed action
223
  screenshot_b64 = await get_screenshot_b64(page)
224
- next_action = await ask_llm_for_action(req.task, page.url, await get_page_text(page))
225
 
226
  return StartResponse(
227
  session_id=session_id,
@@ -235,7 +351,7 @@ async def start_session(req: StartRequest):
235
 
236
  class StepRequest(BaseModel):
237
  session_id: str
238
- instruction: str = "auto" # "auto" or a JSON action string, or a new task description
239
 
240
  class StepResponse(BaseModel):
241
  done: bool
@@ -245,6 +361,7 @@ class StepResponse(BaseModel):
245
  proposed_next_action: dict
246
  current_url: str
247
  action_taken: dict
 
248
 
249
  @app.post("/step", response_model=StepResponse)
250
  async def step(req: StepRequest):
@@ -258,12 +375,13 @@ async def step(req: StepRequest):
258
  result = await execute_one_action(session, req.instruction)
259
  return StepResponse(
260
  done=result["done"],
261
- result=result.get("result", ""),
262
  screenshot=result["screenshot"],
263
  steps=result["steps"],
264
- proposed_next_action=result.get("proposed_next_action", {}),
265
- current_url=result.get("current_url", ""),
266
- action_taken=result.get("action_taken", {})
 
267
  )
268
  except Exception as e:
269
  raise HTTPException(status_code=500, detail=str(e))
@@ -276,34 +394,36 @@ async def stop_session(session_id: str):
276
  return {"status": "stopped"}
277
  return {"status": "not_found"}
278
 
279
- # ---------- Serve the interactive HTML ----------
280
  HTML_PAGE = """
281
  <!DOCTYPE html>
282
- <html lang="en">
283
  <head>
284
- <meta charset="UTF-8" />
285
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
286
- <title>Interactive Agent Control</title>
287
  <style>
288
  * { box-sizing: border-box; margin: 0; padding: 0; }
289
- body { font-family: system-ui, sans-serif; background: #0b0e14; color: #e8edf5; padding: 20px; }
290
- .container { max-width: 1200px; margin: 0 auto; background: #1a1f2a; border-radius: 24px; padding: 30px; }
291
  .row { display: flex; flex-wrap: wrap; gap: 30px; }
292
  .left { flex: 1; min-width: 300px; }
293
  .right { flex: 2; min-width: 500px; }
294
  textarea, input[type="text"] { width: 100%; background: #0f141d; border: 1px solid #2f384a; border-radius: 12px; padding: 12px; color: white; }
295
  .btn { background: #4f7cff; border: none; color: white; padding: 10px 20px; border-radius: 40px; cursor: pointer; margin: 5px; }
296
  .btn:disabled { opacity: 0.5; }
297
- .screenshot-box img { width: 100%; border-radius: 12px; border: 1px solid #2f384a; }
298
  .status { margin: 10px 0; }
299
  .action-card { background: #11171f; padding: 15px; border-radius: 12px; margin: 10px 0; }
300
  pre { background: #0f141d; padding: 10px; border-radius: 8px; overflow: auto; max-height: 200px; }
 
 
 
301
  </style>
302
  </head>
303
  <body>
304
  <div class="container">
305
- <h1>🧠 Step‑by‑Step Agent</h1>
306
- <p>Start a session, then see the screenshot and the proposed action. You can <strong>continue</strong> or <strong>override</strong> with a new instruction.</p>
307
  <hr style="border-color:#2f384a; margin:20px 0;">
308
 
309
  <div class="row">
@@ -315,6 +435,11 @@ HTML_PAGE = """
315
  <button class="btn" id="stopBtn" disabled>⏹ Stop</button>
316
  <div id="status" class="status"></div>
317
  <div id="info"></div>
 
 
 
 
 
318
  </div>
319
 
320
  <div class="right">
@@ -330,12 +455,15 @@ HTML_PAGE = """
330
  <button class="btn" id="overrideBtn">✏️ Override</button>
331
  </div>
332
  <div id="overrideArea" style="display:none; margin-top:10px;">
333
- <input type="text" id="overrideInput" placeholder='e.g. {"action":"click","selector":"#login"}' style="width:70%;" />
334
  <button class="btn" id="sendOverrideBtn">Send</button>
335
- <small style="display:block; color:#8892a8;">You can also type a new task description, e.g. "now search for Python"</small>
336
  </div>
337
  <div id="resultBox" style="margin-top:15px; background:#11171f; padding:15px; border-radius:12px; display:none;">
338
- <strong>✅ Result:</strong> <span id="resultText">—</span>
 
 
 
339
  </div>
340
  </div>
341
  </div>
@@ -344,8 +472,6 @@ HTML_PAGE = """
344
 
345
  <script>
346
  let sessionId = null;
347
- let currentStep = 0;
348
-
349
  const statusDiv = document.getElementById('status');
350
  const infoDiv = document.getElementById('info');
351
  const screenshotContainer = document.getElementById('screenshotContainer');
@@ -354,6 +480,10 @@ HTML_PAGE = """
354
  const proposedAction = document.getElementById('proposedAction');
355
  const resultBox = document.getElementById('resultBox');
356
  const resultText = document.getElementById('resultText');
 
 
 
 
357
 
358
  const startBtn = document.getElementById('startBtn');
359
  const stopBtn = document.getElementById('stopBtn');
@@ -377,10 +507,25 @@ HTML_PAGE = """
377
  proposedAction.textContent = JSON.stringify(action, null, 2);
378
  }
379
 
 
 
 
 
 
 
 
 
 
 
 
380
  function updateUI(data) {
381
- currentStep = data.steps || 0;
382
  showScreenshot(data.screenshot);
383
  showProposedAction(data.proposed_next_action);
 
 
 
 
 
384
  if (data.done) {
385
  resultBox.style.display = 'block';
386
  resultText.textContent = data.result || 'Task completed.';
@@ -388,7 +533,8 @@ HTML_PAGE = """
388
  overrideBtn.disabled = true;
389
  setStatus('✅ Session finished.');
390
  } else {
391
- resultBox.style.display = 'none';
 
392
  continueBtn.disabled = false;
393
  overrideBtn.disabled = false;
394
  setStatus(`👣 Step ${data.steps} done. Next action proposed.`);
@@ -414,7 +560,6 @@ HTML_PAGE = """
414
  const task = document.getElementById('taskInput').value.trim();
415
  if (!task) { setStatus('Please enter a task.', true); return; }
416
  const headless = document.getElementById('headlessCheck').checked;
417
-
418
  startBtn.disabled = true;
419
  setStatus('Starting session...');
420
  try {
@@ -473,6 +618,7 @@ HTML_PAGE = """
473
  setStatus('Session stopped.');
474
  actionContainer.style.display = 'none';
475
  screenshotContainer.style.display = 'none';
 
476
  } catch (e) {
477
  setStatus('Stop error: ' + e.message, true);
478
  }
@@ -486,7 +632,7 @@ HTML_PAGE = """
486
  async def serve_html():
487
  return HTMLResponse(content=HTML_PAGE)
488
 
489
- # ---------- Optional: cleanup on exit ----------
490
  import atexit
491
  @atexit.register
492
  async def cleanup():
 
12
  from groq import AsyncGroq
13
 
14
  app = FastAPI()
 
15
  app.add_middleware(
16
  CORSMiddleware,
17
  allow_origins=["*"],
 
20
  allow_headers=["*"],
21
  )
22
 
23
+ # ---------- Groq ----------
24
  GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
25
  if not GROQ_API_KEY:
26
  raise RuntimeError("GROQ_API_KEY not set.")
 
28
 
29
  # ---------- System prompt ----------
30
  SYSTEM_PROMPT = """
31
+ You are an autonomous web agent. You control a real browser via Playwright.
32
+ Your task is given by the user. You must complete it step by step.
33
+
34
+ You have a memory of the last 5 actions you performed and their outcomes.
35
+ Use this to avoid repeating mistakes (e.g., if a click failed, try a different selector).
36
 
37
+ Available actions – return a JSON object with exactly one action:
38
  - {"action": "navigate", "url": "https://..."}
39
  - {"action": "click", "selector": "css_selector"}
40
  - {"action": "fill", "selector": "css_selector", "value": "text"}
41
+ - {"action": "select", "selector": "css_selector", "value": "option_text_or_value"}
42
+ - {"action": "scroll", "amount": 300} (positive = down, negative = up)
43
+ - {"action": "press", "key": "Enter"} (keyboard key)
44
+ - {"action": "hover", "selector": "..."}
45
+ - {"action": "back"} (go back one page)
46
  - {"action": "wait", "seconds": 2}
47
+ - {"action": "screenshot"} (captures current page)
48
+ - {"action": "get_text", "selector": "...", "variable": "myVar"} (stores text in a variable)
49
+ - {"action": "execute_script", "code": "return document.title"} (run JS, result stored)
50
+ - {"action": "done", "result": "final answer"} (finish the task)
51
+
52
+ Guidelines:
53
+ - If you see a login form, fill it and submit (use click on the submit button).
54
+ - If you need to extract data, use get_text and store it, then continue.
55
+ - If you get stuck, try scrolling to reveal more content.
56
+ - Always prefer using `data-test` or `id` selectors when available.
57
+ - If a page has a table of contents, click on the relevant section link to jump.
58
 
 
59
  Think step by step. Only output the JSON action, nothing else.
60
  """
61
 
 
68
  self.steps = 0
69
  self.last_result = ""
70
  self.done = False
71
+ self.action_history = [] # list of {"action": ..., "result": ...}
72
+ self.variables = {} # store extracted text
73
 
74
  sessions = {}
75
 
 
81
  screenshot_bytes = await page.screenshot(full_page=False)
82
  return base64.b64encode(screenshot_bytes).decode("utf-8")
83
 
84
+ def build_history_text(session):
85
+ """Return a string of last 5 actions with results."""
86
+ history = session.action_history[-5:]
87
+ if not history:
88
+ return "No actions taken yet."
89
+ lines = []
90
+ for i, entry in enumerate(history):
91
+ lines.append(f"{i+1}. Action: {entry['action']} → Result: {entry['result']}")
92
+ return "\n".join(lines)
93
+
94
+ async def ask_llm_for_action(session, user_instruction=None):
95
+ """Ask Groq for the next action. If user_instruction is given, it overrides the task."""
96
+ page = session.page
97
+ url = page.url
98
+ page_text = await get_page_text(page)
99
  if len(page_text) > 4000:
100
  page_text = page_text[:4000] + "\n... (truncated)"
101
+
102
+ history_text = build_history_text(session)
103
+
104
  user_prompt = f"""
105
+ Task: {session.task}
106
 
107
  Current URL: {url}
108
 
109
+ Action history (most recent first):
110
+ {history_text}
111
+
112
+ Current visible text:
113
  {page_text}
114
 
115
+ {user_instruction if user_instruction else "What is your next action?"}
116
+
117
+ Output a JSON action.
118
  """
119
  completion = await client.chat.completions.create(
120
  model="llama-3.3-70b-versatile",
 
136
  action = {"action": "done", "result": f"Invalid LLM output: {raw}"}
137
  return action
138
 
139
+ async def execute_one_action(session, override=None):
140
+ """
141
+ Execute one action.
142
+ override can be:
143
+ - None or "auto" → use LLM to decide.
144
+ - a JSON string → execute that action directly.
145
+ - a free‑text string → treat as a new instruction, then ask LLM for action.
146
+ """
147
  page = session.page
148
+
149
+ if override and override.lower() != "auto":
150
+ # Try to parse as JSON
151
  try:
152
+ action = json.loads(override)
153
+ # If it's a valid JSON action, we'll execute it directly
154
+ except json.JSONDecodeError:
155
+ # Not JSON – treat as new task description
156
+ session.task = override
157
+ action = await ask_llm_for_action(session)
 
 
 
 
158
  else:
159
  # Auto – ask LLM
160
+ action = await ask_llm_for_action(session)
 
 
 
 
161
 
162
  act = action.get("action")
163
  result_text = ""
164
  screenshot_b64 = ""
165
 
166
+ # Execute the action
167
+ try:
168
+ if act == "done":
169
+ session.done = True
170
+ session.last_result = action.get("result", "Task completed.")
171
+ result_text = session.last_result
172
+ screenshot_b64 = await get_screenshot_b64(page)
173
+ # Record history
174
+ session.action_history.append({"action": action, "result": result_text})
175
+ return {"done": True, "result": result_text, "screenshot": screenshot_b64,
176
+ "action_taken": action, "steps": session.steps}
177
+
178
+ elif act == "navigate":
179
+ url = action.get("url")
180
+ if url:
181
+ await page.goto(url, timeout=30000)
182
+ result_text = f"Navigated to {url}"
183
+ else:
184
+ result_text = "Error: missing 'url'"
185
+
186
+ elif act == "click":
187
+ sel = action.get("selector")
188
+ if sel:
189
+ try:
190
+ await page.click(sel, timeout=5000)
191
+ result_text = f"Clicked {sel}"
192
+ except Exception as e:
193
+ result_text = f"Click failed: {str(e)}"
194
+ else:
195
+ result_text = "Error: missing 'selector'"
196
+
197
+ elif act == "fill":
198
+ sel = action.get("selector")
199
+ val = action.get("value")
200
+ if sel and val is not None:
201
+ try:
202
+ await page.fill(sel, val, timeout=5000)
203
+ result_text = f"Filled {sel} with '{val}'"
204
+ except Exception as e:
205
+ result_text = f"Fill failed: {str(e)}"
206
+ else:
207
+ result_text = "Error: missing 'selector' or 'value'"
208
+
209
+ elif act == "select":
210
+ sel = action.get("selector")
211
+ val = action.get("value")
212
+ if sel and val:
213
+ try:
214
+ await page.select_option(sel, val)
215
+ result_text = f"Selected '{val}' on {sel}"
216
+ except Exception as e:
217
+ result_text = f"Select failed: {str(e)}"
218
+ else:
219
+ result_text = "Error: missing 'selector' or 'value'"
220
+
221
+ elif act == "scroll":
222
+ amount = action.get("amount", 300)
223
+ await page.evaluate(f"window.scrollBy(0, {amount})")
224
+ result_text = f"Scrolled by {amount}px"
225
+
226
+ elif act == "press":
227
+ key = action.get("key")
228
+ if key:
229
+ await page.keyboard.press(key)
230
+ result_text = f"Pressed '{key}'"
231
+ else:
232
+ result_text = "Error: missing 'key'"
233
+
234
+ elif act == "hover":
235
+ sel = action.get("selector")
236
+ if sel:
237
+ try:
238
+ await page.hover(sel, timeout=5000)
239
+ result_text = f"Hovered {sel}"
240
+ except Exception as e:
241
+ result_text = f"Hover failed: {str(e)}"
242
+ else:
243
+ result_text = "Error: missing 'selector'"
244
+
245
+ elif act == "back":
246
+ await page.go_back()
247
+ result_text = "Went back one page"
248
+
249
+ elif act == "wait":
250
+ seconds = action.get("seconds", 1)
251
+ await asyncio.sleep(seconds)
252
+ result_text = f"Waited {seconds}s"
253
+
254
+ elif act == "screenshot":
255
+ # just capture later
256
+ result_text = "Screenshot captured"
257
+
258
+ elif act == "get_text":
259
+ sel = action.get("selector")
260
+ var = action.get("variable", "last_text")
261
+ if sel:
262
+ try:
263
+ text = await page.text_content(sel, timeout=5000)
264
+ session.variables[var] = text.strip() if text else ""
265
+ result_text = f"Stored text from {sel} into '{var}'"
266
+ except Exception as e:
267
+ result_text = f"get_text failed: {str(e)}"
268
+ else:
269
+ result_text = "Error: missing 'selector'"
270
+
271
+ elif act == "execute_script":
272
+ code = action.get("code")
273
+ if code:
274
+ try:
275
+ result = await page.evaluate(code)
276
+ session.variables["_last_script"] = result
277
+ result_text = f"Script executed, result stored in _last_script"
278
+ except Exception as e:
279
+ result_text = f"Script failed: {str(e)}"
280
+ else:
281
+ result_text = "Error: missing 'code'"
282
 
 
 
 
 
283
  else:
284
+ result_text = f"Unknown action: {act}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
 
286
+ except Exception as e:
287
+ result_text = f"Action execution error: {str(e)}"
288
+
289
+ # Record history
290
+ session.action_history.append({"action": action, "result": result_text})
291
  session.steps += 1
292
+ session.last_result = result_text
 
293
 
294
  # Capture screenshot after the action
295
  screenshot_b64 = await get_screenshot_b64(page)
296
 
297
  # Ask LLM for the next action (to show the user)
298
+ next_action = await ask_llm_for_action(session)
 
 
 
 
299
 
300
  return {
301
  "done": session.done,
302
+ "result": result_text,
303
  "screenshot": screenshot_b64,
304
  "steps": session.steps,
305
  "proposed_next_action": next_action,
306
  "current_url": page.url,
307
+ "action_taken": action,
308
+ "history": session.action_history[-5:] # send last 5 for display
309
  }
310
 
311
+ # ---------- FastAPI Endpoints ----------
312
  class StartRequest(BaseModel):
313
  task: str
314
  headless: bool = True
 
336
  session = SessionState(req.task, browser, page)
337
  sessions[session_id] = session
338
 
 
339
  screenshot_b64 = await get_screenshot_b64(page)
340
+ next_action = await ask_llm_for_action(session)
341
 
342
  return StartResponse(
343
  session_id=session_id,
 
351
 
352
  class StepRequest(BaseModel):
353
  session_id: str
354
+ instruction: str = "auto"
355
 
356
  class StepResponse(BaseModel):
357
  done: bool
 
361
  proposed_next_action: dict
362
  current_url: str
363
  action_taken: dict
364
+ history: list
365
 
366
  @app.post("/step", response_model=StepResponse)
367
  async def step(req: StepRequest):
 
375
  result = await execute_one_action(session, req.instruction)
376
  return StepResponse(
377
  done=result["done"],
378
+ result=result["result"],
379
  screenshot=result["screenshot"],
380
  steps=result["steps"],
381
+ proposed_next_action=result["proposed_next_action"],
382
+ current_url=result["current_url"],
383
+ action_taken=result["action_taken"],
384
+ history=result["history"]
385
  )
386
  except Exception as e:
387
  raise HTTPException(status_code=500, detail=str(e))
 
394
  return {"status": "stopped"}
395
  return {"status": "not_found"}
396
 
397
+ # ---------- Serve HTML (same as before, but with history display) ----------
398
  HTML_PAGE = """
399
  <!DOCTYPE html>
400
+ <html>
401
  <head>
402
+ <meta charset="UTF-8">
403
+ <title>Interactive Agent</title>
 
404
  <style>
405
  * { box-sizing: border-box; margin: 0; padding: 0; }
406
+ body { font-family: system-ui; background: #0b0e14; color: #e8edf5; padding: 20px; }
407
+ .container { max-width: 1400px; margin: 0 auto; background: #1a1f2a; border-radius: 24px; padding: 30px; }
408
  .row { display: flex; flex-wrap: wrap; gap: 30px; }
409
  .left { flex: 1; min-width: 300px; }
410
  .right { flex: 2; min-width: 500px; }
411
  textarea, input[type="text"] { width: 100%; background: #0f141d; border: 1px solid #2f384a; border-radius: 12px; padding: 12px; color: white; }
412
  .btn { background: #4f7cff; border: none; color: white; padding: 10px 20px; border-radius: 40px; cursor: pointer; margin: 5px; }
413
  .btn:disabled { opacity: 0.5; }
414
+ .screenshot-box img { width: 100%; border-radius: 12px; border: 1px solid #2f384a; max-height: 500px; object-fit: contain; }
415
  .status { margin: 10px 0; }
416
  .action-card { background: #11171f; padding: 15px; border-radius: 12px; margin: 10px 0; }
417
  pre { background: #0f141d; padding: 10px; border-radius: 8px; overflow: auto; max-height: 200px; }
418
+ .history-item { background: #0f141d; padding: 5px 10px; border-radius: 6px; margin: 4px 0; font-size: 13px; }
419
+ .history-item .act { color: #4f7cff; }
420
+ .history-item .res { color: #8bc34a; }
421
  </style>
422
  </head>
423
  <body>
424
  <div class="container">
425
+ <h1>🧠 Interactive Agent</h1>
426
+ <p>Start a session, see the screenshot, and guide the agent step by step.</p>
427
  <hr style="border-color:#2f384a; margin:20px 0;">
428
 
429
  <div class="row">
 
435
  <button class="btn" id="stopBtn" disabled>⏹ Stop</button>
436
  <div id="status" class="status"></div>
437
  <div id="info"></div>
438
+
439
+ <div id="historyBox" style="margin-top:20px; display:none;">
440
+ <h4>📜 Action History</h4>
441
+ <div id="historyList"></div>
442
+ </div>
443
  </div>
444
 
445
  <div class="right">
 
455
  <button class="btn" id="overrideBtn">✏️ Override</button>
456
  </div>
457
  <div id="overrideArea" style="display:none; margin-top:10px;">
458
+ <input type="text" id="overrideInput" placeholder='JSON action OR new task description' style="width:70%;" />
459
  <button class="btn" id="sendOverrideBtn">Send</button>
460
+ <small style="display:block; color:#8892a8;">Example JSON: {"action":"click","selector":"#login"}<br>Or free text: "now search for Python"</small>
461
  </div>
462
  <div id="resultBox" style="margin-top:15px; background:#11171f; padding:15px; border-radius:12px; display:none;">
463
+ <strong>✅ Last result:</strong> <span id="resultText">—</span>
464
+ </div>
465
+ <div id="actionTakenBox" style="margin-top:10px; background:#11171f; padding:15px; border-radius:12px; display:none;">
466
+ <strong>⚡ Action executed:</strong> <pre id="actionTaken" style="margin-top:5px;"></pre>
467
  </div>
468
  </div>
469
  </div>
 
472
 
473
  <script>
474
  let sessionId = null;
 
 
475
  const statusDiv = document.getElementById('status');
476
  const infoDiv = document.getElementById('info');
477
  const screenshotContainer = document.getElementById('screenshotContainer');
 
480
  const proposedAction = document.getElementById('proposedAction');
481
  const resultBox = document.getElementById('resultBox');
482
  const resultText = document.getElementById('resultText');
483
+ const actionTakenBox = document.getElementById('actionTakenBox');
484
+ const actionTaken = document.getElementById('actionTaken');
485
+ const historyBox = document.getElementById('historyBox');
486
+ const historyList = document.getElementById('historyList');
487
 
488
  const startBtn = document.getElementById('startBtn');
489
  const stopBtn = document.getElementById('stopBtn');
 
507
  proposedAction.textContent = JSON.stringify(action, null, 2);
508
  }
509
 
510
+ function updateHistory(history) {
511
+ if (!history || history.length === 0) {
512
+ historyBox.style.display = 'none';
513
+ return;
514
+ }
515
+ historyBox.style.display = 'block';
516
+ historyList.innerHTML = history.map(h =>
517
+ `<div class="history-item"><span class="act">${JSON.stringify(h.action)}</span> → <span class="res">${h.result}</span></div>`
518
+ ).join('');
519
+ }
520
+
521
  function updateUI(data) {
 
522
  showScreenshot(data.screenshot);
523
  showProposedAction(data.proposed_next_action);
524
+ if (data.history) updateHistory(data.history);
525
+ if (data.action_taken) {
526
+ actionTakenBox.style.display = 'block';
527
+ actionTaken.textContent = JSON.stringify(data.action_taken, null, 2);
528
+ }
529
  if (data.done) {
530
  resultBox.style.display = 'block';
531
  resultText.textContent = data.result || 'Task completed.';
 
533
  overrideBtn.disabled = true;
534
  setStatus('✅ Session finished.');
535
  } else {
536
+ resultBox.style.display = 'block';
537
+ resultText.textContent = data.result || 'Action executed.';
538
  continueBtn.disabled = false;
539
  overrideBtn.disabled = false;
540
  setStatus(`👣 Step ${data.steps} done. Next action proposed.`);
 
560
  const task = document.getElementById('taskInput').value.trim();
561
  if (!task) { setStatus('Please enter a task.', true); return; }
562
  const headless = document.getElementById('headlessCheck').checked;
 
563
  startBtn.disabled = true;
564
  setStatus('Starting session...');
565
  try {
 
618
  setStatus('Session stopped.');
619
  actionContainer.style.display = 'none';
620
  screenshotContainer.style.display = 'none';
621
+ historyBox.style.display = 'none';
622
  } catch (e) {
623
  setStatus('Stop error: ' + e.message, true);
624
  }
 
632
  async def serve_html():
633
  return HTMLResponse(content=HTML_PAGE)
634
 
635
+ # ---------- Cleanup ----------
636
  import atexit
637
  @atexit.register
638
  async def cleanup():