browser-use upgrade

frdel committed Aug 26, 2025 at 10:38 UTC f3401076cb9e511f4f33be8f9bb010ce73bdef01
4 files changed +169 -55
agent.py
+4
@@ -335,6 +335,7 @@ class Agent:
335 await self.call_extensions("before_main_llm_call", loop_data=self.loop_data)
336
337 async def reasoning_callback(chunk: str, full: str):
338 + await self.handle_intervention()
339 if chunk == full:
340 printer.print("Reasoning: ") # start of reasoning
341 # Pass chunk and full data to extensions for processing
@@ -349,6 +350,7 @@ class Agent:
350 await self.handle_reasoning_stream(stream_data["full"])
351
352 async def stream_callback(chunk: str, full: str):
353 + await self.handle_intervention()
354 # output the agent response stream
355 if chunk == full:
356 printer.print("Response: ") # start of response
@@ -804,6 +806,7 @@ class Agent:
806 )
807
808 async def handle_reasoning_stream(self, stream: str):
809 + await self.handle_intervention()
810 await self.call_extensions(
811 "reasoning_stream",
812 loop_data=self.loop_data,
@@ -811,6 +814,7 @@ class Agent:
814 )
815
816 async def handle_response_stream(self, stream: str):
817 + await self.handle_intervention()
818 try:
819 if len(stream) < 25:
820 return # no reason to try
models.py
+158 -50
@@ -23,6 +23,7 @@ from python.helpers.dotenv import load_dotenv
23 from python.helpers.providers import get_provider_config
24 from python.helpers.rate_limiter import RateLimiter
25 from python.helpers.tokens import approximate_tokens
26 +from python.helpers import dirty_json
27
28 from langchain_core.language_models.chat_models import SimpleChatModel
29 from langchain_core.outputs.chat_generation import ChatGenerationChunk
@@ -90,6 +91,7 @@ class ChatChunk(TypedDict):
91 rate_limiters: dict[str, RateLimiter] = {}
92 api_keys_round_robin: dict[str, int] = {}
93
94 +
95 def get_api_key(service: str) -> str:
96 # get api key for the service
97 key = (
@@ -116,7 +118,14 @@ def get_rate_limiter(
118 limiter.limits["output"] = output or 0
119 return limiter
120
119 -async def apply_rate_limiter(model_config: ModelConfig|None, input_text: str, rate_limiter_callback: Callable[[str, str, int, int], Awaitable[bool]] | None = None):
121 +
122 +async def apply_rate_limiter(
123 + model_config: ModelConfig | None,
124 + input_text: str,
125 + rate_limiter_callback: (
126 + Callable[[str, str, int, int], Awaitable[bool]] | None
127 + ) = None,
128 +):
129 if not model_config:
130 return
131 limiter = get_rate_limiter(
@@ -131,25 +140,41 @@ async def apply_rate_limiter(model_config: ModelConfig|None, input_text: str, ra
140 await limiter.wait(rate_limiter_callback)
141 return limiter
142
134 -def apply_rate_limiter_sync(model_config: ModelConfig|None, input_text: str, rate_limiter_callback: Callable[[str, str, int, int], Awaitable[bool]] | None = None):
143 +
144 +def apply_rate_limiter_sync(
145 + model_config: ModelConfig | None,
146 + input_text: str,
147 + rate_limiter_callback: (
148 + Callable[[str, str, int, int], Awaitable[bool]] | None
149 + ) = None,
150 +):
151 if not model_config:
152 return
153 import asyncio, nest_asyncio
154 +
155 nest_asyncio.apply()
139 - return asyncio.run(apply_rate_limiter(model_config, input_text, rate_limiter_callback))
156 + return asyncio.run(
157 + apply_rate_limiter(model_config, input_text, rate_limiter_callback)
158 + )
159
160
161 class LiteLLMChatWrapper(SimpleChatModel):
162 model_name: str
163 provider: str
164 kwargs: dict = {}
146 -
165 +
166 class Config:
167 arbitrary_types_allowed = True
168 extra = "allow" # Allow extra attributes
169 validate_assignment = False # Don't validate on assignment
170
152 - def __init__(self, model: str, provider: str, model_config: Optional[ModelConfig] = None, **kwargs: Any):
171 + def __init__(
172 + self,
173 + model: str,
174 + provider: str,
175 + model_config: Optional[ModelConfig] = None,
176 + **kwargs: Any,
177 + ):
178 model_value = f"{provider}/{model}"
179 super().__init__(model_name=model_value, provider=provider, kwargs=kwargs) # type: ignore
180 # Set A0 model config as instance attribute after parent init
@@ -158,7 +183,7 @@ class LiteLLMChatWrapper(SimpleChatModel):
183 @property
184 def _llm_type(self) -> str:
185 return "litellm-chat"
161 -
186 +
187 def _convert_messages(self, messages: List[BaseMessage]) -> List[dict]:
188 result = []
189 # Map LangChain message types to LiteLLM roles
@@ -169,7 +194,9 @@ class LiteLLMChatWrapper(SimpleChatModel):
194 "tool": "tool",
195 }
196 for m in messages:
172 - role = role_mapping.get(m.type, m.type)
197 + m_type = getattr(m, "type", getattr(m, "role", ""))
198 + role = role_mapping.get(m_type, m_type)
199 + content = getattr(m, "content", getattr(m, "text", ""))
200 message_dict = {"role": role, "content": m.content}
201
202 # Handle tool calls for AI messages
@@ -215,12 +242,12 @@ class LiteLLMChatWrapper(SimpleChatModel):
242 **kwargs: Any,
243 ) -> str:
244 import asyncio
218 -
245 +
246 msgs = self._convert_messages(messages)
220 -
247 +
248 # Apply rate limiting if configured
249 apply_rate_limiter_sync(self.a0_model_conf, str(msgs))
223 -
250 +
251 # Call the model
252 resp = completion(
253 model=self.model_name, messages=msgs, stop=stop, **{**self.kwargs, **kwargs}
@@ -238,12 +265,12 @@ class LiteLLMChatWrapper(SimpleChatModel):
265 **kwargs: Any,
266 ) -> Iterator[ChatGenerationChunk]:
267 import asyncio
241 -
268 +
269 msgs = self._convert_messages(messages)
243 -
270 +
271 # Apply rate limiting if configured
272 apply_rate_limiter_sync(self.a0_model_conf, str(msgs))
246 -
273 +
274 for chunk in completion(
275 model=self.model_name,
276 messages=msgs,
@@ -266,11 +293,10 @@ class LiteLLMChatWrapper(SimpleChatModel):
293 **kwargs: Any,
294 ) -> AsyncIterator[ChatGenerationChunk]:
295 msgs = self._convert_messages(messages)
269 -
296 +
297 # Apply rate limiting if configured
298 await apply_rate_limiter(self.a0_model_conf, str(msgs))
272 -
273 -
299 +
300 response = await acompletion(
301 model=self.model_name,
302 messages=msgs,
@@ -294,7 +320,9 @@ class LiteLLMChatWrapper(SimpleChatModel):
320 response_callback: Callable[[str, str], Awaitable[None]] | None = None,
321 reasoning_callback: Callable[[str, str], Awaitable[None]] | None = None,
322 tokens_callback: Callable[[str, int], Awaitable[None]] | None = None,
297 - rate_limiter_callback: Callable[[str, str, int, int], Awaitable[bool]] | None = None,
323 + rate_limiter_callback: (
324 + Callable[[str, str, int, int], Awaitable[bool]] | None
325 + ) = None,
326 **kwargs: Any,
327 ) -> Tuple[str, str]:
328
@@ -312,7 +340,9 @@ class LiteLLMChatWrapper(SimpleChatModel):
340 msgs_conv = self._convert_messages(messages)
341
342 # Apply rate limiting if configured
315 - limiter = await apply_rate_limiter(self.a0_model_conf, str(msgs_conv), rate_limiter_callback)
343 + limiter = await apply_rate_limiter(
344 + self.a0_model_conf, str(msgs_conv), rate_limiter_callback
345 + )
346
347 # call model
348 _completion = await acompletion(
@@ -360,7 +390,27 @@ class LiteLLMChatWrapper(SimpleChatModel):
390 return response, reasoning
391
392
363 -class BrowserCompatibleChatWrapper(LiteLLMChatWrapper):
393 +class AsyncAIChatReplacement:
394 + class _Completions:
395 + def __init__(self, wrapper):
396 + self._wrapper = wrapper
397 +
398 + async def create(self, *args, **kwargs):
399 + # call the async _acall method on the wrapper
400 + return await self._wrapper._acall(*args, **kwargs)
401 +
402 + class _Chat:
403 + def __init__(self, wrapper):
404 + self.completions = AsyncAIChatReplacement._Completions(wrapper)
405 +
406 + def __init__(self, wrapper, *args, **kwargs):
407 + self._wrapper = wrapper
408 + self.chat = AsyncAIChatReplacement._Chat(wrapper)
409 +
410 +
411 +from browser_use.llm import ChatOllama, ChatOpenRouter, ChatGoogle, ChatAnthropic, ChatGroq, ChatOpenAI
412 +
413 +class BrowserCompatibleChatWrapper(ChatOpenRouter):
414 """
415 A wrapper for browser agent that can filter/sanitize messages
416 before sending them to the LLM.
@@ -368,31 +418,61 @@ class BrowserCompatibleChatWrapper(LiteLLMChatWrapper):
418
419 def __init__(self, *args, **kwargs):
420 turn_off_logging()
371 - super().__init__(*args, **kwargs)
421 + # Create the underlying LiteLLM wrapper
422 + self._wrapper = LiteLLMChatWrapper(*args, **kwargs)
423 # Browser-use may expect a 'model' attribute
373 - self.model = self.model_name
424 + self.model = self._wrapper.model_name
425 + self.kwargs = self._wrapper.kwargs
426
375 - def _call(
427 + @property
428 + def model_name(self) -> str:
429 + return self._wrapper.model_name
430 +
431 + @property
432 + def provider(self) -> str:
433 + return self._wrapper.provider
434 +
435 + def get_client(self, *args, **kwargs): # type: ignore
436 + return AsyncAIChatReplacement(self, *args, **kwargs)
437 +
438 + async def _acall(
439 self,
440 messages: List[BaseMessage],
441 stop: Optional[List[str]] = None,
442 run_manager: Optional[CallbackManagerForLLMRun] = None,
443 **kwargs: Any,
381 - ) -> str:
382 - turn_off_logging()
383 - result = super()._call(messages, stop, run_manager, **kwargs)
384 - return result
444 + ):
445 + # Apply rate limiting if configured
446 + apply_rate_limiter_sync(self._wrapper.a0_model_conf, str(messages))
447
386 - async def _astream(
387 - self,
388 - messages: List[BaseMessage],
389 - stop: Optional[List[str]] = None,
390 - run_manager: Optional[AsyncCallbackManagerForLLMRun] = None,
391 - **kwargs: Any,
392 - ) -> AsyncIterator[ChatGenerationChunk]:
393 - turn_off_logging()
394 - async for chunk in super()._astream(messages, stop, run_manager, **kwargs):
395 - yield chunk
448 + # Call the model
449 + try:
450 + model = kwargs.pop("model", None)
451 + kwrgs = {**self._wrapper.kwargs, **kwargs}
452 +
453 + # hack from browser-use to fix json schema for gemini
454 + if "response_format" in kwrgs and "json_schema" in kwrgs["response_format"] and model.startswith("gemini/"):
455 + kwrgs["response_format"]["json_schema"] = ChatGoogle("")._fix_gemini_schema(self._wrapper.kwargs)
456 +
457 + resp = await acompletion(
458 + model=self._wrapper.model_name,
459 + messages=messages,
460 + stop=stop,
461 + **kwrgs,
462 + )
463 + except Exception as e:
464 + raise e
465 +
466 + # another hack for browser-use post process invalid jsons
467 + try:
468 + if "response_format" in kwrgs and "json_schema" in kwrgs["response_format"] or "json_object" in kwrgs["response_format"]:
469 + if resp.choices[0].message.content is not None and not resp.choices[0].message.content.startswith("{"): # type: ignore
470 + js = dirty_json.parse(resp.choices[0].message.content) # type: ignore
471 + resp.choices[0].message.content = dirty_json.stringify(js) # type: ignore
472 + except Exception as e:
473 + pass
474 +
475 + return resp
476
477
478 class LiteLLMEmbeddingWrapper(Embeddings):
@@ -400,15 +480,21 @@ class LiteLLMEmbeddingWrapper(Embeddings):
480 kwargs: dict = {}
481 a0_model_conf: Optional[ModelConfig] = None
482
403 - def __init__(self, model: str, provider: str, model_config: Optional[ModelConfig] = None, **kwargs: Any):
483 + def __init__(
484 + self,
485 + model: str,
486 + provider: str,
487 + model_config: Optional[ModelConfig] = None,
488 + **kwargs: Any,
489 + ):
490 self.model_name = f"{provider}/{model}" if provider != "openai" else model
491 self.kwargs = kwargs
492 self.a0_model_conf = model_config
407 -
493 +
494 def embed_documents(self, texts: List[str]) -> List[List[float]]:
495 # Apply rate limiting if configured
496 apply_rate_limiter_sync(self.a0_model_conf, " ".join(texts))
411 -
497 +
498 resp = embedding(model=self.model_name, input=texts, **self.kwargs)
499 return [
500 item.get("embedding") if isinstance(item, dict) else item.embedding # type: ignore
@@ -418,7 +504,7 @@ class LiteLLMEmbeddingWrapper(Embeddings):
504 def embed_query(self, text: str) -> List[float]:
505 # Apply rate limiting if configured
506 apply_rate_limiter_sync(self.a0_model_conf, text)
421 -
507 +
508 resp = embedding(model=self.model_name, input=[text], **self.kwargs)
509 item = resp.data[0] # type: ignore
510 return item.get("embedding") if isinstance(item, dict) else item.embedding # type: ignore
@@ -427,7 +513,13 @@ class LiteLLMEmbeddingWrapper(Embeddings):
513 class LocalSentenceTransformerWrapper(Embeddings):
514 """Local wrapper for sentence-transformers models to avoid HuggingFace API calls"""
515
430 - def __init__(self, provider: str, model: str, model_config: Optional[ModelConfig] = None, **kwargs: Any):
516 + def __init__(
517 + self,
518 + provider: str,
519 + model: str,
520 + model_config: Optional[ModelConfig] = None,
521 + **kwargs: Any,
522 + ):
523 # Clean common user-input mistakes
524 model = model.strip().strip('"').strip("'")
525
@@ -449,18 +541,18 @@ class LocalSentenceTransformerWrapper(Embeddings):
541 self.model = SentenceTransformer(model, **st_kwargs)
542 self.model_name = model
543 self.a0_model_conf = model_config
452 -
544 +
545 def embed_documents(self, texts: List[str]) -> List[List[float]]:
546 # Apply rate limiting if configured
547 apply_rate_limiter_sync(self.a0_model_conf, " ".join(texts))
456 -
548 +
549 embeddings = self.model.encode(texts, convert_to_tensor=False) # type: ignore
550 return embeddings.tolist() if hasattr(embeddings, "tolist") else embeddings # type: ignore
551
552 def embed_query(self, text: str) -> List[float]:
553 # Apply rate limiting if configured
554 apply_rate_limiter_sync(self.a0_model_conf, text)
463 -
555 +
556 embedding = self.model.encode([text], convert_to_tensor=False) # type: ignore
557 result = (
558 embedding[0].tolist() if hasattr(embedding[0], "tolist") else embedding[0]
@@ -485,10 +577,17 @@ def _get_litellm_chat(
577 provider_name, model_name, kwargs = _adjust_call_args(
578 provider_name, model_name, kwargs
579 )
488 - return cls(provider=provider_name, model=model_name, model_config=model_config, **kwargs)
580 + return cls(
581 + provider=provider_name, model=model_name, model_config=model_config, **kwargs
582 + )
583
584
491 -def _get_litellm_embedding(model_name: str, provider_name: str, model_config: Optional[ModelConfig] = None, **kwargs: Any):
585 +def _get_litellm_embedding(
586 + model_name: str,
587 + provider_name: str,
588 + model_config: Optional[ModelConfig] = None,
589 + **kwargs: Any,
590 +):
591 # Check if this is a local sentence-transformers model
592 if provider_name == "huggingface" and model_name.startswith(
593 "sentence-transformers/"
@@ -498,7 +597,10 @@ def _get_litellm_embedding(model_name: str, provider_name: str, model_config: Op
597 provider_name, model_name, kwargs
598 )
599 return LocalSentenceTransformerWrapper(
501 - provider=provider_name, model=model_name, model_config=model_config, **kwargs
600 + provider=provider_name,
601 + model=model_name,
602 + model_config=model_config,
603 + **kwargs,
604 )
605
606 # use api key from kwargs or env
@@ -511,7 +613,9 @@ def _get_litellm_embedding(model_name: str, provider_name: str, model_config: Op
613 provider_name, model_name, kwargs = _adjust_call_args(
614 provider_name, model_name, kwargs
615 )
514 - return LiteLLMEmbeddingWrapper(model=model_name, provider=provider_name, model_config=model_config, **kwargs)
616 + return LiteLLMEmbeddingWrapper(
617 + model=model_name, provider=provider_name, model_config=model_config, **kwargs
618 + )
619
620
621 def _parse_chunk(chunk: Any) -> ChatChunk:
@@ -599,10 +703,14 @@ def _merge_provider_defaults(
703 return provider_name, kwargs
704
705
602 -def get_chat_model(provider: str, name: str, model_config: Optional[ModelConfig] = None, **kwargs: Any) -> LiteLLMChatWrapper:
706 +def get_chat_model(
707 + provider: str, name: str, model_config: Optional[ModelConfig] = None, **kwargs: Any
708 +) -> LiteLLMChatWrapper:
709 orig = provider.lower()
710 provider_name, kwargs = _merge_provider_defaults("chat", orig, kwargs)
605 - return _get_litellm_chat(LiteLLMChatWrapper, name, provider_name, model_config, **kwargs)
711 + return _get_litellm_chat(
712 + LiteLLMChatWrapper, name, provider_name, model_config, **kwargs
713 + )
714
715
716 def get_browser_model(
python/tools/browser_agent.py
+3 -1
@@ -48,6 +48,7 @@ class State:
48 accept_downloads=True,
49 downloads_dir=files.get_abs_path("tmp/downloads"),
50 downloads_path=files.get_abs_path("tmp/downloads"),
51 + allowed_domains=["*"],
52 executable_path=pw_binary,
53 keep_alive=True,
54 minimum_wait_page_load_time=1.0,
@@ -143,6 +144,7 @@ class State:
144 ),
145 controller=controller,
146 enable_memory=False, # Disable memory to avoid state conflicts
147 + llm_timeout=3000, # TODO rem
148 sensitive_data=cast(dict[str, str | dict[str, str]] | None, secrets_dict or {}), # Pass secrets
149 )
150 except Exception as e:
@@ -382,7 +384,7 @@ class BrowserAgent(Tool):
384 def get_use_agent_log(use_agent: browser_use.Agent | None):
385 result = ["🚦 Starting task"]
386 if use_agent:
385 - action_results = use_agent.state.history.action_results()
387 + action_results = use_agent.history.action_results() or []
388 short_log = []
389 for item in action_results:
390 # final results
requirements.txt
+4 -4
@@ -1,6 +1,6 @@
1 a2wsgi==1.10.8
2 ansio==0.0.1
3 -browser-use==0.2.5
3 +browser-use==0.5.11
4 docker==7.1.0
5 duckduckgo-search==6.1.12
6 faiss-cpu==1.11.0
@@ -19,11 +19,11 @@ langchain-unstructured[all-docs]==0.1.6
19 openai-whisper==20240930
20 lxml_html_clean==0.3.1
21 markdown==3.7
22 -mcp==1.9.0
22 +mcp==1.13.1
23 newspaper3k==0.2.8
24 paramiko==3.5.0
25 playwright==1.52.0
26 -pypdf==4.3.1
26 +pypdf==6.0.0
27 python-dotenv==1.1.0
28 pytz==2024.2
29 sentence-transformers==3.0.1
@@ -33,7 +33,7 @@ unstructured-client==0.31.0
33 webcolors==24.6.0
34 nest-asyncio==1.6.0
35 crontab==1.0.1
36 -litellm==1.76
36 +litellm==1.75.3
37 markdownify==1.1.0
38 pymupdf==1.25.3
39 pytesseract==0.3.13