mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-05-21 18:51:38 +00:00
browser: replace browser-use agent with native browser
Introduce the new built-in Browser plugin for Agent Zero, replacing the legacy browser-use-based browser agent with a direct Playwright-powered browser tool, live WebUI viewer, browser session controls, status APIs, configuration, and extension-management support. Add browser-specific modal behavior so the browser can run as a floating, resizable, no-backdrop window, including modal focus, toggle, and idempotent open helpers for richer WebUI surfaces. Remove the old `_browser_agent` core plugin and the `browser-use` dependency, then clean up stale browser-model wiring and references across agent code, model configuration docs, setup guides, troubleshooting docs, skills, and Agent Zero knowledge. Update regression and WebUI extension-surface coverage for the new browser architecture and modal behavior. The legacy browser-use implementation has been extracted from core so it can continue separately as a community plugin published through the A0 Plugin Index for any user or professional that were relying on it for workflow.
This commit is contained in:
parent
603fc2064b
commit
983d431a5e
65 changed files with 6936 additions and 1926 deletions
|
|
@ -0,0 +1,10 @@
|
|||
from helpers.extension import Extension
|
||||
from plugins._browser.helpers.runtime import close_runtime_sync
|
||||
|
||||
|
||||
class CleanupBrowserRuntimeOnRemove(Extension):
|
||||
def execute(self, data: dict = {}, **kwargs):
|
||||
args = data.get("args", ())
|
||||
context_id = args[0] if isinstance(args, tuple) and args else ""
|
||||
if context_id:
|
||||
close_runtime_sync(str(context_id), delete_profile=True)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
from helpers.extension import Extension
|
||||
from plugins._browser.helpers.runtime import close_runtime_sync
|
||||
|
||||
|
||||
class CleanupBrowserRuntimeOnReset(Extension):
|
||||
def execute(self, data: dict = {}, **kwargs):
|
||||
args = data.get("args", ())
|
||||
context = args[0] if isinstance(args, tuple) and args else None
|
||||
context_id = getattr(context, "id", "")
|
||||
if context_id:
|
||||
close_runtime_sync(context_id, delete_profile=True)
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from agent import LoopData
|
||||
from helpers.extension import Extension
|
||||
from plugins._browser.helpers.runtime import get_runtime
|
||||
|
||||
|
||||
class BrowserContextPrompt(Extension):
|
||||
async def execute(
|
||||
self,
|
||||
system_prompt: list[str] = [],
|
||||
loop_data: LoopData = LoopData(),
|
||||
**kwargs: Any,
|
||||
):
|
||||
if not self.agent:
|
||||
return
|
||||
|
||||
runtime = await get_runtime(self.agent.context.id, create=False)
|
||||
if not runtime:
|
||||
return
|
||||
|
||||
try:
|
||||
listing = await runtime.call("list")
|
||||
except Exception:
|
||||
return
|
||||
|
||||
browsers = listing.get("browsers") or []
|
||||
if not browsers:
|
||||
return
|
||||
|
||||
rows = ["browser id|url|title"]
|
||||
for browser in browsers:
|
||||
rows.append(
|
||||
f"{browser.get('id')}|{browser.get('currentUrl', '')}|{browser.get('title', '')}"
|
||||
)
|
||||
|
||||
section = ["currently open web browsers", "\n".join(rows)]
|
||||
last_id = listing.get("last_interacted_browser_id")
|
||||
if last_id:
|
||||
try:
|
||||
state = await runtime.call("state", last_id)
|
||||
content = await runtime.call("content", last_id, None)
|
||||
document = content.get("document") if isinstance(content, dict) else ""
|
||||
if document:
|
||||
section.extend(
|
||||
[
|
||||
"",
|
||||
"last interacted web browser",
|
||||
f"browser id|url|title\n{state.get('id')}|{state.get('currentUrl', '')}|{state.get('title', '')}",
|
||||
"page content↓",
|
||||
str(document),
|
||||
]
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
system_prompt.append("\n".join(section))
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from helpers.extension import Extension
|
||||
from plugins._browser.api.ws_browser import WsBrowser
|
||||
|
||||
|
||||
class BrowserWebuiWsDisconnect(Extension):
|
||||
async def execute(
|
||||
self,
|
||||
instance: Any = None,
|
||||
sid: str = "",
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
if instance is None:
|
||||
return
|
||||
handler = WsBrowser(
|
||||
instance.socketio,
|
||||
instance.lock,
|
||||
manager=instance.manager,
|
||||
namespace=instance.namespace,
|
||||
)
|
||||
await handler.on_disconnect(sid)
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from helpers.extension import Extension
|
||||
from helpers.ws_manager import WsResult
|
||||
from plugins._browser.api.ws_browser import WsBrowser
|
||||
|
||||
|
||||
class BrowserWebuiWsEvents(Extension):
|
||||
async def execute(
|
||||
self,
|
||||
instance: Any = None,
|
||||
sid: str = "",
|
||||
event_type: str = "",
|
||||
data: dict[str, Any] | None = None,
|
||||
response_data: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
if not event_type.startswith("browser_") or instance is None or response_data is None:
|
||||
return
|
||||
|
||||
handler = WsBrowser(
|
||||
instance.socketio,
|
||||
instance.lock,
|
||||
manager=instance.manager,
|
||||
namespace=instance.namespace,
|
||||
)
|
||||
result = await handler.process(event_type, data or {}, sid)
|
||||
if result is None:
|
||||
return
|
||||
|
||||
if isinstance(result, WsResult):
|
||||
payload = result.as_result(
|
||||
handler_id=handler.identifier,
|
||||
fallback_correlation_id=(data or {}).get("correlationId"),
|
||||
)
|
||||
if payload.get("ok"):
|
||||
response_data.update(payload.get("data") or {})
|
||||
else:
|
||||
response_data["browser_error"] = payload.get("error") or {
|
||||
"code": "BROWSER_ERROR",
|
||||
"error": "Browser request failed",
|
||||
}
|
||||
return
|
||||
|
||||
response_data.update(result)
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<button
|
||||
type="button"
|
||||
class="text-button browser-chat-action"
|
||||
title="Show or hide Browser"
|
||||
aria-label="Show or hide Browser"
|
||||
data-bs-placement="top"
|
||||
data-bs-trigger="hover"
|
||||
@click="window.toggleModal ? window.toggleModal('/plugins/_browser/webui/main.html') : window.openModal('/plugins/_browser/webui/main.html')"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="14" height="14" aria-hidden="true">
|
||||
<rect x="3" y="4" width="18" height="16" rx="2"></rect>
|
||||
<path d="M3 8h18"></path>
|
||||
<path d="M7 6h.01"></path>
|
||||
<path d="M10 6h.01"></path>
|
||||
</svg>
|
||||
<p>Browser</p>
|
||||
</button>
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
import {
|
||||
createActionButton,
|
||||
copyToClipboard,
|
||||
} from "/components/messages/action-buttons/simple-action-buttons.js";
|
||||
import { store as stepDetailStore } from "/components/modals/process-step-detail/step-detail-store.js";
|
||||
import { store as speechStore } from "/components/chat/speech/speech-store.js";
|
||||
import {
|
||||
buildDetailPayload,
|
||||
cleanStepTitle,
|
||||
drawProcessStep,
|
||||
} from "/js/messages.js";
|
||||
|
||||
const BROWSER_MODAL = "/plugins/_browser/webui/main.html";
|
||||
|
||||
export default async function registerBrowserToolHandler(extData) {
|
||||
if (extData?.tool_name === "browser") {
|
||||
extData.handler = drawBrowserTool;
|
||||
}
|
||||
}
|
||||
|
||||
function drawBrowserTool({
|
||||
id,
|
||||
type,
|
||||
heading,
|
||||
content,
|
||||
kvps,
|
||||
timestamp,
|
||||
agentno = 0,
|
||||
...additional
|
||||
}) {
|
||||
const title = cleanStepTitle(heading);
|
||||
const displayKvps = { ...kvps };
|
||||
const headerLabels = [
|
||||
kvps?._tool_name && { label: kvps._tool_name, class: "tool-name-badge" },
|
||||
].filter(Boolean);
|
||||
const contentText = String(content ?? "");
|
||||
const browserButton = createActionButton(
|
||||
"visibility",
|
||||
"Browser",
|
||||
() => {
|
||||
if (window.ensureModalOpen) {
|
||||
void window.ensureModalOpen(BROWSER_MODAL);
|
||||
return;
|
||||
}
|
||||
void window.openModal?.(BROWSER_MODAL);
|
||||
},
|
||||
);
|
||||
browserButton.setAttribute("title", "Open Browser");
|
||||
browserButton.setAttribute("aria-label", "Open Browser");
|
||||
browserButton.setAttribute("data-bs-placement", "top");
|
||||
browserButton.setAttribute("data-bs-trigger", "hover");
|
||||
const actionButtons = [browserButton];
|
||||
|
||||
if (contentText.trim()) {
|
||||
actionButtons.push(
|
||||
createActionButton("detail", "", () =>
|
||||
stepDetailStore.showStepDetail(
|
||||
buildDetailPayload(arguments[0], { headerLabels }),
|
||||
),
|
||||
),
|
||||
createActionButton("speak", "", () => speechStore.speak(contentText)),
|
||||
createActionButton("copy", "", () => copyToClipboard(contentText)),
|
||||
);
|
||||
}
|
||||
|
||||
return drawProcessStep({
|
||||
id,
|
||||
title,
|
||||
code: "WWW",
|
||||
classes: undefined,
|
||||
kvps: displayKvps,
|
||||
content,
|
||||
actionButtons: actionButtons.filter(Boolean),
|
||||
log: arguments[0],
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue