optexity-browser-use 0.9.5__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.
- browser_use/__init__.py +157 -0
- browser_use/actor/__init__.py +11 -0
- browser_use/actor/element.py +1175 -0
- browser_use/actor/mouse.py +134 -0
- browser_use/actor/page.py +561 -0
- browser_use/actor/playground/flights.py +41 -0
- browser_use/actor/playground/mixed_automation.py +54 -0
- browser_use/actor/playground/playground.py +236 -0
- browser_use/actor/utils.py +176 -0
- browser_use/agent/cloud_events.py +282 -0
- browser_use/agent/gif.py +424 -0
- browser_use/agent/judge.py +170 -0
- browser_use/agent/message_manager/service.py +473 -0
- browser_use/agent/message_manager/utils.py +52 -0
- browser_use/agent/message_manager/views.py +98 -0
- browser_use/agent/prompts.py +413 -0
- browser_use/agent/service.py +2316 -0
- browser_use/agent/system_prompt.md +185 -0
- browser_use/agent/system_prompt_flash.md +10 -0
- browser_use/agent/system_prompt_no_thinking.md +183 -0
- browser_use/agent/views.py +743 -0
- browser_use/browser/__init__.py +41 -0
- browser_use/browser/cloud/cloud.py +203 -0
- browser_use/browser/cloud/views.py +89 -0
- browser_use/browser/events.py +578 -0
- browser_use/browser/profile.py +1158 -0
- browser_use/browser/python_highlights.py +548 -0
- browser_use/browser/session.py +3225 -0
- browser_use/browser/session_manager.py +399 -0
- browser_use/browser/video_recorder.py +162 -0
- browser_use/browser/views.py +200 -0
- browser_use/browser/watchdog_base.py +260 -0
- browser_use/browser/watchdogs/__init__.py +0 -0
- browser_use/browser/watchdogs/aboutblank_watchdog.py +253 -0
- browser_use/browser/watchdogs/crash_watchdog.py +335 -0
- browser_use/browser/watchdogs/default_action_watchdog.py +2729 -0
- browser_use/browser/watchdogs/dom_watchdog.py +817 -0
- browser_use/browser/watchdogs/downloads_watchdog.py +1277 -0
- browser_use/browser/watchdogs/local_browser_watchdog.py +461 -0
- browser_use/browser/watchdogs/permissions_watchdog.py +43 -0
- browser_use/browser/watchdogs/popups_watchdog.py +143 -0
- browser_use/browser/watchdogs/recording_watchdog.py +126 -0
- browser_use/browser/watchdogs/screenshot_watchdog.py +62 -0
- browser_use/browser/watchdogs/security_watchdog.py +280 -0
- browser_use/browser/watchdogs/storage_state_watchdog.py +335 -0
- browser_use/cli.py +2359 -0
- browser_use/code_use/__init__.py +16 -0
- browser_use/code_use/formatting.py +192 -0
- browser_use/code_use/namespace.py +665 -0
- browser_use/code_use/notebook_export.py +276 -0
- browser_use/code_use/service.py +1340 -0
- browser_use/code_use/system_prompt.md +574 -0
- browser_use/code_use/utils.py +150 -0
- browser_use/code_use/views.py +171 -0
- browser_use/config.py +505 -0
- browser_use/controller/__init__.py +3 -0
- browser_use/dom/enhanced_snapshot.py +161 -0
- browser_use/dom/markdown_extractor.py +169 -0
- browser_use/dom/playground/extraction.py +312 -0
- browser_use/dom/playground/multi_act.py +32 -0
- browser_use/dom/serializer/clickable_elements.py +200 -0
- browser_use/dom/serializer/code_use_serializer.py +287 -0
- browser_use/dom/serializer/eval_serializer.py +478 -0
- browser_use/dom/serializer/html_serializer.py +212 -0
- browser_use/dom/serializer/paint_order.py +197 -0
- browser_use/dom/serializer/serializer.py +1170 -0
- browser_use/dom/service.py +825 -0
- browser_use/dom/utils.py +129 -0
- browser_use/dom/views.py +906 -0
- browser_use/exceptions.py +5 -0
- browser_use/filesystem/__init__.py +0 -0
- browser_use/filesystem/file_system.py +619 -0
- browser_use/init_cmd.py +376 -0
- browser_use/integrations/gmail/__init__.py +24 -0
- browser_use/integrations/gmail/actions.py +115 -0
- browser_use/integrations/gmail/service.py +225 -0
- browser_use/llm/__init__.py +155 -0
- browser_use/llm/anthropic/chat.py +242 -0
- browser_use/llm/anthropic/serializer.py +312 -0
- browser_use/llm/aws/__init__.py +36 -0
- browser_use/llm/aws/chat_anthropic.py +242 -0
- browser_use/llm/aws/chat_bedrock.py +289 -0
- browser_use/llm/aws/serializer.py +257 -0
- browser_use/llm/azure/chat.py +91 -0
- browser_use/llm/base.py +57 -0
- browser_use/llm/browser_use/__init__.py +3 -0
- browser_use/llm/browser_use/chat.py +201 -0
- browser_use/llm/cerebras/chat.py +193 -0
- browser_use/llm/cerebras/serializer.py +109 -0
- browser_use/llm/deepseek/chat.py +212 -0
- browser_use/llm/deepseek/serializer.py +109 -0
- browser_use/llm/exceptions.py +29 -0
- browser_use/llm/google/__init__.py +3 -0
- browser_use/llm/google/chat.py +542 -0
- browser_use/llm/google/serializer.py +120 -0
- browser_use/llm/groq/chat.py +229 -0
- browser_use/llm/groq/parser.py +158 -0
- browser_use/llm/groq/serializer.py +159 -0
- browser_use/llm/messages.py +238 -0
- browser_use/llm/models.py +271 -0
- browser_use/llm/oci_raw/__init__.py +10 -0
- browser_use/llm/oci_raw/chat.py +443 -0
- browser_use/llm/oci_raw/serializer.py +229 -0
- browser_use/llm/ollama/chat.py +97 -0
- browser_use/llm/ollama/serializer.py +143 -0
- browser_use/llm/openai/chat.py +264 -0
- browser_use/llm/openai/like.py +15 -0
- browser_use/llm/openai/serializer.py +165 -0
- browser_use/llm/openrouter/chat.py +211 -0
- browser_use/llm/openrouter/serializer.py +26 -0
- browser_use/llm/schema.py +176 -0
- browser_use/llm/views.py +48 -0
- browser_use/logging_config.py +330 -0
- browser_use/mcp/__init__.py +18 -0
- browser_use/mcp/__main__.py +12 -0
- browser_use/mcp/client.py +544 -0
- browser_use/mcp/controller.py +264 -0
- browser_use/mcp/server.py +1114 -0
- browser_use/observability.py +204 -0
- browser_use/py.typed +0 -0
- browser_use/sandbox/__init__.py +41 -0
- browser_use/sandbox/sandbox.py +637 -0
- browser_use/sandbox/views.py +132 -0
- browser_use/screenshots/__init__.py +1 -0
- browser_use/screenshots/service.py +52 -0
- browser_use/sync/__init__.py +6 -0
- browser_use/sync/auth.py +357 -0
- browser_use/sync/service.py +161 -0
- browser_use/telemetry/__init__.py +51 -0
- browser_use/telemetry/service.py +112 -0
- browser_use/telemetry/views.py +101 -0
- browser_use/tokens/__init__.py +0 -0
- browser_use/tokens/custom_pricing.py +24 -0
- browser_use/tokens/mappings.py +4 -0
- browser_use/tokens/service.py +580 -0
- browser_use/tokens/views.py +108 -0
- browser_use/tools/registry/service.py +572 -0
- browser_use/tools/registry/views.py +174 -0
- browser_use/tools/service.py +1675 -0
- browser_use/tools/utils.py +82 -0
- browser_use/tools/views.py +100 -0
- browser_use/utils.py +670 -0
- optexity_browser_use-0.9.5.dist-info/METADATA +344 -0
- optexity_browser_use-0.9.5.dist-info/RECORD +147 -0
- optexity_browser_use-0.9.5.dist-info/WHEEL +4 -0
- optexity_browser_use-0.9.5.dist-info/entry_points.txt +3 -0
- optexity_browser_use-0.9.5.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Utility functions for code-use agent."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def truncate_message_content(content: str, max_length: int = 10000) -> str:
|
|
7
|
+
"""Truncate message content to max_length characters for history."""
|
|
8
|
+
if len(content) <= max_length:
|
|
9
|
+
return content
|
|
10
|
+
# Truncate and add marker
|
|
11
|
+
return content[:max_length] + f'\n\n[... truncated {len(content) - max_length} characters for history]'
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def detect_token_limit_issue(
|
|
15
|
+
completion: str,
|
|
16
|
+
completion_tokens: int | None,
|
|
17
|
+
max_tokens: int | None,
|
|
18
|
+
stop_reason: str | None,
|
|
19
|
+
) -> tuple[bool, str | None]:
|
|
20
|
+
"""
|
|
21
|
+
Detect if the LLM response hit token limits or is repetitive garbage.
|
|
22
|
+
|
|
23
|
+
Returns: (is_problematic, error_message)
|
|
24
|
+
"""
|
|
25
|
+
# Check 1: Stop reason indicates max_tokens
|
|
26
|
+
if stop_reason == 'max_tokens':
|
|
27
|
+
return True, f'Response terminated due to max_tokens limit (stop_reason: {stop_reason})'
|
|
28
|
+
|
|
29
|
+
# Check 2: Used 90%+ of max_tokens (if we have both values)
|
|
30
|
+
if completion_tokens is not None and max_tokens is not None and max_tokens > 0:
|
|
31
|
+
usage_ratio = completion_tokens / max_tokens
|
|
32
|
+
if usage_ratio >= 0.9:
|
|
33
|
+
return True, f'Response used {usage_ratio:.1%} of max_tokens ({completion_tokens}/{max_tokens})'
|
|
34
|
+
|
|
35
|
+
# Check 3: Last 6 characters repeat 40+ times (repetitive garbage)
|
|
36
|
+
if len(completion) >= 6:
|
|
37
|
+
last_6 = completion[-6:]
|
|
38
|
+
repetition_count = completion.count(last_6)
|
|
39
|
+
if repetition_count >= 40:
|
|
40
|
+
return True, f'Repetitive output detected: last 6 chars "{last_6}" appears {repetition_count} times'
|
|
41
|
+
|
|
42
|
+
return False, None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def extract_url_from_task(task: str) -> str | None:
|
|
46
|
+
"""Extract URL from task string using naive pattern matching."""
|
|
47
|
+
# Remove email addresses from task before looking for URLs
|
|
48
|
+
task_without_emails = re.sub(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', '', task)
|
|
49
|
+
|
|
50
|
+
# Look for common URL patterns
|
|
51
|
+
patterns = [
|
|
52
|
+
r'https?://[^\s<>"\']+', # Full URLs with http/https
|
|
53
|
+
r'(?:www\.)?[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*\.[a-zA-Z]{2,}(?:/[^\s<>"\']*)?', # Domain names with subdomains and optional paths
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
found_urls = []
|
|
57
|
+
for pattern in patterns:
|
|
58
|
+
matches = re.finditer(pattern, task_without_emails)
|
|
59
|
+
for match in matches:
|
|
60
|
+
url = match.group(0)
|
|
61
|
+
|
|
62
|
+
# Remove trailing punctuation that's not part of URLs
|
|
63
|
+
url = re.sub(r'[.,;:!?()\[\]]+$', '', url)
|
|
64
|
+
# Add https:// if missing
|
|
65
|
+
if not url.startswith(('http://', 'https://')):
|
|
66
|
+
url = 'https://' + url
|
|
67
|
+
found_urls.append(url)
|
|
68
|
+
|
|
69
|
+
unique_urls = list(set(found_urls))
|
|
70
|
+
# If multiple URLs found, skip auto-navigation to avoid ambiguity
|
|
71
|
+
if len(unique_urls) > 1:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
# If exactly one URL found, return it
|
|
75
|
+
if len(unique_urls) == 1:
|
|
76
|
+
return unique_urls[0]
|
|
77
|
+
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def extract_code_blocks(text: str) -> dict[str, str]:
|
|
82
|
+
"""Extract all code blocks from markdown response.
|
|
83
|
+
|
|
84
|
+
Supports:
|
|
85
|
+
- ```python, ```js, ```javascript, ```bash, ```markdown, ```md
|
|
86
|
+
- Named blocks: ```js variable_name → saved as 'variable_name' in namespace
|
|
87
|
+
- Nested blocks: Use 4+ backticks for outer block when inner content has 3 backticks
|
|
88
|
+
|
|
89
|
+
Returns dict mapping block_name -> content
|
|
90
|
+
|
|
91
|
+
Note: Python blocks are NO LONGER COMBINED. Each python block executes separately
|
|
92
|
+
to allow sequential execution with JS/bash blocks in between.
|
|
93
|
+
"""
|
|
94
|
+
# Pattern to match code blocks with language identifier and optional variable name
|
|
95
|
+
# Matches: ```lang\n or ```lang varname\n or ````+lang\n (4+ backticks for nested blocks)
|
|
96
|
+
# Uses non-greedy matching and backreferences to match opening/closing backticks
|
|
97
|
+
pattern = r'(`{3,})(\w+)(?:\s+(\w+))?\n(.*?)\1(?:\n|$)'
|
|
98
|
+
matches = re.findall(pattern, text, re.DOTALL)
|
|
99
|
+
|
|
100
|
+
blocks: dict[str, str] = {}
|
|
101
|
+
python_block_counter = 0
|
|
102
|
+
|
|
103
|
+
for backticks, lang, var_name, content in matches:
|
|
104
|
+
lang = lang.lower()
|
|
105
|
+
|
|
106
|
+
# Normalize language names
|
|
107
|
+
if lang in ('javascript', 'js'):
|
|
108
|
+
lang_normalized = 'js'
|
|
109
|
+
elif lang in ('markdown', 'md'):
|
|
110
|
+
lang_normalized = 'markdown'
|
|
111
|
+
elif lang in ('sh', 'shell'):
|
|
112
|
+
lang_normalized = 'bash'
|
|
113
|
+
elif lang == 'python':
|
|
114
|
+
lang_normalized = 'python'
|
|
115
|
+
else:
|
|
116
|
+
# Unknown language, skip
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
# Only process supported types
|
|
120
|
+
if lang_normalized in ('python', 'js', 'bash', 'markdown'):
|
|
121
|
+
content = content.rstrip() # Only strip trailing whitespace, preserve leading for indentation
|
|
122
|
+
if content:
|
|
123
|
+
# Determine the key to use
|
|
124
|
+
if var_name:
|
|
125
|
+
# Named block - use the variable name
|
|
126
|
+
block_key = var_name
|
|
127
|
+
blocks[block_key] = content
|
|
128
|
+
elif lang_normalized == 'python':
|
|
129
|
+
# Unnamed Python blocks - give each a unique key to preserve order
|
|
130
|
+
block_key = f'python_{python_block_counter}'
|
|
131
|
+
blocks[block_key] = content
|
|
132
|
+
python_block_counter += 1
|
|
133
|
+
else:
|
|
134
|
+
# Other unnamed blocks (js, bash, markdown) - keep last one only
|
|
135
|
+
blocks[lang_normalized] = content
|
|
136
|
+
|
|
137
|
+
# If we have multiple python blocks, mark the first one as 'python' for backward compat
|
|
138
|
+
if python_block_counter > 0:
|
|
139
|
+
blocks['python'] = blocks['python_0']
|
|
140
|
+
|
|
141
|
+
# Fallback: if no python block but there's generic ``` block, treat as python
|
|
142
|
+
if python_block_counter == 0 and 'python' not in blocks:
|
|
143
|
+
generic_pattern = r'```\n(.*?)```'
|
|
144
|
+
generic_matches = re.findall(generic_pattern, text, re.DOTALL)
|
|
145
|
+
if generic_matches:
|
|
146
|
+
combined = '\n\n'.join(m.strip() for m in generic_matches if m.strip())
|
|
147
|
+
if combined:
|
|
148
|
+
blocks['python'] = combined
|
|
149
|
+
|
|
150
|
+
return blocks
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Data models for code-use mode."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
7
|
+
from uuid_extensions import uuid7str
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CellType(str, Enum):
|
|
11
|
+
"""Type of notebook cell."""
|
|
12
|
+
|
|
13
|
+
CODE = 'code'
|
|
14
|
+
MARKDOWN = 'markdown'
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ExecutionStatus(str, Enum):
|
|
18
|
+
"""Execution status of a cell."""
|
|
19
|
+
|
|
20
|
+
PENDING = 'pending'
|
|
21
|
+
RUNNING = 'running'
|
|
22
|
+
SUCCESS = 'success'
|
|
23
|
+
ERROR = 'error'
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CodeCell(BaseModel):
|
|
27
|
+
"""Represents a code cell in the notebook-like execution."""
|
|
28
|
+
|
|
29
|
+
model_config = ConfigDict(extra='forbid')
|
|
30
|
+
|
|
31
|
+
id: str = Field(default_factory=uuid7str)
|
|
32
|
+
cell_type: CellType = CellType.CODE
|
|
33
|
+
source: str = Field(description='The code to execute')
|
|
34
|
+
output: str | None = Field(default=None, description='The output of the code execution')
|
|
35
|
+
execution_count: int | None = Field(default=None, description='The execution count')
|
|
36
|
+
status: ExecutionStatus = Field(default=ExecutionStatus.PENDING)
|
|
37
|
+
error: str | None = Field(default=None, description='Error message if execution failed')
|
|
38
|
+
browser_state: str | None = Field(default=None, description='Browser state after execution')
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class NotebookSession(BaseModel):
|
|
42
|
+
"""Represents a notebook-like session."""
|
|
43
|
+
|
|
44
|
+
model_config = ConfigDict(extra='forbid')
|
|
45
|
+
|
|
46
|
+
id: str = Field(default_factory=uuid7str)
|
|
47
|
+
cells: list[CodeCell] = Field(default_factory=list)
|
|
48
|
+
current_execution_count: int = Field(default=0)
|
|
49
|
+
namespace: dict[str, Any] = Field(default_factory=dict, description='Current namespace state')
|
|
50
|
+
|
|
51
|
+
def add_cell(self, source: str) -> CodeCell:
|
|
52
|
+
"""Add a new code cell to the session."""
|
|
53
|
+
cell = CodeCell(source=source)
|
|
54
|
+
self.cells.append(cell)
|
|
55
|
+
return cell
|
|
56
|
+
|
|
57
|
+
def get_cell(self, cell_id: str) -> CodeCell | None:
|
|
58
|
+
"""Get a cell by ID."""
|
|
59
|
+
for cell in self.cells:
|
|
60
|
+
if cell.id == cell_id:
|
|
61
|
+
return cell
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
def get_latest_cell(self) -> CodeCell | None:
|
|
65
|
+
"""Get the most recently added cell."""
|
|
66
|
+
if self.cells:
|
|
67
|
+
return self.cells[-1]
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
def increment_execution_count(self) -> int:
|
|
71
|
+
"""Increment and return the execution count."""
|
|
72
|
+
self.current_execution_count += 1
|
|
73
|
+
return self.current_execution_count
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class NotebookExport(BaseModel):
|
|
77
|
+
"""Export format for Jupyter notebook."""
|
|
78
|
+
|
|
79
|
+
model_config = ConfigDict(extra='forbid')
|
|
80
|
+
|
|
81
|
+
nbformat: int = Field(default=4)
|
|
82
|
+
nbformat_minor: int = Field(default=5)
|
|
83
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
84
|
+
cells: list[dict[str, Any]] = Field(default_factory=list)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class CodeAgentModelOutput(BaseModel):
|
|
88
|
+
"""Model output for CodeAgent - contains the code and full LLM response."""
|
|
89
|
+
|
|
90
|
+
model_config = ConfigDict(extra='forbid')
|
|
91
|
+
|
|
92
|
+
model_output: str = Field(description='The extracted code from the LLM response')
|
|
93
|
+
full_response: str = Field(description='The complete LLM response including any text/reasoning')
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class CodeAgentResult(BaseModel):
|
|
97
|
+
"""Result of executing a code cell in CodeAgent."""
|
|
98
|
+
|
|
99
|
+
model_config = ConfigDict(extra='forbid')
|
|
100
|
+
|
|
101
|
+
extracted_content: str | None = Field(default=None, description='Output from code execution')
|
|
102
|
+
error: str | None = Field(default=None, description='Error message if execution failed')
|
|
103
|
+
is_done: bool = Field(default=False, description='Whether task is marked as done')
|
|
104
|
+
success: bool | None = Field(default=None, description='Self-reported success from done() call')
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class CodeAgentState(BaseModel):
|
|
108
|
+
"""State information for a CodeAgent step."""
|
|
109
|
+
|
|
110
|
+
model_config = ConfigDict(extra='forbid', arbitrary_types_allowed=True)
|
|
111
|
+
|
|
112
|
+
url: str | None = Field(default=None, description='Current page URL')
|
|
113
|
+
title: str | None = Field(default=None, description='Current page title')
|
|
114
|
+
screenshot_path: str | None = Field(default=None, description='Path to screenshot file')
|
|
115
|
+
|
|
116
|
+
def get_screenshot(self) -> str | None:
|
|
117
|
+
"""Load screenshot from disk and return as base64 string."""
|
|
118
|
+
if not self.screenshot_path:
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
import base64
|
|
122
|
+
from pathlib import Path
|
|
123
|
+
|
|
124
|
+
path_obj = Path(self.screenshot_path)
|
|
125
|
+
if not path_obj.exists():
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
with open(path_obj, 'rb') as f:
|
|
130
|
+
screenshot_data = f.read()
|
|
131
|
+
return base64.b64encode(screenshot_data).decode('utf-8')
|
|
132
|
+
except Exception:
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class CodeAgentStepMetadata(BaseModel):
|
|
137
|
+
"""Metadata for a single CodeAgent step including timing and token information."""
|
|
138
|
+
|
|
139
|
+
model_config = ConfigDict(extra='forbid')
|
|
140
|
+
|
|
141
|
+
input_tokens: int | None = Field(default=None, description='Number of input tokens used')
|
|
142
|
+
output_tokens: int | None = Field(default=None, description='Number of output tokens used')
|
|
143
|
+
step_start_time: float = Field(description='Step start timestamp (Unix time)')
|
|
144
|
+
step_end_time: float = Field(description='Step end timestamp (Unix time)')
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def duration_seconds(self) -> float:
|
|
148
|
+
"""Calculate step duration in seconds."""
|
|
149
|
+
return self.step_end_time - self.step_start_time
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class CodeAgentHistory(BaseModel):
|
|
153
|
+
"""History item for CodeAgent actions."""
|
|
154
|
+
|
|
155
|
+
model_config = ConfigDict(extra='forbid', arbitrary_types_allowed=True)
|
|
156
|
+
|
|
157
|
+
model_output: CodeAgentModelOutput | None = Field(default=None, description='LLM output for this step')
|
|
158
|
+
result: list[CodeAgentResult] = Field(default_factory=list, description='Results from code execution')
|
|
159
|
+
state: CodeAgentState = Field(description='Browser state at this step')
|
|
160
|
+
metadata: CodeAgentStepMetadata | None = Field(default=None, description='Step timing and token metadata')
|
|
161
|
+
screenshot_path: str | None = Field(default=None, description='Legacy field for screenshot path')
|
|
162
|
+
|
|
163
|
+
def model_dump(self, **kwargs) -> dict[str, Any]:
|
|
164
|
+
"""Custom serialization for CodeAgentHistory."""
|
|
165
|
+
return {
|
|
166
|
+
'model_output': self.model_output.model_dump() if self.model_output else None,
|
|
167
|
+
'result': [r.model_dump() for r in self.result],
|
|
168
|
+
'state': self.state.model_dump(),
|
|
169
|
+
'metadata': self.metadata.model_dump() if self.metadata else None,
|
|
170
|
+
'screenshot_path': self.screenshot_path,
|
|
171
|
+
}
|