aip-agents-binary 0.5.20__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aip_agents/__init__.py +65 -0
- aip_agents/a2a/__init__.py +19 -0
- aip_agents/a2a/server/__init__.py +10 -0
- aip_agents/a2a/server/base_executor.py +1086 -0
- aip_agents/a2a/server/google_adk_executor.py +198 -0
- aip_agents/a2a/server/langflow_executor.py +180 -0
- aip_agents/a2a/server/langgraph_executor.py +270 -0
- aip_agents/a2a/types.py +232 -0
- aip_agents/agent/__init__.py +27 -0
- aip_agents/agent/base_agent.py +970 -0
- aip_agents/agent/base_langgraph_agent.py +2942 -0
- aip_agents/agent/google_adk_agent.py +926 -0
- aip_agents/agent/google_adk_constants.py +6 -0
- aip_agents/agent/hitl/__init__.py +24 -0
- aip_agents/agent/hitl/config.py +28 -0
- aip_agents/agent/hitl/langgraph_hitl_mixin.py +515 -0
- aip_agents/agent/hitl/manager.py +532 -0
- aip_agents/agent/hitl/models.py +18 -0
- aip_agents/agent/hitl/prompt/__init__.py +9 -0
- aip_agents/agent/hitl/prompt/base.py +42 -0
- aip_agents/agent/hitl/prompt/deferred.py +73 -0
- aip_agents/agent/hitl/registry.py +149 -0
- aip_agents/agent/interface.py +138 -0
- aip_agents/agent/interfaces.py +65 -0
- aip_agents/agent/langflow_agent.py +464 -0
- aip_agents/agent/langgraph_memory_enhancer_agent.py +433 -0
- aip_agents/agent/langgraph_react_agent.py +2514 -0
- aip_agents/agent/system_instruction_context.py +34 -0
- aip_agents/clients/__init__.py +10 -0
- aip_agents/clients/langflow/__init__.py +10 -0
- aip_agents/clients/langflow/client.py +477 -0
- aip_agents/clients/langflow/types.py +18 -0
- aip_agents/constants.py +23 -0
- aip_agents/credentials/manager.py +132 -0
- aip_agents/examples/__init__.py +5 -0
- aip_agents/examples/compare_streaming_client.py +783 -0
- aip_agents/examples/compare_streaming_server.py +142 -0
- aip_agents/examples/demo_memory_recall.py +401 -0
- aip_agents/examples/hello_world_a2a_google_adk_client.py +49 -0
- aip_agents/examples/hello_world_a2a_google_adk_client_agent.py +48 -0
- aip_agents/examples/hello_world_a2a_google_adk_client_streaming.py +60 -0
- aip_agents/examples/hello_world_a2a_google_adk_server.py +79 -0
- aip_agents/examples/hello_world_a2a_langchain_client.py +39 -0
- aip_agents/examples/hello_world_a2a_langchain_client_agent.py +39 -0
- aip_agents/examples/hello_world_a2a_langchain_client_lm_invoker.py +37 -0
- aip_agents/examples/hello_world_a2a_langchain_client_streaming.py +41 -0
- aip_agents/examples/hello_world_a2a_langchain_reference_client_streaming.py +60 -0
- aip_agents/examples/hello_world_a2a_langchain_reference_server.py +105 -0
- aip_agents/examples/hello_world_a2a_langchain_server.py +79 -0
- aip_agents/examples/hello_world_a2a_langchain_server_lm_invoker.py +78 -0
- aip_agents/examples/hello_world_a2a_langflow_client.py +83 -0
- aip_agents/examples/hello_world_a2a_langflow_server.py +82 -0
- aip_agents/examples/hello_world_a2a_langgraph_artifact_client.py +73 -0
- aip_agents/examples/hello_world_a2a_langgraph_artifact_client_streaming.py +76 -0
- aip_agents/examples/hello_world_a2a_langgraph_artifact_server.py +92 -0
- aip_agents/examples/hello_world_a2a_langgraph_client.py +54 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_agent.py +54 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_agent_lm_invoker.py +32 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_streaming.py +50 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_streaming_lm_invoker.py +44 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_streaming_tool_streaming.py +92 -0
- aip_agents/examples/hello_world_a2a_langgraph_server.py +84 -0
- aip_agents/examples/hello_world_a2a_langgraph_server_lm_invoker.py +79 -0
- aip_agents/examples/hello_world_a2a_langgraph_server_tool_streaming.py +132 -0
- aip_agents/examples/hello_world_a2a_mcp_langgraph.py +196 -0
- aip_agents/examples/hello_world_a2a_three_level_agent_hierarchy_client.py +244 -0
- aip_agents/examples/hello_world_a2a_three_level_agent_hierarchy_server.py +251 -0
- aip_agents/examples/hello_world_a2a_with_metadata_langchain_client.py +57 -0
- aip_agents/examples/hello_world_a2a_with_metadata_langchain_server_lm_invoker.py +80 -0
- aip_agents/examples/hello_world_google_adk.py +41 -0
- aip_agents/examples/hello_world_google_adk_mcp_http.py +34 -0
- aip_agents/examples/hello_world_google_adk_mcp_http_stream.py +40 -0
- aip_agents/examples/hello_world_google_adk_mcp_sse.py +44 -0
- aip_agents/examples/hello_world_google_adk_mcp_sse_stream.py +48 -0
- aip_agents/examples/hello_world_google_adk_mcp_stdio.py +44 -0
- aip_agents/examples/hello_world_google_adk_mcp_stdio_stream.py +48 -0
- aip_agents/examples/hello_world_google_adk_stream.py +44 -0
- aip_agents/examples/hello_world_langchain.py +28 -0
- aip_agents/examples/hello_world_langchain_lm_invoker.py +15 -0
- aip_agents/examples/hello_world_langchain_mcp_http.py +34 -0
- aip_agents/examples/hello_world_langchain_mcp_http_interactive.py +130 -0
- aip_agents/examples/hello_world_langchain_mcp_http_stream.py +42 -0
- aip_agents/examples/hello_world_langchain_mcp_multi_server.py +155 -0
- aip_agents/examples/hello_world_langchain_mcp_sse.py +34 -0
- aip_agents/examples/hello_world_langchain_mcp_sse_stream.py +40 -0
- aip_agents/examples/hello_world_langchain_mcp_stdio.py +30 -0
- aip_agents/examples/hello_world_langchain_mcp_stdio_stream.py +41 -0
- aip_agents/examples/hello_world_langchain_stream.py +36 -0
- aip_agents/examples/hello_world_langchain_stream_lm_invoker.py +39 -0
- aip_agents/examples/hello_world_langflow_agent.py +163 -0
- aip_agents/examples/hello_world_langgraph.py +39 -0
- aip_agents/examples/hello_world_langgraph_bosa_twitter.py +41 -0
- aip_agents/examples/hello_world_langgraph_mcp_http.py +31 -0
- aip_agents/examples/hello_world_langgraph_mcp_http_stream.py +34 -0
- aip_agents/examples/hello_world_langgraph_mcp_sse.py +35 -0
- aip_agents/examples/hello_world_langgraph_mcp_sse_stream.py +50 -0
- aip_agents/examples/hello_world_langgraph_mcp_stdio.py +35 -0
- aip_agents/examples/hello_world_langgraph_mcp_stdio_stream.py +50 -0
- aip_agents/examples/hello_world_langgraph_stream.py +43 -0
- aip_agents/examples/hello_world_langgraph_stream_lm_invoker.py +37 -0
- aip_agents/examples/hello_world_model_switch_cli.py +210 -0
- aip_agents/examples/hello_world_multi_agent_adk.py +75 -0
- aip_agents/examples/hello_world_multi_agent_langchain.py +54 -0
- aip_agents/examples/hello_world_multi_agent_langgraph.py +66 -0
- aip_agents/examples/hello_world_multi_agent_langgraph_lm_invoker.py +69 -0
- aip_agents/examples/hello_world_pii_logger.py +21 -0
- aip_agents/examples/hello_world_sentry.py +133 -0
- aip_agents/examples/hello_world_step_limits.py +273 -0
- aip_agents/examples/hello_world_stock_a2a_server.py +103 -0
- aip_agents/examples/hello_world_tool_output_client.py +46 -0
- aip_agents/examples/hello_world_tool_output_server.py +114 -0
- aip_agents/examples/hitl_demo.py +724 -0
- aip_agents/examples/mcp_configs/configs.py +63 -0
- aip_agents/examples/mcp_servers/common.py +76 -0
- aip_agents/examples/mcp_servers/mcp_name.py +29 -0
- aip_agents/examples/mcp_servers/mcp_server_http.py +19 -0
- aip_agents/examples/mcp_servers/mcp_server_sse.py +19 -0
- aip_agents/examples/mcp_servers/mcp_server_stdio.py +19 -0
- aip_agents/examples/mcp_servers/mcp_time.py +10 -0
- aip_agents/examples/pii_demo_langgraph_client.py +69 -0
- aip_agents/examples/pii_demo_langgraph_server.py +126 -0
- aip_agents/examples/pii_demo_multi_agent_client.py +80 -0
- aip_agents/examples/pii_demo_multi_agent_server.py +247 -0
- aip_agents/examples/todolist_planning_a2a_langchain_client.py +70 -0
- aip_agents/examples/todolist_planning_a2a_langgraph_server.py +88 -0
- aip_agents/examples/tools/__init__.py +27 -0
- aip_agents/examples/tools/adk_arithmetic_tools.py +36 -0
- aip_agents/examples/tools/adk_weather_tool.py +60 -0
- aip_agents/examples/tools/data_generator_tool.py +103 -0
- aip_agents/examples/tools/data_visualization_tool.py +312 -0
- aip_agents/examples/tools/image_artifact_tool.py +136 -0
- aip_agents/examples/tools/langchain_arithmetic_tools.py +26 -0
- aip_agents/examples/tools/langchain_currency_exchange_tool.py +88 -0
- aip_agents/examples/tools/langchain_graph_artifact_tool.py +172 -0
- aip_agents/examples/tools/langchain_weather_tool.py +48 -0
- aip_agents/examples/tools/langgraph_streaming_tool.py +130 -0
- aip_agents/examples/tools/mock_retrieval_tool.py +56 -0
- aip_agents/examples/tools/pii_demo_tools.py +189 -0
- aip_agents/examples/tools/random_chart_tool.py +142 -0
- aip_agents/examples/tools/serper_tool.py +202 -0
- aip_agents/examples/tools/stock_tools.py +82 -0
- aip_agents/examples/tools/table_generator_tool.py +167 -0
- aip_agents/examples/tools/time_tool.py +82 -0
- aip_agents/examples/tools/weather_forecast_tool.py +38 -0
- aip_agents/executor/agent_executor.py +473 -0
- aip_agents/executor/base.py +48 -0
- aip_agents/mcp/__init__.py +1 -0
- aip_agents/mcp/client/__init__.py +14 -0
- aip_agents/mcp/client/base_mcp_client.py +369 -0
- aip_agents/mcp/client/connection_manager.py +193 -0
- aip_agents/mcp/client/google_adk/__init__.py +11 -0
- aip_agents/mcp/client/google_adk/client.py +381 -0
- aip_agents/mcp/client/langchain/__init__.py +11 -0
- aip_agents/mcp/client/langchain/client.py +265 -0
- aip_agents/mcp/client/persistent_session.py +359 -0
- aip_agents/mcp/client/session_pool.py +351 -0
- aip_agents/mcp/client/transports.py +215 -0
- aip_agents/mcp/utils/__init__.py +7 -0
- aip_agents/mcp/utils/config_validator.py +139 -0
- aip_agents/memory/__init__.py +14 -0
- aip_agents/memory/adapters/__init__.py +10 -0
- aip_agents/memory/adapters/base_adapter.py +717 -0
- aip_agents/memory/adapters/mem0.py +84 -0
- aip_agents/memory/base.py +84 -0
- aip_agents/memory/constants.py +49 -0
- aip_agents/memory/factory.py +86 -0
- aip_agents/memory/guidance.py +20 -0
- aip_agents/memory/simple_memory.py +47 -0
- aip_agents/middleware/__init__.py +17 -0
- aip_agents/middleware/base.py +88 -0
- aip_agents/middleware/manager.py +128 -0
- aip_agents/middleware/todolist.py +274 -0
- aip_agents/schema/__init__.py +69 -0
- aip_agents/schema/a2a.py +56 -0
- aip_agents/schema/agent.py +111 -0
- aip_agents/schema/hitl.py +157 -0
- aip_agents/schema/langgraph.py +37 -0
- aip_agents/schema/model_id.py +97 -0
- aip_agents/schema/step_limit.py +108 -0
- aip_agents/schema/storage.py +40 -0
- aip_agents/sentry/__init__.py +11 -0
- aip_agents/sentry/sentry.py +151 -0
- aip_agents/storage/__init__.py +41 -0
- aip_agents/storage/base.py +85 -0
- aip_agents/storage/clients/__init__.py +12 -0
- aip_agents/storage/clients/minio_client.py +318 -0
- aip_agents/storage/config.py +62 -0
- aip_agents/storage/providers/__init__.py +15 -0
- aip_agents/storage/providers/base.py +106 -0
- aip_agents/storage/providers/memory.py +114 -0
- aip_agents/storage/providers/object_storage.py +214 -0
- aip_agents/tools/__init__.py +33 -0
- aip_agents/tools/bosa_tools.py +105 -0
- aip_agents/tools/browser_use/__init__.py +82 -0
- aip_agents/tools/browser_use/action_parser.py +103 -0
- aip_agents/tools/browser_use/browser_use_tool.py +1112 -0
- aip_agents/tools/browser_use/llm_config.py +120 -0
- aip_agents/tools/browser_use/minio_storage.py +198 -0
- aip_agents/tools/browser_use/schemas.py +119 -0
- aip_agents/tools/browser_use/session.py +76 -0
- aip_agents/tools/browser_use/session_errors.py +132 -0
- aip_agents/tools/browser_use/steel_session_recording.py +317 -0
- aip_agents/tools/browser_use/streaming.py +813 -0
- aip_agents/tools/browser_use/structured_data_parser.py +257 -0
- aip_agents/tools/browser_use/structured_data_recovery.py +204 -0
- aip_agents/tools/browser_use/types.py +78 -0
- aip_agents/tools/code_sandbox/__init__.py +26 -0
- aip_agents/tools/code_sandbox/constant.py +13 -0
- aip_agents/tools/code_sandbox/e2b_cloud_sandbox_extended.py +257 -0
- aip_agents/tools/code_sandbox/e2b_sandbox_tool.py +411 -0
- aip_agents/tools/constants.py +165 -0
- aip_agents/tools/document_loader/__init__.py +44 -0
- aip_agents/tools/document_loader/base_reader.py +302 -0
- aip_agents/tools/document_loader/docx_reader_tool.py +68 -0
- aip_agents/tools/document_loader/excel_reader_tool.py +171 -0
- aip_agents/tools/document_loader/pdf_reader_tool.py +79 -0
- aip_agents/tools/document_loader/pdf_splitter.py +169 -0
- aip_agents/tools/gl_connector/__init__.py +5 -0
- aip_agents/tools/gl_connector/tool.py +351 -0
- aip_agents/tools/memory_search/__init__.py +22 -0
- aip_agents/tools/memory_search/base.py +200 -0
- aip_agents/tools/memory_search/mem0.py +258 -0
- aip_agents/tools/memory_search/schema.py +48 -0
- aip_agents/tools/memory_search_tool.py +26 -0
- aip_agents/tools/time_tool.py +117 -0
- aip_agents/tools/tool_config_injector.py +300 -0
- aip_agents/tools/web_search/__init__.py +15 -0
- aip_agents/tools/web_search/serper_tool.py +187 -0
- aip_agents/types/__init__.py +70 -0
- aip_agents/types/a2a_events.py +13 -0
- aip_agents/utils/__init__.py +79 -0
- aip_agents/utils/a2a_connector.py +1757 -0
- aip_agents/utils/artifact_helpers.py +502 -0
- aip_agents/utils/constants.py +22 -0
- aip_agents/utils/datetime/__init__.py +34 -0
- aip_agents/utils/datetime/normalization.py +231 -0
- aip_agents/utils/datetime/timezone.py +206 -0
- aip_agents/utils/env_loader.py +27 -0
- aip_agents/utils/event_handler_registry.py +58 -0
- aip_agents/utils/file_prompt_utils.py +176 -0
- aip_agents/utils/final_response_builder.py +211 -0
- aip_agents/utils/formatter_llm_client.py +231 -0
- aip_agents/utils/langgraph/__init__.py +19 -0
- aip_agents/utils/langgraph/converter.py +128 -0
- aip_agents/utils/langgraph/tool_managers/__init__.py +15 -0
- aip_agents/utils/langgraph/tool_managers/a2a_tool_manager.py +99 -0
- aip_agents/utils/langgraph/tool_managers/base_tool_manager.py +66 -0
- aip_agents/utils/langgraph/tool_managers/delegation_tool_manager.py +1071 -0
- aip_agents/utils/langgraph/tool_output_management.py +967 -0
- aip_agents/utils/logger.py +195 -0
- aip_agents/utils/metadata/__init__.py +27 -0
- aip_agents/utils/metadata/activity_metadata_helper.py +407 -0
- aip_agents/utils/metadata/activity_narrative/__init__.py +35 -0
- aip_agents/utils/metadata/activity_narrative/builder.py +817 -0
- aip_agents/utils/metadata/activity_narrative/constants.py +51 -0
- aip_agents/utils/metadata/activity_narrative/context.py +49 -0
- aip_agents/utils/metadata/activity_narrative/formatters.py +230 -0
- aip_agents/utils/metadata/activity_narrative/utils.py +35 -0
- aip_agents/utils/metadata/schemas/__init__.py +16 -0
- aip_agents/utils/metadata/schemas/activity_schema.py +29 -0
- aip_agents/utils/metadata/schemas/thinking_schema.py +31 -0
- aip_agents/utils/metadata/thinking_metadata_helper.py +38 -0
- aip_agents/utils/metadata_helper.py +358 -0
- aip_agents/utils/name_preprocessor/__init__.py +17 -0
- aip_agents/utils/name_preprocessor/base_name_preprocessor.py +73 -0
- aip_agents/utils/name_preprocessor/google_name_preprocessor.py +100 -0
- aip_agents/utils/name_preprocessor/name_preprocessor.py +87 -0
- aip_agents/utils/name_preprocessor/openai_name_preprocessor.py +48 -0
- aip_agents/utils/pii/__init__.py +25 -0
- aip_agents/utils/pii/pii_handler.py +397 -0
- aip_agents/utils/pii/pii_helper.py +207 -0
- aip_agents/utils/pii/uuid_deanonymizer_mapping.py +195 -0
- aip_agents/utils/reference_helper.py +273 -0
- aip_agents/utils/sse_chunk_transformer.py +831 -0
- aip_agents/utils/step_limit_manager.py +265 -0
- aip_agents/utils/token_usage_helper.py +156 -0
- aip_agents_binary-0.5.20.dist-info/METADATA +681 -0
- aip_agents_binary-0.5.20.dist-info/RECORD +280 -0
- aip_agents_binary-0.5.20.dist-info/WHEEL +5 -0
- aip_agents_binary-0.5.20.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1112 @@
|
|
|
1
|
+
"""Tool to execute web automation tasks using browser-use framework.
|
|
2
|
+
|
|
3
|
+
This tool provides browser automation capabilities using the browser-use framework
|
|
4
|
+
with Steel session management and streaming support.
|
|
5
|
+
|
|
6
|
+
Streaming Contract
|
|
7
|
+
|
|
8
|
+
The tool emits streaming events with the following structure:
|
|
9
|
+
|
|
10
|
+
Thinking Markers
|
|
11
|
+
- **thinking_start**: Indicates beginning of a thinking phase
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"event_type": "status_update",
|
|
15
|
+
"content": "Thinking start",
|
|
16
|
+
"thinking_and_activity_info": {
|
|
17
|
+
"data_type": "thinking_start",
|
|
18
|
+
"data_value": "",
|
|
19
|
+
"id": "default_thinking_id"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- thinking: Contains actual thinking content
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"event_type": "tool_result",
|
|
28
|
+
"content": "Completed go_to_url, extract_structured_data",
|
|
29
|
+
"thinking_and_activity_info": {
|
|
30
|
+
"data_type": "thinking",
|
|
31
|
+
"data_value": "**Starting fresh with the task...**",
|
|
32
|
+
"id": "default_thinking_id"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- thinking_end: Marks end of thinking phase
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"event_type": "status_update",
|
|
41
|
+
"content": "Thinking end",
|
|
42
|
+
"thinking_and_activity_info": {
|
|
43
|
+
"data_type": "thinking_end",
|
|
44
|
+
"data_value": "",
|
|
45
|
+
"id": "default_thinking_id"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Iframe Activities
|
|
51
|
+
|
|
52
|
+
- Streaming URL: Live browser session URL
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"event_type": "status_update",
|
|
56
|
+
"content": "Receive streaming URL",
|
|
57
|
+
"thinking_and_activity_info": {
|
|
58
|
+
"data_type": "activity",
|
|
59
|
+
"data_value": "{'type': 'iframe', 'message': 'https://steel.dev/...'}"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- Recording URL: Session recording URL
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"event_type": "status_update",
|
|
68
|
+
"content": "Receive recording URL",
|
|
69
|
+
"thinking_and_activity_info": {
|
|
70
|
+
"data_type": "activity",
|
|
71
|
+
"data_value": "{'type': 'iframe', 'message': 'http://minio/...'}"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Event Flow
|
|
77
|
+
|
|
78
|
+
1. Initialize session → Yield streaming URL
|
|
79
|
+
2. For each agent step:
|
|
80
|
+
- thinking_start
|
|
81
|
+
- tool_result with thinking content (emitted between the markers)
|
|
82
|
+
- thinking_end
|
|
83
|
+
3. Complete → Yield recording URL
|
|
84
|
+
|
|
85
|
+
Authors:
|
|
86
|
+
Reinhart Linanda (reinhart.linanda@gdplabs.id)
|
|
87
|
+
Fachriza Adhiatma (fachriza.d.adhiatma@gdplabs.id)
|
|
88
|
+
Saul Sayerz (saul.sayerz@gdplabs.id)
|
|
89
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
90
|
+
|
|
91
|
+
References:
|
|
92
|
+
https://github.com/GDP-ADMIN/glair-rnd/tree/main/browser-use/steeldev
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
import asyncio
|
|
96
|
+
import json
|
|
97
|
+
from collections.abc import Callable
|
|
98
|
+
from typing import Any, Literal
|
|
99
|
+
|
|
100
|
+
from browser_use import Agent
|
|
101
|
+
from langchain_core.runnables import RunnableConfig
|
|
102
|
+
from langchain_core.tools import BaseTool
|
|
103
|
+
from pydantic import BaseModel
|
|
104
|
+
from steel import Steel
|
|
105
|
+
from steel.types import Session
|
|
106
|
+
|
|
107
|
+
from aip_agents.tools.browser_use import session_errors
|
|
108
|
+
from aip_agents.tools.browser_use.action_parser import ActionParser
|
|
109
|
+
from aip_agents.tools.browser_use.llm_config import build_browser_use_llm, configure_browser_use_environment
|
|
110
|
+
from aip_agents.tools.browser_use.schemas import BrowserUseToolConfig, BrowserUseToolInput
|
|
111
|
+
from aip_agents.tools.browser_use.session import BrowserSession
|
|
112
|
+
from aip_agents.tools.browser_use.steel_session_recording import SteelSessionRecorder
|
|
113
|
+
from aip_agents.tools.browser_use.streaming import (
|
|
114
|
+
create_error_response,
|
|
115
|
+
create_step_response,
|
|
116
|
+
generate_step_content,
|
|
117
|
+
generate_thinking_message,
|
|
118
|
+
yield_iframe_activity,
|
|
119
|
+
yield_status_message,
|
|
120
|
+
yield_thinking_marker,
|
|
121
|
+
)
|
|
122
|
+
from aip_agents.tools.browser_use.structured_data_parser import detect_structured_data_failure
|
|
123
|
+
from aip_agents.tools.browser_use.types import (
|
|
124
|
+
BrowserUseFatalError,
|
|
125
|
+
RetryDecision,
|
|
126
|
+
StreamingResponse,
|
|
127
|
+
StreamingState,
|
|
128
|
+
ToolCallInfo,
|
|
129
|
+
)
|
|
130
|
+
from aip_agents.utils.logger import get_logger
|
|
131
|
+
|
|
132
|
+
logger = get_logger(__name__)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class BrowserUseTool(BaseTool):
|
|
136
|
+
"""Tool to execute web automation tasks using browser-use framework.
|
|
137
|
+
|
|
138
|
+
This tool provides step-by-step execution of browser automation tasks with detailed
|
|
139
|
+
logging of intermediate steps, including the agent's thinking process, goals, and
|
|
140
|
+
results at each step.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
name: str = "browser_use_tool"
|
|
144
|
+
description: str = (
|
|
145
|
+
"Execute web automation tasks using browser-use framework. "
|
|
146
|
+
"Connects browser-use agent, and runs the specified task step by step. "
|
|
147
|
+
"Provides detailed logging of intermediate steps including agent thinking and goals. "
|
|
148
|
+
"Returns the task execution results with step-by-step information."
|
|
149
|
+
)
|
|
150
|
+
args_schema: type[BaseModel] = BrowserUseToolInput
|
|
151
|
+
tool_config_schema: type[BaseModel] = BrowserUseToolConfig
|
|
152
|
+
|
|
153
|
+
MAX_SESSION_RELEASE_RETRIES: int = 3
|
|
154
|
+
SESSION_RELEASE_SLEEP_TIME_IN_S: int = 10
|
|
155
|
+
|
|
156
|
+
def _run(self, task: str, config: RunnableConfig | None = None) -> str:
|
|
157
|
+
"""Run the tool synchronously.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
task (str): The task prompt for the AI agent to execute in the browser.
|
|
161
|
+
config (RunnableConfig): RunnableConfig containing tool configuration.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
str: The result of the task execution or an error message if the task fails.
|
|
165
|
+
"""
|
|
166
|
+
return asyncio.run(self._arun(task, config))
|
|
167
|
+
|
|
168
|
+
async def _arun(self, task: str, config: RunnableConfig | None = None) -> str:
|
|
169
|
+
"""Execute a web automation task using browser-use asynchronously.
|
|
170
|
+
|
|
171
|
+
This method creates a Steel browser session, initializes a browser-use agent with
|
|
172
|
+
the specified task, and executes the automation. It handles session cleanup and
|
|
173
|
+
error handling.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
task (str): The task prompt for the AI agent to execute in the browser.
|
|
177
|
+
config (RunnableConfig): RunnableConfig containing tool configuration.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
str: A success message with the task result, or an error message if the task
|
|
181
|
+
execution fails. The result includes the final output from the browser-use
|
|
182
|
+
agent's execution.
|
|
183
|
+
|
|
184
|
+
Raises:
|
|
185
|
+
Exception: Any exception that occurs during task execution will be caught
|
|
186
|
+
and returned as an error message string.
|
|
187
|
+
"""
|
|
188
|
+
tool_config = self._get_tool_config(config)
|
|
189
|
+
|
|
190
|
+
if not tool_config.browser_use_llm_openai_api_key:
|
|
191
|
+
return self._log_and_return("warning", "Browser-use LLM OpenAI API key is required.")
|
|
192
|
+
if not tool_config.browser_use_page_extraction_llm_openai_api_key:
|
|
193
|
+
return self._log_and_return("warning", "Browser-use Page Extraction OpenAI API key is required.")
|
|
194
|
+
if not tool_config.steel_api_key:
|
|
195
|
+
return self._log_and_return("warning", "Steel API key is required.")
|
|
196
|
+
|
|
197
|
+
client = self._init_steel_client(tool_config)
|
|
198
|
+
|
|
199
|
+
session = None
|
|
200
|
+
try:
|
|
201
|
+
session = self._create_steel_session(client, tool_config)
|
|
202
|
+
cdp_url = self._construct_cdp_url(session.id, tool_config)
|
|
203
|
+
agent = self._create_browser_use_agent(task, cdp_url, tool_config)
|
|
204
|
+
result = await agent.run()
|
|
205
|
+
return self._log_and_return("info", f"Task completed successfully!\nResult: {result.final_result()}")
|
|
206
|
+
|
|
207
|
+
except Exception as e:
|
|
208
|
+
return self._log_and_return("warning", f"Error during task execution: {str(e)}")
|
|
209
|
+
finally:
|
|
210
|
+
await self._release_session(session, client)
|
|
211
|
+
|
|
212
|
+
def _get_tool_config(self, config: RunnableConfig | None) -> BrowserUseToolConfig:
|
|
213
|
+
"""Get the tool configuration.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
config (RunnableConfig): RunnableConfig containing tool configuration.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
BrowserUseToolConfig: The tool configuration.
|
|
220
|
+
"""
|
|
221
|
+
tool_config: BrowserUseToolConfig | None = None
|
|
222
|
+
if hasattr(self, "get_tool_config") and callable(self.get_tool_config):
|
|
223
|
+
tool_config = self.get_tool_config(config)
|
|
224
|
+
if not tool_config:
|
|
225
|
+
tool_config = BrowserUseToolConfig()
|
|
226
|
+
return tool_config
|
|
227
|
+
|
|
228
|
+
def _log_and_return(self, level: Literal["info", "warning"], message: str) -> str:
|
|
229
|
+
"""Log a message and return it.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
level (Literal["info", "warning"]): The log level.
|
|
233
|
+
message (str): The message to log and return.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
str: The logged message.
|
|
237
|
+
"""
|
|
238
|
+
if level == "info":
|
|
239
|
+
logger.info(message)
|
|
240
|
+
elif level == "warning":
|
|
241
|
+
logger.warning(message)
|
|
242
|
+
return message
|
|
243
|
+
|
|
244
|
+
def _init_steel_client(self, tool_config: BrowserUseToolConfig) -> Steel:
|
|
245
|
+
"""Initialize the Steel client.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
tool_config: Tool configuration containing Steel settings.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Steel: The Steel client.
|
|
252
|
+
"""
|
|
253
|
+
return Steel(steel_api_key=tool_config.steel_api_key, base_url=tool_config.steel_base_url)
|
|
254
|
+
|
|
255
|
+
def _create_steel_session(self, client: Steel, tool_config: BrowserUseToolConfig) -> Session:
|
|
256
|
+
"""Create the Steel session.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
client: The Steel client.
|
|
260
|
+
tool_config: Tool configuration containing Steel session settings.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Session: The Steel session.
|
|
264
|
+
"""
|
|
265
|
+
return client.sessions.create(use_proxy=False, api_timeout=tool_config.steel_timeout_in_ms)
|
|
266
|
+
|
|
267
|
+
def _construct_cdp_url(self, session_id: str, tool_config: BrowserUseToolConfig) -> str:
|
|
268
|
+
"""Construct the CDP URL for browser connection.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
session_id: The Steel session ID.
|
|
272
|
+
tool_config: Tool configuration containing Steel WebSocket URL.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
str: The CDP URL.
|
|
276
|
+
"""
|
|
277
|
+
return f"{tool_config.steel_ws_url}?apiKey={tool_config.steel_api_key}&sessionId={session_id}"
|
|
278
|
+
|
|
279
|
+
def _create_browser_use_agent(self, task: str, cdp_url: str, tool_config: BrowserUseToolConfig) -> Agent:
|
|
280
|
+
"""Create OpenAI LLM and browser-use agent bound to the provided task and CDP URL.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
task: The task prompt for the AI agent
|
|
284
|
+
cdp_url: The CDP URL for the browser session
|
|
285
|
+
tool_config: Tool configuration containing OpenAI model settings
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Agent: The browser-use agent
|
|
289
|
+
"""
|
|
290
|
+
llm = build_browser_use_llm(
|
|
291
|
+
model=tool_config.browser_use_llm_openai_model,
|
|
292
|
+
reasoning_effort=tool_config.browser_use_llm_openai_reasoning_effort,
|
|
293
|
+
temperature=tool_config.browser_use_llm_openai_temperature,
|
|
294
|
+
api_key=tool_config.browser_use_llm_openai_api_key,
|
|
295
|
+
base_url=tool_config.browser_use_llm_openai_base_url,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
page_extraction_llm = build_browser_use_llm(
|
|
299
|
+
model=tool_config.browser_use_page_extraction_llm_openai_model,
|
|
300
|
+
reasoning_effort=tool_config.browser_use_page_extraction_llm_openai_reasoning_effort,
|
|
301
|
+
temperature=tool_config.browser_use_page_extraction_llm_openai_temperature,
|
|
302
|
+
api_key=tool_config.browser_use_page_extraction_llm_openai_api_key,
|
|
303
|
+
base_url=tool_config.browser_use_page_extraction_llm_openai_base_url,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
browser_session = BrowserSession(cdp_url=cdp_url)
|
|
307
|
+
# Mark the proxy-created session as owned so browser-use does not clone it with warnings.
|
|
308
|
+
setattr(browser_session, "_owns_browser_resources", True)
|
|
309
|
+
|
|
310
|
+
configure_browser_use_environment(
|
|
311
|
+
enable_cloud_sync=tool_config.browser_use_enable_cloud_sync,
|
|
312
|
+
logging_level=tool_config.browser_use_logging_level,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
return Agent(
|
|
316
|
+
task=task,
|
|
317
|
+
llm=llm,
|
|
318
|
+
page_extraction_llm=page_extraction_llm,
|
|
319
|
+
browser_session=browser_session,
|
|
320
|
+
extend_system_message=tool_config.browser_use_extend_system_message,
|
|
321
|
+
vision_detail_level=tool_config.browser_use_vision_detail_level,
|
|
322
|
+
llm_timeout=tool_config.browser_use_llm_timeout_in_s,
|
|
323
|
+
step_timeout=tool_config.browser_use_step_timeout_in_s,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
def _create_agent_session(
|
|
327
|
+
self,
|
|
328
|
+
client: Steel,
|
|
329
|
+
recorder: SteelSessionRecorder,
|
|
330
|
+
tool_config: BrowserUseToolConfig,
|
|
331
|
+
task: str,
|
|
332
|
+
) -> tuple[Session, StreamingState, Agent]:
|
|
333
|
+
"""Provision a Steel session, streaming state, and browser-use agent.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
client: Initialized Steel SDK client.
|
|
337
|
+
recorder: Recorder responsible for generating session recordings.
|
|
338
|
+
tool_config: Tool configuration containing browser-use settings.
|
|
339
|
+
task: The textual task prompt the agent must execute.
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
tuple[Session, StreamingState, Agent]: Provisioned Steel session,
|
|
343
|
+
streaming state metadata, and browser-use agent instance.
|
|
344
|
+
"""
|
|
345
|
+
session = self._create_steel_session(client, tool_config)
|
|
346
|
+
streaming_state = self._create_streaming_state(session, recorder)
|
|
347
|
+
cdp_url = self._construct_cdp_url(session.id, tool_config)
|
|
348
|
+
agent = self._create_browser_use_agent(task, cdp_url, tool_config)
|
|
349
|
+
return session, streaming_state, agent
|
|
350
|
+
|
|
351
|
+
async def _retry_stream_events(self, task: str, tool_config: BrowserUseToolConfig):
|
|
352
|
+
"""Yield streaming events while automatically retrying Steel sessions.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
task: Textual task prompt.
|
|
356
|
+
tool_config: Tool configuration containing Steel/OpenAI settings.
|
|
357
|
+
|
|
358
|
+
Yields:
|
|
359
|
+
dict: Streaming events produced by the browser-use agent.
|
|
360
|
+
"""
|
|
361
|
+
client = self._init_steel_client(tool_config)
|
|
362
|
+
recorder = SteelSessionRecorder(tool_config.steel_base_url, tool_config.steel_api_key)
|
|
363
|
+
|
|
364
|
+
retry_state = self._init_retry_state(tool_config)
|
|
365
|
+
|
|
366
|
+
while True:
|
|
367
|
+
state_holder: dict[str, StreamingState | None] = {"state": None}
|
|
368
|
+
store_state = self._make_state_store(state_holder)
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
async for event in self._session_event_stream(
|
|
372
|
+
client,
|
|
373
|
+
recorder,
|
|
374
|
+
tool_config,
|
|
375
|
+
task,
|
|
376
|
+
store_state,
|
|
377
|
+
):
|
|
378
|
+
yield event
|
|
379
|
+
return
|
|
380
|
+
|
|
381
|
+
except Exception as exc: # pragma: no cover - defensive orchestration
|
|
382
|
+
async for event in self._emit_retry_exception(
|
|
383
|
+
exc,
|
|
384
|
+
state_holder,
|
|
385
|
+
recorder,
|
|
386
|
+
tool_config,
|
|
387
|
+
retry_state,
|
|
388
|
+
):
|
|
389
|
+
yield event
|
|
390
|
+
|
|
391
|
+
def _init_retry_state(self, tool_config: BrowserUseToolConfig) -> dict[str, int]:
|
|
392
|
+
"""Initialize retry state tracking.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
tool_config: Tool configuration containing retry settings.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
Dictionary with retry state counters.
|
|
399
|
+
"""
|
|
400
|
+
return {
|
|
401
|
+
"retries_remaining": max(0, tool_config.browser_use_max_session_retries),
|
|
402
|
+
"attempted_retries": 0,
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async def _handle_fatal_error_during_retry(
|
|
406
|
+
self,
|
|
407
|
+
fatal_error: BrowserUseFatalError,
|
|
408
|
+
state_holder: dict[str, StreamingState | None],
|
|
409
|
+
tool_config: BrowserUseToolConfig,
|
|
410
|
+
retry_state: dict[str, int],
|
|
411
|
+
):
|
|
412
|
+
"""Handle fatal errors during retry attempts.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
fatal_error: The fatal error that occurred.
|
|
416
|
+
state_holder: Container for current streaming state.
|
|
417
|
+
tool_config: Tool configuration for retry logic.
|
|
418
|
+
retry_state: Current retry counters and state.
|
|
419
|
+
"""
|
|
420
|
+
streaming_state = state_holder["state"]
|
|
421
|
+
decision, pending_event = await self._handle_retryable_fatal_error(
|
|
422
|
+
fatal_error,
|
|
423
|
+
streaming_state,
|
|
424
|
+
retry_state["retries_remaining"],
|
|
425
|
+
retry_state["attempted_retries"],
|
|
426
|
+
tool_config,
|
|
427
|
+
)
|
|
428
|
+
if pending_event:
|
|
429
|
+
yield pending_event
|
|
430
|
+
retry_state["retries_remaining"], retry_state["attempted_retries"] = self._apply_retry_decision(
|
|
431
|
+
fatal_error, decision
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
async def _handle_cancellation_during_retry(
|
|
435
|
+
self,
|
|
436
|
+
state_holder: dict[str, StreamingState | None],
|
|
437
|
+
recorder: SteelSessionRecorder,
|
|
438
|
+
):
|
|
439
|
+
"""Handle cancellation during retry attempts.
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
state_holder: Container for current streaming state.
|
|
443
|
+
recorder: Session recorder for cleanup operations.
|
|
444
|
+
"""
|
|
445
|
+
streaming_state = state_holder["state"]
|
|
446
|
+
if streaming_state:
|
|
447
|
+
self._finalize_streaming_on_cancel(streaming_state, recorder)
|
|
448
|
+
if streaming_state.recording_url:
|
|
449
|
+
yield yield_iframe_activity(streaming_state.recording_url, "Receive recording URL")
|
|
450
|
+
|
|
451
|
+
async def _handle_unexpected_error_during_retry(
|
|
452
|
+
self,
|
|
453
|
+
exc: Exception,
|
|
454
|
+
state_holder: dict[str, StreamingState | None],
|
|
455
|
+
):
|
|
456
|
+
"""Handle unexpected errors during retry attempts.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
exc: The unexpected exception that occurred.
|
|
460
|
+
state_holder: Container for current streaming state.
|
|
461
|
+
"""
|
|
462
|
+
streaming_state = state_holder["state"]
|
|
463
|
+
error_url = streaming_state.recording_url if streaming_state else ""
|
|
464
|
+
yield create_error_response(f"Error during task execution: {str(exc)}", error_url)
|
|
465
|
+
|
|
466
|
+
@staticmethod
|
|
467
|
+
def _make_state_store(state_holder: dict[str, StreamingState | None]) -> Callable[[StreamingState], None]:
|
|
468
|
+
"""Return a callback that records the latest streaming state.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
state_holder: Mutable container storing the latest streaming state.
|
|
472
|
+
"""
|
|
473
|
+
|
|
474
|
+
def store(streaming_state: StreamingState) -> None:
|
|
475
|
+
"""Persist the latest streaming state in the shared holder.
|
|
476
|
+
|
|
477
|
+
Args:
|
|
478
|
+
streaming_state: Streaming state emitted by browser-use callbacks.
|
|
479
|
+
"""
|
|
480
|
+
state_holder["state"] = streaming_state
|
|
481
|
+
|
|
482
|
+
return store
|
|
483
|
+
|
|
484
|
+
def _apply_retry_decision(
|
|
485
|
+
self, fatal_error: BrowserUseFatalError, decision: RetryDecision | None
|
|
486
|
+
) -> tuple[int, int]:
|
|
487
|
+
"""Update retry counters or re-raise when no retries remain.
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
fatal_error: The fatal error that triggered the retry decision.
|
|
491
|
+
decision: The retry decision containing retry counters, or None if no retries available.
|
|
492
|
+
|
|
493
|
+
Returns:
|
|
494
|
+
tuple[int, int]: A tuple containing (retries_remaining, attempted_retries).
|
|
495
|
+
"""
|
|
496
|
+
if not decision:
|
|
497
|
+
raise fatal_error
|
|
498
|
+
return decision.retries_remaining, decision.attempted_retries
|
|
499
|
+
|
|
500
|
+
async def _emit_retry_exception(
|
|
501
|
+
self,
|
|
502
|
+
exc: Exception,
|
|
503
|
+
state_holder: dict[str, StreamingState | None],
|
|
504
|
+
recorder: SteelSessionRecorder,
|
|
505
|
+
tool_config: BrowserUseToolConfig,
|
|
506
|
+
retry_state: dict[str, int],
|
|
507
|
+
):
|
|
508
|
+
"""Normalize retry exception handling and yield resulting events.
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
exc: Exception raised during the streaming attempt.
|
|
512
|
+
state_holder: Container for current streaming state.
|
|
513
|
+
recorder: Session recorder for cleanup operations.
|
|
514
|
+
tool_config: Tool configuration containing retry settings.
|
|
515
|
+
retry_state: Mutable retry counters.
|
|
516
|
+
"""
|
|
517
|
+
if isinstance(exc, asyncio.CancelledError):
|
|
518
|
+
async for event in self._handle_cancellation_during_retry(state_holder, recorder):
|
|
519
|
+
yield event
|
|
520
|
+
raise exc
|
|
521
|
+
|
|
522
|
+
if isinstance(exc, BrowserUseFatalError):
|
|
523
|
+
async for event in self._handle_fatal_error_during_retry(exc, state_holder, tool_config, retry_state):
|
|
524
|
+
yield event
|
|
525
|
+
return
|
|
526
|
+
|
|
527
|
+
async for event in self._handle_unexpected_error_during_retry(exc, state_holder):
|
|
528
|
+
yield event
|
|
529
|
+
raise BrowserUseFatalError(str(exc)) from exc
|
|
530
|
+
|
|
531
|
+
async def _session_event_stream(
|
|
532
|
+
self,
|
|
533
|
+
client: Steel,
|
|
534
|
+
recorder: SteelSessionRecorder,
|
|
535
|
+
tool_config: BrowserUseToolConfig,
|
|
536
|
+
task: str,
|
|
537
|
+
on_state_ready: Callable[[StreamingState], None],
|
|
538
|
+
):
|
|
539
|
+
"""Yield streaming events for a single Steel session attempt.
|
|
540
|
+
|
|
541
|
+
Args:
|
|
542
|
+
client: Steel SDK client instance.
|
|
543
|
+
recorder: Steel session recorder.
|
|
544
|
+
tool_config: Tool configuration for the agent.
|
|
545
|
+
task: Task prompt assigned to the agent.
|
|
546
|
+
on_state_ready: Callback invoked when streaming state is initialized.
|
|
547
|
+
|
|
548
|
+
Yields:
|
|
549
|
+
dict: Streaming events describing agent progress.
|
|
550
|
+
"""
|
|
551
|
+
session: Session | None = None
|
|
552
|
+
streaming_state: StreamingState | None = None
|
|
553
|
+
try:
|
|
554
|
+
session, streaming_state, agent = self._create_agent_session(client, recorder, tool_config, task)
|
|
555
|
+
on_state_ready(streaming_state)
|
|
556
|
+
iframe_event = yield_iframe_activity(streaming_state.debug_url, "Receive streaming URL")
|
|
557
|
+
self._log_stream_event("iframe_start", iframe_event)
|
|
558
|
+
yield iframe_event
|
|
559
|
+
async for event in self._stream_agent_with_markers(agent, streaming_state, recorder):
|
|
560
|
+
self._log_stream_event("agent_event", event)
|
|
561
|
+
yield event
|
|
562
|
+
recording_event = self._recording_event(streaming_state)
|
|
563
|
+
if recording_event:
|
|
564
|
+
self._log_stream_event("recording_event", recording_event)
|
|
565
|
+
yield recording_event
|
|
566
|
+
finally:
|
|
567
|
+
await self._release_session(session, client)
|
|
568
|
+
|
|
569
|
+
async def _release_session(self, session: Session | None, client: Steel) -> None:
|
|
570
|
+
"""Release a Steel browser session.
|
|
571
|
+
|
|
572
|
+
This method attempts to release the provided Steel session.
|
|
573
|
+
It retries the release operation up to MAX_SESSION_RELEASE_RETRIES times, waiting
|
|
574
|
+
SESSION_RELEASE_SLEEP_TIME_IN_S seconds between attempts. If the session is None, the method
|
|
575
|
+
returns immediately.
|
|
576
|
+
|
|
577
|
+
Args:
|
|
578
|
+
session (Session | None): The Steel session to release. If None, no action is taken.
|
|
579
|
+
client (Steel): The Steel client used to release the session.
|
|
580
|
+
"""
|
|
581
|
+
if not session:
|
|
582
|
+
return
|
|
583
|
+
|
|
584
|
+
for attempt in range(self.MAX_SESSION_RELEASE_RETRIES):
|
|
585
|
+
try:
|
|
586
|
+
await asyncio.sleep(self.SESSION_RELEASE_SLEEP_TIME_IN_S)
|
|
587
|
+
client.sessions.release(session.id)
|
|
588
|
+
logger.info(f"Session {session.id} released")
|
|
589
|
+
break
|
|
590
|
+
except Exception as e:
|
|
591
|
+
if attempt == self.MAX_SESSION_RELEASE_RETRIES - 1:
|
|
592
|
+
logger.warning(f"Failed to release session after {self.MAX_SESSION_RELEASE_RETRIES} attempts: {e}")
|
|
593
|
+
|
|
594
|
+
async def arun_streaming(self, task: str = None, config: RunnableConfig | None = None, **kwargs):
|
|
595
|
+
"""Execute a web automation task using browser-use asynchronously with streaming output.
|
|
596
|
+
|
|
597
|
+
This method creates a Steel browser session, initializes a browser-use agent with
|
|
598
|
+
the specified task, and executes the automation step by step, yielding results
|
|
599
|
+
in streaming fashion. Starts background recording after completion.
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
task (str, optional): The task prompt for the AI agent to execute in the browser.
|
|
603
|
+
If not provided, will attempt to extract from kwargs.
|
|
604
|
+
config (RunnableConfig): RunnableConfig containing tool configuration.
|
|
605
|
+
**kwargs: Additional parameters that may contain the task or other tool-specific arguments.
|
|
606
|
+
|
|
607
|
+
Yields:
|
|
608
|
+
dict: Step-by-step results in standardized StreamingResponse format.
|
|
609
|
+
|
|
610
|
+
Raises:
|
|
611
|
+
Exception: Any exception that occurs during task execution will be caught
|
|
612
|
+
and yielded as an error message.
|
|
613
|
+
"""
|
|
614
|
+
if task is None:
|
|
615
|
+
task = kwargs.get("task")
|
|
616
|
+
|
|
617
|
+
tool_config = self._get_tool_config(config)
|
|
618
|
+
|
|
619
|
+
missing_key_error = self._missing_api_key_error(tool_config)
|
|
620
|
+
if missing_key_error:
|
|
621
|
+
yield create_error_response(missing_key_error)
|
|
622
|
+
return
|
|
623
|
+
|
|
624
|
+
async for event in self._retry_stream_events(task, tool_config):
|
|
625
|
+
yield event
|
|
626
|
+
|
|
627
|
+
@staticmethod
|
|
628
|
+
def _missing_api_key_error(tool_config: BrowserUseToolConfig) -> str | None:
|
|
629
|
+
"""Return a descriptive error when required API keys are missing.
|
|
630
|
+
|
|
631
|
+
Args:
|
|
632
|
+
tool_config: The tool configuration containing API key settings.
|
|
633
|
+
|
|
634
|
+
Returns:
|
|
635
|
+
str | None: Error message if API keys are missing, otherwise None.
|
|
636
|
+
"""
|
|
637
|
+
if not tool_config.browser_use_llm_openai_api_key:
|
|
638
|
+
return "Browser-use LLM OpenAI API key is required."
|
|
639
|
+
if not tool_config.browser_use_page_extraction_llm_openai_api_key:
|
|
640
|
+
return "Browser-use Page Extraction OpenAI API key is required."
|
|
641
|
+
if not tool_config.steel_api_key:
|
|
642
|
+
return "Steel API key is required."
|
|
643
|
+
return None
|
|
644
|
+
|
|
645
|
+
def _create_streaming_state(self, session: Session, recorder: SteelSessionRecorder) -> StreamingState:
|
|
646
|
+
"""Build initial streaming state for a Steel session.
|
|
647
|
+
|
|
648
|
+
Args:
|
|
649
|
+
session: The Steel browser session to create streaming state for.
|
|
650
|
+
recorder: The Steel session recorder for video capture.
|
|
651
|
+
|
|
652
|
+
Returns:
|
|
653
|
+
StreamingState: The initialized streaming state with debug URL, recording URL, and session ID.
|
|
654
|
+
"""
|
|
655
|
+
video_filename = recorder.generate_video_filename(session.id)
|
|
656
|
+
recording_url = recorder.minio_storage.get_file_url(video_filename) if recorder.minio_storage else ""
|
|
657
|
+
return StreamingState(debug_url=session.debug_url, recording_url=recording_url, session_id=session.id)
|
|
658
|
+
|
|
659
|
+
def _should_retry_session(self, error_message: str, retries_remaining: int) -> bool:
|
|
660
|
+
"""Return True when the failure qualifies for an automatic Steel session retry.
|
|
661
|
+
|
|
662
|
+
Args:
|
|
663
|
+
error_message: The error message from the failed session.
|
|
664
|
+
retries_remaining: The number of retry attempts still available.
|
|
665
|
+
|
|
666
|
+
Returns:
|
|
667
|
+
bool: True if the error is recoverable and retries are available, False otherwise.
|
|
668
|
+
"""
|
|
669
|
+
if retries_remaining <= 0:
|
|
670
|
+
return False
|
|
671
|
+
return session_errors.is_recoverable_message(error_message)
|
|
672
|
+
|
|
673
|
+
def _build_retry_decision(
|
|
674
|
+
self,
|
|
675
|
+
error_message: str,
|
|
676
|
+
retries_remaining: int,
|
|
677
|
+
attempted_retries: int,
|
|
678
|
+
tool_config: BrowserUseToolConfig,
|
|
679
|
+
) -> RetryDecision | None:
|
|
680
|
+
"""Create a retry decision payload when a Steel session disconnects.
|
|
681
|
+
|
|
682
|
+
Args:
|
|
683
|
+
error_message: Message describing the fatal error.
|
|
684
|
+
retries_remaining: Number of retries still allowed.
|
|
685
|
+
attempted_retries: Number of retries already attempted.
|
|
686
|
+
tool_config: Tool configuration containing retry settings.
|
|
687
|
+
|
|
688
|
+
Returns:
|
|
689
|
+
RetryDecision | None: A retry decision when the error is recoverable,
|
|
690
|
+
otherwise None.
|
|
691
|
+
"""
|
|
692
|
+
if not self._should_retry_session(error_message, retries_remaining):
|
|
693
|
+
return None
|
|
694
|
+
|
|
695
|
+
new_retries = retries_remaining - 1
|
|
696
|
+
new_attempts = attempted_retries + 1
|
|
697
|
+
retry_total = tool_config.browser_use_max_session_retries or 1
|
|
698
|
+
retry_message = f"Steel session disconnected. Attempting automatic recovery ({new_attempts}/{retry_total})."
|
|
699
|
+
delay = max(0.0, tool_config.browser_use_session_retry_delay_in_s)
|
|
700
|
+
return RetryDecision(
|
|
701
|
+
retries_remaining=new_retries,
|
|
702
|
+
attempted_retries=new_attempts,
|
|
703
|
+
message=retry_message,
|
|
704
|
+
delay=delay,
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
def _recording_event(self, streaming_state: StreamingState | None) -> dict | None:
|
|
708
|
+
"""Return an iframe activity event for successful runs.
|
|
709
|
+
|
|
710
|
+
Args:
|
|
711
|
+
streaming_state: Current streaming metadata, if available.
|
|
712
|
+
|
|
713
|
+
Returns:
|
|
714
|
+
dict | None: Iframe activity event when a recording is available.
|
|
715
|
+
"""
|
|
716
|
+
if not streaming_state or streaming_state.terminal_error or not streaming_state.recording_url:
|
|
717
|
+
return None
|
|
718
|
+
return yield_iframe_activity(streaming_state.recording_url, "Receive recording URL")
|
|
719
|
+
|
|
720
|
+
async def _handle_retryable_fatal_error(
|
|
721
|
+
self,
|
|
722
|
+
fatal_error: BrowserUseFatalError,
|
|
723
|
+
streaming_state: StreamingState | None,
|
|
724
|
+
retries_remaining: int,
|
|
725
|
+
attempted_retries: int,
|
|
726
|
+
tool_config: BrowserUseToolConfig,
|
|
727
|
+
) -> tuple[RetryDecision | None, dict | None]:
|
|
728
|
+
"""Handle a fatal error by either yielding an error response or scheduling a retry.
|
|
729
|
+
|
|
730
|
+
Args:
|
|
731
|
+
fatal_error: The raised fatal exception.
|
|
732
|
+
streaming_state: Current streaming metadata, if any.
|
|
733
|
+
retries_remaining: Number of retries still allowed.
|
|
734
|
+
attempted_retries: Number of retries already attempted.
|
|
735
|
+
tool_config: Tool configuration containing retry settings.
|
|
736
|
+
|
|
737
|
+
Returns:
|
|
738
|
+
tuple[RetryDecision | None, dict | None]: Retry decision (None when unrecoverable)
|
|
739
|
+
and a streaming event to emit (error or status).
|
|
740
|
+
"""
|
|
741
|
+
decision = self._build_retry_decision(
|
|
742
|
+
str(fatal_error),
|
|
743
|
+
retries_remaining,
|
|
744
|
+
attempted_retries,
|
|
745
|
+
tool_config,
|
|
746
|
+
)
|
|
747
|
+
if not decision:
|
|
748
|
+
error_url = streaming_state.recording_url if streaming_state else ""
|
|
749
|
+
return None, create_error_response(str(fatal_error), error_url)
|
|
750
|
+
|
|
751
|
+
status_event = yield_status_message(decision.message)
|
|
752
|
+
self._log_stream_event("retry_status", status_event)
|
|
753
|
+
if decision.delay:
|
|
754
|
+
await asyncio.sleep(decision.delay)
|
|
755
|
+
return decision, status_event
|
|
756
|
+
|
|
757
|
+
async def _stream_agent_with_markers(
|
|
758
|
+
self, agent: Agent, streaming_state: StreamingState, recorder: SteelSessionRecorder
|
|
759
|
+
):
|
|
760
|
+
"""Yield agent progress events surrounded by thinking markers.
|
|
761
|
+
|
|
762
|
+
Args:
|
|
763
|
+
self: The BrowserUseTool instance.
|
|
764
|
+
agent: The browser-use agent to execute steps with.
|
|
765
|
+
streaming_state: State management for the streaming operation.
|
|
766
|
+
recorder: The Steel session recorder for video capture.
|
|
767
|
+
|
|
768
|
+
Yields:
|
|
769
|
+
dict: Streaming events including thinking start/end markers and step results.
|
|
770
|
+
"""
|
|
771
|
+
async for stream_item in self._execute_agent_steps_streaming(agent, streaming_state, recorder):
|
|
772
|
+
payload = stream_item["payload"]
|
|
773
|
+
wrap_markers = stream_item.get("wrap_markers", False)
|
|
774
|
+
label = stream_item.get("label", "step_result")
|
|
775
|
+
|
|
776
|
+
if wrap_markers:
|
|
777
|
+
start_marker = yield_thinking_marker("start")
|
|
778
|
+
self._log_stream_event(f"{label}_thinking_start", start_marker)
|
|
779
|
+
yield start_marker
|
|
780
|
+
|
|
781
|
+
self._log_stream_event(label, payload)
|
|
782
|
+
yield payload
|
|
783
|
+
|
|
784
|
+
end_marker = yield_thinking_marker("end")
|
|
785
|
+
self._log_stream_event(f"{label}_thinking_end", end_marker)
|
|
786
|
+
yield end_marker
|
|
787
|
+
else:
|
|
788
|
+
self._log_stream_event(label, payload)
|
|
789
|
+
yield payload
|
|
790
|
+
|
|
791
|
+
async def _execute_agent_steps_streaming(
|
|
792
|
+
self, agent: Agent, streaming_state: StreamingState, recorder: SteelSessionRecorder
|
|
793
|
+
):
|
|
794
|
+
"""Execute agent steps one by one and yield intermediate progress in streaming fashion.
|
|
795
|
+
|
|
796
|
+
This method uses the take_step approach to execute the agent's task step by step,
|
|
797
|
+
yielding each step's information in a standardized format. Starts background recording after completion.
|
|
798
|
+
|
|
799
|
+
Args:
|
|
800
|
+
agent (Agent): The browser-use agent to execute.
|
|
801
|
+
streaming_state (StreamingState): State management for the streaming operation.
|
|
802
|
+
recorder (SteelSessionRecorder): The Steel session recorder.
|
|
803
|
+
|
|
804
|
+
Yields:
|
|
805
|
+
dict: Step information in standardized StreamingResponse format.
|
|
806
|
+
"""
|
|
807
|
+
while not streaming_state.is_complete:
|
|
808
|
+
events = await self._next_step_events(agent, streaming_state, recorder)
|
|
809
|
+
for event in events:
|
|
810
|
+
yield event
|
|
811
|
+
if streaming_state.is_complete:
|
|
812
|
+
break
|
|
813
|
+
|
|
814
|
+
async def _perform_agent_step(
|
|
815
|
+
self, agent: Agent, streaming_state: StreamingState, recorder: SteelSessionRecorder
|
|
816
|
+
) -> StreamingResponse:
|
|
817
|
+
"""Execute a single agent step and return its streaming payload.
|
|
818
|
+
|
|
819
|
+
Args:
|
|
820
|
+
agent: The browser-use agent currently executing the plan.
|
|
821
|
+
streaming_state: Mutable state tracking streaming progress and errors.
|
|
822
|
+
recorder: Steel recorder used to schedule background video exports.
|
|
823
|
+
|
|
824
|
+
Returns:
|
|
825
|
+
StreamingResponse: Serialized streaming payload for the current step.
|
|
826
|
+
"""
|
|
827
|
+
is_done, _ = await agent.take_step()
|
|
828
|
+
streaming_state.step_count += 1
|
|
829
|
+
|
|
830
|
+
tool_calls = ActionParser.extract_actions(agent.state.last_model_output, agent.state.last_result)
|
|
831
|
+
logger.info("Browser-use raw tool calls: %s", [call.__dict__ for call in tool_calls])
|
|
832
|
+
|
|
833
|
+
error_message = self._resolve_step_error(agent, streaming_state, recorder, tool_calls)
|
|
834
|
+
if error_message:
|
|
835
|
+
raise BrowserUseFatalError(error_message)
|
|
836
|
+
|
|
837
|
+
if is_done:
|
|
838
|
+
self._update_completion_state(streaming_state, recorder)
|
|
839
|
+
|
|
840
|
+
tool_calls_dict = [{"name": tc.name, "args": tc.args, "output": tc.output} for tc in tool_calls]
|
|
841
|
+
content = generate_step_content(tool_calls, is_done)
|
|
842
|
+
thinking_message = await generate_thinking_message(content, tool_calls_dict, is_final=is_done)
|
|
843
|
+
|
|
844
|
+
step_response = create_step_response(agent, tool_calls, is_done, content, thinking_message)
|
|
845
|
+
logger.info("Browser-use step event: %s", step_response.to_dict())
|
|
846
|
+
|
|
847
|
+
self._log_stream_event("step_streaming_response", step_response)
|
|
848
|
+
|
|
849
|
+
return step_response
|
|
850
|
+
|
|
851
|
+
def _prepare_step_events(
|
|
852
|
+
self,
|
|
853
|
+
response: StreamingResponse,
|
|
854
|
+
streaming_state: StreamingState,
|
|
855
|
+
) -> list[dict[str, Any]]:
|
|
856
|
+
"""Construct serialized events for a streaming step.
|
|
857
|
+
|
|
858
|
+
Args:
|
|
859
|
+
response: Primary streaming response generated for the current step.
|
|
860
|
+
streaming_state: Streaming state describing overall progress/completion.
|
|
861
|
+
|
|
862
|
+
Returns:
|
|
863
|
+
List of serialized event dictionaries ready to be emitted.
|
|
864
|
+
"""
|
|
865
|
+
wrap_markers = not streaming_state.is_complete
|
|
866
|
+
return [
|
|
867
|
+
{
|
|
868
|
+
"payload": response.to_dict(),
|
|
869
|
+
"wrap_markers": wrap_markers,
|
|
870
|
+
"label": "step_result",
|
|
871
|
+
}
|
|
872
|
+
]
|
|
873
|
+
|
|
874
|
+
async def _next_step_events(
|
|
875
|
+
self,
|
|
876
|
+
agent: Agent,
|
|
877
|
+
streaming_state: StreamingState,
|
|
878
|
+
recorder: SteelSessionRecorder,
|
|
879
|
+
) -> list[dict[str, Any]]:
|
|
880
|
+
"""Execute a step and return serialized events, handling errors consistently.
|
|
881
|
+
|
|
882
|
+
Args:
|
|
883
|
+
agent: Browser-use agent executing the step.
|
|
884
|
+
streaming_state: Mutable streaming state metadata.
|
|
885
|
+
recorder: Recorder used for background capture scheduling.
|
|
886
|
+
|
|
887
|
+
Returns:
|
|
888
|
+
List of serialized event dictionaries produced by the step.
|
|
889
|
+
"""
|
|
890
|
+
try:
|
|
891
|
+
response = await self._perform_agent_step(agent, streaming_state, recorder)
|
|
892
|
+
return self._prepare_step_events(response, streaming_state)
|
|
893
|
+
except asyncio.CancelledError as cancel:
|
|
894
|
+
self._finalize_streaming_on_cancel(streaming_state, recorder)
|
|
895
|
+
raise cancel
|
|
896
|
+
except BrowserUseFatalError:
|
|
897
|
+
raise
|
|
898
|
+
except Exception as error: # pragma: no cover - defensive path
|
|
899
|
+
error_message = f"Error in step execution: {error}"
|
|
900
|
+
raise BrowserUseFatalError(error_message) from error
|
|
901
|
+
|
|
902
|
+
def _resolve_step_error(
|
|
903
|
+
self,
|
|
904
|
+
agent: Agent,
|
|
905
|
+
streaming_state: StreamingState,
|
|
906
|
+
recorder: SteelSessionRecorder,
|
|
907
|
+
tool_calls: list[ToolCallInfo],
|
|
908
|
+
) -> str | None:
|
|
909
|
+
"""Handle terminal or extraction errors detected during a step.
|
|
910
|
+
|
|
911
|
+
Args:
|
|
912
|
+
agent: Browser-use agent with latest results.
|
|
913
|
+
streaming_state: Current streaming state object.
|
|
914
|
+
recorder: Recorder used to schedule background exports.
|
|
915
|
+
tool_calls: Tool calls extracted from the agent step.
|
|
916
|
+
|
|
917
|
+
Returns:
|
|
918
|
+
Error message when the step must terminate, otherwise ``None``.
|
|
919
|
+
"""
|
|
920
|
+
terminal_error = self._detect_terminal_session_error(agent, tool_calls)
|
|
921
|
+
if terminal_error:
|
|
922
|
+
message = f"Browser session disconnected unexpectedly. Steel reported: {terminal_error}"
|
|
923
|
+
self._finalize_stream_with_error(streaming_state, recorder, message)
|
|
924
|
+
return message
|
|
925
|
+
|
|
926
|
+
extraction_error = detect_structured_data_failure(tool_calls, self._summarize_terminal_error)
|
|
927
|
+
if extraction_error:
|
|
928
|
+
self._finalize_stream_with_error(streaming_state, recorder, extraction_error)
|
|
929
|
+
return extraction_error
|
|
930
|
+
|
|
931
|
+
return None
|
|
932
|
+
|
|
933
|
+
def _finalize_stream_with_error(
|
|
934
|
+
self,
|
|
935
|
+
streaming_state: StreamingState,
|
|
936
|
+
recorder: SteelSessionRecorder,
|
|
937
|
+
message: str,
|
|
938
|
+
) -> None:
|
|
939
|
+
"""Mark the stream as complete due to an unrecoverable error.
|
|
940
|
+
|
|
941
|
+
Args:
|
|
942
|
+
streaming_state: Current streaming state to update.
|
|
943
|
+
recorder: Recorder used to schedule background exports.
|
|
944
|
+
message: Human-readable error message to persist.
|
|
945
|
+
"""
|
|
946
|
+
streaming_state.terminal_error = message
|
|
947
|
+
streaming_state.is_complete = True
|
|
948
|
+
if streaming_state.session_id:
|
|
949
|
+
self._start_background_recording(recorder, streaming_state.session_id)
|
|
950
|
+
streaming_state.recording_started = True
|
|
951
|
+
|
|
952
|
+
def _update_completion_state(
|
|
953
|
+
self,
|
|
954
|
+
streaming_state: StreamingState,
|
|
955
|
+
recorder: SteelSessionRecorder,
|
|
956
|
+
) -> None:
|
|
957
|
+
"""Mark the streaming state as complete and schedule recording when needed.
|
|
958
|
+
|
|
959
|
+
Args:
|
|
960
|
+
streaming_state: Current streaming state to mutate.
|
|
961
|
+
recorder: Recorder used to kick off background exports.
|
|
962
|
+
"""
|
|
963
|
+
streaming_state.is_complete = True
|
|
964
|
+
if streaming_state.session_id:
|
|
965
|
+
self._start_background_recording(recorder, streaming_state.session_id)
|
|
966
|
+
streaming_state.recording_started = True
|
|
967
|
+
|
|
968
|
+
def _detect_terminal_session_error(self, agent: Agent, tool_calls: list[ToolCallInfo]) -> str | None:
|
|
969
|
+
"""Inspect tool outputs and agent results for terminal Steel session failures.
|
|
970
|
+
|
|
971
|
+
Args:
|
|
972
|
+
agent: The browser-use agent providing result context.
|
|
973
|
+
tool_calls: Tool call descriptors extracted for the current step.
|
|
974
|
+
|
|
975
|
+
Returns:
|
|
976
|
+
str | None: A concise terminal error summary when detected, otherwise None.
|
|
977
|
+
"""
|
|
978
|
+
candidates = self._collect_session_messages(agent, tool_calls)
|
|
979
|
+
|
|
980
|
+
fatal_match = session_errors.find_fatal_message(candidates)
|
|
981
|
+
if fatal_match:
|
|
982
|
+
fatal_message, _ = fatal_match
|
|
983
|
+
return self._summarize_terminal_error(fatal_message)
|
|
984
|
+
|
|
985
|
+
self._log_session_warnings(candidates)
|
|
986
|
+
|
|
987
|
+
return None
|
|
988
|
+
|
|
989
|
+
def _collect_session_messages(self, agent: Agent, tool_calls: list[ToolCallInfo]) -> list[str]:
|
|
990
|
+
"""Gather outputs and errors emitted during a step for analysis.
|
|
991
|
+
|
|
992
|
+
Args:
|
|
993
|
+
agent: Browser-use agent providing result context.
|
|
994
|
+
tool_calls: Tool calls extracted from the step execution.
|
|
995
|
+
|
|
996
|
+
Returns:
|
|
997
|
+
List of raw output/error messages produced during the step.
|
|
998
|
+
"""
|
|
999
|
+
tool_outputs = [call.output for call in tool_calls if call.output]
|
|
1000
|
+
result_errors = []
|
|
1001
|
+
if agent.state.last_result:
|
|
1002
|
+
result_errors = [result.error for result in agent.state.last_result if result and result.error]
|
|
1003
|
+
return [message for message in (*tool_outputs, *result_errors) if message]
|
|
1004
|
+
|
|
1005
|
+
def _log_session_warnings(self, candidates: list[str]) -> None:
|
|
1006
|
+
"""Emit debug logs for non-fatal Steel session warnings.
|
|
1007
|
+
|
|
1008
|
+
Args:
|
|
1009
|
+
candidates: Messages captured during the step to inspect for warnings.
|
|
1010
|
+
"""
|
|
1011
|
+
logged_warnings: set[str] = set()
|
|
1012
|
+
for candidate in candidates:
|
|
1013
|
+
warning_name = session_errors.categorize_warning_message(candidate)
|
|
1014
|
+
if warning_name and warning_name not in logged_warnings:
|
|
1015
|
+
logger.debug("Detected session warning=%s; continuing stream.", warning_name)
|
|
1016
|
+
logged_warnings.add(warning_name)
|
|
1017
|
+
|
|
1018
|
+
def _detect_terminal_error_from_exception(self, error_message: str) -> str | None:
|
|
1019
|
+
"""Detect terminal Steel session failures from raised exceptions.
|
|
1020
|
+
|
|
1021
|
+
Args:
|
|
1022
|
+
error_message: The exception message captured during step execution.
|
|
1023
|
+
|
|
1024
|
+
Returns:
|
|
1025
|
+
str | None: Terminal error summary when matched, otherwise None.
|
|
1026
|
+
"""
|
|
1027
|
+
fatal_match = session_errors.find_fatal_message([error_message])
|
|
1028
|
+
if fatal_match:
|
|
1029
|
+
fatal_message, _ = fatal_match
|
|
1030
|
+
return self._summarize_terminal_error(fatal_message)
|
|
1031
|
+
warning_name = session_errors.categorize_warning_message(error_message)
|
|
1032
|
+
if warning_name:
|
|
1033
|
+
logger.debug(
|
|
1034
|
+
"Detected session warning=%s from exception; continuing stream.",
|
|
1035
|
+
warning_name,
|
|
1036
|
+
)
|
|
1037
|
+
return None
|
|
1038
|
+
|
|
1039
|
+
@staticmethod
|
|
1040
|
+
def _summarize_terminal_error(error_message: str, max_length: int = 200) -> str:
|
|
1041
|
+
"""Produce a concise summary for terminal error messages.
|
|
1042
|
+
|
|
1043
|
+
Args:
|
|
1044
|
+
error_message: The original terminal error message.
|
|
1045
|
+
max_length: Maximum length for the summary text.
|
|
1046
|
+
|
|
1047
|
+
Returns:
|
|
1048
|
+
str: Sanitized and truncated error summary.
|
|
1049
|
+
"""
|
|
1050
|
+
sanitized = " ".join(error_message.split())
|
|
1051
|
+
if len(sanitized) <= max_length:
|
|
1052
|
+
return sanitized
|
|
1053
|
+
return sanitized[: max_length - 1] + "…"
|
|
1054
|
+
|
|
1055
|
+
def _log_stream_event(self, label: str, event: dict[str, Any] | StreamingResponse) -> None:
|
|
1056
|
+
"""Log streaming events to trace emission order during debugging.
|
|
1057
|
+
|
|
1058
|
+
Args:
|
|
1059
|
+
label: Descriptive label for the event source.
|
|
1060
|
+
event: Streaming payload (dict or StreamingResponse).
|
|
1061
|
+
"""
|
|
1062
|
+
try:
|
|
1063
|
+
payload = event.to_dict() if isinstance(event, StreamingResponse) else event
|
|
1064
|
+
if isinstance(payload, dict):
|
|
1065
|
+
message = json.dumps(payload, default=str)
|
|
1066
|
+
else:
|
|
1067
|
+
message = repr(payload)
|
|
1068
|
+
logger.info("Streaming event (%s): %s", label, message)
|
|
1069
|
+
except Exception as error: # pragma: no cover - defensive logging
|
|
1070
|
+
logger.info("Streaming event (%s): logging failed: %s", label, error)
|
|
1071
|
+
|
|
1072
|
+
def _start_background_recording(self, recorder: SteelSessionRecorder, session_id: str) -> None:
|
|
1073
|
+
"""Start background process to record and convert Steel session to video.
|
|
1074
|
+
|
|
1075
|
+
This method starts a background task to fetch rrweb events and convert them to video
|
|
1076
|
+
after the browser-use agent has completed its task. The recording happens asynchronously
|
|
1077
|
+
to avoid blocking the main execution flow.
|
|
1078
|
+
|
|
1079
|
+
Args:
|
|
1080
|
+
recorder: The Steel session recorder instance.
|
|
1081
|
+
session_id: The Steel session ID to record.
|
|
1082
|
+
|
|
1083
|
+
Note:
|
|
1084
|
+
The recording task is created using asyncio.create_task() to run in the
|
|
1085
|
+
background. This allows the main execution to continue while video
|
|
1086
|
+
generation happens asynchronously.
|
|
1087
|
+
"""
|
|
1088
|
+
recording_task = asyncio.create_task(recorder.record_session_to_video(session_id=session_id))
|
|
1089
|
+
background_tasks: set[asyncio.Task[Any]] = getattr(self, "_background_tasks", set())
|
|
1090
|
+
background_tasks.add(recording_task)
|
|
1091
|
+
recording_task.add_done_callback(background_tasks.discard)
|
|
1092
|
+
self._background_tasks = background_tasks
|
|
1093
|
+
|
|
1094
|
+
def _finalize_streaming_on_cancel(
|
|
1095
|
+
self, streaming_state: StreamingState | None, recorder: SteelSessionRecorder
|
|
1096
|
+
) -> None:
|
|
1097
|
+
"""Mark streaming as complete and trigger recording when execution is cancelled.
|
|
1098
|
+
|
|
1099
|
+
Args:
|
|
1100
|
+
streaming_state: Current state container for streaming progress.
|
|
1101
|
+
recorder: Recorder instance responsible for exporting session recordings.
|
|
1102
|
+
"""
|
|
1103
|
+
if not streaming_state or streaming_state.is_complete:
|
|
1104
|
+
return
|
|
1105
|
+
|
|
1106
|
+
streaming_state.is_complete = True
|
|
1107
|
+
if not streaming_state.terminal_error:
|
|
1108
|
+
streaming_state.terminal_error = "Execution cancelled by upstream request."
|
|
1109
|
+
|
|
1110
|
+
if streaming_state.session_id and not streaming_state.recording_started:
|
|
1111
|
+
self._start_background_recording(recorder, streaming_state.session_id)
|
|
1112
|
+
streaming_state.recording_started = True
|