Mayank2027 commited on
Commit
34a9e56
ยท
verified ยท
1 Parent(s): e1934d5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +59 -236
app.py CHANGED
@@ -26,44 +26,26 @@ if not GROQ_API_KEY:
26
  raise RuntimeError("GROQ_API_KEY not set.")
27
  client = AsyncGroq(api_key=GROQ_API_KEY)
28
 
29
- # ---------- System prompt ----------
30
  SYSTEM_PROMPT = """
31
- You are an expert web automation agent. Your goal is to complete the user's task efficiently and robustly.
32
-
33
- **BEFORE ACTING, THINK:**
34
- 1. What is the current page state? (URL, visible text, any popups/forms)
35
- 2. What have I tried so far? (review history)
36
- 3. What is the most logical next step? If I have a plan, follow it; if not, create one.
37
- 4. If an action previously failed, try an alternative selector (e.g., use data-test, id, class, or text content).
38
-
39
- **Available actions:**
40
- - navigate, click, fill, select, scroll, press, hover, back, wait, screenshot, done
41
- - wait_for_selector: {"action":"wait_for_selector","selector":"...","timeout":5000}
42
- - extract_table: {"action":"extract_table","selector":"table","variable":"tableData"} (returns array of rows)
43
- - store_variable: {"action":"store_variable","variable":"name","value":"..."} (manual store)
44
- - log: {"action":"log","message":"..."} (debugging)
45
- - execute_script (advanced)
46
-
47
- **You MUST output a JSON object with two fields:**
48
- {
49
- "thought": "Your reasoning about the current situation and what you plan to do.",
50
- "action": { ... one of the above actions ... }
51
- }
52
-
53
- **Examples:**
54
- - Thought: "The login form is visible. I need to fill username and password and click submit."
55
- Action: {"action":"fill","selector":"#username","value":"testuser"}
56
-
57
- - Thought: "The table of results is on the page. I'll extract it and store for later."
58
- Action: {"action":"extract_table","selector":".results-table","variable":"results"}
59
-
60
- - Thought: "I've tried clicking the 'Next' button twice, but it's not working. Maybe the pagination uses a different selector. I'll try the 'next' link with text."
61
- Action: {"action":"click","selector":"a:has-text('Next')"}
62
-
63
- **Always prefer `data-test` attributes, then `id`, then class, then text.**
64
- **If you get a CAPTCHA or a modal, try to close it or wait; if impossible, return 'CAPTCHA' in result.**
65
- **When extracting data, use `extract_table` for structured data, or `get_text` for single values.**
66
- **You can store values with `store_variable` and use them later in `fill` or other actions.**
67
  """
68
 
69
  # ---------- Session state ----------
@@ -75,9 +57,7 @@ class SessionState:
75
  self.steps = 0
76
  self.last_result = ""
77
  self.done = False
78
- self.action_history = [] # list of {"thought": ..., "action": ..., "result": ...}
79
- self.variables = {} # store extracted text
80
- self.plan = [] # optional step plan
81
 
82
  sessions = {}
83
 
@@ -90,20 +70,16 @@ async def get_screenshot_b64(page):
90
  return base64.b64encode(screenshot_bytes).decode("utf-8")
91
 
92
  def build_history_text(session):
93
- """Return a string of last 5 actions with thoughts and results."""
94
  history = session.action_history[-5:]
95
  if not history:
96
  return "No actions taken yet."
97
  lines = []
98
  for i, entry in enumerate(history):
99
- thought = entry.get('thought', '')
100
- act = entry.get('action', {})
101
- result = entry.get('result', '')
102
- lines.append(f"{i+1}. Thought: {thought}\n Action: {json.dumps(act)}\n Result: {result}")
103
  return "\n".join(lines)
104
 
105
  async def ask_llm_for_action(session, user_instruction=None):
106
- """Ask Groq for next action with thought. If user_instruction is given, treat as new task."""
107
  page = session.page
108
  url = page.url
109
  page_text = await get_page_text(page)
@@ -123,12 +99,12 @@ Action history (most recent first):
123
  Current visible text:
124
  {page_text}
125
 
126
- {user_instruction if user_instruction else "What is your next action? Include your thought and the action."}
127
 
128
- Output a JSON with "thought" and "action".
129
  """
130
  completion = await client.chat.completions.create(
131
- model="mixtral-8x7b-32768", # smarter model
132
  messages=[
133
  {"role": "system", "content": SYSTEM_PROMPT},
134
  {"role": "user", "content": user_prompt}
@@ -138,76 +114,52 @@ Output a JSON with "thought" and "action".
138
  )
139
  raw = completion.choices[0].message.content
140
  try:
141
- data = json.loads(raw)
142
- # Ensure it has "thought" and "action"
143
- if "thought" not in data:
144
- data["thought"] = "No thought provided."
145
- if "action" not in data:
146
- data["action"] = {"action": "done", "result": "No action provided."}
147
- return data
148
  except json.JSONDecodeError:
149
  match = re.search(r'\{.*\}', raw, re.DOTALL)
150
  if match:
151
- data = json.loads(match.group())
152
- if "thought" not in data:
153
- data["thought"] = "Extracted from raw."
154
- if "action" not in data:
155
- data["action"] = {"action": "done", "result": f"Invalid LLM output: {raw}"}
156
- return data
157
  else:
158
- return {
159
- "thought": "Could not parse LLM output.",
160
- "action": {"action": "done", "result": f"Invalid LLM output: {raw}"}
161
- }
162
 
163
  async def execute_one_action(session, override=None):
164
  """
165
  Execute one action.
166
  override can be:
167
  - None or "auto" โ†’ use LLM.
168
- - a JSON string โ†’ treat as action directly (with optional thought).
169
- - free text โ†’ treat as new instruction, then ask LLM.
170
  """
171
  page = session.page
172
 
173
  if override and override.lower() != "auto":
174
  # Try to parse as JSON
175
  try:
176
- data = json.loads(override)
177
- # If it has "action" field, use it; if not, treat as a single action.
178
- if "action" in data:
179
- thought = data.get("thought", "User override.")
180
- action = data["action"]
181
- else:
182
- # Assume the whole thing is the action
183
- thought = "User override with JSON action."
184
- action = data
185
  except json.JSONDecodeError:
186
  # Not JSON โ€“ treat as new task description
187
  session.task = override
188
- data = await ask_llm_for_action(session)
189
- thought = data.get("thought", "Replanned after user instruction.")
190
- action = data.get("action", {"action": "done", "result": "No action from LLM"})
191
  else:
192
  # Auto โ€“ ask LLM
193
- data = await ask_llm_for_action(session)
194
- thought = data.get("thought", "Auto decision.")
195
- action = data.get("action", {"action": "done", "result": "No action from LLM"})
196
 
197
  act = action.get("action")
198
  result_text = ""
199
  screenshot_b64 = ""
200
 
201
- # Execute the action
202
  try:
203
  if act == "done":
204
  session.done = True
205
  session.last_result = action.get("result", "Task completed.")
206
  result_text = session.last_result
207
  screenshot_b64 = await get_screenshot_b64(page)
208
- session.action_history.append({"thought": thought, "action": action, "result": result_text})
209
  return {"done": True, "result": result_text, "screenshot": screenshot_b64,
210
- "action_taken": action, "thought": thought, "steps": session.steps}
211
 
212
  elif act == "navigate":
213
  url = action.get("url")
@@ -224,12 +176,7 @@ async def execute_one_action(session, override=None):
224
  await page.click(sel, timeout=5000)
225
  result_text = f"Clicked {sel}"
226
  except Exception as e:
227
- # Try alternative: find by text
228
- try:
229
- await page.click(f"text='{sel}'", timeout=5000)
230
- result_text = f"Clicked by text: {sel}"
231
- except:
232
- result_text = f"Click failed: {str(e)}"
233
  else:
234
  result_text = "Error: missing 'selector'"
235
 
@@ -241,12 +188,7 @@ async def execute_one_action(session, override=None):
241
  await page.fill(sel, val, timeout=5000)
242
  result_text = f"Filled {sel} with '{val}'"
243
  except Exception as e:
244
- # try by placeholder or label
245
- try:
246
- await page.fill(f"input[placeholder='{sel}']", val, timeout=5000)
247
- result_text = f"Filled by placeholder: {sel}"
248
- except:
249
- result_text = f"Fill failed: {str(e)}"
250
  else:
251
  result_text = "Error: missing 'selector' or 'value'"
252
 
@@ -295,80 +237,7 @@ async def execute_one_action(session, override=None):
295
  await asyncio.sleep(seconds)
296
  result_text = f"Waited {seconds}s"
297
 
298
- elif act == "wait_for_selector":
299
- sel = action.get("selector")
300
- timeout = action.get("timeout", 5000)
301
- if sel:
302
- try:
303
- await page.wait_for_selector(sel, timeout=timeout)
304
- result_text = f"Selector '{sel}' appeared."
305
- except Exception as e:
306
- result_text = f"Wait for selector failed: {str(e)}"
307
- else:
308
- result_text = "Error: missing 'selector'"
309
-
310
- elif act == "extract_table":
311
- sel = action.get("selector")
312
- var = action.get("variable", "table_data")
313
- if sel:
314
- try:
315
- # Extract table as array of objects
316
- rows = await page.evaluate(f"""
317
- (sel) => {{
318
- const table = document.querySelector(sel);
319
- if (!table) return [];
320
- const headers = Array.from(table.querySelectorAll('thead th, tr:first-child th')).map(th => th.innerText.trim());
321
- const dataRows = table.querySelectorAll('tbody tr');
322
- const result = [];
323
- dataRows.forEach(row => {{
324
- const cells = row.querySelectorAll('td');
325
- if (cells.length) {{
326
- const obj = {{}};
327
- cells.forEach((cell, i) => {{
328
- obj[headers[i] || `col_${{i}}`] = cell.innerText.trim();
329
- }});
330
- result.push(obj);
331
- }}
332
- }});
333
- return result;
334
- }}
335
- """, sel)
336
- session.variables[var] = rows
337
- result_text = f"Extracted table with {len(rows)} rows into '{var}'"
338
- except Exception as e:
339
- result_text = f"Extract table failed: {str(e)}"
340
- else:
341
- result_text = "Error: missing 'selector'"
342
-
343
- elif act == "store_variable":
344
- var = action.get("variable")
345
- val = action.get("value")
346
- if var and val is not None:
347
- session.variables[var] = val
348
- result_text = f"Stored '{val}' in '{var}'"
349
- else:
350
- result_text = "Error: missing 'variable' or 'value'"
351
-
352
- elif act == "log":
353
- msg = action.get("message", "")
354
- result_text = f"LOG: {msg}"
355
- # Also print to console (will appear in Space logs)
356
- print(f"AGENT LOG: {msg}")
357
-
358
- elif act == "execute_script":
359
- code = action.get("code")
360
- if code:
361
- try:
362
- result = await page.evaluate(code)
363
- session.variables["_last_script"] = result
364
- result_text = f"Script executed, result stored in _last_script"
365
- except Exception as e:
366
- result_text = f"Script failed: {str(e)}"
367
- else:
368
- result_text = "Error: missing 'code'"
369
-
370
  elif act == "screenshot":
371
- # just capture later
372
  result_text = "Screenshot captured"
373
 
374
  else:
@@ -378,7 +247,7 @@ async def execute_one_action(session, override=None):
378
  result_text = f"Action execution error: {str(e)}"
379
 
380
  # Record history
381
- session.action_history.append({"thought": thought, "action": action, "result": result_text})
382
  session.steps += 1
383
  session.last_result = result_text
384
 
@@ -386,22 +255,17 @@ async def execute_one_action(session, override=None):
386
  screenshot_b64 = await get_screenshot_b64(page)
387
 
388
  # Ask LLM for next action (to show the user)
389
- next_data = await ask_llm_for_action(session)
390
- next_thought = next_data.get("thought", "")
391
- next_action = next_data.get("action", {"action": "wait", "seconds": 1})
392
 
393
  return {
394
  "done": session.done,
395
  "result": result_text,
396
  "screenshot": screenshot_b64,
397
  "steps": session.steps,
398
- "thought": thought,
399
- "action_taken": action,
400
- "proposed_thought": next_thought,
401
  "proposed_next_action": next_action,
402
  "current_url": page.url,
403
- "history": session.action_history[-5:],
404
- "variables": session.variables
405
  }
406
 
407
  # ---------- FastAPI Endpoints ----------
@@ -412,7 +276,6 @@ class StartRequest(BaseModel):
412
  class StartResponse(BaseModel):
413
  session_id: str
414
  screenshot: str
415
- proposed_thought: str
416
  proposed_next_action: dict
417
  current_url: str
418
  steps: int
@@ -434,13 +297,12 @@ async def start_session(req: StartRequest):
434
  sessions[session_id] = session
435
 
436
  screenshot_b64 = await get_screenshot_b64(page)
437
- data = await ask_llm_for_action(session)
438
 
439
  return StartResponse(
440
  session_id=session_id,
441
  screenshot=screenshot_b64,
442
- proposed_thought=data.get("thought", ""),
443
- proposed_next_action=data.get("action", {"action": "wait", "seconds": 1}),
444
  current_url=page.url,
445
  steps=0
446
  )
@@ -456,13 +318,10 @@ class StepResponse(BaseModel):
456
  result: str
457
  screenshot: str
458
  steps: int
459
- thought: str
460
- action_taken: dict
461
- proposed_thought: str
462
  proposed_next_action: dict
463
  current_url: str
 
464
  history: list
465
- variables: dict
466
 
467
  @app.post("/step", response_model=StepResponse)
468
  async def step(req: StepRequest):
@@ -479,13 +338,10 @@ async def step(req: StepRequest):
479
  result=result["result"],
480
  screenshot=result["screenshot"],
481
  steps=result["steps"],
482
- thought=result.get("thought", ""),
483
- action_taken=result["action_taken"],
484
- proposed_thought=result.get("proposed_thought", ""),
485
  proposed_next_action=result["proposed_next_action"],
486
  current_url=result["current_url"],
487
- history=result["history"],
488
- variables=result.get("variables", {})
489
  )
490
  except Exception as e:
491
  raise HTTPException(status_code=500, detail=str(e))
@@ -498,13 +354,13 @@ async def stop_session(session_id: str):
498
  return {"status": "stopped"}
499
  return {"status": "not_found"}
500
 
501
- # ---------- Serve HTML (with thought and variables) ----------
502
  HTML_PAGE = """
503
  <!DOCTYPE html>
504
  <html>
505
  <head>
506
  <meta charset="UTF-8">
507
- <title>Ultraโ€‘Intelligent Agent</title>
508
  <style>
509
  * { box-sizing: border-box; margin: 0; padding: 0; }
510
  body { font-family: system-ui; background: #0b0e14; color: #e8edf5; padding: 20px; }
@@ -519,17 +375,14 @@ HTML_PAGE = """
519
  .status { margin: 10px 0; }
520
  .action-card { background: #11171f; padding: 15px; border-radius: 12px; margin: 10px 0; }
521
  pre { background: #0f141d; padding: 10px; border-radius: 8px; overflow: auto; max-height: 200px; }
522
- .history-item { background: #0f141d; padding: 8px 12px; border-radius: 6px; margin: 6px 0; font-size: 13px; border-left: 3px solid #4f7cff; }
523
- .history-item .thought { color: #ffd54f; }
524
- .history-item .action { color: #4fc3f7; }
525
- .history-item .result { color: #8bc34a; }
526
- .variables-box { background: #0f141d; padding: 12px; border-radius: 12px; margin-top: 15px; }
527
- .variables-box pre { margin: 5px 0; }
528
  </style>
529
  </head>
530
  <body>
531
  <div class="container">
532
- <h1>๐Ÿง  Ultraโ€‘Intelligent Agent</h1>
533
  <p>Start a session, see the screenshot, and guide the agent step by step.</p>
534
  <hr style="border-color:#2f384a; margin:20px 0;">
535
 
@@ -547,11 +400,6 @@ HTML_PAGE = """
547
  <h4>๐Ÿ“œ Action History</h4>
548
  <div id="historyList"></div>
549
  </div>
550
-
551
- <div id="variablesBox" style="margin-top:15px; display:none;">
552
- <h4>๐Ÿ“ฆ Variables</h4>
553
- <div class="variables-box"><pre id="variablesContent">{}</pre></div>
554
- </div>
555
  </div>
556
 
557
  <div class="right">
@@ -560,18 +408,16 @@ HTML_PAGE = """
560
  <div class="screenshot-box"><img id="screenshotImg" src="" alt="Screenshot" /></div>
561
  </div>
562
  <div id="actionContainer" style="display:none;">
563
- <h3>๐Ÿค– Thought</h3>
564
- <div style="background:#0f141d; padding:12px; border-radius:12px; margin-bottom:10px;"><span id="thoughtDisplay">โ€”</span></div>
565
- <h3>๐ŸŽฏ Proposed Next Action</h3>
566
  <pre id="proposedAction">โ€”</pre>
567
  <div>
568
  <button class="btn" id="continueBtn">โ–ถ Continue (auto)</button>
569
  <button class="btn" id="overrideBtn">โœ๏ธ Override</button>
570
  </div>
571
  <div id="overrideArea" style="display:none; margin-top:10px;">
572
- <input type="text" id="overrideInput" placeholder='JSON with thought+action OR free text' style="width:70%;" />
573
  <button class="btn" id="sendOverrideBtn">Send</button>
574
- <small style="display:block; color:#8892a8;">Example: {"thought":"I want to navigate","action":{"action":"navigate","url":"https://example.com"}}<br>Or free text: "now search for Python"</small>
575
  </div>
576
  <div id="resultBox" style="margin-top:15px; background:#11171f; padding:15px; border-radius:12px; display:none;">
577
  <strong>โœ… Last result:</strong> <span id="resultText">โ€”</span>
@@ -592,15 +438,12 @@ HTML_PAGE = """
592
  const screenshotImg = document.getElementById('screenshotImg');
593
  const actionContainer = document.getElementById('actionContainer');
594
  const proposedAction = document.getElementById('proposedAction');
595
- const thoughtDisplay = document.getElementById('thoughtDisplay');
596
  const resultBox = document.getElementById('resultBox');
597
  const resultText = document.getElementById('resultText');
598
  const actionTakenBox = document.getElementById('actionTakenBox');
599
  const actionTaken = document.getElementById('actionTaken');
600
  const historyBox = document.getElementById('historyBox');
601
  const historyList = document.getElementById('historyList');
602
- const variablesBox = document.getElementById('variablesBox');
603
- const variablesContent = document.getElementById('variablesContent');
604
 
605
  const startBtn = document.getElementById('startBtn');
606
  const stopBtn = document.getElementById('stopBtn');
@@ -620,10 +463,6 @@ HTML_PAGE = """
620
  screenshotImg.src = 'data:image/png;base64,' + b64;
621
  }
622
 
623
- function showThought(thought) {
624
- thoughtDisplay.textContent = thought || 'No thought provided.';
625
- }
626
-
627
  function showProposedAction(action) {
628
  proposedAction.textContent = JSON.stringify(action, null, 2);
629
  }
@@ -635,29 +474,14 @@ HTML_PAGE = """
635
  }
636
  historyBox.style.display = 'block';
637
  historyList.innerHTML = history.map(h =>
638
- `<div class="history-item">
639
- <div class="thought">๐Ÿ’ญ ${h.thought || ''}</div>
640
- <div class="action">โšก ${JSON.stringify(h.action)}</div>
641
- <div class="result">โœ… ${h.result}</div>
642
- </div>`
643
  ).join('');
644
  }
645
 
646
- function updateVariables(vars) {
647
- if (!vars || Object.keys(vars).length === 0) {
648
- variablesBox.style.display = 'none';
649
- return;
650
- }
651
- variablesBox.style.display = 'block';
652
- variablesContent.textContent = JSON.stringify(vars, null, 2);
653
- }
654
-
655
  function updateUI(data) {
656
  showScreenshot(data.screenshot);
657
- showThought(data.proposed_thought || '');
658
  showProposedAction(data.proposed_next_action);
659
  if (data.history) updateHistory(data.history);
660
- if (data.variables) updateVariables(data.variables);
661
  if (data.action_taken) {
662
  actionTakenBox.style.display = 'block';
663
  actionTaken.textContent = JSON.stringify(data.action_taken, null, 2);
@@ -755,7 +579,6 @@ HTML_PAGE = """
755
  actionContainer.style.display = 'none';
756
  screenshotContainer.style.display = 'none';
757
  historyBox.style.display = 'none';
758
- variablesBox.style.display = 'none';
759
  } catch (e) {
760
  setStatus('Stop error: ' + e.message, true);
761
  }
 
26
  raise RuntimeError("GROQ_API_KEY not set.")
27
  client = AsyncGroq(api_key=GROQ_API_KEY)
28
 
29
+ # ---------- System prompt (simple) ----------
30
  SYSTEM_PROMPT = """
31
+ You are an autonomous web agent. You have access to a browser via Playwright.
32
+ Your goal is to complete the user's task by issuing a sequence of actions.
33
+
34
+ Available actions (return a JSON object with exactly one of these):
35
+ - {"action": "navigate", "url": "https://..."}
36
+ - {"action": "click", "selector": "css_selector"}
37
+ - {"action": "fill", "selector": "css_selector", "value": "text"}
38
+ - {"action": "select", "selector": "css_selector", "value": "option_text"}
39
+ - {"action": "scroll", "amount": 300} (positive = down, negative = up)
40
+ - {"action": "press", "key": "Enter"}
41
+ - {"action": "hover", "selector": "..."}
42
+ - {"action": "back"}
43
+ - {"action": "wait", "seconds": 2}
44
+ - {"action": "screenshot"}
45
+ - {"action": "done", "result": "final answer"}
46
+
47
+ You will receive the current page URL and visible text.
48
+ Think step by step. Only output the JSON action, nothing else.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  """
50
 
51
  # ---------- Session state ----------
 
57
  self.steps = 0
58
  self.last_result = ""
59
  self.done = False
60
+ self.action_history = [] # list of {"action": ..., "result": ...}
 
 
61
 
62
  sessions = {}
63
 
 
70
  return base64.b64encode(screenshot_bytes).decode("utf-8")
71
 
72
  def build_history_text(session):
 
73
  history = session.action_history[-5:]
74
  if not history:
75
  return "No actions taken yet."
76
  lines = []
77
  for i, entry in enumerate(history):
78
+ lines.append(f"{i+1}. Action: {json.dumps(entry['action'])} โ†’ Result: {entry['result']}")
 
 
 
79
  return "\n".join(lines)
80
 
81
  async def ask_llm_for_action(session, user_instruction=None):
82
+ """Ask Groq for the next action."""
83
  page = session.page
84
  url = page.url
85
  page_text = await get_page_text(page)
 
99
  Current visible text:
100
  {page_text}
101
 
102
+ {user_instruction if user_instruction else "What is your next action?"}
103
 
104
+ Output a JSON action.
105
  """
106
  completion = await client.chat.completions.create(
107
+ model="llama-3.3-70b-versatile",
108
  messages=[
109
  {"role": "system", "content": SYSTEM_PROMPT},
110
  {"role": "user", "content": user_prompt}
 
114
  )
115
  raw = completion.choices[0].message.content
116
  try:
117
+ action = json.loads(raw)
 
 
 
 
 
 
118
  except json.JSONDecodeError:
119
  match = re.search(r'\{.*\}', raw, re.DOTALL)
120
  if match:
121
+ action = json.loads(match.group())
 
 
 
 
 
122
  else:
123
+ action = {"action": "done", "result": f"Invalid LLM output: {raw}"}
124
+ return action
 
 
125
 
126
  async def execute_one_action(session, override=None):
127
  """
128
  Execute one action.
129
  override can be:
130
  - None or "auto" โ†’ use LLM.
131
+ - a JSON string โ†’ execute that action directly.
132
+ - free text โ†’ treat as new task, then ask LLM for action.
133
  """
134
  page = session.page
135
 
136
  if override and override.lower() != "auto":
137
  # Try to parse as JSON
138
  try:
139
+ action = json.loads(override)
140
+ # If it's valid JSON, treat as action
 
 
 
 
 
 
 
141
  except json.JSONDecodeError:
142
  # Not JSON โ€“ treat as new task description
143
  session.task = override
144
+ action = await ask_llm_for_action(session)
 
 
145
  else:
146
  # Auto โ€“ ask LLM
147
+ action = await ask_llm_for_action(session)
 
 
148
 
149
  act = action.get("action")
150
  result_text = ""
151
  screenshot_b64 = ""
152
 
153
+ # Execute
154
  try:
155
  if act == "done":
156
  session.done = True
157
  session.last_result = action.get("result", "Task completed.")
158
  result_text = session.last_result
159
  screenshot_b64 = await get_screenshot_b64(page)
160
+ session.action_history.append({"action": action, "result": result_text})
161
  return {"done": True, "result": result_text, "screenshot": screenshot_b64,
162
+ "action_taken": action, "steps": session.steps}
163
 
164
  elif act == "navigate":
165
  url = action.get("url")
 
176
  await page.click(sel, timeout=5000)
177
  result_text = f"Clicked {sel}"
178
  except Exception as e:
179
+ result_text = f"Click failed: {str(e)}"
 
 
 
 
 
180
  else:
181
  result_text = "Error: missing 'selector'"
182
 
 
188
  await page.fill(sel, val, timeout=5000)
189
  result_text = f"Filled {sel} with '{val}'"
190
  except Exception as e:
191
+ result_text = f"Fill failed: {str(e)}"
 
 
 
 
 
192
  else:
193
  result_text = "Error: missing 'selector' or 'value'"
194
 
 
237
  await asyncio.sleep(seconds)
238
  result_text = f"Waited {seconds}s"
239
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  elif act == "screenshot":
 
241
  result_text = "Screenshot captured"
242
 
243
  else:
 
247
  result_text = f"Action execution error: {str(e)}"
248
 
249
  # Record history
250
+ session.action_history.append({"action": action, "result": result_text})
251
  session.steps += 1
252
  session.last_result = result_text
253
 
 
255
  screenshot_b64 = await get_screenshot_b64(page)
256
 
257
  # Ask LLM for next action (to show the user)
258
+ next_action = await ask_llm_for_action(session)
 
 
259
 
260
  return {
261
  "done": session.done,
262
  "result": result_text,
263
  "screenshot": screenshot_b64,
264
  "steps": session.steps,
 
 
 
265
  "proposed_next_action": next_action,
266
  "current_url": page.url,
267
+ "action_taken": action,
268
+ "history": session.action_history[-5:]
269
  }
270
 
271
  # ---------- FastAPI Endpoints ----------
 
276
  class StartResponse(BaseModel):
277
  session_id: str
278
  screenshot: str
 
279
  proposed_next_action: dict
280
  current_url: str
281
  steps: int
 
297
  sessions[session_id] = session
298
 
299
  screenshot_b64 = await get_screenshot_b64(page)
300
+ next_action = await ask_llm_for_action(session)
301
 
302
  return StartResponse(
303
  session_id=session_id,
304
  screenshot=screenshot_b64,
305
+ proposed_next_action=next_action,
 
306
  current_url=page.url,
307
  steps=0
308
  )
 
318
  result: str
319
  screenshot: str
320
  steps: int
 
 
 
321
  proposed_next_action: dict
322
  current_url: str
323
+ action_taken: dict
324
  history: list
 
325
 
326
  @app.post("/step", response_model=StepResponse)
327
  async def step(req: StepRequest):
 
338
  result=result["result"],
339
  screenshot=result["screenshot"],
340
  steps=result["steps"],
 
 
 
341
  proposed_next_action=result["proposed_next_action"],
342
  current_url=result["current_url"],
343
+ action_taken=result["action_taken"],
344
+ history=result["history"]
345
  )
346
  except Exception as e:
347
  raise HTTPException(status_code=500, detail=str(e))
 
354
  return {"status": "stopped"}
355
  return {"status": "not_found"}
356
 
357
+ # ---------- Serve HTML (with working override) ----------
358
  HTML_PAGE = """
359
  <!DOCTYPE html>
360
  <html>
361
  <head>
362
  <meta charset="UTF-8">
363
+ <title>Interactive Agent</title>
364
  <style>
365
  * { box-sizing: border-box; margin: 0; padding: 0; }
366
  body { font-family: system-ui; background: #0b0e14; color: #e8edf5; padding: 20px; }
 
375
  .status { margin: 10px 0; }
376
  .action-card { background: #11171f; padding: 15px; border-radius: 12px; margin: 10px 0; }
377
  pre { background: #0f141d; padding: 10px; border-radius: 8px; overflow: auto; max-height: 200px; }
378
+ .history-item { background: #0f141d; padding: 5px 10px; border-radius: 6px; margin: 4px 0; font-size: 13px; }
379
+ .history-item .act { color: #4f7cff; }
380
+ .history-item .res { color: #8bc34a; }
 
 
 
381
  </style>
382
  </head>
383
  <body>
384
  <div class="container">
385
+ <h1>๐Ÿง  Interactive Agent</h1>
386
  <p>Start a session, see the screenshot, and guide the agent step by step.</p>
387
  <hr style="border-color:#2f384a; margin:20px 0;">
388
 
 
400
  <h4>๐Ÿ“œ Action History</h4>
401
  <div id="historyList"></div>
402
  </div>
 
 
 
 
 
403
  </div>
404
 
405
  <div class="right">
 
408
  <div class="screenshot-box"><img id="screenshotImg" src="" alt="Screenshot" /></div>
409
  </div>
410
  <div id="actionContainer" style="display:none;">
411
+ <h3>๐Ÿค– Proposed Next Action</h3>
 
 
412
  <pre id="proposedAction">โ€”</pre>
413
  <div>
414
  <button class="btn" id="continueBtn">โ–ถ Continue (auto)</button>
415
  <button class="btn" id="overrideBtn">โœ๏ธ Override</button>
416
  </div>
417
  <div id="overrideArea" style="display:none; margin-top:10px;">
418
+ <input type="text" id="overrideInput" placeholder='JSON action OR new task description' style="width:70%;" />
419
  <button class="btn" id="sendOverrideBtn">Send</button>
420
+ <small style="display:block; color:#8892a8;">Example JSON: {"action":"click","selector":"#login"}<br>Or free text: "now search for Python"</small>
421
  </div>
422
  <div id="resultBox" style="margin-top:15px; background:#11171f; padding:15px; border-radius:12px; display:none;">
423
  <strong>โœ… Last result:</strong> <span id="resultText">โ€”</span>
 
438
  const screenshotImg = document.getElementById('screenshotImg');
439
  const actionContainer = document.getElementById('actionContainer');
440
  const proposedAction = document.getElementById('proposedAction');
 
441
  const resultBox = document.getElementById('resultBox');
442
  const resultText = document.getElementById('resultText');
443
  const actionTakenBox = document.getElementById('actionTakenBox');
444
  const actionTaken = document.getElementById('actionTaken');
445
  const historyBox = document.getElementById('historyBox');
446
  const historyList = document.getElementById('historyList');
 
 
447
 
448
  const startBtn = document.getElementById('startBtn');
449
  const stopBtn = document.getElementById('stopBtn');
 
463
  screenshotImg.src = 'data:image/png;base64,' + b64;
464
  }
465
 
 
 
 
 
466
  function showProposedAction(action) {
467
  proposedAction.textContent = JSON.stringify(action, null, 2);
468
  }
 
474
  }
475
  historyBox.style.display = 'block';
476
  historyList.innerHTML = history.map(h =>
477
+ `<div class="history-item"><span class="act">${JSON.stringify(h.action)}</span> โ†’ <span class="res">${h.result}</span></div>`
 
 
 
 
478
  ).join('');
479
  }
480
 
 
 
 
 
 
 
 
 
 
481
  function updateUI(data) {
482
  showScreenshot(data.screenshot);
 
483
  showProposedAction(data.proposed_next_action);
484
  if (data.history) updateHistory(data.history);
 
485
  if (data.action_taken) {
486
  actionTakenBox.style.display = 'block';
487
  actionTaken.textContent = JSON.stringify(data.action_taken, null, 2);
 
579
  actionContainer.style.display = 'none';
580
  screenshotContainer.style.display = 'none';
581
  historyBox.style.display = 'none';
 
582
  } catch (e) {
583
  setStatus('Stop error: ' + e.message, true);
584
  }