metadata + cleanup some actions

This commit is contained in:
STJ 2026-03-13 14:38:41 -07:00
parent 9836e3fc84
commit d9d6fb2e42
7 changed files with 87 additions and 122 deletions

View file

@ -33,7 +33,7 @@ RUN apt-get update && \
net-tools dnsutils whois \
jq parallel ripgrep grep \
less man-db procps htop \
iproute2 iputils-ping netcat-traditional socat \
iproute2 iputils-ping netcat-traditional \
nmap ncat ndiff \
sqlmap nuclei subfinder naabu ffuf \
nodejs npm pipx \

View file

@ -387,9 +387,10 @@ class BrowserRenderer(BaseToolRenderer):
@classmethod
def _build_fallback(cls, action: str, status: str) -> Text:
if not action:
return Text()
text = Text("@ ", style=cls.DIM)
if action:
text.append(action, style=cls.DIM)
text.append(action, style=cls.DIM)
text.append_text(cls._status_mark(status))
return text

View file

@ -247,6 +247,7 @@ class LLM:
"messages": messages,
"timeout": self.config.timeout,
"stream_options": {"include_usage": True},
"metadata": {"litellm_session_id": self.agent_id},
}
if self.config.api_key:

View file

@ -2,6 +2,7 @@ import asyncio
import json
import logging
import re
from functools import partial
from typing import Any, Literal
from browser_use import Agent
@ -26,7 +27,6 @@ BrowserUseLocalAction = Literal[
"launch",
"run",
"close_browser",
"search",
"navigate",
"go_back",
"wait",
@ -46,10 +46,6 @@ BrowserUseLocalAction = Literal[
"evaluate",
"switch",
"close_tab",
"write_file",
"read_file",
"replace_file",
"read_long_content",
"done",
]
@ -69,7 +65,7 @@ def _is_ws_error(exc: BaseException) -> bool:
return any(kw in msg for kw in _WS_ERRORS)
def _build_llm() -> Any:
def _build_llm(metadata: dict[str, Any] | None = None) -> Any:
from strix.config.config import resolve_llm_config
from .litellm.chat import ChatLiteLLM
@ -82,6 +78,7 @@ def _build_llm() -> Any:
model=model,
api_key=api_key,
api_base=api_base,
metadata=metadata,
)
@ -135,8 +132,9 @@ async def _run_browser_agent(
session: BrowserSession,
task: str,
return_fields: list[str] | None,
metadata: dict[str, Any] | None = None,
) -> dict[str, Any]:
llm = _build_llm()
llm = _build_llm(metadata=metadata)
agent: Any = Agent(
task=task,
llm=llm,
@ -148,35 +146,26 @@ async def _run_browser_agent(
async def log_step(step: Any) -> None:
logger.info("Agent step completed: %s", step)
try:
result = await agent.run(on_step_end=log_step)
if hasattr(result, "is_successful") and not result.is_successful():
final_result = (
result.final_result() if callable(result.final_result) else result.final_result
)
return {"error": final_result or "Agent failed", "is_running": False}
result = await agent.run(on_step_end=log_step)
if hasattr(result, "is_successful") and not result.is_successful():
final_result = (
result.final_result()
if hasattr(result, "final_result") and callable(result.final_result)
else getattr(result, "final_result", str(result))
result.final_result() if callable(result.final_result) else result.final_result
)
out = {"message": "Task completed", "result": final_result, "is_running": False}
return {"error": final_result or "Agent failed", "is_running": False}
if return_fields:
fields = {f: getattr(result, f, None) for f in return_fields}
out["fields"] = fields
final_result = (
result.final_result()
if hasattr(result, "final_result") and callable(result.final_result)
else getattr(result, "final_result", str(result))
)
out = {"message": "Task completed", "result": final_result, "is_running": False}
return out
finally:
for name in ("close", "stop"):
if fn := getattr(agent, name, None):
try:
if asyncio.iscoroutine(coro := fn()):
await asyncio.wait_for(coro, timeout=5)
except Exception: # noqa: BLE001,S110
pass
if return_fields:
fields = {f: getattr(result, f, None) for f in return_fields}
out["fields"] = fields
return out
def _fix_json_in_xml(kws: dict[str, Any]) -> dict[str, Any]:
@ -198,11 +187,12 @@ async def _run_browser_tool(
session: BrowserSession,
action: str,
params: dict[str, Any],
metadata: dict[str, Any] | None = None,
) -> Any:
if not session.browser.is_cdp_connected:
await session.browser.start()
llm = _build_llm()
llm = _build_llm(metadata=metadata)
if session.local:
from pathlib import Path
@ -234,6 +224,43 @@ async def _run_browser_tool(
)
async def populate_response(session: BrowserSession, response: dict[str, Any]) -> dict[str, Any]:
try:
screenshot = await session.browser.take_screenshot()
except Exception as e: # noqa: BLE001
screenshot = None
response["screenshot_error"] = str(e)
try:
title = await session.browser.get_current_page_title()
url = await session.browser.get_current_page_url()
all_tabs = await session.browser.get_tabs()
except Exception as e: # noqa: BLE001
# TODO: Consider a fallback way to retrieve the url?
url = f"URL retrieval failed: {e}"
title = "Title retrieval failed with same error"
all_tabs = []
try:
# this can fail if the browser disconnects during the tool completion
vp = getattr(session.browser.browser_profile, "viewport", None)
viewport = {
"width": vp.width if vp else None,
"height": vp.height if vp else None,
}
except Exception as e: # noqa: BLE001
viewport = {"error": f"Viewport retrieval failed: {e}"}
return {
**response,
"screenshot": screenshot,
"url": url,
"title": title,
"viewport": viewport,
"tabs": all_tabs,
}
@register_tool(sandbox_execution=False)
async def browser_actions(
action: BrowserUseLocalAction,
@ -249,6 +276,8 @@ async def browser_actions(
agent_id = get_current_agent_id()
metadata = {"litellm_session_id": agent_id}
if action == "launch":
has_sandbox = agent_state and getattr(agent_state, "sandbox_info", None)
if not has_sandbox:
@ -285,18 +314,19 @@ async def browser_actions(
if not task:
return {"error": "task required for run action", "is_running": False}
return await _execute_task(
session,
lambda: _run_browser_agent(session, task, return_fields),
task,
)
runner = partial(_run_browser_agent, session, task, return_fields, metadata)
desc = task
else:
params = _fix_json_in_xml(kwargs)
runner = partial(_run_browser_tool, session, action, params, metadata)
desc = f"{action}({list(kwargs.keys())[:3]})"
params = _fix_json_in_xml(kwargs)
return await _execute_task(
session,
lambda: _run_browser_tool(session, action, params),
f"{action}({list(params.keys())[:3]})",
)
task_output = await _execute_task(session, runner, desc)
if "error" in task_output:
return task_output
return await populate_response(session, task_output)
except Exception as error:
logger.exception("browser_actions error: %s", action)

View file

@ -25,7 +25,6 @@
Optionally pass 'return_fields' to select which history data to include in the response.
**Navigation & Browser Control:**
- search: Search queries (DuckDuckGo, Google, Bing). Requires 'query', optional 'engine'.
- navigate: Navigate to URLs. Requires 'url', optional 'new_tab'.
- go_back: Go back in browser history.
- wait: Wait for specified seconds. Optional 'seconds'.
@ -58,12 +57,6 @@
- switch: Switch between tabs. Requires 'tab_id'.
- close_tab: Close browser tab. Requires 'tab_id'.
**File Operations:** [Requires to be running in non-sandbox environment]
- write_file: Write content to files. Requires 'file_name' and 'content', optional 'append', 'trailing_newline', 'leading_newline'.
- read_file: Read file contents. Requires 'file_name'.
- replace_file: Replace text in files. Requires 'file_name', 'old_str', 'new_str'.
- read_long_content: Read long content intelligently. Requires 'goal', optional 'source', 'context'.
**Task Completion:**
- done: Complete the task. Requires 'text', optional 'success', 'files_to_display'.</description>
</parameter>
@ -76,10 +69,7 @@
directory to use, e.g. "Default", "Profile 1". If not specified, auto-selected.</description>
</parameter>
<parameter name="query" type="string" required="false">
<description>Search query. Required for 'search' and 'extract' actions.</description>
</parameter>
<parameter name="engine" type="string" required="false">
<description>Search engine for 'search' action. Options: "google", "duckduckgo", "bing".</description>
<description>Query string. Required for 'extract' action.</description>
</parameter>
<parameter name="url" type="string" required="false">
<description>URL to navigate to. Required for 'navigate' action.</description>
@ -168,33 +158,6 @@
<parameter name="tab_id" type="string" required="false">
<description>Tab ID for 'switch' and 'close_tab' actions. Last 4 chars of target_id from browser state.</description>
</parameter>
<parameter name="content" type="string" required="false">
<description>File content for 'write_file' action.</description>
</parameter>
<parameter name="append" type="boolean" required="false">
<description>Append to file instead of overwriting for 'write_file' action. Default: false.</description>
</parameter>
<parameter name="trailing_newline" type="boolean" required="false">
<description>Add trailing newline for 'write_file' action.</description>
</parameter>
<parameter name="leading_newline" type="boolean" required="false">
<description>Add leading newline for 'write_file' action.</description>
</parameter>
<parameter name="old_str" type="string" required="false">
<description>String to replace for 'replace_file' action.</description>
</parameter>
<parameter name="new_str" type="string" required="false">
<description>Replacement string for 'replace_file' action.</description>
</parameter>
<parameter name="goal" type="string" required="false">
<description>Information goal for 'read_long_content' action.</description>
</parameter>
<parameter name="source" type="string" required="false">
<description>Content source for 'read_long_content' action. Options: "page", file path.</description>
</parameter>
<parameter name="context" type="string" required="false">
<description>Additional context for 'read_long_content' action.</description>
</parameter>
<parameter name="success" type="boolean" required="false">
<description>Task success status for 'done' action.</description>
</parameter>
@ -259,12 +222,6 @@
<parameter=task>Go to https://example.com/login, fill in username "admin" and password "secret", then click the login button</parameter>
</function>
# Search with DuckDuckGo
<function=browser_actions>
<parameter=action>search</parameter>
<parameter=query>Python web scraping</parameter>
</function>
# Navigate to a URL
<function=browser_actions>
<parameter=action>navigate</parameter>
@ -387,27 +344,6 @@
<parameter=tab_id>a3f2</parameter>
</function>
# Write content to file
<function=browser_actions>
<parameter=action>write_file</parameter>
<parameter=file_name>results.txt</parameter>
<parameter=content>Extracted data here</parameter>
</function>
# Read a file
<function=browser_actions>
<parameter=action>read_file</parameter>
<parameter=file_name>data.json</parameter>
</function>
# Replace text in file
<function=browser_actions>
<parameter=action>replace_file</parameter>
<parameter=file_name>config.txt</parameter>
<parameter=old_str>old_value</parameter>
<parameter=new_str>new_value</parameter>
</function>
# Complete the task
<function=browser_actions>
<parameter=action>done</parameter>

View file

@ -5,6 +5,8 @@ import logging
import time
from typing import Any
from browser_use import Browser
logger = logging.getLogger(__name__)
@ -79,7 +81,7 @@ class BrowserSession:
def __init__(
self,
browser: Any,
browser: Browser,
cdp_url: str,
ws_url: str,
*,

View file

@ -43,17 +43,10 @@ class ChatLiteLLM(BaseChatModel):
temperature: float | None = 0.0
max_tokens: int | None = 4096
max_retries: int = 3
metadata: dict[str, Any] | None = None
_provider_name: str = field(
default="",
init=False,
repr=False,
)
_clean_model: str = field(
default="",
init=False,
repr=False,
)
_provider_name: str = field(default="", init=False, repr=False)
_clean_model: str = field(default="", init=False, repr=False)
def __post_init__(self) -> None:
"""Resolve provider info from the model string via litellm."""
@ -163,6 +156,8 @@ class ChatLiteLLM(BaseChatModel):
params["api_key"] = self.api_key
if self.api_base:
params["api_base"] = self.api_base
if self.metadata:
params["metadata"] = self.metadata
if output_format is not None:
schema = SchemaOptimizer.create_optimized_json_schema(output_format)