import asyncio
import json
import logging
from datetime import datetime
from typing import AsyncGenerator, List, Dict, Any, Tuple
from pinchtab_browser import PinchTabBrowser
from web_agent_config import WEB_AGENT_SYSTEM_PROMPT, WEB_TOOL_CONTENT
import shared_utils
import app # Still needed for generate_response and MAX_NEW_TOKENS for now
logger = logging.getLogger(__name__)
async def run_web_agent_streaming(
goal: str,
max_rounds: int = 15,
inference_mode: str = "Auto (local + cloud backup)"
) -> AsyncGenerator[str, None]:
mode = app._INFERENCE_MODE_MAP.get(inference_mode, "auto")
browser = PinchTabBrowser()
html_parts = [shared_utils.render_user_message(goal)]
yield ''.join(html_parts)
system_prompt = WEB_AGENT_SYSTEM_PROMPT.format(current_date=datetime.now().strftime('%Y-%m-%d'))
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": goal}
]
tools = WEB_TOOL_CONTENT
context_budget = await app.get_context_budget(mode)
try:
for round_num in range(1, max_rounds + 1):
html_parts.append(shared_utils.render_round_badge(round_num, max_rounds))
yield ''.join(html_parts)
# Context management
if shared_utils.estimate_tokens(messages) > context_budget:
messages = shared_utils.compress_messages(messages, keep_last_rounds=3)
html_parts.append('
Processing...
')
yield ''.join(html_parts)
try:
api_result = await app.generate_response(messages, tools, max_new_tokens=app.MAX_NEW_TOKENS, mode=mode)
html_parts.pop() # Remove placeholder
except Exception as e:
html_parts.pop()
html_parts.append(f"Generation Error: {str(e)}
")
yield ''.join(html_parts)
return
generated = api_result["content"]
reasoning, content = shared_utils.extract_thinking(generated)
tool_call, clean_content = shared_utils.parse_tool_call(content)
if reasoning:
html_parts.append(shared_utils.render_thinking_collapsed(reasoning))
yield ''.join(html_parts)
if tool_call:
fn_name = tool_call.get("name", "unknown")
args = tool_call.get("arguments", {})
html_parts.append(shared_utils.render_tool_call(fn_name, args))
yield ''.join(html_parts)
if clean_content.strip() and not tool_call:
html_parts.append(f'{clean_content}
')
yield ''.join(html_parts)
messages.append({
"role": "assistant",
"content": clean_content if tool_call is None else "",
"reasoning_content": reasoning,
"tool_calls": [{
"id": str(round_num),
"type": "function",
"function": {
"name": tool_call.get("name", ""),
"arguments": tool_call.get("arguments", {})
}
}] if tool_call else None
})
if tool_call:
fn_name = tool_call.get("name", "")
actual_fn = fn_name.split(".", 1)[1] if "." in fn_name else fn_name
args = tool_call.get("arguments", {})
result = ""
try:
if actual_fn == "navigate":
res = await browser.navigate(args.get("url"))
result = json.dumps(res, indent=2)
elif actual_fn == "snapshot":
res = await browser.snapshot(args.get("filter", "interactive"))
result = json.dumps(res, indent=2)
elif actual_fn == "click":
res = await browser.action("click", ref=args.get("ref"))
result = json.dumps(res, indent=2)
elif actual_fn == "type":
res = await browser.action("type", ref=args.get("ref"), text=args.get("text"))
result = json.dumps(res, indent=2)
elif actual_fn == "press":
res = await browser.action("press", key=args.get("key"))
result = json.dumps(res, indent=2)
elif actual_fn == "get_text":
res = await browser.get_text()
result = json.dumps(res, indent=2)
else:
result = f"Unknown tool: {fn_name}"
except Exception as e:
result = f"Tool error: {str(e)}"
html_parts.append(shared_utils.render_tool_result(result, fn_name))
yield ''.join(html_parts)
messages.append({
"role": "tool",
"tool_call_id": str(round_num),
"content": result
})
continue
if shared_utils.is_final_answer(generated):
html_parts.append(shared_utils.render_completion())
yield ''.join(html_parts)
break
finally:
await browser.close()