mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-05-19 16:31:30 +00:00
Merge pull request #1312 from 3clyp50/browse-extract
browser-use builtin plugin, tool message hooks, Plugin Hub, WebUI polish
This commit is contained in:
commit
a5921397ce
35 changed files with 782 additions and 258 deletions
11
README.md
11
README.md
|
|
@ -93,13 +93,20 @@ docker run -p 50001:80 agent0ai/agent-zero
|
|||
|
||||

|
||||
|
||||
### Browser Agent
|
||||
|
||||
- Browser automation is provided by the built-in `_browser_agent` plugin.
|
||||
- It uses the effective Main Model resolved by `_model_config`; there is no separate browser model slot.
|
||||
- Browser vision follows the Main Model's vision setting.
|
||||
- Playwright Chromium: **Docker** images ship the headless shell preinstalled. **Local development** installs it on first Browser Agent use via `ensure_playwright_binary()` in `plugins/_browser_agent/helpers/playwright.py` (into `tmp/playwright`); you can pre-install manually (see [Development Setup](docs/setup/dev-setup.md)) to skip the wait.
|
||||
|
||||
4. **Completely Customizable and Extensible**
|
||||
|
||||
- Almost nothing in this framework is hard-coded. Nothing is hidden. Everything can be extended or changed by the user.
|
||||
- The whole behavior is defined by a system prompt in the **prompts/default/agent.system.md** file. Change this prompt and change the framework dramatically.
|
||||
- The framework does not guide or limit the agent in any way. There are no hard-coded rails that agents have to follow.
|
||||
- Every prompt, every small message template sent to the agent in its communication loop can be found in the **prompts/** folder and changed.
|
||||
- Every default tool can be found in the **python/tools/** folder and changed or copied to create new predefined tools.
|
||||
- Built-in tools live in the core **tools/** folder or in built-in plugins under **plugins/** and can be adapted or extended.
|
||||
- **Automated configuration** via `A0_SET_` environment variables for deployment automation and easy setup.
|
||||
|
||||

|
||||
|
|
@ -238,7 +245,7 @@ docker run -p 50001:80 agent0ai/agent-zero
|
|||
- Secrets management - agent can use credentials without seeing them
|
||||
- Agent can copy paste messages and files without rewriting them
|
||||
- LiteLLM global configuration field
|
||||
- Custom HTTP headers field for browser agent
|
||||
- Browser agent configuration improvements
|
||||
- Progressive web app support
|
||||
- Extra model params support for JSON
|
||||
- Short IDs for files and memories to prevent LLM errors
|
||||
|
|
|
|||
|
|
@ -170,6 +170,8 @@ Place *.js files in extensions/webui/<extension_point>/ and export a default asy
|
|||
|
||||
Core JS hooks can also expose runtime UI surfaces when static HTML breakpoints are not a fit. For example, `confirm_dialog_after_render` runs after the shared confirm dialog is built and receives the rendered dialog/body/footer nodes plus any caller-provided `extensionContext`.
|
||||
|
||||
For tool chat rows (`type === "tool"`), after built-in badge rules, core calls `get_tool_message_handler` with a mutable object containing `tool_name`, `kvps`, and `handler`. Plugins can set `handler` to entirely take over rendering for their `_tool_name`. A plugin can import `drawMessageToolSimple` and call it internally (e.g., passing `{ ...args, code: "WWW" }`) if it just wants standard tool row styling with a custom badge.
|
||||
|
||||
### User Feedback: Notifications, Not Inline Errors
|
||||
Plugin UI must use the **A0 notification system** for errors, success, and warnings. Do not render dedicated error/success boxes (e.g. a red block bound to `store.error`). Use the notification store so toasts and notification history stay consistent across the app.
|
||||
|
||||
|
|
|
|||
|
|
@ -114,4 +114,4 @@ Community-tested and reliable MCP servers:
|
|||
- **VSCode MCP** - IDE workflows
|
||||
|
||||
> [!TIP]
|
||||
> For browser automation tasks, MCP-based browser tools are more reliable than the built-in browser agent.
|
||||
> For browser automation tasks, the built-in Browser Agent plugin covers the default workflow. MCP-based browser tools are still useful when you need a different browser stack, remote browser control, or an alternative to the built-in Playwright Chromium (preinstalled in Docker; on demand via `ensure_playwright_binary()` in local dev).
|
||||
|
|
|
|||
|
|
@ -228,7 +228,7 @@ SMTP_PASSWORD=email_pwd_here
|
|||
|
||||
### Subagent Configuration
|
||||
|
||||
Projects can enable or disable specific subagents (like the Browser Agent). This is configured via the UI and stored in `.a0proj/agents.json`.
|
||||
Projects can enable or disable specific subagents. This is configured via the UI and stored in `.a0proj/agents.json`. The Browser Agent is not a subagent; it is a built-in plugin.
|
||||
|
||||
### Knowledge Files
|
||||
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ Refer to the [Choosing your LLMs](../setup/installation.md#installing-and-using-
|
|||
**7. How can I make Agent Zero retain memory between sessions?**
|
||||
Use **Settings → Backup & Restore** and avoid mapping the entire `/a0` directory. See [How to update Agent Zero](../setup/installation.md#how-to-update-agent-zero).
|
||||
|
||||
**8. My browser agent fails or is unreliable. What now?**
|
||||
The built-in browser agent is currently unstable on some systems. Use Skills or MCP alternatives such as Browser OS, Chrome DevTools, or Vercel's Agent Browser. See [MCP Setup](mcp-setup.md).
|
||||
**8. My browser agent fails or says Playwright is missing. What now?**
|
||||
The built-in Browser Agent is a plugin that uses the Main Model from `_model_config`. **Docker:** the Chromium headless shell is shipped preinstalled (typically under `/a0/tmp/playwright`). **Local development:** if the binary is missing, `ensure_playwright_binary()` in `plugins/_browser_agent/helpers/playwright.py` runs `playwright install chromium --only-shell` into `tmp/playwright` on first Browser Agent use (you may see UI notifications). To install ahead of time, run `PLAYWRIGHT_BROWSERS_PATH=tmp/playwright playwright install chromium --only-shell` after `pip install -r requirements.txt`. If you prefer an external browser stack, use MCP alternatives such as Browser OS, Chrome DevTools, or Playwright MCP. See [MCP Setup](mcp-setup.md).
|
||||
|
||||
**9. My secrets disappeared after a backup restore.**
|
||||
Secrets are stored in `/a0/usr/secrets.env` and are not always included in backup archives. Copy them manually.
|
||||
|
|
@ -36,7 +36,7 @@ Secrets are stored in `/a0/usr/secrets.env` and are not always included in backu
|
|||
- Join the Agent Zero [Skool](https://www.skool.com/agent-zero) or [Discord](https://discord.gg/B8KZKNsPpj) community.
|
||||
|
||||
**11. How do I adjust API rate limits?**
|
||||
Use the model rate limit fields in Settings (Chat/Utility/Browser model sections) to set request/input/output limits. These map to the model config limits (for example `limit_requests`, `limit_input`, `limit_output`).
|
||||
Use the model rate limit fields in Settings (Main Model and Utility Model sections) to set request/input/output limits. The Browser Agent inherits the Main Model limits. These map to the model config limits (for example `limit_requests`, `limit_input`, `limit_output`).
|
||||
|
||||
**12. My `code_execution_tool` doesn't work, what's wrong?**
|
||||
- Ensure Docker is installed and running.
|
||||
|
|
|
|||
|
|
@ -127,7 +127,9 @@ Agent Zero's power comes from its ability to use [tools](../developer/architectu
|
|||
- **Understand Tools:** Agent Zero includes default tools like knowledge (powered by SearXNG), code execution, and communication. Understand the capabilities of these tools and how to invoke them.
|
||||
|
||||
### Browser Agent Status & MCP Alternatives
|
||||
The built-in browser agent currently has dependency issues on some systems. If web automation is critical, prefer MCP-based browser tools instead:
|
||||
The built-in Browser Agent is provided by the `_browser_agent` plugin. It uses the effective Main Model from `_model_config`, including per-chat overrides and the Main Model vision flag. Playwright Chromium is preinstalled in **Docker**; in **local development** it is installed on demand when needed via `ensure_playwright_binary()` (see [Development Setup](../setup/dev-setup.md) to pre-install).
|
||||
|
||||
If you need a different browser stack or want external browser tooling, MCP-based browser tools are still a strong option:
|
||||
|
||||
- **Browser OS MCP**
|
||||
- **Chrome DevTools MCP**
|
||||
|
|
|
|||
|
|
@ -67,9 +67,9 @@ Now when you select one of the python files in the project, you should see prope
|
|||
3. Install dependencies. Run these two commands in the terminal:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
playwright install chromium
|
||||
PLAYWRIGHT_BROWSERS_PATH=tmp/playwright playwright install chromium --only-shell
|
||||
```
|
||||
These will install all the python packages and browser binaries for playwright (browser agent).
|
||||
The first command installs Python dependencies. The second installs the Chromium headless shell into `tmp/playwright` ahead of time (same path in Docker: `/a0/tmp/playwright`). If you skip the second command, **local development** still downloads the shell on first Browser Agent use through `ensure_playwright_binary()` in `plugins/_browser_agent/helpers/playwright.py`. Pre-installing avoids that wait. **Docker** images ship the shell preinstalled; runtime install is for local dev when the binary is missing.
|
||||
Errors in the code editor caused by missing packages should now be gone. If not, try reloading the window.
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -330,17 +330,19 @@ The Settings page is the control center for selecting the Large Language Models
|
|||
|
||||
| LLM Role | Description |
|
||||
| --- | --- |
|
||||
| `chat_llm` | This is the primary LLM used for conversations and generating responses. |
|
||||
| `chat_llm` | This is the primary LLM used for conversations, agent reasoning, tool use, and the built-in browser agent. Vision support controls browser vision and image understanding. |
|
||||
| `utility_llm` | This LLM handles internal tasks like summarizing messages, managing memory, and processing internal prompts. Using a smaller, less expensive model here can improve efficiency. |
|
||||
| `browser_llm` | This LLM powers the browser agent for web navigation and interaction tasks. Vision support is recommended for better page understanding. |
|
||||
| `embedding_llm` | The embedding model shipped with A0 runs on CPU and is responsible for generating embeddings used for memory retrieval and knowledge base lookups. Changing the `embedding_llm` will re-index all of A0's memory. |
|
||||
|
||||
**How to Change:**
|
||||
|
||||
1. Open Settings page in the Web UI.
|
||||
2. Choose the provider for the LLM for each role (Chat model, Utility model, Browser model, Embedding model) and write the model name.
|
||||
2. Choose the provider for the LLM for each role (Main Model, Utility Model, Embedding Model) and write the model name.
|
||||
3. Click "Save" to apply the changes.
|
||||
|
||||
> [!NOTE]
|
||||
> The Browser Agent does not have a separate model slot. It uses the effective Main Model resolved by `_model_config`, including per-chat overrides and the Main Model vision flag.
|
||||
|
||||
### Important Considerations
|
||||
|
||||
#### Model Naming by Provider
|
||||
|
|
|
|||
|
|
@ -2,16 +2,15 @@
|
|||
|
||||
## LLM Roles
|
||||
|
||||
Agent Zero uses four distinct LLM roles, each configurable independently:
|
||||
Agent Zero uses three configurable LLM roles:
|
||||
|
||||
| Role | Purpose |
|
||||
|------|---------|
|
||||
| `chat_llm` | Primary model for all agent reasoning and tool use |
|
||||
| `chat_llm` | Primary model for all agent reasoning, tool use, and the Browser Agent |
|
||||
| `utility_llm` | Secondary model for internal framework tasks: memory summarization, query generation, history compression, memory recall filtering |
|
||||
| `browser_llm` | Model used by the browser agent; vision capability recommended |
|
||||
| `embedding_llm` | Produces vector embeddings for memory and knowledge indexing |
|
||||
|
||||
The utility model handles high-volume, lower-stakes operations and can be a cheaper/faster model than the chat model. Changing the embedding model invalidates the existing vector index - the entire knowledge base is re-indexed automatically.
|
||||
The utility model handles high-volume, lower-stakes operations and can be a cheaper/faster model than the chat model. The Browser Agent uses the effective chat model resolved by `_model_config`, including per-chat overrides and the chat model vision flag. Changing the embedding model invalidates the existing vector index - the entire knowledge base is re-indexed automatically.
|
||||
|
||||
## Model Providers
|
||||
|
||||
|
|
|
|||
111
models.py
111
models.py
|
|
@ -25,7 +25,7 @@ from helpers.dotenv import load_dotenv
|
|||
from helpers.providers import ModelType as ProviderModelType, get_provider_config
|
||||
from helpers.rate_limiter import RateLimiter
|
||||
from helpers.tokens import approximate_tokens
|
||||
from helpers import dirty_json, browser_use_monkeypatch
|
||||
from helpers import dirty_json
|
||||
|
||||
from langchain_core.language_models.chat_models import SimpleChatModel
|
||||
from langchain_core.outputs.chat_generation import ChatGenerationChunk
|
||||
|
|
@ -57,9 +57,6 @@ def turn_off_logging():
|
|||
# init
|
||||
load_dotenv()
|
||||
turn_off_logging()
|
||||
browser_use_monkeypatch.apply()
|
||||
|
||||
litellm.modify_params = True # helps fix anthropic tool calls by browser-use
|
||||
|
||||
class ModelType(Enum):
|
||||
CHAT = "Chat"
|
||||
|
|
@ -578,101 +575,6 @@ class LiteLLMChatWrapper(SimpleChatModel):
|
|||
await asyncio.sleep(retry_delay_s)
|
||||
|
||||
|
||||
class AsyncAIChatReplacement:
|
||||
class _Completions:
|
||||
def __init__(self, wrapper):
|
||||
self._wrapper = wrapper
|
||||
|
||||
async def create(self, *args, **kwargs):
|
||||
# call the async _acall method on the wrapper
|
||||
return await self._wrapper._acall(*args, **kwargs)
|
||||
|
||||
class _Chat:
|
||||
def __init__(self, wrapper):
|
||||
self.completions = AsyncAIChatReplacement._Completions(wrapper)
|
||||
|
||||
def __init__(self, wrapper, *args, **kwargs):
|
||||
self._wrapper = wrapper
|
||||
self.chat = AsyncAIChatReplacement._Chat(wrapper)
|
||||
|
||||
|
||||
from browser_use.llm import ChatOllama, ChatOpenRouter, ChatGoogle, ChatAnthropic, ChatGroq, ChatOpenAI
|
||||
|
||||
class BrowserCompatibleChatWrapper(ChatOpenRouter):
|
||||
"""
|
||||
A wrapper for browser agent that can filter/sanitize messages
|
||||
before sending them to the LLM.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
turn_off_logging()
|
||||
# Create the underlying LiteLLM wrapper
|
||||
self._wrapper = LiteLLMChatWrapper(*args, **kwargs)
|
||||
# Browser-use may expect a 'model' attribute
|
||||
self.model = self._wrapper.model_name
|
||||
self.kwargs = self._wrapper.kwargs
|
||||
|
||||
@property
|
||||
def model_name(self) -> str:
|
||||
return self._wrapper.model_name
|
||||
|
||||
@property
|
||||
def provider(self) -> str:
|
||||
return self._wrapper.provider
|
||||
|
||||
def get_client(self, *args, **kwargs): # type: ignore
|
||||
return AsyncAIChatReplacement(self, *args, **kwargs)
|
||||
|
||||
async def _acall(
|
||||
self,
|
||||
messages: List[BaseMessage],
|
||||
stop: Optional[List[str]] = None,
|
||||
run_manager: Optional[CallbackManagerForLLMRun] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
# Apply rate limiting if configured
|
||||
apply_rate_limiter_sync(self._wrapper.a0_model_conf, str(messages))
|
||||
|
||||
# Call the model
|
||||
try:
|
||||
model = kwargs.pop("model", None)
|
||||
kwrgs = {**self._wrapper.kwargs, **kwargs}
|
||||
|
||||
# hack from browser-use to fix json schema for gemini (additionalProperties, $defs, $ref)
|
||||
if "response_format" in kwrgs and "json_schema" in kwrgs["response_format"] and model.startswith("gemini/"):
|
||||
kwrgs["response_format"]["json_schema"] = ChatGoogle("")._fix_gemini_schema(kwrgs["response_format"]["json_schema"])
|
||||
|
||||
resp = await acompletion(
|
||||
model=self._wrapper.model_name,
|
||||
messages=messages,
|
||||
stop=stop,
|
||||
**kwrgs,
|
||||
)
|
||||
|
||||
# Gemini: strip triple backticks and conform schema
|
||||
try:
|
||||
msg = resp.choices[0].message # type: ignore
|
||||
if self.provider == "gemini" and isinstance(getattr(msg, "content", None), str):
|
||||
cleaned = browser_use_monkeypatch.gemini_clean_and_conform(msg.content) # type: ignore
|
||||
if cleaned:
|
||||
msg.content = cleaned
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
# another hack for browser-use post process invalid jsons
|
||||
try:
|
||||
if "response_format" in kwrgs and "json_schema" in kwrgs["response_format"] or "json_object" in kwrgs["response_format"]:
|
||||
if resp.choices[0].message.content is not None and not resp.choices[0].message.content.startswith("{"): # type: ignore
|
||||
js = dirty_json.parse(resp.choices[0].message.content) # type: ignore
|
||||
resp.choices[0].message.content = dirty_json.stringify(js) # type: ignore
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return resp
|
||||
|
||||
class LiteLLMEmbeddingWrapper(Embeddings):
|
||||
model_name: str
|
||||
kwargs: dict = {}
|
||||
|
|
@ -910,17 +812,6 @@ def get_chat_model(
|
|||
LiteLLMChatWrapper, name, provider_name, model_config, **kwargs
|
||||
)
|
||||
|
||||
|
||||
def get_browser_model(
|
||||
provider: str, name: str, model_config: Optional[ModelConfig] = None, **kwargs: Any
|
||||
) -> BrowserCompatibleChatWrapper:
|
||||
orig = provider.lower()
|
||||
provider_name, kwargs = _merge_provider_defaults("chat", orig, kwargs)
|
||||
return _get_litellm_chat(
|
||||
BrowserCompatibleChatWrapper, name, provider_name, model_config, **kwargs
|
||||
)
|
||||
|
||||
|
||||
def get_embedding_model(
|
||||
provider: str, name: str, model_config: Optional[ModelConfig] = None, **kwargs: Any
|
||||
) -> LiteLLMEmbeddingWrapper | LocalSentenceTransformerWrapper:
|
||||
|
|
|
|||
45
plugins/_browser_agent/api/status.py
Normal file
45
plugins/_browser_agent/api/status.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import importlib.metadata
|
||||
|
||||
from helpers.api import ApiHandler, Request, Response
|
||||
from plugins._browser_agent.helpers.playwright import (
|
||||
get_playwright_binary,
|
||||
get_playwright_cache_dir,
|
||||
)
|
||||
from plugins._model_config.helpers.model_config import get_chat_model_config
|
||||
|
||||
|
||||
class Status(ApiHandler):
|
||||
async def process(self, input: dict, request: Request) -> dict | Response:
|
||||
cfg = get_chat_model_config()
|
||||
binary = get_playwright_binary()
|
||||
|
||||
browser_use_ok = False
|
||||
browser_use_error = ""
|
||||
browser_use_version = ""
|
||||
try:
|
||||
import browser_use # noqa: F401
|
||||
|
||||
browser_use_ok = True
|
||||
browser_use_version = importlib.metadata.version("browser-use")
|
||||
except Exception as e:
|
||||
browser_use_error = str(e)
|
||||
|
||||
return {
|
||||
"plugin": "_browser_agent",
|
||||
"model_source": "Main Model via _model_config",
|
||||
"model": {
|
||||
"provider": cfg.get("provider", ""),
|
||||
"name": cfg.get("name", ""),
|
||||
"vision": bool(cfg.get("vision", False)),
|
||||
},
|
||||
"playwright": {
|
||||
"cache_dir": get_playwright_cache_dir(),
|
||||
"binary_found": bool(binary),
|
||||
"binary_path": str(binary) if binary else "",
|
||||
},
|
||||
"browser_use": {
|
||||
"import_ok": browser_use_ok,
|
||||
"version": browser_use_version,
|
||||
"error": browser_use_error,
|
||||
},
|
||||
}
|
||||
|
|
@ -243,4 +243,4 @@
|
|||
// }
|
||||
// return element;
|
||||
// };
|
||||
// })();
|
||||
// })();
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
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";
|
||||
|
||||
export default async function registerBrowserAgentHandler(extData) {
|
||||
if (extData?.type === "browser") {
|
||||
extData.handler = drawMessageBrowserAgent;
|
||||
}
|
||||
}
|
||||
|
||||
function drawMessageBrowserAgent({
|
||||
id,
|
||||
type,
|
||||
heading,
|
||||
content,
|
||||
kvps,
|
||||
timestamp,
|
||||
agentno = 0,
|
||||
...additional
|
||||
}) {
|
||||
const title = cleanStepTitle(heading);
|
||||
const displayKvps = { ...kvps };
|
||||
const answerText = String(kvps?.answer ?? "");
|
||||
const actionButtons = answerText.trim()
|
||||
? [
|
||||
createActionButton("detail", "", () =>
|
||||
stepDetailStore.showStepDetail(
|
||||
buildDetailPayload(arguments[0], { headerLabels: [] }),
|
||||
),
|
||||
),
|
||||
createActionButton("speak", "", () => speechStore.speak(answerText)),
|
||||
createActionButton("copy", "", () => copyToClipboard(answerText)),
|
||||
].filter(Boolean)
|
||||
: [];
|
||||
|
||||
return drawProcessStep({
|
||||
id,
|
||||
title,
|
||||
code: "WWW",
|
||||
classes: undefined,
|
||||
kvps: displayKvps,
|
||||
content,
|
||||
actionButtons,
|
||||
log: arguments[0],
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { drawMessageToolSimple } from "/js/messages.js";
|
||||
|
||||
/**
|
||||
* Registers the browser_agent tool message handler to set the custom badge.
|
||||
* @param {object} extData
|
||||
*/
|
||||
export default async function registerBrowserToolHandler(extData) {
|
||||
if (extData?.tool_name === "browser_agent") {
|
||||
extData.handler = drawBrowserTool;
|
||||
}
|
||||
}
|
||||
|
||||
function drawBrowserTool(args) {
|
||||
return drawMessageToolSimple({ ...args, code: "WWW" });
|
||||
}
|
||||
1
plugins/_browser_agent/helpers/__init__.py
Normal file
1
plugins/_browser_agent/helpers/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Built-in browser agent helpers.
|
||||
131
plugins/_browser_agent/helpers/browser_llm.py
Normal file
131
plugins/_browser_agent/helpers/browser_llm.py
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
from typing import Any, List, Optional
|
||||
import litellm
|
||||
from litellm import acompletion
|
||||
from langchain_core.callbacks.manager import CallbackManagerForLLMRun
|
||||
from langchain_core.messages import BaseMessage
|
||||
|
||||
import models
|
||||
from browser_use.llm import ChatGoogle, ChatOpenRouter
|
||||
from helpers import dirty_json
|
||||
|
||||
from plugins._browser_agent.helpers import browser_use_monkeypatch
|
||||
|
||||
|
||||
_BROWSER_USE_PATCHED = False
|
||||
|
||||
|
||||
def apply_browser_use_patches() -> None:
|
||||
global _BROWSER_USE_PATCHED
|
||||
if _BROWSER_USE_PATCHED:
|
||||
return
|
||||
|
||||
browser_use_monkeypatch.apply()
|
||||
litellm.modify_params = True
|
||||
_BROWSER_USE_PATCHED = True
|
||||
|
||||
|
||||
class AsyncAIChatReplacement:
|
||||
class _Completions:
|
||||
def __init__(self, wrapper):
|
||||
self._wrapper = wrapper
|
||||
|
||||
async def create(self, *args, **kwargs):
|
||||
return await self._wrapper._acall(*args, **kwargs)
|
||||
|
||||
class _Chat:
|
||||
def __init__(self, wrapper):
|
||||
self.completions = AsyncAIChatReplacement._Completions(wrapper)
|
||||
|
||||
def __init__(self, wrapper, *args, **kwargs):
|
||||
self._wrapper = wrapper
|
||||
self.chat = AsyncAIChatReplacement._Chat(wrapper)
|
||||
|
||||
|
||||
class BrowserCompatibleChatWrapper(ChatOpenRouter):
|
||||
"""
|
||||
A wrapper for browser agent that can filter/sanitize messages
|
||||
before sending them to the LLM.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
apply_browser_use_patches()
|
||||
models.turn_off_logging()
|
||||
self._wrapper = models.LiteLLMChatWrapper(*args, **kwargs)
|
||||
self.model = self._wrapper.model_name
|
||||
self.kwargs = self._wrapper.kwargs
|
||||
|
||||
@property
|
||||
def model_name(self) -> str:
|
||||
return self._wrapper.model_name
|
||||
|
||||
@property
|
||||
def provider(self) -> str:
|
||||
return self._wrapper.provider
|
||||
|
||||
def get_client(self, *args, **kwargs): # type: ignore
|
||||
return AsyncAIChatReplacement(self, *args, **kwargs)
|
||||
|
||||
async def _acall(
|
||||
self,
|
||||
messages: List[BaseMessage],
|
||||
stop: Optional[List[str]] = None,
|
||||
run_manager: Optional[CallbackManagerForLLMRun] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
models.apply_rate_limiter_sync(self._wrapper.a0_model_conf, str(messages))
|
||||
|
||||
try:
|
||||
model = kwargs.pop("model", None)
|
||||
kwrgs = {**self._wrapper.kwargs, **kwargs}
|
||||
|
||||
# hack from browser-use to fix json schema for gemini (additionalProperties, $defs, $ref)
|
||||
if "response_format" in kwrgs and "json_schema" in kwrgs["response_format"] and model and model.startswith("gemini/"):
|
||||
kwrgs["response_format"]["json_schema"] = ChatGoogle("")._fix_gemini_schema(kwrgs["response_format"]["json_schema"])
|
||||
|
||||
resp = await acompletion(
|
||||
model=self._wrapper.model_name,
|
||||
messages=messages,
|
||||
stop=stop,
|
||||
**kwrgs,
|
||||
)
|
||||
|
||||
# Gemini: strip triple backticks and conform schema
|
||||
try:
|
||||
msg = resp.choices[0].message # type: ignore
|
||||
if self.provider == "gemini" and isinstance(getattr(msg, "content", None), str):
|
||||
cleaned = browser_use_monkeypatch.gemini_clean_and_conform(msg.content) # type: ignore
|
||||
if cleaned:
|
||||
msg.content = cleaned
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
# another hack for browser-use post process invalid jsons
|
||||
try:
|
||||
if "response_format" in kwrgs and "json_schema" in kwrgs["response_format"] or "json_object" in kwrgs["response_format"]:
|
||||
if resp.choices[0].message.content is not None and not resp.choices[0].message.content.startswith("{"): # type: ignore
|
||||
js = dirty_json.parse(resp.choices[0].message.content) # type: ignore
|
||||
resp.choices[0].message.content = dirty_json.stringify(js) # type: ignore
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return resp
|
||||
|
||||
|
||||
def build_browser_model_from_config(
|
||||
model_config: models.ModelConfig,
|
||||
) -> BrowserCompatibleChatWrapper:
|
||||
apply_browser_use_patches()
|
||||
original_provider = model_config.provider.lower()
|
||||
provider_name, kwargs = models._merge_provider_defaults( # type: ignore[attr-defined]
|
||||
"chat", original_provider, model_config.build_kwargs()
|
||||
)
|
||||
return models._get_litellm_chat( # type: ignore[attr-defined]
|
||||
BrowserCompatibleChatWrapper,
|
||||
model_config.name,
|
||||
provider_name,
|
||||
model_config,
|
||||
**kwargs,
|
||||
)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
from helpers import dotenv
|
||||
dotenv.save_dotenv_value("ANONYMIZED_TELEMETRY", "false")
|
||||
import browser_use
|
||||
import browser_use.utils
|
||||
import browser_use.utils
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
|
@ -36,4 +35,4 @@ def ensure_playwright_binary():
|
|||
bin = get_playwright_binary()
|
||||
if not bin:
|
||||
raise Exception("Playwright binary not found after installation")
|
||||
return bin
|
||||
return bin
|
||||
8
plugins/_browser_agent/plugin.yaml
Normal file
8
plugins/_browser_agent/plugin.yaml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
name: _browser_agent
|
||||
title: Browser Agent
|
||||
description: Built-in browser-use automation tool.
|
||||
version: 1.0.0
|
||||
always_enabled: false
|
||||
settings_sections: []
|
||||
per_project_config: false
|
||||
per_agent_config: false
|
||||
|
|
@ -19,4 +19,4 @@ When you have completed the assigned task OR are waiting for further instruction
|
|||
- Response field is used to answer to user's task or ask additional questions
|
||||
- If you navigate to a website and no further actions are requested, call "Complete task" immediately
|
||||
- If you complete any requested interaction (clicking, typing, etc.), call "Complete task"
|
||||
- Never leave a task running indefinitely - always conclude with "Complete task"
|
||||
- Never leave a task running indefinitely - always conclude with "Complete task"
|
||||
|
|
@ -6,9 +6,9 @@ from pathlib import Path
|
|||
|
||||
from helpers.tool import Tool, Response
|
||||
from helpers import files, defer, persist_chat, strings
|
||||
from helpers.browser_use import browser_use # type: ignore[attr-defined]
|
||||
from plugins._browser_agent.helpers.browser_use import browser_use # type: ignore[attr-defined]
|
||||
from helpers.print_style import PrintStyle
|
||||
from helpers.playwright import ensure_playwright_binary
|
||||
from plugins._browser_agent.helpers.playwright import ensure_playwright_binary
|
||||
from helpers.secrets import get_secrets_manager
|
||||
from extensions.python.message_loop_start._10_iteration_no import get_iter_no
|
||||
from pydantic import BaseModel
|
||||
|
|
@ -16,6 +16,9 @@ import uuid
|
|||
from helpers.dirty_json import DirtyJson
|
||||
|
||||
|
||||
PLUGIN_DIR = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class State:
|
||||
@staticmethod
|
||||
async def create(agent: Agent):
|
||||
|
|
@ -105,7 +108,7 @@ class State:
|
|||
|
||||
# Add init script to the browser session
|
||||
if self.browser_session and self.browser_session.browser_context:
|
||||
js_override = files.get_abs_path("lib/browser/init_override.js")
|
||||
js_override = str(PLUGIN_DIR / "assets" / "init_override.js")
|
||||
await self.browser_session.browser_context.add_init_script(path=js_override) if self.browser_session else None
|
||||
|
||||
def start_task(self, task: str):
|
||||
204
plugins/_browser_agent/webui/main.html
Normal file
204
plugins/_browser_agent/webui/main.html
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Browser Agent</title>
|
||||
<script type="module">
|
||||
import { callJsonApi } from "/js/api.js";
|
||||
import "/components/plugins/list/pluginListStore.js";
|
||||
|
||||
globalThis.browserAgentStatusApi = { callJsonApi };
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
x-data="{
|
||||
loading: true,
|
||||
error: '',
|
||||
status: null,
|
||||
async init() {
|
||||
try {
|
||||
this.status = await browserAgentStatusApi.callJsonApi('/plugins/_browser_agent/status', {});
|
||||
} catch (error) {
|
||||
this.error = error instanceof Error ? error.message : String(error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}"
|
||||
x-init="init()"
|
||||
class="browser-agent-page"
|
||||
>
|
||||
<div class="section-title">Browser Agent</div>
|
||||
<div class="section-description">
|
||||
Built-in browser automation plugin backed by `browser-use` and Playwright.
|
||||
Model selection stays in `_model_config`; the browser agent follows the effective Main Model.
|
||||
</div>
|
||||
|
||||
<div class="browser-agent-card" x-show="loading">
|
||||
<div class="status-row">
|
||||
<span class="material-symbols-outlined spinning">progress_activity</span>
|
||||
<span>Loading browser status...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="browser-agent-card error" x-show="!loading && error">
|
||||
<div class="field-title">Status check failed</div>
|
||||
<div class="field-description" x-text="error"></div>
|
||||
</div>
|
||||
|
||||
<template x-if="!loading && status">
|
||||
<div class="browser-agent-grid">
|
||||
<div class="browser-agent-card">
|
||||
<div class="field-title">Model Source</div>
|
||||
<div class="field-description" x-text="status.model_source"></div>
|
||||
</div>
|
||||
|
||||
<div class="browser-agent-card">
|
||||
<div class="field-title">Resolved Main Model</div>
|
||||
<div class="status-row">
|
||||
<span class="status-key">Provider</span>
|
||||
<span class="status-value" x-text="status.model.provider || 'Not configured'"></span>
|
||||
</div>
|
||||
<div class="status-row">
|
||||
<span class="status-key">Model</span>
|
||||
<span class="status-value" x-text="status.model.name || 'Not configured'"></span>
|
||||
</div>
|
||||
<div class="status-row">
|
||||
<span class="status-key">Vision</span>
|
||||
<span class="status-badge" :class="status.model.vision ? 'ok' : 'warn'" x-text="status.model.vision ? 'Enabled' : 'Disabled'"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="browser-agent-card">
|
||||
<div class="field-title">Playwright Runtime</div>
|
||||
<div class="status-row">
|
||||
<span class="status-key">Binary</span>
|
||||
<span class="status-badge" :class="status.playwright.binary_found ? 'ok' : 'fail'" x-text="status.playwright.binary_found ? 'Found' : 'Missing'"></span>
|
||||
</div>
|
||||
<div class="status-row">
|
||||
<span class="status-key">Cache</span>
|
||||
<span class="status-value mono" x-text="status.playwright.cache_dir"></span>
|
||||
</div>
|
||||
<div class="status-row" x-show="status.playwright.binary_path">
|
||||
<span class="status-key">Path</span>
|
||||
<span class="status-value mono" x-text="status.playwright.binary_path"></span>
|
||||
</div>
|
||||
<div class="field-description" x-show="!status.playwright.binary_found">
|
||||
Docker images ship the Playwright Chromium shell preinstalled. In local development, the first run installs it on demand via <span class="mono">ensure_playwright_binary()</span> if missing.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="browser-agent-card">
|
||||
<div class="field-title">browser-use</div>
|
||||
<div class="status-row">
|
||||
<span class="status-key">Import</span>
|
||||
<span class="status-badge" :class="status.browser_use.import_ok ? 'ok' : 'fail'" x-text="status.browser_use.import_ok ? 'Ready' : 'Error'"></span>
|
||||
</div>
|
||||
<div class="status-row" x-show="status.browser_use.version">
|
||||
<span class="status-key">Version</span>
|
||||
<span class="status-value" x-text="status.browser_use.version"></span>
|
||||
</div>
|
||||
<div class="field-description mono" x-show="status.browser_use.error" x-text="status.browser_use.error"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="browser-agent-actions">
|
||||
<button
|
||||
class="btn btn-field"
|
||||
@click="$store.pluginListStore.openPluginConfig({ name: '_model_config', has_config_screen: true })"
|
||||
>
|
||||
Open Model Settings
|
||||
</button>
|
||||
<button class="btn btn-field" @click="openModal('/plugins/_model_config/webui/main.html')">
|
||||
Open Presets
|
||||
</button>
|
||||
<button class="btn btn-field" @click="openModal('/plugins/_model_config/webui/api-keys.html')">
|
||||
Open API Keys
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.browser-agent-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.browser-agent-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
}
|
||||
|
||||
.browser-agent-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 14px;
|
||||
background: var(--color-input);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.browser-agent-card.error {
|
||||
border-color: rgba(214, 40, 40, 0.35);
|
||||
}
|
||||
|
||||
.browser-agent-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.status-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
.status-key {
|
||||
opacity: 0.7;
|
||||
min-width: 64px;
|
||||
}
|
||||
|
||||
.status-value {
|
||||
text-align: right;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.76rem;
|
||||
font-weight: 600;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.status-badge.ok {
|
||||
color: #1b5e20;
|
||||
background: rgba(46, 125, 50, 0.14);
|
||||
border-color: rgba(46, 125, 50, 0.24);
|
||||
}
|
||||
|
||||
.status-badge.warn {
|
||||
color: #8a6100;
|
||||
background: rgba(191, 144, 0, 0.14);
|
||||
border-color: rgba(191, 144, 0, 0.24);
|
||||
}
|
||||
|
||||
.status-badge.fail {
|
||||
color: #9f1239;
|
||||
background: rgba(190, 24, 93, 0.12);
|
||||
border-color: rgba(190, 24, 93, 0.24);
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
BIN
plugins/_browser_agent/webui/thumbnail.jpg
Normal file
BIN
plugins/_browser_agent/webui/thumbnail.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.7 KiB |
|
|
@ -187,13 +187,15 @@ def build_utility_model(agent=None):
|
|||
|
||||
|
||||
def build_browser_model(agent=None):
|
||||
"""Build and return a BrowserCompatibleChatWrapper using chat model config."""
|
||||
"""Build and return the browser-use adapter using chat model config."""
|
||||
cfg = get_chat_model_config(agent)
|
||||
mc = build_model_config(cfg, models.ModelType.CHAT)
|
||||
return models.get_browser_model(
|
||||
mc.provider, mc.name, model_config=mc, **mc.build_kwargs()
|
||||
from plugins._browser_agent.helpers.browser_llm import (
|
||||
build_browser_model_from_config,
|
||||
)
|
||||
|
||||
return build_browser_model_from_config(mc)
|
||||
|
||||
|
||||
def build_embedding_model(agent=None):
|
||||
"""Build and return an embedding model wrapper."""
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@
|
|||
<body>
|
||||
<div x-data>
|
||||
<template x-if="$store.pluginInstallStore">
|
||||
<div class="pi-browse-shell"
|
||||
x-create="$store.pluginInstallStore.resetIndex(); $store.pluginInstallStore.fetchIndex()">
|
||||
<div class="pi-browse-shell">
|
||||
|
||||
<section class="pi-browse-hero">
|
||||
<div class="pi-browse-copy">
|
||||
|
|
|
|||
|
|
@ -64,6 +64,10 @@ const model = {
|
|||
setTab(tab) {
|
||||
this.activeTab = tab;
|
||||
this.result = null;
|
||||
if (tab === "store") {
|
||||
this.resetIndex();
|
||||
void this.fetchIndex();
|
||||
}
|
||||
},
|
||||
|
||||
setBrowseFilter(filter) {
|
||||
|
|
|
|||
80
tests/test_browser_agent_regressions.py
Normal file
80
tests/test_browser_agent_regressions.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import asyncio
|
||||
import importlib
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
import plugins._browser_agent.helpers.browser_use_monkeypatch as browser_use_monkeypatch
|
||||
import plugins._browser_agent.tools.browser_agent as browser_agent_module
|
||||
|
||||
|
||||
def test_gemini_clean_and_conform_normalizes_known_single_action_shapes():
|
||||
raw = (
|
||||
'{"action":['
|
||||
'{"complete_task":{"title":"T","response":"R","page_summary":"S"}}'
|
||||
']}'
|
||||
)
|
||||
|
||||
cleaned = browser_use_monkeypatch.gemini_clean_and_conform(raw)
|
||||
|
||||
assert cleaned is not None
|
||||
parsed = json.loads(cleaned)
|
||||
assert parsed["action"] == [
|
||||
{
|
||||
"done": {
|
||||
"success": True,
|
||||
"data": {
|
||||
"title": "T",
|
||||
"response": "R",
|
||||
"page_summary": "S",
|
||||
},
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class DummyBrowserSession:
|
||||
def __init__(self) -> None:
|
||||
self.kill_called = False
|
||||
self.close_called = False
|
||||
|
||||
async def kill(self) -> None:
|
||||
self.kill_called = True
|
||||
|
||||
async def close(self) -> None:
|
||||
self.close_called = True
|
||||
|
||||
|
||||
class DummyAgent:
|
||||
def __init__(self) -> None:
|
||||
self.context = SimpleNamespace(id="ctx", task=None)
|
||||
|
||||
|
||||
def test_browser_session_teardown_prefers_kill_for_keep_alive_sessions():
|
||||
state = browser_agent_module.State(DummyAgent())
|
||||
session = DummyBrowserSession()
|
||||
state.browser_session = session
|
||||
|
||||
state.kill_task()
|
||||
|
||||
assert session.kill_called is True
|
||||
assert session.close_called is False
|
||||
|
||||
|
||||
def test_browser_cleanup_extensions_follow_new_extensible_path_layout():
|
||||
extension = importlib.import_module("helpers.extension")
|
||||
remove_classes = extension._get_extension_classes( # type: ignore[attr-defined]
|
||||
"_functions/agent/AgentContext/remove/start"
|
||||
)
|
||||
reset_classes = extension._get_extension_classes( # type: ignore[attr-defined]
|
||||
"_functions/agent/AgentContext/reset/start"
|
||||
)
|
||||
|
||||
assert any(cls.__name__ == "CleanupBrowserStateOnRemove" for cls in remove_classes)
|
||||
assert any(cls.__name__ == "CleanupBrowserStateOnReset" for cls in reset_classes)
|
||||
|
|
@ -85,7 +85,7 @@
|
|||
/* Critical for allowing flex children to shrink */
|
||||
overflow: hidden;
|
||||
justify-content: space-between;
|
||||
margin-top: 3.5rem;
|
||||
margin-top: 6rem;
|
||||
padding: var(--spacing-md) var(--spacing-md) 0 var(--spacing-md);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,86 +6,212 @@
|
|||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="icons-section" id="hide-button" x-data>
|
||||
<!-- Sidebar Toggle Button -->
|
||||
<button id="toggle-sidebar" class="toggle-sidebar-button" @click="$store.sidebar.toggle()" aria-label="Toggle Sidebar" aria-expanded="false">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">menu</span>
|
||||
</button>
|
||||
<div class="sidebar-header-stack">
|
||||
<!--
|
||||
Single chrome block for logo + toolbar (tokens aligned with .config-button in quick-actions).
|
||||
Spacer reserves in-flow height so quick-actions clears the fixed overlay (see .left-panel-top margin-top).
|
||||
-->
|
||||
<div id="sidebar-header-panel" class="sidebar-header-panel">
|
||||
<div id="logo-bar">
|
||||
<a href="https://github.com/agent0ai/agent-zero" target="_blank" rel="noopener noreferrer" aria-label="Agent Zero on GitHub">
|
||||
<span id="a0-logo" role="img" aria-label="Agent Zero"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="logo-container">
|
||||
<a href="https://github.com/agent0ai/agent-zero" target="_blank" rel="noopener noreferrer" aria-label="Agent Zero on GitHub">
|
||||
<span id="a0-logo" role="img" aria-label="Agent Zero"></span>
|
||||
</a>
|
||||
<div class="icons-section" id="hide-button" x-data>
|
||||
<!-- Sidebar Toggle Button -->
|
||||
<button id="toggle-sidebar" class="toggle-sidebar-button" @click="$store.sidebar.toggle()" aria-label="Toggle Sidebar" aria-expanded="false">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">menu</span>
|
||||
</button>
|
||||
|
||||
<button class="config-button header-action-button" id="header-dashboard" @click="deselectChat()" title="Dashboard" aria-label="Dashboard">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">home</span>
|
||||
</button>
|
||||
|
||||
<button class="config-button header-action-button" id="header-plugins" @click="openModal('components/plugins/list/plugin-list.html')" title="Plugins" aria-label="Plugins">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">extension</span>
|
||||
</button>
|
||||
|
||||
<button class="config-button header-action-button" id="header-settings" @click="openModal('settings/settings.html')" title="Settings" aria-label="Settings">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">settings</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<style>
|
||||
/* Sidebar toggle button + logo container (migrated) */
|
||||
.toggle-sidebar-button {
|
||||
height: 2.6rem;
|
||||
width: 2.6rem;
|
||||
.sidebar-header-stack {
|
||||
/*
|
||||
Panel top (viewport) minus .left-panel-top margin-top: flow starts at 5rem; reserve overlap region.
|
||||
Tuned for: padding + logo 1.47rem + gap + icon row ~2.2rem + padding.
|
||||
*/
|
||||
--sidebar-header-flow-clearance: calc(1.9rem + (var(--spacing-xs) * 5) + 1.47rem + 2.2rem - 5rem);
|
||||
}
|
||||
|
||||
/* Shell: same language as .config-button (quick-actions.html) */
|
||||
.sidebar-header-panel {
|
||||
position: fixed;
|
||||
top: var(--spacing-md);
|
||||
left: var(--spacing-md);
|
||||
width: calc(250px - (var(--spacing-md) * 2));
|
||||
box-sizing: border-box;
|
||||
z-index: 1004;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
background-color: var(--color-panel);
|
||||
border: 0.05rem solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
transition: background-color var(--transition-speed),
|
||||
border-color var(--transition-speed), box-shadow var(--transition-speed),
|
||||
opacity var(--transition-speed) ease-in-out;
|
||||
}
|
||||
|
||||
#logo-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
padding-top: var(--spacing-xxs);
|
||||
}
|
||||
|
||||
#logo-bar a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
width: min(100%, 11.75rem);
|
||||
}
|
||||
|
||||
#hide-button {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: var(--spacing-xs);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Toolbar icons sit on the panel surface (no nested config-button boxes) */
|
||||
.sidebar-header-panel #hide-button .config-button,
|
||||
.sidebar-header-panel .toggle-sidebar-button {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.sidebar-header-panel #hide-button .config-button {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.8;
|
||||
padding: var(--spacing-xs);
|
||||
min-width: 0;
|
||||
min-height: 2.2rem;
|
||||
transition: background-color var(--transition-speed),
|
||||
border-color var(--transition-speed), box-shadow var(--transition-speed),
|
||||
opacity var(--transition-speed);
|
||||
}
|
||||
|
||||
.sidebar-header-panel #hide-button .config-button .material-symbols-outlined {
|
||||
font-size: 22px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.sidebar-header-panel #hide-button .config-button:hover {
|
||||
background-color: var(--color-background-hover);
|
||||
opacity: 1;
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 18%, transparent);
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sidebar-header-panel #hide-button .config-button:active {
|
||||
opacity: 0.5;
|
||||
box-shadow: inset 0 0 0 999px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.toggle-sidebar-button {
|
||||
color: var(--color-text);
|
||||
opacity: 0.8;
|
||||
cursor: pointer;
|
||||
left: var(--spacing-md);
|
||||
padding: 0.47rem 0.56rem;
|
||||
position: absolute;
|
||||
top: 1.4rem;
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
min-height: 2.2rem;
|
||||
padding: var(--spacing-xs);
|
||||
-webkit-transition: all var(--transition-speed) ease-in-out;
|
||||
transition: all var(--transition-speed) ease-in-out;
|
||||
}
|
||||
.toggle-sidebar-button:hover { background-color: transparent; opacity: 1; }
|
||||
.toggle-sidebar-button:active { opacity: 0.5; }
|
||||
.toggle-sidebar-button .material-symbols-outlined {
|
||||
|
||||
.toggle-sidebar-button:hover {
|
||||
background-color: var(--color-background-hover);
|
||||
opacity: 1;
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 18%, transparent);
|
||||
}
|
||||
|
||||
.toggle-sidebar-button:active {
|
||||
opacity: 0.5;
|
||||
box-shadow: inset 0 0 0 999px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.toggle-sidebar-button .material-symbols-outlined {
|
||||
font-size: 22px;
|
||||
-webkit-transition: all var(--transition-speed) ease;
|
||||
transition: all var(--transition-speed) ease;
|
||||
-webkit-transition: all var(--transition-speed) ease;
|
||||
transition: all var(--transition-speed) ease;
|
||||
}
|
||||
|
||||
.toggle-sidebar-button:active .material-symbols-outlined { -webkit-transform: scaleY(0.8); transform: scaleY(0.8); }
|
||||
|
||||
#logo-container {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: fixed;
|
||||
left: 4rem;
|
||||
top: var(--spacing-md);
|
||||
z-index: 1004;
|
||||
-webkit-transition: margin-left var(--transition-speed) ease-in-out;
|
||||
transition: margin-left var(--transition-speed) ease-in-out;
|
||||
.toggle-sidebar-button:active .material-symbols-outlined {
|
||||
-webkit-transform: scaleY(0.8);
|
||||
transform: scaleY(0.8);
|
||||
}
|
||||
#logo-container a { color: inherit; text-decoration: none; }
|
||||
|
||||
#a0-logo {
|
||||
display: block;
|
||||
height: 2.6rem;
|
||||
aspect-ratio: 120 / 50;
|
||||
-webkit-mask-image: url('/public/a0-fullDark.svg');
|
||||
mask-image: url('/public/a0-fullDark.svg');
|
||||
width: 100%;
|
||||
height: 1.64rem;
|
||||
-webkit-mask-image: url(/public/a0-fullDark.svg);
|
||||
mask-image: url(/public/a0-fullDark.svg);
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-size: contain;
|
||||
mask-size: contain;
|
||||
-webkit-mask-position: center;
|
||||
-webkit-mask-position: left;
|
||||
mask-position: center;
|
||||
background-color: var(--color-text);
|
||||
opacity: 0.8;
|
||||
-webkit-transition: opacity var(--transition-speed) ease, background-color var(--transition-speed) ease;
|
||||
transition: opacity var(--transition-speed) ease, background-color var(--transition-speed) ease;
|
||||
}
|
||||
#logo-container a:hover #a0-logo { opacity: 1; }
|
||||
|
||||
#logo-bar a:hover #a0-logo {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.light-mode .sidebar-header-panel {
|
||||
background-color: var(--color-background);
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.light-mode .sidebar-header-panel #hide-button .config-button:hover,
|
||||
.light-mode .sidebar-header-panel .toggle-sidebar-button:hover {
|
||||
background-color: var(--color-background-hover);
|
||||
}
|
||||
|
||||
.light-mode .sidebar-header-panel #hide-button .config-button:active,
|
||||
.light-mode .sidebar-header-panel .toggle-sidebar-button:active {
|
||||
background-color: var(--color-background-hover);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.toggle-sidebar-button { position: fixed; left: var(--spacing-md); z-index: 1004; }
|
||||
#logo-container { -webkit-transition: all 0.3s ease; transition: all 0.3s ease; z-index: 1004; }
|
||||
.sidebar-header-panel {
|
||||
-webkit-transition: all 0.3s ease;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -14,21 +14,6 @@
|
|||
|
||||
<x-extension id="sidebar-quick-actions-main-start"></x-extension>
|
||||
|
||||
<!-- Dashboard -->
|
||||
<button class="config-button" id="dashboard" @click="deselectChat()" title="Dashboard">
|
||||
<span class="material-symbols-outlined">home</span>
|
||||
</button>
|
||||
|
||||
<!-- Scheduler -->
|
||||
<button class="config-button" id="plugins" @click="openModal('components/plugins/list/plugin-list.html')" title="Plugins">
|
||||
<span class="material-symbols-outlined">extension</span>
|
||||
</button>
|
||||
|
||||
<!-- Settings -->
|
||||
<button class="config-button" id="settings" @click="openModal('settings/settings.html')" title="Settings">
|
||||
<span class="material-symbols-outlined">settings</span>
|
||||
</button>
|
||||
|
||||
<x-extension id="sidebar-quick-actions-main-end"></x-extension>
|
||||
|
||||
<!-- Dropdown Toggle -->
|
||||
|
|
@ -126,6 +111,11 @@
|
|||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.quick-actions-anchor {
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#quick-actions > x-extension:not(:empty) {
|
||||
display: contents;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -200,11 +200,7 @@ const model = {
|
|||
window.openModal("modals/scheduler/scheduler-modal.html");
|
||||
break;
|
||||
case "settings":
|
||||
// Open settings modal
|
||||
const settingsButton = document.getElementById("settings");
|
||||
if (settingsButton) {
|
||||
settingsButton.click();
|
||||
}
|
||||
window.openModal("settings/settings.html");
|
||||
break;
|
||||
case "projects":
|
||||
projectsStore.openProjectsModal();
|
||||
|
|
|
|||
|
|
@ -94,8 +94,6 @@ export async function getMessageHandler(type) {
|
|||
return drawMessageResponse;
|
||||
case "tool":
|
||||
return drawMessageTool;
|
||||
case "browser":
|
||||
return drawMessageBrowser;
|
||||
case "progress":
|
||||
return drawMessageProgress;
|
||||
case "mcp":
|
||||
|
|
@ -1124,9 +1122,9 @@ export function drawMessageUser({
|
|||
|
||||
/**
|
||||
* @param {MessageHandlerArgs & Record<string, any>} param0
|
||||
* @returns {MessageHandlerResult}
|
||||
* @returns {Promise<MessageHandlerResult>}
|
||||
*/
|
||||
export function drawMessageTool({
|
||||
export async function drawMessageTool({
|
||||
id,
|
||||
type,
|
||||
heading,
|
||||
|
|
@ -1148,13 +1146,21 @@ export function drawMessageTool({
|
|||
return drawMessageToolSimple({ ...arguments[0], code: "EYE" });
|
||||
} else if (kvps._tool_name === "search_engine") {
|
||||
return drawMessageToolSimple({ ...arguments[0], code: "WEB" });
|
||||
} else if (kvps._tool_name === "browser_agent") {
|
||||
return drawMessageToolSimple({ ...arguments[0], code: "WWW" });
|
||||
} else if (kvps._tool_name.startsWith("memory_")) {
|
||||
return drawMessageToolSimple({ ...arguments[0], code: "MEM" });
|
||||
} else {
|
||||
return drawMessageToolSimple({ ...arguments[0] });
|
||||
}
|
||||
|
||||
/** @type {{ tool_name: string, kvps: any, handler: Function | undefined }} */
|
||||
const extData = {
|
||||
tool_name,
|
||||
kvps,
|
||||
handler: undefined,
|
||||
};
|
||||
await callJsExtensions("get_tool_message_handler", extData);
|
||||
if (typeof extData.handler === "function") {
|
||||
return extData.handler(arguments[0]);
|
||||
}
|
||||
return drawMessageToolSimple({ ...arguments[0] });
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1204,48 +1210,6 @@ export function drawMessageToolSimple({
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MessageHandlerArgs & Record<string, any>} param0
|
||||
* @returns {MessageHandlerResult}
|
||||
*/
|
||||
export function drawMessageBrowser({
|
||||
id,
|
||||
type,
|
||||
heading,
|
||||
content,
|
||||
kvps,
|
||||
timestamp,
|
||||
agentno = 0,
|
||||
...additional
|
||||
}) {
|
||||
const title = cleanStepTitle(heading);
|
||||
let displayKvps = { ...kvps };
|
||||
const answerText = String(kvps?.answer ?? "");
|
||||
const actionButtons = answerText.trim()
|
||||
? [
|
||||
createActionButton("detail", "", () =>
|
||||
stepDetailStore.showStepDetail(
|
||||
buildDetailPayload(arguments[0], { headerLabels: [] }),
|
||||
),
|
||||
),
|
||||
createActionButton("speak", "", () => speechStore.speak(answerText)),
|
||||
createActionButton("copy", "", () => copyToClipboard(answerText)),
|
||||
].filter(Boolean)
|
||||
: [];
|
||||
|
||||
return drawProcessStep({
|
||||
id,
|
||||
title,
|
||||
code: "WWW",
|
||||
classes: undefined,
|
||||
kvps: displayKvps,
|
||||
content,
|
||||
// contentClasses: [],
|
||||
actionButtons,
|
||||
log: arguments[0],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MessageHandlerArgs & Record<string, any>} param0
|
||||
* @returns {MessageHandlerResult}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 104 50" width="104" height="50"><style>.a{fill:#7a7a7a}</style><path class="a" d="m31.9 40.7c-4.1-6.9-8.1-13.8-12.2-20.9-4.1 7.1-8.1 14-12.2 20.9h-6.2c6.1-10.5 18.4-31.4 18.4-31.4 0 0 12.3 20.8 18.4 31.4 0 0-6.2 0-6.2 0z"/><path class="a" d="m27.5 40.7h-15.8q1.6-2.7 3.1-5.3h9.7c1 1.7 2 3.5 3 5.3zm52.3-6.5c0.6 1 1.2 1.9 1.9 2.9h-2.1c-0.6-0.8-1.2-1.7-1.8-2.5h-2.4v2.5h-1.6v-8.6q1.4 0 2.8 0c0.7 0 1.5-0.1 2.3 0 1.2 0.2 2.1 0.9 2.4 2.1 0.2 1.3 0 2.5-1.2 3.4-0.1 0.1-0.2 0.1-0.3 0.2q0 0 0 0zm-4.4-1.3c1.1 0 2.2 0.1 3.2 0 0.7 0 1-0.6 1-1.3 0.1-0.7-0.3-1.3-1-1.3-1-0.1-2.1 0-3.2 0zm-22.1-4.5v1.3c-1.7 1.8-3.4 3.7-5.2 5.7h5.1v1.6h-7.3v-1.7c1.5-1.6 3-3.3 4.7-5.2h-4.7v-1.7zm29.2-15.7c2.1 2.1 4 4 6.1 6v-4.9h1.5v8.7c-2.1-2-4-3.9-6.1-6v5.2h-1.5v-9q0 0 0 0zm6.3 22.2c0.4-0.2 1-0.5 1.4-0.7 1.4 1.8 3.4 1.4 4.3 0.6 0.9-0.8 1.1-1.9 0.8-3.1-0.4-1.1-1.4-1.9-2.8-1.9-1.4 0.1-2.6 1.1-2.7 2.7h-1.7c0-1.6 1.2-3.5 3.1-4 2.3-0.7 4.6 0.4 5.5 2.7 0.8 2-0.2 4.4-2.3 5.4-2 1-4.5 0.3-5.6-1.7q0 0 0 0zm-26.2-19.9c-0.3 0.2-0.8 0.5-1.1 0.8-1.8-1.5-3.8-1-4.6 0.9-0.4 1.2 0.2 2.8 1.6 3.3 1.2 0.4 2.5 0 2.8-1-0.3-0.1-0.5-0.2-0.9-0.3 0-0.4-0.1-0.8-0.1-1.3h3.1c0.3 1.8-1.2 3.8-3 4.2-2 0.5-4.2-0.6-4.9-2.5-0.8-2 0.1-4.2 2-5.2 1.8-0.9 4-0.5 5.1 1.1zm-17.1-1.8c1.6 3 3 5.7 4.5 8.6h-1.7c-0.9-1.6-1.8-3.3-2.8-5-0.9 1.7-1.8 3.3-2.7 5h-1.8c1.5-2.8 3-5.6 4.5-8.6zm52.9 2h-2.9v-1.5h7.2v1.5h-2.7v6.6h-1.6v-6.6zm-36.3 14.8v2.1h2c0 0 0 1 0 1.6-0.8 0.1-1.7 0-2.5-0.1-0.7-0.2-1.2-0.8-1.2-1.5 0-1.2 0-2.5 0-3.7h7v1.6h-5.3q0 0 0 0zm-1.7 7v-2.2q0.7 0 1.3-0.1c0.1 0.3 0.1 0.7 0.3 0.7 0.3 0 3.5 0 5.3 0v1.6c0 0-6.9 0-6.9 0zm11-21.8v2h1.9c0 0 0 0.9 0 1.5-0.8 0-1.6 0-2.3-0.2-0.7-0.1-1.1-0.6-1.1-1.3-0.1-1.1-0.1-2.3-0.1-3.4h6.5v1.4h-4.9zm-1.5 6.5v-2q0.6 0 1.2-0.1c0.1 0.2 0.1 0.6 0.3 0.6 0.2 0 3.2 0 4.9 0v1.5h-6.4z"/></svg>
|
||||
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 163 26" width="163" height="26"><style>.a{fill:#7a7a7a}</style><path class="a" d="m138.7 15.3c0.8 1.2 1.6 2.4 2.5 3.7h-2.7c-0.8-1-1.6-2.2-2.4-3.3h-3.1v3.3h-2.1v-11.3q1.8 0 3.6 0c1 0 2-0.1 3 0.1 1.6 0.2 2.8 1.1 3.2 2.7 0.3 1.7 0 3.3-1.5 4.4-0.2 0.1-0.3 0.2-0.5 0.4zm-5.7-1.7c1.5 0 2.8 0 4.2-0.1 0.9 0 1.3-0.7 1.3-1.6 0-1-0.4-1.7-1.3-1.8-1.4-0.2-2.8-0.1-4.2-0.1zm-29-5.8v1.6c-2.2 2.5-4.4 5-6.7 7.6h6.7v2.1h-9.6v-2.2c2-2.2 4-4.4 6.2-6.9h-6.2v-2.2zm-48.2-0.7c2.8 2.7 5.3 5.2 8 7.9v-6.5h2v11.4c-2.7-2.6-5.2-5.1-8-7.8v6.8h-2zm94.6 9.3c0.6-0.3 1.4-0.7 1.9-0.9 1.7 2.3 4.4 1.8 5.6 0.7 1.1-1 1.5-2.5 1-4-0.5-1.4-1.8-2.6-3.6-2.5-1.8 0.1-3.4 1.4-3.6 3.5h-2.1c0-2.1 1.5-4.6 3.9-5.3 3.1-0.9 6.1 0.6 7.3 3.5 1 2.8-0.2 5.9-3 7.2-2.6 1.3-6 0.3-7.4-2.2zm-120.5-6.3c-0.5 0.3-1.1 0.7-1.5 1-2.3-1.9-5-1.3-6 1.2-0.5 1.6 0.3 3.7 2.1 4.3 1.5 0.6 3.2 0 3.6-1.3-0.3-0.1-0.7-0.2-1.1-0.3-0.1-0.6-0.1-1.1-0.2-1.8h4.1c0.3 2.4-1.6 5-4 5.6-2.6 0.7-5.4-0.8-6.4-3.4-0.9-2.5 0.2-5.5 2.6-6.7 2.4-1.3 5.4-0.7 6.8 1.4zm-22.4-2.3c2 3.8 3.9 7.5 5.9 11.2h-2.3q-1.8-3.2-3.6-6.6c-1.3 2.3-2.4 4.4-3.6 6.6h-2.3c2-3.7 3.9-7.4 5.9-11.2zm69.1 2.5h-3.8v-1.9h9.5v1.9h-3.6v8.7h-2.1zm38.9-0.4v2.7l2.7 0.1c0 0 0 1.2 0 2.1-1.1 0-2.3 0-3.3-0.2-0.9-0.2-1.6-1-1.6-2-0.1-1.5 0-3.2 0-4.8h9.1v2.1zm-2.2 9.2v-2.9c0.5 0 1.1-0.1 1.7-0.1 0.1 0.3 0.1 0.9 0.4 0.9 0.3 0 4.5 0 6.9 0v2.1zm-71.9-8.7v2.6h2.5c0 0 0 1.2 0 2-1 0-2.1 0-3.1-0.2-0.8-0.2-1.4-0.9-1.5-1.8q0-2.2 0-4.5h8.5v1.9zm-2.1 8.5v-2.6c0.6-0.1 1.1-0.1 1.6-0.1 0.2 0.3 0.2 0.8 0.4 0.8 0.3 0 4.2 0 6.4 0v1.9z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.5 KiB |
Loading…
Add table
Add a link
Reference in a new issue