Spaces:
Paused
Paused
Update app.py
Browse files
app.py
CHANGED
|
@@ -4,14 +4,17 @@ import asyncio
|
|
| 4 |
import base64
|
| 5 |
import re
|
| 6 |
import uuid
|
|
|
|
| 7 |
import httpx
|
| 8 |
-
from fastapi import FastAPI, HTTPException
|
| 9 |
from fastapi.middleware.cors import CORSMiddleware
|
| 10 |
-
from fastapi.responses import HTMLResponse
|
| 11 |
from pydantic import BaseModel
|
| 12 |
from playwright.async_api import async_playwright, Browser, Page
|
| 13 |
|
| 14 |
app = FastAPI()
|
|
|
|
|
|
|
| 15 |
app.add_middleware(
|
| 16 |
CORSMiddleware,
|
| 17 |
allow_origins=["*"],
|
|
@@ -20,39 +23,33 @@ app.add_middleware(
|
|
| 20 |
allow_headers=["*"],
|
| 21 |
)
|
| 22 |
|
| 23 |
-
# -----
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
print(f"🚀 LLM Provider: {LLM_PROVIDER}")
|
| 30 |
print(f"🔑 API Key set: {'Yes' if POLLINATIONS_API_KEY else 'No (rate limits apply)'}")
|
| 31 |
print(f"🧠 Model: {POLLINATIONS_MODEL}")
|
| 32 |
|
| 33 |
-
# -----
|
| 34 |
class LLMClient:
|
| 35 |
@staticmethod
|
| 36 |
async def chat(messages, temperature=0.1):
|
| 37 |
-
if LLM_PROVIDER == "
|
| 38 |
-
# Groq is not used, but keep for reference
|
| 39 |
-
from groq import AsyncGroq
|
| 40 |
-
groq_key = os.environ.get("GROQ_API_KEY")
|
| 41 |
-
if not groq_key:
|
| 42 |
-
raise ValueError("GROQ_API_KEY not set")
|
| 43 |
-
client = AsyncGroq(api_key=groq_key)
|
| 44 |
-
completion = await client.chat.completions.create(
|
| 45 |
-
model="llama-3.3-70b-versatile",
|
| 46 |
-
messages=messages,
|
| 47 |
-
temperature=temperature,
|
| 48 |
-
response_format={"type": "json_object"}
|
| 49 |
-
)
|
| 50 |
-
return completion.choices[0].message.content
|
| 51 |
-
|
| 52 |
-
elif LLM_PROVIDER == "pollinations":
|
| 53 |
url = "https://gen.pollinations.ai/text"
|
| 54 |
|
| 55 |
-
# Extract
|
| 56 |
user_messages = [m for m in messages if m["role"] == "user"]
|
| 57 |
system_messages = [m for m in messages if m["role"] == "system"]
|
| 58 |
|
|
@@ -76,31 +73,38 @@ class LLMClient:
|
|
| 76 |
response = await client.post(url, json=payload, headers=headers)
|
| 77 |
response.raise_for_status()
|
| 78 |
except httpx.HTTPStatusError as e:
|
| 79 |
-
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
except Exception as e:
|
| 82 |
print(f"❌ Pollinations request error: {str(e)}")
|
| 83 |
raise
|
| 84 |
|
| 85 |
-
#
|
| 86 |
try:
|
| 87 |
data = response.json()
|
| 88 |
if isinstance(data, dict):
|
| 89 |
-
# Try common fields
|
| 90 |
result = data.get("text") or data.get("response") or data.get("content")
|
| 91 |
if result:
|
| 92 |
return result
|
| 93 |
-
# If no field, return the whole JSON as string
|
| 94 |
return json.dumps(data)
|
| 95 |
-
|
| 96 |
-
return str(data)
|
| 97 |
except json.JSONDecodeError:
|
| 98 |
-
# Plain text response – return as is
|
| 99 |
return response.text
|
| 100 |
else:
|
| 101 |
raise ValueError(f"Unknown LLM provider: {LLM_PROVIDER}")
|
| 102 |
|
| 103 |
-
# -----
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
SYSTEM_PROMPT = """
|
| 105 |
You are an expert web automation agent. Your goal is to complete the user's task efficiently and robustly.
|
| 106 |
|
|
@@ -113,33 +117,18 @@ You are an expert web automation agent. Your goal is to complete the user's task
|
|
| 113 |
**Available actions:**
|
| 114 |
- navigate, click, fill, select, scroll, press, hover, back, wait, screenshot, done
|
| 115 |
- wait_for_selector: {"action":"wait_for_selector","selector":"...","timeout":5000}
|
| 116 |
-
- extract_table: {"action":"extract_table","selector":"table","variable":"tableData"}
|
| 117 |
-
- store_variable: {"action":"store_variable","variable":"name","value":"..."}
|
| 118 |
-
- execute_script: {"action":"execute_script","code":"..."}
|
| 119 |
|
| 120 |
**You MUST output a JSON object with two fields:**
|
| 121 |
{
|
| 122 |
-
"thought": "Your reasoning
|
| 123 |
"action": { ... one of the above actions ... }
|
| 124 |
}
|
| 125 |
-
|
| 126 |
-
**Examples:**
|
| 127 |
-
- Thought: "The login form is visible. I need to fill username and password and click submit."
|
| 128 |
-
Action: {"action":"fill","selector":"#username","value":"testuser"}
|
| 129 |
-
|
| 130 |
-
- Thought: "The table of results is on the page. I'll extract it and store for later."
|
| 131 |
-
Action: {"action":"extract_table","selector":".results-table","variable":"results"}
|
| 132 |
-
|
| 133 |
-
- 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."
|
| 134 |
-
Action: {"action":"click","selector":"a:has-text('Next')"}
|
| 135 |
-
|
| 136 |
-
**Always prefer `data-test` attributes, then `id`, then class, then text.**
|
| 137 |
-
**If you get a CAPTCHA or a modal, try to close it or wait; if impossible, return 'CAPTCHA' in result.**
|
| 138 |
-
**When extracting data, use `extract_table` for structured data, or `get_text` for single values.**
|
| 139 |
-
**You can store values with `store_variable` and use them later in `fill` or other actions.**
|
| 140 |
"""
|
| 141 |
|
| 142 |
-
# -----
|
| 143 |
class SessionState:
|
| 144 |
def __init__(self, task: str, browser: Browser, page: Page):
|
| 145 |
self.task = task
|
|
@@ -148,12 +137,12 @@ class SessionState:
|
|
| 148 |
self.steps = 0
|
| 149 |
self.last_result = ""
|
| 150 |
self.done = False
|
| 151 |
-
self.action_history = []
|
| 152 |
self.variables = {}
|
| 153 |
|
| 154 |
sessions = {}
|
| 155 |
|
| 156 |
-
# -----
|
| 157 |
async def get_page_text(page):
|
| 158 |
return await page.evaluate("""() => document.body.innerText""")
|
| 159 |
|
|
@@ -174,15 +163,12 @@ def build_history_text(session):
|
|
| 174 |
return "\n".join(lines)
|
| 175 |
|
| 176 |
async def ask_llm_for_action(session, user_instruction=None):
|
| 177 |
-
"""Ask LLM for next action with thought."""
|
| 178 |
page = session.page
|
| 179 |
url = page.url
|
| 180 |
page_text = await get_page_text(page)
|
| 181 |
if len(page_text) > 4000:
|
| 182 |
page_text = page_text[:4000] + "\n... (truncated)"
|
| 183 |
-
|
| 184 |
history_text = build_history_text(session)
|
| 185 |
-
|
| 186 |
user_prompt = f"""
|
| 187 |
Task: {session.task}
|
| 188 |
|
|
@@ -226,9 +212,7 @@ Output a JSON with "thought" and "action".
|
|
| 226 |
}
|
| 227 |
|
| 228 |
async def execute_one_action(session, override=None):
|
| 229 |
-
"""Execute one action with override support."""
|
| 230 |
page = session.page
|
| 231 |
-
|
| 232 |
if override and override.lower() != "auto":
|
| 233 |
try:
|
| 234 |
data = json.loads(override)
|
|
@@ -239,7 +223,6 @@ async def execute_one_action(session, override=None):
|
|
| 239 |
thought = "User override with JSON action."
|
| 240 |
action = data
|
| 241 |
except json.JSONDecodeError:
|
| 242 |
-
# Not JSON – treat as new task
|
| 243 |
session.task = override
|
| 244 |
data = await ask_llm_for_action(session)
|
| 245 |
thought = data.get("thought", "Replanned after user instruction.")
|
|
@@ -278,7 +261,6 @@ async def execute_one_action(session, override=None):
|
|
| 278 |
await page.click(sel, timeout=5000)
|
| 279 |
result_text = f"Clicked {sel}"
|
| 280 |
except Exception as e:
|
| 281 |
-
# fallback to text
|
| 282 |
try:
|
| 283 |
await page.click(f"text='{sel}'", timeout=5000)
|
| 284 |
result_text = f"Clicked by text: {sel}"
|
|
@@ -295,7 +277,6 @@ async def execute_one_action(session, override=None):
|
|
| 295 |
await page.fill(sel, val, timeout=5000)
|
| 296 |
result_text = f"Filled {sel} with '{val}'"
|
| 297 |
except Exception as e:
|
| 298 |
-
# try placeholder
|
| 299 |
try:
|
| 300 |
await page.fill(f"input[placeholder='{sel}']", val, timeout=5000)
|
| 301 |
result_text = f"Filled by placeholder: {sel}"
|
|
@@ -429,7 +410,6 @@ async def execute_one_action(session, override=None):
|
|
| 429 |
|
| 430 |
screenshot_b64 = await get_screenshot_b64(page)
|
| 431 |
|
| 432 |
-
# Ask LLM for next action (to show the user)
|
| 433 |
next_data = await ask_llm_for_action(session)
|
| 434 |
next_thought = next_data.get("thought", "")
|
| 435 |
next_action = next_data.get("action", {"action": "wait", "seconds": 1})
|
|
@@ -448,7 +428,7 @@ async def execute_one_action(session, override=None):
|
|
| 448 |
"variables": session.variables
|
| 449 |
}
|
| 450 |
|
| 451 |
-
# -----
|
| 452 |
class StartRequest(BaseModel):
|
| 453 |
task: str
|
| 454 |
headless: bool = True
|
|
@@ -461,8 +441,9 @@ class StartResponse(BaseModel):
|
|
| 461 |
current_url: str
|
| 462 |
steps: int
|
| 463 |
|
| 464 |
-
@app.post("/start"
|
| 465 |
async def start_session(req: StartRequest):
|
|
|
|
| 466 |
try:
|
| 467 |
playwright = await async_playwright().start()
|
| 468 |
browser = await playwright.chromium.launch(headless=req.headless)
|
|
@@ -489,8 +470,7 @@ async def start_session(req: StartRequest):
|
|
| 489 |
steps=0
|
| 490 |
)
|
| 491 |
except Exception as e:
|
| 492 |
-
|
| 493 |
-
print(f"❌ /start error: {str(e)}")
|
| 494 |
traceback.print_exc()
|
| 495 |
raise HTTPException(status_code=500, detail=str(e))
|
| 496 |
|
|
@@ -511,7 +491,7 @@ class StepResponse(BaseModel):
|
|
| 511 |
history: list
|
| 512 |
variables: dict
|
| 513 |
|
| 514 |
-
@app.post("/step"
|
| 515 |
async def step(req: StepRequest):
|
| 516 |
session = sessions.get(req.session_id)
|
| 517 |
if not session:
|
|
@@ -535,8 +515,7 @@ async def step(req: StepRequest):
|
|
| 535 |
variables=result.get("variables", {})
|
| 536 |
)
|
| 537 |
except Exception as e:
|
| 538 |
-
|
| 539 |
-
print(f"❌ /step error: {str(e)}")
|
| 540 |
traceback.print_exc()
|
| 541 |
raise HTTPException(status_code=500, detail=str(e))
|
| 542 |
|
|
@@ -548,278 +527,18 @@ async def stop_session(session_id: str):
|
|
| 548 |
return {"status": "stopped"}
|
| 549 |
return {"status": "not_found"}
|
| 550 |
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
<html>
|
| 555 |
-
<head>
|
| 556 |
-
<meta charset="UTF-8">
|
| 557 |
-
<title>Ultra‑Intelligent Agent</title>
|
| 558 |
-
<style>
|
| 559 |
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 560 |
-
body { font-family: system-ui; background: #0b0e14; color: #e8edf5; padding: 20px; }
|
| 561 |
-
.container { max-width: 1400px; margin: 0 auto; background: #1a1f2a; border-radius: 24px; padding: 30px; }
|
| 562 |
-
.row { display: flex; flex-wrap: wrap; gap: 30px; }
|
| 563 |
-
.left { flex: 1; min-width: 300px; }
|
| 564 |
-
.right { flex: 2; min-width: 500px; }
|
| 565 |
-
textarea, input[type="text"] { width: 100%; background: #0f141d; border: 1px solid #2f384a; border-radius: 12px; padding: 12px; color: white; }
|
| 566 |
-
.btn { background: #4f7cff; border: none; color: white; padding: 10px 20px; border-radius: 40px; cursor: pointer; margin: 5px; }
|
| 567 |
-
.btn:disabled { opacity: 0.5; }
|
| 568 |
-
.screenshot-box img { width: 100%; border-radius: 12px; border: 1px solid #2f384a; max-height: 500px; object-fit: contain; }
|
| 569 |
-
.status { margin: 10px 0; }
|
| 570 |
-
.action-card { background: #11171f; padding: 15px; border-radius: 12px; margin: 10px 0; }
|
| 571 |
-
pre { background: #0f141d; padding: 10px; border-radius: 8px; overflow: auto; max-height: 200px; }
|
| 572 |
-
.history-item { background: #0f141d; padding: 8px 12px; border-radius: 6px; margin: 6px 0; font-size: 13px; border-left: 3px solid #4f7cff; }
|
| 573 |
-
.history-item .thought { color: #ffd54f; }
|
| 574 |
-
.history-item .action { color: #4fc3f7; }
|
| 575 |
-
.history-item .result { color: #8bc34a; }
|
| 576 |
-
.variables-box { background: #0f141d; padding: 12px; border-radius: 12px; margin-top: 15px; }
|
| 577 |
-
.variables-box pre { margin: 5px 0; }
|
| 578 |
-
</style>
|
| 579 |
-
</head>
|
| 580 |
-
<body>
|
| 581 |
-
<div class="container">
|
| 582 |
-
<h1>🧠 Ultra‑Intelligent Agent</h1>
|
| 583 |
-
<p>Start a session, see the screenshot, and guide the agent step by step.</p>
|
| 584 |
-
<hr style="border-color:#2f384a; margin:20px 0;">
|
| 585 |
-
|
| 586 |
-
<div class="row">
|
| 587 |
-
<div class="left">
|
| 588 |
-
<h3>📝 Task</h3>
|
| 589 |
-
<textarea id="taskInput" rows="3" placeholder="e.g. Go to example.com and click 'More information'">Go to example.com and click the 'More information' link</textarea>
|
| 590 |
-
<label><input type="checkbox" id="headlessCheck" checked /> Headless</label>
|
| 591 |
-
<button class="btn" id="startBtn">🚀 Start Session</button>
|
| 592 |
-
<button class="btn" id="stopBtn" disabled>⏹ Stop</button>
|
| 593 |
-
<div id="status" class="status"></div>
|
| 594 |
-
<div id="info"></div>
|
| 595 |
-
|
| 596 |
-
<div id="historyBox" style="margin-top:20px; display:none;">
|
| 597 |
-
<h4>📜 Action History</h4>
|
| 598 |
-
<div id="historyList"></div>
|
| 599 |
-
</div>
|
| 600 |
-
|
| 601 |
-
<div id="variablesBox" style="margin-top:15px; display:none;">
|
| 602 |
-
<h4>📦 Variables</h4>
|
| 603 |
-
<div class="variables-box"><pre id="variablesContent">{}</pre></div>
|
| 604 |
-
</div>
|
| 605 |
-
</div>
|
| 606 |
-
|
| 607 |
-
<div class="right">
|
| 608 |
-
<div id="screenshotContainer" style="display:none;">
|
| 609 |
-
<h3>🖼️ Current Screen</h3>
|
| 610 |
-
<div class="screenshot-box"><img id="screenshotImg" src="" alt="Screenshot" /></div>
|
| 611 |
-
</div>
|
| 612 |
-
<div id="actionContainer" style="display:none;">
|
| 613 |
-
<h3>🤖 Thought</h3>
|
| 614 |
-
<div style="background:#0f141d; padding:12px; border-radius:12px; margin-bottom:10px;"><span id="thoughtDisplay">—</span></div>
|
| 615 |
-
<h3>🎯 Proposed Next Action</h3>
|
| 616 |
-
<pre id="proposedAction">—</pre>
|
| 617 |
-
<div>
|
| 618 |
-
<button class="btn" id="continueBtn">▶ Continue (auto)</button>
|
| 619 |
-
<button class="btn" id="overrideBtn">✏️ Override</button>
|
| 620 |
-
</div>
|
| 621 |
-
<div id="overrideArea" style="display:none; margin-top:10px;">
|
| 622 |
-
<input type="text" id="overrideInput" placeholder='JSON with thought+action OR free text' style="width:70%;" />
|
| 623 |
-
<button class="btn" id="sendOverrideBtn">Send</button>
|
| 624 |
-
<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>
|
| 625 |
-
</div>
|
| 626 |
-
<div id="resultBox" style="margin-top:15px; background:#11171f; padding:15px; border-radius:12px; display:none;">
|
| 627 |
-
<strong>✅ Last result:</strong> <span id="resultText">—</span>
|
| 628 |
-
</div>
|
| 629 |
-
<div id="actionTakenBox" style="margin-top:10px; background:#11171f; padding:15px; border-radius:12px; display:none;">
|
| 630 |
-
<strong>⚡ Action executed:</strong> <pre id="actionTaken" style="margin-top:5px;"></pre>
|
| 631 |
-
</div>
|
| 632 |
-
</div>
|
| 633 |
-
</div>
|
| 634 |
-
</div>
|
| 635 |
-
</div>
|
| 636 |
-
|
| 637 |
-
<script>
|
| 638 |
-
let sessionId = null;
|
| 639 |
-
const statusDiv = document.getElementById('status');
|
| 640 |
-
const infoDiv = document.getElementById('info');
|
| 641 |
-
const screenshotContainer = document.getElementById('screenshotContainer');
|
| 642 |
-
const screenshotImg = document.getElementById('screenshotImg');
|
| 643 |
-
const actionContainer = document.getElementById('actionContainer');
|
| 644 |
-
const proposedAction = document.getElementById('proposedAction');
|
| 645 |
-
const thoughtDisplay = document.getElementById('thoughtDisplay');
|
| 646 |
-
const resultBox = document.getElementById('resultBox');
|
| 647 |
-
const resultText = document.getElementById('resultText');
|
| 648 |
-
const actionTakenBox = document.getElementById('actionTakenBox');
|
| 649 |
-
const actionTaken = document.getElementById('actionTaken');
|
| 650 |
-
const historyBox = document.getElementById('historyBox');
|
| 651 |
-
const historyList = document.getElementById('historyList');
|
| 652 |
-
const variablesBox = document.getElementById('variablesBox');
|
| 653 |
-
const variablesContent = document.getElementById('variablesContent');
|
| 654 |
-
|
| 655 |
-
const startBtn = document.getElementById('startBtn');
|
| 656 |
-
const stopBtn = document.getElementById('stopBtn');
|
| 657 |
-
const continueBtn = document.getElementById('continueBtn');
|
| 658 |
-
const overrideBtn = document.getElementById('overrideBtn');
|
| 659 |
-
const overrideArea = document.getElementById('overrideArea');
|
| 660 |
-
const overrideInput = document.getElementById('overrideInput');
|
| 661 |
-
const sendOverrideBtn = document.getElementById('sendOverrideBtn');
|
| 662 |
-
|
| 663 |
-
function setStatus(msg, isError=false) {
|
| 664 |
-
statusDiv.innerHTML = msg;
|
| 665 |
-
statusDiv.style.color = isError ? '#ff6b6b' : '#8892a8';
|
| 666 |
-
}
|
| 667 |
-
|
| 668 |
-
function showScreenshot(b64) {
|
| 669 |
-
screenshotContainer.style.display = 'block';
|
| 670 |
-
screenshotImg.src = 'data:image/png;base64,' + b64;
|
| 671 |
-
}
|
| 672 |
-
|
| 673 |
-
function showThought(thought) {
|
| 674 |
-
thoughtDisplay.textContent = thought || 'No thought provided.';
|
| 675 |
-
}
|
| 676 |
-
|
| 677 |
-
function showProposedAction(action) {
|
| 678 |
-
proposedAction.textContent = JSON.stringify(action, null, 2);
|
| 679 |
-
}
|
| 680 |
-
|
| 681 |
-
function updateHistory(history) {
|
| 682 |
-
if (!history || history.length === 0) {
|
| 683 |
-
historyBox.style.display = 'none';
|
| 684 |
-
return;
|
| 685 |
-
}
|
| 686 |
-
historyBox.style.display = 'block';
|
| 687 |
-
historyList.innerHTML = history.map(h =>
|
| 688 |
-
`<div class="history-item">
|
| 689 |
-
<div class="thought">💭 ${h.thought || ''}</div>
|
| 690 |
-
<div class="action">⚡ ${JSON.stringify(h.action)}</div>
|
| 691 |
-
<div class="result">✅ ${h.result}</div>
|
| 692 |
-
</div>`
|
| 693 |
-
).join('');
|
| 694 |
-
}
|
| 695 |
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
variablesBox.style.display = 'none';
|
| 699 |
-
return;
|
| 700 |
-
}
|
| 701 |
-
variablesBox.style.display = 'block';
|
| 702 |
-
variablesContent.textContent = JSON.stringify(vars, null, 2);
|
| 703 |
-
}
|
| 704 |
-
|
| 705 |
-
function updateUI(data) {
|
| 706 |
-
showScreenshot(data.screenshot);
|
| 707 |
-
showThought(data.proposed_thought || '');
|
| 708 |
-
showProposedAction(data.proposed_next_action);
|
| 709 |
-
if (data.history) updateHistory(data.history);
|
| 710 |
-
if (data.variables) updateVariables(data.variables);
|
| 711 |
-
if (data.action_taken) {
|
| 712 |
-
actionTakenBox.style.display = 'block';
|
| 713 |
-
actionTaken.textContent = JSON.stringify(data.action_taken, null, 2);
|
| 714 |
-
}
|
| 715 |
-
if (data.done) {
|
| 716 |
-
resultBox.style.display = 'block';
|
| 717 |
-
resultText.textContent = data.result || 'Task completed.';
|
| 718 |
-
continueBtn.disabled = true;
|
| 719 |
-
overrideBtn.disabled = true;
|
| 720 |
-
setStatus('✅ Session finished.');
|
| 721 |
-
} else {
|
| 722 |
-
resultBox.style.display = 'block';
|
| 723 |
-
resultText.textContent = data.result || 'Action executed.';
|
| 724 |
-
continueBtn.disabled = false;
|
| 725 |
-
overrideBtn.disabled = false;
|
| 726 |
-
setStatus(`👣 Step ${data.steps} done. Next action proposed.`);
|
| 727 |
-
}
|
| 728 |
-
infoDiv.innerHTML = `URL: ${data.current_url || 'about:blank'}`;
|
| 729 |
-
actionContainer.style.display = 'block';
|
| 730 |
-
}
|
| 731 |
-
|
| 732 |
-
async function callAPI(endpoint, payload) {
|
| 733 |
-
const res = await fetch(endpoint, {
|
| 734 |
-
method: 'POST',
|
| 735 |
-
headers: { 'Content-Type': 'application/json' },
|
| 736 |
-
body: JSON.stringify(payload)
|
| 737 |
-
});
|
| 738 |
-
if (!res.ok) {
|
| 739 |
-
const err = await res.json();
|
| 740 |
-
throw new Error(err.detail || `HTTP ${res.status}`);
|
| 741 |
-
}
|
| 742 |
-
return res.json();
|
| 743 |
-
}
|
| 744 |
-
|
| 745 |
-
startBtn.addEventListener('click', async () => {
|
| 746 |
-
const task = document.getElementById('taskInput').value.trim();
|
| 747 |
-
if (!task) { setStatus('Please enter a task.', true); return; }
|
| 748 |
-
const headless = document.getElementById('headlessCheck').checked;
|
| 749 |
-
startBtn.disabled = true;
|
| 750 |
-
setStatus('Starting session...');
|
| 751 |
-
try {
|
| 752 |
-
const data = await callAPI('/start', { task, headless });
|
| 753 |
-
sessionId = data.session_id;
|
| 754 |
-
stopBtn.disabled = false;
|
| 755 |
-
startBtn.disabled = true;
|
| 756 |
-
updateUI(data);
|
| 757 |
-
} catch (e) {
|
| 758 |
-
setStatus('Error: ' + e.message, true);
|
| 759 |
-
startBtn.disabled = false;
|
| 760 |
-
}
|
| 761 |
-
});
|
| 762 |
-
|
| 763 |
-
continueBtn.addEventListener('click', async () => {
|
| 764 |
-
if (!sessionId) return;
|
| 765 |
-
continueBtn.disabled = true;
|
| 766 |
-
setStatus('Executing next action...');
|
| 767 |
-
try {
|
| 768 |
-
const data = await callAPI('/step', { session_id: sessionId, instruction: 'auto' });
|
| 769 |
-
updateUI(data);
|
| 770 |
-
} catch (e) {
|
| 771 |
-
setStatus('Error: ' + e.message, true);
|
| 772 |
-
}
|
| 773 |
-
continueBtn.disabled = false;
|
| 774 |
-
});
|
| 775 |
-
|
| 776 |
-
overrideBtn.addEventListener('click', () => {
|
| 777 |
-
overrideArea.style.display = overrideArea.style.display === 'none' ? 'block' : 'none';
|
| 778 |
-
});
|
| 779 |
-
|
| 780 |
-
sendOverrideBtn.addEventListener('click', async () => {
|
| 781 |
-
const instr = overrideInput.value.trim();
|
| 782 |
-
if (!instr) { setStatus('Please enter an instruction.', true); return; }
|
| 783 |
-
if (!sessionId) { setStatus('No active session.', true); return; }
|
| 784 |
-
sendOverrideBtn.disabled = true;
|
| 785 |
-
setStatus('Sending override...');
|
| 786 |
-
try {
|
| 787 |
-
const data = await callAPI('/step', { session_id: sessionId, instruction: instr });
|
| 788 |
-
updateUI(data);
|
| 789 |
-
overrideArea.style.display = 'none';
|
| 790 |
-
overrideInput.value = '';
|
| 791 |
-
} catch (e) {
|
| 792 |
-
setStatus('Error: ' + e.message, true);
|
| 793 |
-
}
|
| 794 |
-
sendOverrideBtn.disabled = false;
|
| 795 |
-
});
|
| 796 |
-
|
| 797 |
-
stopBtn.addEventListener('click', async () => {
|
| 798 |
-
if (!sessionId) return;
|
| 799 |
-
try {
|
| 800 |
-
await callAPI('/stop', { session_id: sessionId });
|
| 801 |
-
sessionId = null;
|
| 802 |
-
stopBtn.disabled = true;
|
| 803 |
-
startBtn.disabled = false;
|
| 804 |
-
setStatus('Session stopped.');
|
| 805 |
-
actionContainer.style.display = 'none';
|
| 806 |
-
screenshotContainer.style.display = 'none';
|
| 807 |
-
historyBox.style.display = 'none';
|
| 808 |
-
variablesBox.style.display = 'none';
|
| 809 |
-
} catch (e) {
|
| 810 |
-
setStatus('Stop error: ' + e.message, true);
|
| 811 |
-
}
|
| 812 |
-
});
|
| 813 |
-
</script>
|
| 814 |
-
</body>
|
| 815 |
-
</html>
|
| 816 |
-
"""
|
| 817 |
|
| 818 |
@app.get("/", response_class=HTMLResponse)
|
| 819 |
async def serve_html():
|
| 820 |
return HTMLResponse(content=HTML_PAGE)
|
| 821 |
|
| 822 |
-
# -----
|
| 823 |
import atexit
|
| 824 |
@atexit.register
|
| 825 |
async def cleanup():
|
|
|
|
| 4 |
import base64
|
| 5 |
import re
|
| 6 |
import uuid
|
| 7 |
+
import traceback
|
| 8 |
import httpx
|
| 9 |
+
from fastapi import FastAPI, HTTPException, Request
|
| 10 |
from fastapi.middleware.cors import CORSMiddleware
|
| 11 |
+
from fastapi.responses import HTMLResponse, JSONResponse
|
| 12 |
from pydantic import BaseModel
|
| 13 |
from playwright.async_api import async_playwright, Browser, Page
|
| 14 |
|
| 15 |
app = FastAPI()
|
| 16 |
+
|
| 17 |
+
# ----- CORS -----
|
| 18 |
app.add_middleware(
|
| 19 |
CORSMiddleware,
|
| 20 |
allow_origins=["*"],
|
|
|
|
| 23 |
allow_headers=["*"],
|
| 24 |
)
|
| 25 |
|
| 26 |
+
# ----- Global exception handler to prevent crashes -----
|
| 27 |
+
@app.exception_handler(Exception)
|
| 28 |
+
async def global_exception_handler(request: Request, exc: Exception):
|
| 29 |
+
print(f"❌ Unhandled exception: {exc}")
|
| 30 |
+
traceback.print_exc()
|
| 31 |
+
return JSONResponse(
|
| 32 |
+
status_code=500,
|
| 33 |
+
content={"detail": f"Internal server error: {str(exc)}"}
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
# ----- Hardcoded configuration -----
|
| 37 |
+
LLM_PROVIDER = "pollinations" # force Pollinations
|
| 38 |
+
POLLINATIONS_API_KEY = os.environ.get("POLLINATIONS_API_KEY", "")
|
| 39 |
+
POLLINATIONS_MODEL = os.environ.get("POLLINATIONS_MODEL", "mistral") # stable
|
| 40 |
|
| 41 |
print(f"🚀 LLM Provider: {LLM_PROVIDER}")
|
| 42 |
print(f"🔑 API Key set: {'Yes' if POLLINATIONS_API_KEY else 'No (rate limits apply)'}")
|
| 43 |
print(f"🧠 Model: {POLLINATIONS_MODEL}")
|
| 44 |
|
| 45 |
+
# ----- LLM Client -----
|
| 46 |
class LLMClient:
|
| 47 |
@staticmethod
|
| 48 |
async def chat(messages, temperature=0.1):
|
| 49 |
+
if LLM_PROVIDER == "pollinations":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
url = "https://gen.pollinations.ai/text"
|
| 51 |
|
| 52 |
+
# Extract prompt and system
|
| 53 |
user_messages = [m for m in messages if m["role"] == "user"]
|
| 54 |
system_messages = [m for m in messages if m["role"] == "system"]
|
| 55 |
|
|
|
|
| 73 |
response = await client.post(url, json=payload, headers=headers)
|
| 74 |
response.raise_for_status()
|
| 75 |
except httpx.HTTPStatusError as e:
|
| 76 |
+
# If it's a 401/403 and we have a key, retry without key
|
| 77 |
+
if e.response.status_code in (401, 403) and POLLINATIONS_API_KEY:
|
| 78 |
+
print("⚠️ API key rejected. Retrying without key...")
|
| 79 |
+
headers.pop("Authorization", None)
|
| 80 |
+
response = await client.post(url, json=payload, headers=headers)
|
| 81 |
+
response.raise_for_status()
|
| 82 |
+
else:
|
| 83 |
+
print(f"❌ Pollinations API error: {e.response.status_code} - {e.response.text}")
|
| 84 |
+
raise
|
| 85 |
except Exception as e:
|
| 86 |
print(f"❌ Pollinations request error: {str(e)}")
|
| 87 |
raise
|
| 88 |
|
| 89 |
+
# Parse response
|
| 90 |
try:
|
| 91 |
data = response.json()
|
| 92 |
if isinstance(data, dict):
|
|
|
|
| 93 |
result = data.get("text") or data.get("response") or data.get("content")
|
| 94 |
if result:
|
| 95 |
return result
|
|
|
|
| 96 |
return json.dumps(data)
|
| 97 |
+
return str(data)
|
|
|
|
| 98 |
except json.JSONDecodeError:
|
|
|
|
| 99 |
return response.text
|
| 100 |
else:
|
| 101 |
raise ValueError(f"Unknown LLM provider: {LLM_PROVIDER}")
|
| 102 |
|
| 103 |
+
# ----- The rest: system prompt, session state, helpers, actions, endpoints -----
|
| 104 |
+
# (Keep exactly as in the previous version, but ensure all functions are defined)
|
| 105 |
+
# I'll paste the full code again with the changes applied.
|
| 106 |
+
|
| 107 |
+
# ----- SYSTEM PROMPT (same as before) -----
|
| 108 |
SYSTEM_PROMPT = """
|
| 109 |
You are an expert web automation agent. Your goal is to complete the user's task efficiently and robustly.
|
| 110 |
|
|
|
|
| 117 |
**Available actions:**
|
| 118 |
- navigate, click, fill, select, scroll, press, hover, back, wait, screenshot, done
|
| 119 |
- wait_for_selector: {"action":"wait_for_selector","selector":"...","timeout":5000}
|
| 120 |
+
- extract_table: {"action":"extract_table","selector":"table","variable":"tableData"}
|
| 121 |
+
- store_variable: {"action":"store_variable","variable":"name","value":"..."}
|
| 122 |
+
- execute_script: {"action":"execute_script","code":"..."}
|
| 123 |
|
| 124 |
**You MUST output a JSON object with two fields:**
|
| 125 |
{
|
| 126 |
+
"thought": "Your reasoning...",
|
| 127 |
"action": { ... one of the above actions ... }
|
| 128 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
"""
|
| 130 |
|
| 131 |
+
# ----- Session state -----
|
| 132 |
class SessionState:
|
| 133 |
def __init__(self, task: str, browser: Browser, page: Page):
|
| 134 |
self.task = task
|
|
|
|
| 137 |
self.steps = 0
|
| 138 |
self.last_result = ""
|
| 139 |
self.done = False
|
| 140 |
+
self.action_history = []
|
| 141 |
self.variables = {}
|
| 142 |
|
| 143 |
sessions = {}
|
| 144 |
|
| 145 |
+
# ----- Helpers -----
|
| 146 |
async def get_page_text(page):
|
| 147 |
return await page.evaluate("""() => document.body.innerText""")
|
| 148 |
|
|
|
|
| 163 |
return "\n".join(lines)
|
| 164 |
|
| 165 |
async def ask_llm_for_action(session, user_instruction=None):
|
|
|
|
| 166 |
page = session.page
|
| 167 |
url = page.url
|
| 168 |
page_text = await get_page_text(page)
|
| 169 |
if len(page_text) > 4000:
|
| 170 |
page_text = page_text[:4000] + "\n... (truncated)"
|
|
|
|
| 171 |
history_text = build_history_text(session)
|
|
|
|
| 172 |
user_prompt = f"""
|
| 173 |
Task: {session.task}
|
| 174 |
|
|
|
|
| 212 |
}
|
| 213 |
|
| 214 |
async def execute_one_action(session, override=None):
|
|
|
|
| 215 |
page = session.page
|
|
|
|
| 216 |
if override and override.lower() != "auto":
|
| 217 |
try:
|
| 218 |
data = json.loads(override)
|
|
|
|
| 223 |
thought = "User override with JSON action."
|
| 224 |
action = data
|
| 225 |
except json.JSONDecodeError:
|
|
|
|
| 226 |
session.task = override
|
| 227 |
data = await ask_llm_for_action(session)
|
| 228 |
thought = data.get("thought", "Replanned after user instruction.")
|
|
|
|
| 261 |
await page.click(sel, timeout=5000)
|
| 262 |
result_text = f"Clicked {sel}"
|
| 263 |
except Exception as e:
|
|
|
|
| 264 |
try:
|
| 265 |
await page.click(f"text='{sel}'", timeout=5000)
|
| 266 |
result_text = f"Clicked by text: {sel}"
|
|
|
|
| 277 |
await page.fill(sel, val, timeout=5000)
|
| 278 |
result_text = f"Filled {sel} with '{val}'"
|
| 279 |
except Exception as e:
|
|
|
|
| 280 |
try:
|
| 281 |
await page.fill(f"input[placeholder='{sel}']", val, timeout=5000)
|
| 282 |
result_text = f"Filled by placeholder: {sel}"
|
|
|
|
| 410 |
|
| 411 |
screenshot_b64 = await get_screenshot_b64(page)
|
| 412 |
|
|
|
|
| 413 |
next_data = await ask_llm_for_action(session)
|
| 414 |
next_thought = next_data.get("thought", "")
|
| 415 |
next_action = next_data.get("action", {"action": "wait", "seconds": 1})
|
|
|
|
| 428 |
"variables": session.variables
|
| 429 |
}
|
| 430 |
|
| 431 |
+
# ----- FastAPI Endpoints -----
|
| 432 |
class StartRequest(BaseModel):
|
| 433 |
task: str
|
| 434 |
headless: bool = True
|
|
|
|
| 441 |
current_url: str
|
| 442 |
steps: int
|
| 443 |
|
| 444 |
+
@app.post("/start")
|
| 445 |
async def start_session(req: StartRequest):
|
| 446 |
+
print(f"🚀 /start called with task: {req.task}")
|
| 447 |
try:
|
| 448 |
playwright = await async_playwright().start()
|
| 449 |
browser = await playwright.chromium.launch(headless=req.headless)
|
|
|
|
| 470 |
steps=0
|
| 471 |
)
|
| 472 |
except Exception as e:
|
| 473 |
+
print(f"❌ /start error: {e}")
|
|
|
|
| 474 |
traceback.print_exc()
|
| 475 |
raise HTTPException(status_code=500, detail=str(e))
|
| 476 |
|
|
|
|
| 491 |
history: list
|
| 492 |
variables: dict
|
| 493 |
|
| 494 |
+
@app.post("/step")
|
| 495 |
async def step(req: StepRequest):
|
| 496 |
session = sessions.get(req.session_id)
|
| 497 |
if not session:
|
|
|
|
| 515 |
variables=result.get("variables", {})
|
| 516 |
)
|
| 517 |
except Exception as e:
|
| 518 |
+
print(f"❌ /step error: {e}")
|
|
|
|
| 519 |
traceback.print_exc()
|
| 520 |
raise HTTPException(status_code=500, detail=str(e))
|
| 521 |
|
|
|
|
| 527 |
return {"status": "stopped"}
|
| 528 |
return {"status": "not_found"}
|
| 529 |
|
| 530 |
+
@app.get("/health")
|
| 531 |
+
async def health():
|
| 532 |
+
return {"status": "ok"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 533 |
|
| 534 |
+
# ----- Serve HTML (same as before) -----
|
| 535 |
+
HTML_PAGE = """ ... (keep the same HTML as before) ... """
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 536 |
|
| 537 |
@app.get("/", response_class=HTMLResponse)
|
| 538 |
async def serve_html():
|
| 539 |
return HTMLResponse(content=HTML_PAGE)
|
| 540 |
|
| 541 |
+
# ----- Cleanup -----
|
| 542 |
import atexit
|
| 543 |
@atexit.register
|
| 544 |
async def cleanup():
|