connectonion 0.5.10__tar.gz → 0.6.1__tar.gz
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.
- {connectonion-0.5.10 → connectonion-0.6.1}/PKG-INFO +4 -3
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/__init__.py +17 -16
- connectonion-0.6.1/connectonion/cli/browser_agent/browser.py +586 -0
- connectonion-0.6.1/connectonion/cli/browser_agent/scroll_strategies.py +276 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/cli/commands/copy_commands.py +24 -1
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/cli/commands/deploy_commands.py +15 -0
- connectonion-0.6.1/connectonion/cli/commands/eval_commands.py +286 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/cli/commands/project_cmd_lib.py +1 -1
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/cli/main.py +11 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/console.py +5 -5
- connectonion-0.6.1/connectonion/core/__init__.py +53 -0
- {connectonion-0.5.10/connectonion → connectonion-0.6.1/connectonion/core}/agent.py +18 -15
- {connectonion-0.5.10/connectonion → connectonion-0.6.1/connectonion/core}/llm.py +9 -19
- {connectonion-0.5.10/connectonion → connectonion-0.6.1/connectonion/core}/tool_executor.py +3 -2
- {connectonion-0.5.10/connectonion → connectonion-0.6.1/connectonion/core}/tool_factory.py +3 -1
- connectonion-0.6.1/connectonion/debug/__init__.py +51 -0
- connectonion-0.5.10/connectonion/interactive_debugger.py → connectonion-0.6.1/connectonion/debug/auto_debug.py +7 -7
- {connectonion-0.5.10/connectonion → connectonion-0.6.1/connectonion/debug}/auto_debug_exception.py +3 -3
- connectonion-0.5.10/connectonion/debugger_ui.py → connectonion-0.6.1/connectonion/debug/auto_debug_ui.py +1 -1
- {connectonion-0.5.10/connectonion → connectonion-0.6.1/connectonion/debug}/debug_explainer/explain_agent.py +1 -1
- {connectonion-0.5.10/connectonion → connectonion-0.6.1/connectonion/debug}/debug_explainer/explain_context.py +1 -1
- {connectonion-0.5.10/connectonion → connectonion-0.6.1/connectonion/debug}/execution_analyzer/execution_analysis.py +1 -1
- connectonion-0.6.1/connectonion/debug/runtime_inspector/__init__.py +13 -0
- {connectonion-0.5.10/connectonion/debug_agent → connectonion-0.6.1/connectonion/debug/runtime_inspector}/agent.py +1 -1
- {connectonion-0.5.10/connectonion → connectonion-0.6.1/connectonion/debug}/xray.py +1 -1
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/llm_do.py +1 -1
- connectonion-0.6.1/connectonion/logger.py +470 -0
- connectonion-0.6.1/connectonion/network/__init__.py +37 -0
- {connectonion-0.5.10/connectonion → connectonion-0.6.1/connectonion/network}/announce.py +1 -1
- {connectonion-0.5.10/connectonion → connectonion-0.6.1/connectonion/network}/asgi.py +122 -2
- {connectonion-0.5.10/connectonion → connectonion-0.6.1/connectonion/network}/connect.py +1 -1
- connectonion-0.6.1/connectonion/network/connection.py +123 -0
- {connectonion-0.5.10/connectonion → connectonion-0.6.1/connectonion/network}/host.py +31 -11
- {connectonion-0.5.10/connectonion → connectonion-0.6.1/connectonion/network}/trust.py +1 -1
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/tui/__init__.py +22 -0
- connectonion-0.6.1/connectonion/tui/chat.py +647 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/useful_events_handlers/reflect.py +2 -2
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/useful_plugins/__init__.py +4 -3
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/useful_plugins/calendar_plugin.py +2 -2
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/useful_plugins/eval.py +2 -2
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/useful_plugins/gmail_plugin.py +2 -2
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/useful_plugins/image_result_formatter.py +2 -2
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/useful_plugins/re_act.py +2 -2
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/useful_plugins/shell_approval.py +2 -2
- connectonion-0.6.1/connectonion/useful_plugins/ui_stream.py +164 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/docs/network/README.md +1 -0
- connectonion-0.6.1/docs/tui/README.md +95 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/pyproject.toml +4 -2
- connectonion-0.5.10/connectonion/cli/browser_agent/browser.py +0 -243
- connectonion-0.5.10/connectonion/debug_agent/__init__.py +0 -13
- connectonion-0.5.10/connectonion/logger.py +0 -300
- connectonion-0.5.10/docs/tui/README.md +0 -56
- {connectonion-0.5.10 → connectonion-0.6.1}/.gitignore +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/README.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/address.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/cli/__init__.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/cli/browser_agent/__init__.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/cli/browser_agent/prompt.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/cli/commands/__init__.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/cli/commands/auth_commands.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/cli/commands/browser_commands.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/cli/commands/create.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/cli/commands/doctor_commands.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/cli/commands/init.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/cli/commands/reset_commands.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/cli/commands/status_commands.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/cli/docs/co-vibecoding-principles-docs-contexts-all-in-one.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/cli/docs/connectonion.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/cli/docs.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/cli/templates/meta-agent/README.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/cli/templates/meta-agent/agent.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/cli/templates/meta-agent/prompts/answer_prompt.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/cli/templates/meta-agent/prompts/docs_retrieve_prompt.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/cli/templates/meta-agent/prompts/metagent.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/cli/templates/meta-agent/prompts/think_prompt.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/cli/templates/minimal/README.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/cli/templates/minimal/agent.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/cli/templates/playwright/README.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/cli/templates/playwright/agent.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/cli/templates/playwright/prompt.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/cli/templates/playwright/requirements.txt +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/cli/templates/web-research/agent.py +0 -0
- {connectonion-0.5.10/connectonion → connectonion-0.6.1/connectonion/core}/events.py +0 -0
- {connectonion-0.5.10/connectonion → connectonion-0.6.1/connectonion/core}/tool_registry.py +0 -0
- {connectonion-0.5.10/connectonion → connectonion-0.6.1/connectonion/core}/usage.py +0 -0
- {connectonion-0.5.10/connectonion → connectonion-0.6.1/connectonion/debug}/debug_explainer/__init__.py +0 -0
- {connectonion-0.5.10/connectonion → connectonion-0.6.1/connectonion/debug}/debug_explainer/explainer_prompt.md +0 -0
- {connectonion-0.5.10/connectonion → connectonion-0.6.1/connectonion/debug}/debug_explainer/root_cause_analysis_prompt.md +0 -0
- {connectonion-0.5.10/connectonion → connectonion-0.6.1/connectonion/debug}/decorators.py +0 -0
- {connectonion-0.5.10/connectonion → connectonion-0.6.1/connectonion/debug}/execution_analyzer/__init__.py +0 -0
- {connectonion-0.5.10/connectonion → connectonion-0.6.1/connectonion/debug}/execution_analyzer/execution_analysis_prompt.md +0 -0
- {connectonion-0.5.10/connectonion/debug_agent → connectonion-0.6.1/connectonion/debug/runtime_inspector}/prompts/debug_assistant.md +0 -0
- {connectonion-0.5.10/connectonion/debug_agent → connectonion-0.6.1/connectonion/debug/runtime_inspector}/runtime_inspector.py +0 -0
- {connectonion-0.5.10/connectonion → connectonion-0.6.1/connectonion/network}/relay.py +0 -0
- {connectonion-0.5.10/connectonion → connectonion-0.6.1/connectonion/network}/static/docs.html +0 -0
- {connectonion-0.5.10/connectonion → connectonion-0.6.1/connectonion/network}/trust_agents.py +0 -0
- {connectonion-0.5.10/connectonion → connectonion-0.6.1/connectonion/network}/trust_functions.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/prompt_files/__init__.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/prompt_files/analyze_contact.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/prompt_files/eval_expected.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/prompt_files/react_evaluate.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/prompt_files/react_plan.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/prompt_files/reflect.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/prompts.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/transcribe.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/tui/divider.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/tui/dropdown.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/tui/footer.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/tui/fuzzy.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/tui/input.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/tui/keys.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/tui/pick.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/tui/providers.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/tui/status_bar.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/useful_events_handlers/__init__.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/useful_tools/__init__.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/useful_tools/diff_writer.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/useful_tools/get_emails.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/useful_tools/gmail.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/useful_tools/google_calendar.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/useful_tools/memory.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/useful_tools/microsoft_calendar.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/useful_tools/outlook.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/useful_tools/send_email.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/useful_tools/shell.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/useful_tools/slash_command.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/useful_tools/terminal.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/useful_tools/todo_list.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/connectonion/useful_tools/web_fetch.py +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/docs/README.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/docs/cli/README.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/docs/debug/README.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/docs/integrations/README.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/docs/templates/README.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/docs/useful_plugins/README.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/docs/useful_tools/README.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/examples/README.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/examples/browser-agent/README.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/examples/email-agent/README.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/examples/simple-agent/README.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/prompts/README.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/prompts/formats/README.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/tests/README.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/tests/cli/README.md +0 -0
- {connectonion-0.5.10 → connectonion-0.6.1}/tests/cli/aws/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: connectonion
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: A simple Python framework for creating AI agents with behavior tracking
|
|
5
5
|
Project-URL: Homepage, https://github.com/openonion/connectonion
|
|
6
6
|
Project-URL: Documentation, https://docs.connectonion.com
|
|
@@ -30,6 +30,7 @@ Requires-Dist: httpx>=0.24.0
|
|
|
30
30
|
Requires-Dist: litellm>=1.0.0
|
|
31
31
|
Requires-Dist: mnemonic>=0.20
|
|
32
32
|
Requires-Dist: openai>=1.0.0
|
|
33
|
+
Requires-Dist: playwright>=1.40.0
|
|
33
34
|
Requires-Dist: pydantic>=2.0.0
|
|
34
35
|
Requires-Dist: pyjwt>=2.0.0
|
|
35
36
|
Requires-Dist: pynacl>=1.5.0
|
|
@@ -38,12 +39,12 @@ Requires-Dist: pyyaml>=6.0.0
|
|
|
38
39
|
Requires-Dist: questionary>=2.0.0
|
|
39
40
|
Requires-Dist: requests>=2.25.0
|
|
40
41
|
Requires-Dist: rich>=13.0.0
|
|
42
|
+
Requires-Dist: textual-autocomplete>=3.0.0
|
|
43
|
+
Requires-Dist: textual>=0.86.0
|
|
41
44
|
Requires-Dist: toml>=0.10.2
|
|
42
45
|
Requires-Dist: typer>=0.20.0
|
|
43
46
|
Requires-Dist: uvicorn>=0.20.0
|
|
44
47
|
Requires-Dist: websockets>=11.0.0
|
|
45
|
-
Provides-Extra: browser
|
|
46
|
-
Requires-Dist: playwright>=1.40.0; extra == 'browser'
|
|
47
48
|
Provides-Extra: dev
|
|
48
49
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
49
50
|
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""ConnectOnion - A simple agent framework with behavior tracking."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.
|
|
3
|
+
__version__ = "0.6.1"
|
|
4
4
|
|
|
5
5
|
# Auto-load .env files for the entire framework
|
|
6
6
|
from dotenv import load_dotenv
|
|
@@ -10,20 +10,8 @@ from pathlib import Path as _Path
|
|
|
10
10
|
# NOT from the module's location (framework directory)
|
|
11
11
|
load_dotenv(_Path.cwd() / ".env")
|
|
12
12
|
|
|
13
|
-
from .
|
|
14
|
-
from .
|
|
15
|
-
from .llm import LLM
|
|
16
|
-
from .logger import Logger
|
|
17
|
-
from .llm_do import llm_do
|
|
18
|
-
from .transcribe import transcribe
|
|
19
|
-
from .prompts import load_system_prompt
|
|
20
|
-
from .xray import xray
|
|
21
|
-
from .decorators import replay, xray_replay
|
|
22
|
-
from .useful_tools import send_email, get_emails, mark_read, mark_unread, Memory, Gmail, GoogleCalendar, Outlook, MicrosoftCalendar, WebFetch, Shell, DiffWriter, pick, yes_no, autocomplete, TodoList, SlashCommand
|
|
23
|
-
from .auto_debug_exception import auto_debug_exception
|
|
24
|
-
from .connect import connect, RemoteAgent
|
|
25
|
-
from .host import host, create_app
|
|
26
|
-
from .events import (
|
|
13
|
+
from .core import Agent, LLM, create_tool_from_function
|
|
14
|
+
from .core import (
|
|
27
15
|
after_user_input,
|
|
28
16
|
before_llm,
|
|
29
17
|
after_llm,
|
|
@@ -32,8 +20,17 @@ from .events import (
|
|
|
32
20
|
after_each_tool,
|
|
33
21
|
after_tools,
|
|
34
22
|
on_error,
|
|
35
|
-
on_complete
|
|
23
|
+
on_complete,
|
|
36
24
|
)
|
|
25
|
+
from .logger import Logger
|
|
26
|
+
from .llm_do import llm_do
|
|
27
|
+
from .transcribe import transcribe
|
|
28
|
+
from .prompts import load_system_prompt
|
|
29
|
+
from .debug import xray, auto_debug_exception, replay, xray_replay
|
|
30
|
+
from .useful_tools import send_email, get_emails, mark_read, mark_unread, Memory, Gmail, GoogleCalendar, Outlook, MicrosoftCalendar, WebFetch, Shell, DiffWriter, pick, yes_no, autocomplete, TodoList, SlashCommand
|
|
31
|
+
from .network import connect, RemoteAgent, host, create_app, Connection
|
|
32
|
+
from .network import relay, announce
|
|
33
|
+
from . import address
|
|
37
34
|
|
|
38
35
|
__all__ = [
|
|
39
36
|
"Agent",
|
|
@@ -68,6 +65,10 @@ __all__ = [
|
|
|
68
65
|
"RemoteAgent",
|
|
69
66
|
"host",
|
|
70
67
|
"create_app",
|
|
68
|
+
"Connection",
|
|
69
|
+
"relay",
|
|
70
|
+
"announce",
|
|
71
|
+
"address",
|
|
71
72
|
"after_user_input",
|
|
72
73
|
"before_llm",
|
|
73
74
|
"after_llm",
|
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
"""Browser Agent for CLI - Natural language browser automation.
|
|
2
|
+
|
|
3
|
+
This module provides a browser automation agent that understands natural language
|
|
4
|
+
requests for browser operations via the ConnectOnion CLI.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- Chrome profile support for persistent sessions (cookies, logins)
|
|
8
|
+
- AI-powered element finding using natural language
|
|
9
|
+
- Form handling: find, fill, submit
|
|
10
|
+
- Screenshot with viewport presets
|
|
11
|
+
- Universal scroll with AI strategy selection
|
|
12
|
+
- Manual login pause for 2FA/CAPTCHA
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import base64
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from typing import Optional, List, Dict, Any
|
|
20
|
+
from connectonion import Agent, llm_do
|
|
21
|
+
from dotenv import load_dotenv
|
|
22
|
+
from pydantic import BaseModel, Field
|
|
23
|
+
|
|
24
|
+
# Default screenshots directory
|
|
25
|
+
SCREENSHOTS_DIR = Path.cwd() / ".tmp"
|
|
26
|
+
|
|
27
|
+
# Check Playwright availability
|
|
28
|
+
try:
|
|
29
|
+
from playwright.sync_api import sync_playwright, Page, Browser, Playwright
|
|
30
|
+
PLAYWRIGHT_AVAILABLE = True
|
|
31
|
+
except ImportError:
|
|
32
|
+
PLAYWRIGHT_AVAILABLE = False
|
|
33
|
+
|
|
34
|
+
# Path to the browser agent system prompt
|
|
35
|
+
PROMPT_PATH = Path(__file__).parent / "prompt.md"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class FormField(BaseModel):
|
|
39
|
+
"""A form field on a web page."""
|
|
40
|
+
name: str = Field(..., description="Field name or identifier")
|
|
41
|
+
label: str = Field(..., description="User-facing label")
|
|
42
|
+
type: str = Field(..., description="Input type (text, email, select, etc.)")
|
|
43
|
+
value: Optional[str] = Field(None, description="Current value")
|
|
44
|
+
required: bool = Field(False, description="Is this field required?")
|
|
45
|
+
options: List[str] = Field(default_factory=list, description="Available options for select/radio")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class BrowserAutomation:
|
|
49
|
+
"""Browser automation with natural language support.
|
|
50
|
+
|
|
51
|
+
Simple interface for complex web interactions.
|
|
52
|
+
Auto-initializes browser on creation for immediate use.
|
|
53
|
+
Supports Chrome profile for persistent sessions.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, use_chrome_profile: bool = False, headless: bool = True):
|
|
57
|
+
"""Initialize browser automation.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
use_chrome_profile: If True, uses your Chrome cookies/sessions.
|
|
61
|
+
Chrome must be closed before running.
|
|
62
|
+
headless: If True, browser runs without visible window (default True).
|
|
63
|
+
"""
|
|
64
|
+
self.playwright: Optional[Playwright] = None
|
|
65
|
+
self.browser: Optional[Browser] = None
|
|
66
|
+
self.page: Optional[Page] = None
|
|
67
|
+
self.current_url: str = ""
|
|
68
|
+
self.form_data: Dict[str, Any] = {}
|
|
69
|
+
self.use_chrome_profile = use_chrome_profile
|
|
70
|
+
self._screenshots = []
|
|
71
|
+
self._headless = headless
|
|
72
|
+
# Auto-initialize browser so it's ready immediately
|
|
73
|
+
self._initialize_browser()
|
|
74
|
+
|
|
75
|
+
def _initialize_browser(self):
|
|
76
|
+
"""Initialize the browser instance on startup."""
|
|
77
|
+
if not PLAYWRIGHT_AVAILABLE:
|
|
78
|
+
return
|
|
79
|
+
self.open_browser(headless=self._headless)
|
|
80
|
+
|
|
81
|
+
def open_browser(self, headless: bool = True) -> str:
|
|
82
|
+
"""Open a new browser window.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
headless: If True, browser runs without visible window.
|
|
86
|
+
|
|
87
|
+
Note: If use_chrome_profile=True, Chrome must be completely closed.
|
|
88
|
+
"""
|
|
89
|
+
if not PLAYWRIGHT_AVAILABLE:
|
|
90
|
+
return "Browser tools not installed. Run: pip install playwright && playwright install chromium"
|
|
91
|
+
|
|
92
|
+
if self.browser:
|
|
93
|
+
return "Browser already open"
|
|
94
|
+
|
|
95
|
+
self.playwright = sync_playwright().start()
|
|
96
|
+
|
|
97
|
+
if self.use_chrome_profile:
|
|
98
|
+
# Use Chromium with Chrome profile copy
|
|
99
|
+
chromium_profile = Path.cwd() / "chromium_automation_profile"
|
|
100
|
+
|
|
101
|
+
if not chromium_profile.exists():
|
|
102
|
+
import shutil
|
|
103
|
+
home = Path.home()
|
|
104
|
+
if os.name == 'nt': # Windows
|
|
105
|
+
source_profile = home / "AppData/Local/Google/Chrome/User Data"
|
|
106
|
+
elif os.uname().sysname == 'Darwin': # macOS
|
|
107
|
+
source_profile = home / "Library/Application Support/Google/Chrome"
|
|
108
|
+
else: # Linux
|
|
109
|
+
source_profile = home / ".config/google-chrome"
|
|
110
|
+
|
|
111
|
+
if source_profile.exists():
|
|
112
|
+
shutil.copytree(
|
|
113
|
+
source_profile,
|
|
114
|
+
chromium_profile,
|
|
115
|
+
ignore=shutil.ignore_patterns('*Cache*', '*cache*', 'Service Worker', 'ShaderCache'),
|
|
116
|
+
dirs_exist_ok=True
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
self.browser = self.playwright.chromium.launch_persistent_context(
|
|
120
|
+
str(chromium_profile),
|
|
121
|
+
headless=headless,
|
|
122
|
+
args=['--disable-blink-features=AutomationControlled'],
|
|
123
|
+
ignore_default_args=['--enable-automation'],
|
|
124
|
+
timeout=120000,
|
|
125
|
+
)
|
|
126
|
+
self.page = self.browser.pages[0] if self.browser.pages else self.browser.new_page()
|
|
127
|
+
self.page.add_init_script("""
|
|
128
|
+
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
|
129
|
+
""")
|
|
130
|
+
return f"Browser opened with Chrome profile: {chromium_profile}"
|
|
131
|
+
else:
|
|
132
|
+
self.browser = self.playwright.chromium.launch(headless=headless)
|
|
133
|
+
self.page = self.browser.new_page()
|
|
134
|
+
return "Browser opened successfully"
|
|
135
|
+
|
|
136
|
+
def go_to(self, url: str) -> str:
|
|
137
|
+
"""Navigate to a URL."""
|
|
138
|
+
if not self.page:
|
|
139
|
+
self.open_browser()
|
|
140
|
+
|
|
141
|
+
if not url.startswith(('http://', 'https://')):
|
|
142
|
+
url = f'https://{url}' if '.' in url else f'http://{url}'
|
|
143
|
+
|
|
144
|
+
self.page.goto(url, wait_until='networkidle', timeout=30000)
|
|
145
|
+
self.page.wait_for_timeout(2000)
|
|
146
|
+
self.current_url = self.page.url
|
|
147
|
+
return f"Navigated to {self.current_url}"
|
|
148
|
+
|
|
149
|
+
def find_element_by_description(self, description: str) -> str:
|
|
150
|
+
"""Find element using natural language description.
|
|
151
|
+
|
|
152
|
+
Uses AI to analyze HTML and find the best matching element.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
description: e.g., "the submit button", "email input field"
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
CSS selector for the element, or error message
|
|
159
|
+
"""
|
|
160
|
+
if not self.page:
|
|
161
|
+
return "Browser not open"
|
|
162
|
+
|
|
163
|
+
html = self.page.content()
|
|
164
|
+
|
|
165
|
+
class ElementSelector(BaseModel):
|
|
166
|
+
selector: str = Field(..., description="CSS selector for the element")
|
|
167
|
+
confidence: float = Field(..., description="Confidence score 0-1")
|
|
168
|
+
explanation: str = Field(..., description="Why this element matches")
|
|
169
|
+
|
|
170
|
+
result = llm_do(
|
|
171
|
+
f"""Analyze this HTML and find the CSS selector for: "{description}"
|
|
172
|
+
|
|
173
|
+
HTML (first 15000 chars): {html[:15000]}
|
|
174
|
+
|
|
175
|
+
Return the most specific CSS selector that uniquely identifies this element.
|
|
176
|
+
""",
|
|
177
|
+
output=ElementSelector,
|
|
178
|
+
model="gpt-4o",
|
|
179
|
+
temperature=0.1
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
if self.page.locator(result.selector).count() > 0:
|
|
183
|
+
return result.selector
|
|
184
|
+
else:
|
|
185
|
+
return f"Found selector {result.selector} but element not on page"
|
|
186
|
+
|
|
187
|
+
def click(self, description: str) -> str:
|
|
188
|
+
"""Click on an element using natural language description.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
description: e.g., "the blue submit button", "link to contact page"
|
|
192
|
+
"""
|
|
193
|
+
if not self.page:
|
|
194
|
+
return "Browser not open"
|
|
195
|
+
|
|
196
|
+
selector = self.find_element_by_description(description)
|
|
197
|
+
|
|
198
|
+
if selector.startswith("Could not") or selector.startswith("Found selector"):
|
|
199
|
+
if self.page.locator(f"text='{description}'").count() > 0:
|
|
200
|
+
self.page.click(f"text='{description}'")
|
|
201
|
+
return f"Clicked on '{description}' (by text)"
|
|
202
|
+
return selector
|
|
203
|
+
|
|
204
|
+
self.page.click(selector)
|
|
205
|
+
return f"Clicked on '{description}'"
|
|
206
|
+
|
|
207
|
+
def type_text(self, field_description: str, text: str) -> str:
|
|
208
|
+
"""Type text into a form field.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
field_description: e.g., "email field", "password input"
|
|
212
|
+
text: The text to type
|
|
213
|
+
"""
|
|
214
|
+
if not self.page:
|
|
215
|
+
return "Browser not open"
|
|
216
|
+
|
|
217
|
+
selector = self.find_element_by_description(field_description)
|
|
218
|
+
|
|
219
|
+
if selector.startswith("Could not") or selector.startswith("Found selector"):
|
|
220
|
+
for fallback in [
|
|
221
|
+
f"input[placeholder*='{field_description}' i]",
|
|
222
|
+
f"[aria-label*='{field_description}' i]",
|
|
223
|
+
f"input[name*='{field_description}' i]"
|
|
224
|
+
]:
|
|
225
|
+
if self.page.locator(fallback).count() > 0:
|
|
226
|
+
self.page.fill(fallback, text)
|
|
227
|
+
self.form_data[field_description] = text
|
|
228
|
+
return f"Typed into {field_description}"
|
|
229
|
+
return f"Could not find field '{field_description}'"
|
|
230
|
+
|
|
231
|
+
self.page.fill(selector, text)
|
|
232
|
+
self.form_data[field_description] = text
|
|
233
|
+
return f"Typed into {field_description}"
|
|
234
|
+
|
|
235
|
+
def get_text(self) -> str:
|
|
236
|
+
"""Get all visible text from the page."""
|
|
237
|
+
if not self.page:
|
|
238
|
+
return "Browser not open"
|
|
239
|
+
return self.page.inner_text("body")
|
|
240
|
+
|
|
241
|
+
def get_current_url(self) -> str:
|
|
242
|
+
"""Get the current page URL."""
|
|
243
|
+
if not self.page:
|
|
244
|
+
return "Browser not open"
|
|
245
|
+
return self.page.url
|
|
246
|
+
|
|
247
|
+
def get_current_page_html(self) -> str:
|
|
248
|
+
"""Get the HTML content of the current page."""
|
|
249
|
+
if not self.page:
|
|
250
|
+
return "Browser not open"
|
|
251
|
+
return self.page.content()
|
|
252
|
+
|
|
253
|
+
def take_screenshot(self, url: str = None, path: str = "",
|
|
254
|
+
width: int = 1920, height: int = 1080,
|
|
255
|
+
full_page: bool = False) -> str:
|
|
256
|
+
"""Take a screenshot of a URL or current page.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
url: URL to screenshot (optional - uses current page if not provided)
|
|
260
|
+
path: Optional path to save (auto-generates if empty)
|
|
261
|
+
width: Viewport width in pixels (default 1920)
|
|
262
|
+
height: Viewport height in pixels (default 1080)
|
|
263
|
+
full_page: If True, captures entire page height
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Path to saved screenshot
|
|
267
|
+
"""
|
|
268
|
+
if not PLAYWRIGHT_AVAILABLE:
|
|
269
|
+
return 'Browser tools not installed. Run: pip install playwright && playwright install chromium'
|
|
270
|
+
|
|
271
|
+
if not self.page:
|
|
272
|
+
return "Browser not open"
|
|
273
|
+
|
|
274
|
+
# Navigate if URL provided
|
|
275
|
+
if url:
|
|
276
|
+
self.go_to(url)
|
|
277
|
+
|
|
278
|
+
# Set viewport size
|
|
279
|
+
self.page.set_viewport_size({"width": width, "height": height})
|
|
280
|
+
|
|
281
|
+
# Generate filename if needed
|
|
282
|
+
if not path:
|
|
283
|
+
SCREENSHOTS_DIR.mkdir(parents=True, exist_ok=True)
|
|
284
|
+
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
285
|
+
path = str(SCREENSHOTS_DIR / f'screenshot_{timestamp}.png')
|
|
286
|
+
elif not path.startswith('/'):
|
|
287
|
+
SCREENSHOTS_DIR.mkdir(parents=True, exist_ok=True)
|
|
288
|
+
if not path.endswith(('.png', '.jpg', '.jpeg')):
|
|
289
|
+
path += '.png'
|
|
290
|
+
path = str(SCREENSHOTS_DIR / path)
|
|
291
|
+
elif not path.endswith(('.png', '.jpg', '.jpeg')):
|
|
292
|
+
path += '.png'
|
|
293
|
+
|
|
294
|
+
# Ensure directory exists
|
|
295
|
+
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
|
296
|
+
|
|
297
|
+
# Take screenshot
|
|
298
|
+
self.page.screenshot(path=path, full_page=full_page)
|
|
299
|
+
self._screenshots.append(path)
|
|
300
|
+
return f'Screenshot saved: {path}'
|
|
301
|
+
|
|
302
|
+
def set_viewport(self, width: int, height: int) -> str:
|
|
303
|
+
"""Set the browser viewport size."""
|
|
304
|
+
if not self.page:
|
|
305
|
+
return "Browser not open"
|
|
306
|
+
self.page.set_viewport_size({"width": width, "height": height})
|
|
307
|
+
return f"Viewport set to {width}x{height}"
|
|
308
|
+
|
|
309
|
+
def screenshot_mobile(self, url: str = None) -> str:
|
|
310
|
+
"""Take screenshot with iPhone viewport (390x844)."""
|
|
311
|
+
if url:
|
|
312
|
+
self.go_to(url)
|
|
313
|
+
self.set_viewport(390, 844)
|
|
314
|
+
return self.take_screenshot()
|
|
315
|
+
|
|
316
|
+
def screenshot_tablet(self, url: str = None) -> str:
|
|
317
|
+
"""Take screenshot with iPad viewport (768x1024)."""
|
|
318
|
+
if url:
|
|
319
|
+
self.go_to(url)
|
|
320
|
+
self.set_viewport(768, 1024)
|
|
321
|
+
return self.take_screenshot()
|
|
322
|
+
|
|
323
|
+
def screenshot_desktop(self, url: str = None) -> str:
|
|
324
|
+
"""Take screenshot with desktop viewport (1920x1080)."""
|
|
325
|
+
if url:
|
|
326
|
+
self.go_to(url)
|
|
327
|
+
self.set_viewport(1920, 1080)
|
|
328
|
+
return self.take_screenshot()
|
|
329
|
+
|
|
330
|
+
def find_forms(self) -> List[FormField]:
|
|
331
|
+
"""Find all form fields on the current page."""
|
|
332
|
+
if not self.page:
|
|
333
|
+
return []
|
|
334
|
+
|
|
335
|
+
fields_data = self.page.evaluate("""
|
|
336
|
+
() => {
|
|
337
|
+
const fields = [];
|
|
338
|
+
document.querySelectorAll('input, textarea, select').forEach(input => {
|
|
339
|
+
const label = input.labels?.[0]?.textContent ||
|
|
340
|
+
input.placeholder || input.name || input.id || 'Unknown';
|
|
341
|
+
fields.push({
|
|
342
|
+
name: input.name || input.id || label,
|
|
343
|
+
label: label.trim(),
|
|
344
|
+
type: input.type || input.tagName.toLowerCase(),
|
|
345
|
+
value: input.value || '',
|
|
346
|
+
required: input.required || false,
|
|
347
|
+
options: input.tagName === 'SELECT' ?
|
|
348
|
+
Array.from(input.options).map(o => o.text) : []
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
return fields;
|
|
352
|
+
}
|
|
353
|
+
""")
|
|
354
|
+
return [FormField(**field) for field in fields_data]
|
|
355
|
+
|
|
356
|
+
def fill_form(self, data: Dict[str, str]) -> str:
|
|
357
|
+
"""Fill multiple form fields at once."""
|
|
358
|
+
if not self.page:
|
|
359
|
+
return "Browser not open"
|
|
360
|
+
|
|
361
|
+
results = []
|
|
362
|
+
for field_name, value in data.items():
|
|
363
|
+
result = self.type_text(field_name, value)
|
|
364
|
+
results.append(f"{field_name}: {result}")
|
|
365
|
+
return "\n".join(results)
|
|
366
|
+
|
|
367
|
+
def submit_form(self) -> str:
|
|
368
|
+
"""Submit the current form."""
|
|
369
|
+
if not self.page:
|
|
370
|
+
return "Browser not open"
|
|
371
|
+
|
|
372
|
+
for selector in [
|
|
373
|
+
"button[type='submit']",
|
|
374
|
+
"input[type='submit']",
|
|
375
|
+
"button:has-text('Submit')",
|
|
376
|
+
"button:has-text('Send')",
|
|
377
|
+
"button:has-text('Continue')",
|
|
378
|
+
"button:has-text('Next')"
|
|
379
|
+
]:
|
|
380
|
+
if self.page.locator(selector).count() > 0:
|
|
381
|
+
self.page.click(selector)
|
|
382
|
+
return "Form submitted"
|
|
383
|
+
|
|
384
|
+
return "Could not find submit button"
|
|
385
|
+
|
|
386
|
+
def select_option(self, field_description: str, option: str) -> str:
|
|
387
|
+
"""Select an option from a dropdown."""
|
|
388
|
+
if not self.page:
|
|
389
|
+
return "Browser not open"
|
|
390
|
+
|
|
391
|
+
selector = self.find_element_by_description(field_description)
|
|
392
|
+
if selector.startswith("Could not"):
|
|
393
|
+
return selector
|
|
394
|
+
|
|
395
|
+
self.page.select_option(selector, label=option)
|
|
396
|
+
return f"Selected '{option}' in {field_description}"
|
|
397
|
+
|
|
398
|
+
def check_checkbox(self, description: str, checked: bool = True) -> str:
|
|
399
|
+
"""Check or uncheck a checkbox."""
|
|
400
|
+
if not self.page:
|
|
401
|
+
return "Browser not open"
|
|
402
|
+
|
|
403
|
+
selector = self.find_element_by_description(description)
|
|
404
|
+
if selector.startswith("Could not"):
|
|
405
|
+
return selector
|
|
406
|
+
|
|
407
|
+
if checked:
|
|
408
|
+
self.page.check(selector)
|
|
409
|
+
return f"Checked {description}"
|
|
410
|
+
else:
|
|
411
|
+
self.page.uncheck(selector)
|
|
412
|
+
return f"Unchecked {description}"
|
|
413
|
+
|
|
414
|
+
def wait_for_element(self, description: str, timeout: int = 30) -> str:
|
|
415
|
+
"""Wait for an element to appear."""
|
|
416
|
+
if not self.page:
|
|
417
|
+
return "Browser not open"
|
|
418
|
+
|
|
419
|
+
selector = self.find_element_by_description(description)
|
|
420
|
+
if selector.startswith("Could not"):
|
|
421
|
+
self.page.wait_for_selector(f"text='{description}'", timeout=timeout * 1000)
|
|
422
|
+
return f"Found text: '{description}'"
|
|
423
|
+
|
|
424
|
+
self.page.wait_for_selector(selector, timeout=timeout * 1000)
|
|
425
|
+
return f"Element appeared: {description}"
|
|
426
|
+
|
|
427
|
+
def wait_for_text(self, text: str, timeout: int = 30) -> str:
|
|
428
|
+
"""Wait for specific text to appear on the page."""
|
|
429
|
+
if not self.page:
|
|
430
|
+
return "Browser not open"
|
|
431
|
+
|
|
432
|
+
self.page.wait_for_selector(f"text='{text}'", timeout=timeout * 1000)
|
|
433
|
+
return f"Found text: '{text}'"
|
|
434
|
+
|
|
435
|
+
def wait(self, seconds: float) -> str:
|
|
436
|
+
"""Wait for a specified number of seconds."""
|
|
437
|
+
if not self.page:
|
|
438
|
+
return "Browser not open"
|
|
439
|
+
self.page.wait_for_timeout(seconds * 1000)
|
|
440
|
+
return f"Waited for {seconds} seconds"
|
|
441
|
+
|
|
442
|
+
def scroll(self, times: int = 5, description: str = "the main content area") -> str:
|
|
443
|
+
"""Universal scroll with automatic strategy selection.
|
|
444
|
+
|
|
445
|
+
Tries multiple strategies until one works:
|
|
446
|
+
1. AI-generated strategy (analyzes page structure)
|
|
447
|
+
2. Element scrolling
|
|
448
|
+
3. Page scrolling
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
times: Number of scroll iterations
|
|
452
|
+
description: What to scroll (e.g., "the email list")
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
Status message with successful strategy
|
|
456
|
+
"""
|
|
457
|
+
from . import scroll_strategies
|
|
458
|
+
return scroll_strategies.scroll_with_verification(
|
|
459
|
+
page=self.page,
|
|
460
|
+
take_screenshot=self.take_screenshot,
|
|
461
|
+
times=times,
|
|
462
|
+
description=description
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
def scroll_page(self, direction: str = "down", amount: int = 1000) -> str:
|
|
466
|
+
"""Scroll the page in a direction.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
direction: "down", "up", "top", or "bottom"
|
|
470
|
+
amount: Pixels to scroll (ignored for "bottom"/"top")
|
|
471
|
+
"""
|
|
472
|
+
if not self.page:
|
|
473
|
+
return "Browser not open"
|
|
474
|
+
|
|
475
|
+
if direction == "bottom":
|
|
476
|
+
self.page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
|
|
477
|
+
return "Scrolled to bottom of page"
|
|
478
|
+
elif direction == "top":
|
|
479
|
+
self.page.evaluate("window.scrollTo(0, 0)")
|
|
480
|
+
return "Scrolled to top of page"
|
|
481
|
+
elif direction == "down":
|
|
482
|
+
self.page.evaluate(f"window.scrollBy(0, {amount})")
|
|
483
|
+
return f"Scrolled down {amount} pixels"
|
|
484
|
+
elif direction == "up":
|
|
485
|
+
self.page.evaluate(f"window.scrollBy(0, -{amount})")
|
|
486
|
+
return f"Scrolled up {amount} pixels"
|
|
487
|
+
else:
|
|
488
|
+
return f"Unknown direction: {direction}"
|
|
489
|
+
|
|
490
|
+
def scroll_element(self, selector: str, amount: int = 1000) -> str:
|
|
491
|
+
"""Scroll a specific element by CSS selector."""
|
|
492
|
+
if not self.page:
|
|
493
|
+
return "Browser not open"
|
|
494
|
+
|
|
495
|
+
result = self.page.evaluate(f"""
|
|
496
|
+
(() => {{
|
|
497
|
+
const element = document.querySelector('{selector}');
|
|
498
|
+
if (!element) return 'Element not found: {selector}';
|
|
499
|
+
const beforeScroll = element.scrollTop;
|
|
500
|
+
element.scrollTop += {amount};
|
|
501
|
+
const afterScroll = element.scrollTop;
|
|
502
|
+
return `Scrolled from ${{beforeScroll}}px to ${{afterScroll}}px`;
|
|
503
|
+
}})()
|
|
504
|
+
""")
|
|
505
|
+
return result
|
|
506
|
+
|
|
507
|
+
def wait_for_manual_login(self, site_name: str = "the website") -> str:
|
|
508
|
+
"""Pause automation for user to login manually.
|
|
509
|
+
|
|
510
|
+
Useful for sites with 2FA or CAPTCHA.
|
|
511
|
+
|
|
512
|
+
Args:
|
|
513
|
+
site_name: Name of the site (e.g., "Gmail")
|
|
514
|
+
|
|
515
|
+
Returns:
|
|
516
|
+
Confirmation when user is ready to continue
|
|
517
|
+
"""
|
|
518
|
+
if not self.page:
|
|
519
|
+
return "Browser not open"
|
|
520
|
+
|
|
521
|
+
print(f"\n{'='*60}")
|
|
522
|
+
print(f" MANUAL LOGIN REQUIRED")
|
|
523
|
+
print(f"{'='*60}")
|
|
524
|
+
print(f"Please login to {site_name} in the browser window.")
|
|
525
|
+
print(f"Once you're logged in and ready to continue:")
|
|
526
|
+
print(f" Type 'yes' or 'Y' and press Enter")
|
|
527
|
+
print(f"{'='*60}\n")
|
|
528
|
+
|
|
529
|
+
while True:
|
|
530
|
+
response = input("Ready to continue? (yes/Y): ").strip().lower()
|
|
531
|
+
if response in ['yes', 'y']:
|
|
532
|
+
print("Continuing automation...\n")
|
|
533
|
+
return f"User confirmed login to {site_name} - continuing"
|
|
534
|
+
else:
|
|
535
|
+
print("Please type 'yes' or 'Y' when ready.")
|
|
536
|
+
|
|
537
|
+
def extract_data(self, selector: str) -> List[str]:
|
|
538
|
+
"""Extract text from elements matching a selector."""
|
|
539
|
+
if not self.page:
|
|
540
|
+
return []
|
|
541
|
+
|
|
542
|
+
elements = self.page.locator(selector)
|
|
543
|
+
count = elements.count()
|
|
544
|
+
return [elements.nth(i).inner_text() for i in range(count)]
|
|
545
|
+
|
|
546
|
+
def close(self) -> str:
|
|
547
|
+
"""Close the browser."""
|
|
548
|
+
if self.page:
|
|
549
|
+
self.page.close()
|
|
550
|
+
if self.browser:
|
|
551
|
+
self.browser.close()
|
|
552
|
+
if self.playwright:
|
|
553
|
+
self.playwright.stop()
|
|
554
|
+
|
|
555
|
+
self.page = None
|
|
556
|
+
self.browser = None
|
|
557
|
+
self.playwright = None
|
|
558
|
+
return "Browser closed"
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def execute_browser_command(command: str) -> str:
|
|
562
|
+
"""Execute a browser command using natural language.
|
|
563
|
+
|
|
564
|
+
Returns the agent's natural language response directly.
|
|
565
|
+
"""
|
|
566
|
+
api_key = os.getenv('OPENONION_API_KEY')
|
|
567
|
+
|
|
568
|
+
if not api_key:
|
|
569
|
+
global_env = Path.home() / ".co" / "keys.env"
|
|
570
|
+
if global_env.exists():
|
|
571
|
+
load_dotenv(global_env)
|
|
572
|
+
api_key = os.getenv('OPENONION_API_KEY')
|
|
573
|
+
|
|
574
|
+
if not api_key:
|
|
575
|
+
return 'Browser agent requires authentication. Run: co auth'
|
|
576
|
+
|
|
577
|
+
browser = BrowserAutomation()
|
|
578
|
+
agent = Agent(
|
|
579
|
+
name="browser_cli",
|
|
580
|
+
model="co/gemini-2.5-pro",
|
|
581
|
+
api_key=api_key,
|
|
582
|
+
system_prompt=PROMPT_PATH,
|
|
583
|
+
tools=[browser],
|
|
584
|
+
max_iterations=20
|
|
585
|
+
)
|
|
586
|
+
return agent.input(command)
|