lybic-guiagents 0.1.0__py3-none-any.whl → 0.2.1__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.
Potentially problematic release.
This version of lybic-guiagents might be problematic. Click here for more details.
- gui_agents/__init__.py +63 -0
- gui_agents/agents/Action.py +3 -3
- gui_agents/agents/Backend/ADBBackend.py +62 -0
- gui_agents/agents/Backend/Backend.py +28 -0
- gui_agents/agents/Backend/LybicBackend.py +354 -0
- gui_agents/agents/Backend/PyAutoGUIBackend.py +183 -0
- gui_agents/agents/Backend/PyAutoGUIVMwareBackend.py +250 -0
- gui_agents/agents/Backend/__init__.py +0 -0
- gui_agents/agents/agent_s.py +0 -2
- gui_agents/agents/grounding.py +1 -6
- gui_agents/agents/hardware_interface.py +24 -7
- gui_agents/agents/manager.py +0 -3
- gui_agents/agents/translator.py +1 -1
- gui_agents/agents/worker.py +1 -2
- gui_agents/cli_app.py +143 -8
- gui_agents/core/engine.py +0 -2
- gui_agents/core/knowledge.py +0 -2
- gui_agents/lybic_client/__init__.py +0 -0
- gui_agents/lybic_client/lybic_client.py +88 -0
- gui_agents/prompts/__init__.py +0 -0
- gui_agents/prompts/prompts.py +869 -0
- gui_agents/service/__init__.py +19 -0
- gui_agents/service/agent_service.py +527 -0
- gui_agents/service/api_models.py +136 -0
- gui_agents/service/config.py +241 -0
- gui_agents/service/exceptions.py +35 -0
- gui_agents/store/__init__.py +0 -0
- gui_agents/store/registry.py +22 -0
- gui_agents/tools/tools.py +0 -4
- gui_agents/unit_test/test_manager.py +0 -2
- gui_agents/unit_test/test_worker.py +0 -2
- gui_agents/utils/analyze_display.py +1 -1
- gui_agents/utils/common_utils.py +0 -2
- {lybic_guiagents-0.1.0.dist-info → lybic_guiagents-0.2.1.dist-info}/METADATA +203 -75
- {lybic_guiagents-0.1.0.dist-info → lybic_guiagents-0.2.1.dist-info}/RECORD +38 -21
- {lybic_guiagents-0.1.0.dist-info → lybic_guiagents-0.2.1.dist-info}/WHEEL +0 -0
- {lybic_guiagents-0.1.0.dist-info → lybic_guiagents-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {lybic_guiagents-0.1.0.dist-info → lybic_guiagents-0.2.1.dist-info}/top_level.txt +0 -0
gui_agents/cli_app.py
CHANGED
|
@@ -4,13 +4,11 @@ import io
|
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
6
|
import platform
|
|
7
|
-
import pyautogui
|
|
8
7
|
import sys
|
|
9
8
|
import time
|
|
10
9
|
import datetime
|
|
11
10
|
from pathlib import Path
|
|
12
11
|
from dotenv import load_dotenv
|
|
13
|
-
from gui_agents.agents.Backend.PyAutoGUIBackend import PyAutoGUIBackend
|
|
14
12
|
|
|
15
13
|
env_path = Path(os.path.dirname(os.path.abspath(__file__))) / '.env'
|
|
16
14
|
if env_path.exists():
|
|
@@ -35,6 +33,80 @@ from gui_agents.utils.analyze_display import analyze_display_json, aggregate_res
|
|
|
35
33
|
|
|
36
34
|
current_platform = platform.system().lower()
|
|
37
35
|
|
|
36
|
+
# Display environment detection and backend compatibility validation
|
|
37
|
+
def check_display_environment():
|
|
38
|
+
"""
|
|
39
|
+
Check if the current environment supports GUI operations.
|
|
40
|
+
Returns (has_display, pyautogui_available, error_message)
|
|
41
|
+
"""
|
|
42
|
+
has_display = False
|
|
43
|
+
pyautogui_available = False
|
|
44
|
+
error_message = None
|
|
45
|
+
|
|
46
|
+
# Check DISPLAY environment variable (Linux/Unix)
|
|
47
|
+
if current_platform == "linux":
|
|
48
|
+
display_env = os.environ.get('DISPLAY')
|
|
49
|
+
if display_env:
|
|
50
|
+
has_display = True
|
|
51
|
+
else:
|
|
52
|
+
error_message = "No DISPLAY environment variable found. Running in headless/containerized environment."
|
|
53
|
+
elif current_platform == "darwin":
|
|
54
|
+
# macOS typically has display available unless running in special contexts
|
|
55
|
+
has_display = True
|
|
56
|
+
elif current_platform == "windows":
|
|
57
|
+
# Windows typically has display available
|
|
58
|
+
has_display = True
|
|
59
|
+
|
|
60
|
+
# Try to import and initialize pyautogui if display is available
|
|
61
|
+
if has_display:
|
|
62
|
+
try:
|
|
63
|
+
import pyautogui
|
|
64
|
+
# Test if pyautogui can actually work
|
|
65
|
+
pyautogui.size() # This will fail if no display is available
|
|
66
|
+
pyautogui_available = True
|
|
67
|
+
except Exception as e:
|
|
68
|
+
pyautogui_available = False
|
|
69
|
+
error_message = f"PyAutoGUI not available: {str(e)}"
|
|
70
|
+
|
|
71
|
+
return has_display, pyautogui_available, error_message
|
|
72
|
+
|
|
73
|
+
def get_compatible_backends(has_display, pyautogui_available):
|
|
74
|
+
"""
|
|
75
|
+
Get list of backends compatible with current environment.
|
|
76
|
+
"""
|
|
77
|
+
compatible_backends = []
|
|
78
|
+
incompatible_backends = []
|
|
79
|
+
|
|
80
|
+
# Lybic backend works in headless environments (cloud-based)
|
|
81
|
+
compatible_backends.append("lybic")
|
|
82
|
+
|
|
83
|
+
# ADB backend works without display (for Android devices)
|
|
84
|
+
compatible_backends.append("adb")
|
|
85
|
+
|
|
86
|
+
# PyAutoGUI-based backends require display
|
|
87
|
+
if has_display and pyautogui_available:
|
|
88
|
+
compatible_backends.extend(["pyautogui", "pyautogui_vmware"])
|
|
89
|
+
else:
|
|
90
|
+
incompatible_backends.extend(["pyautogui", "pyautogui_vmware"])
|
|
91
|
+
|
|
92
|
+
return compatible_backends, incompatible_backends
|
|
93
|
+
|
|
94
|
+
def validate_backend_compatibility(backend, compatible_backends, incompatible_backends):
|
|
95
|
+
"""
|
|
96
|
+
Validate if the requested backend is compatible with current environment.
|
|
97
|
+
Returns (is_compatible, recommended_backend, warning_message)
|
|
98
|
+
"""
|
|
99
|
+
if backend in compatible_backends:
|
|
100
|
+
return True, backend, None
|
|
101
|
+
elif backend in incompatible_backends:
|
|
102
|
+
# Recommend lybic as the primary fallback for headless environments
|
|
103
|
+
recommended = "lybic"
|
|
104
|
+
warning = f"Backend '{backend}' is not compatible with current environment (no display/GUI). Recommending '{recommended}' backend instead."
|
|
105
|
+
return False, recommended, warning
|
|
106
|
+
else:
|
|
107
|
+
# Unknown backend, let it fail naturally
|
|
108
|
+
return True, backend, f"Unknown backend '{backend}', compatibility cannot be determined."
|
|
109
|
+
|
|
38
110
|
logger = logging.getLogger()
|
|
39
111
|
logger.setLevel(logging.DEBUG)
|
|
40
112
|
|
|
@@ -172,10 +244,18 @@ def show_permission_dialog(code: str, action_description: str):
|
|
|
172
244
|
def scale_screenshot_dimensions(screenshot: Image.Image, hwi_para: HardwareInterface):
|
|
173
245
|
screenshot_high = screenshot.height
|
|
174
246
|
screenshot_width = screenshot.width
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
247
|
+
|
|
248
|
+
# Only try to scale if we have a PyAutoGUI backend and pyautogui is available
|
|
249
|
+
try:
|
|
250
|
+
from gui_agents.agents.Backend.PyAutoGUIBackend import PyAutoGUIBackend
|
|
251
|
+
if isinstance(hwi_para.backend, PyAutoGUIBackend):
|
|
252
|
+
import pyautogui
|
|
253
|
+
screen_width, screen_height = pyautogui.size()
|
|
254
|
+
if screen_width != screenshot_width or screen_height != screenshot_high:
|
|
255
|
+
screenshot = screenshot.resize((screen_width, screen_height), Image.Resampling.LANCZOS)
|
|
256
|
+
except Exception as e:
|
|
257
|
+
# Any error (e.g., no display, import error), skip scaling
|
|
258
|
+
logger.warning(f"Could not scale screenshot dimensions: {e}")
|
|
179
259
|
|
|
180
260
|
return screenshot
|
|
181
261
|
|
|
@@ -453,8 +533,37 @@ def main():
|
|
|
453
533
|
type=str,
|
|
454
534
|
default=None,
|
|
455
535
|
help='Lybic precreated sandbox ID (if not provided, will use LYBIC_PRECREATE_SID environment variable)')
|
|
536
|
+
parser.add_argument(
|
|
537
|
+
'--force-backend',
|
|
538
|
+
action='store_true',
|
|
539
|
+
help='Force the use of specified backend even if incompatible with current environment')
|
|
456
540
|
args = parser.parse_args()
|
|
457
541
|
|
|
542
|
+
# Check environment compatibility
|
|
543
|
+
has_display, pyautogui_available, env_error = check_display_environment()
|
|
544
|
+
compatible_backends, incompatible_backends = get_compatible_backends(has_display, pyautogui_available)
|
|
545
|
+
|
|
546
|
+
# Log environment status
|
|
547
|
+
logger.info(f"Environment check: Display available={has_display}, PyAutoGUI available={pyautogui_available}")
|
|
548
|
+
if env_error:
|
|
549
|
+
logger.info(f"Environment note: {env_error}")
|
|
550
|
+
logger.info(f"Compatible backends: {compatible_backends}")
|
|
551
|
+
if incompatible_backends:
|
|
552
|
+
logger.info(f"Incompatible backends: {incompatible_backends}")
|
|
553
|
+
|
|
554
|
+
# Validate backend compatibility
|
|
555
|
+
is_compatible, recommended_backend, warning = validate_backend_compatibility(
|
|
556
|
+
args.backend, compatible_backends, incompatible_backends)
|
|
557
|
+
|
|
558
|
+
if not is_compatible and not args.force_backend:
|
|
559
|
+
logger.warning(warning)
|
|
560
|
+
logger.info(f"Switching from '{args.backend}' to '{recommended_backend}' backend")
|
|
561
|
+
args.backend = recommended_backend
|
|
562
|
+
elif not is_compatible and args.force_backend:
|
|
563
|
+
logger.warning(f"Forcing incompatible backend '{args.backend}' - this may cause errors")
|
|
564
|
+
elif warning:
|
|
565
|
+
logger.info(warning)
|
|
566
|
+
|
|
458
567
|
# Ensure necessary directory structure exists
|
|
459
568
|
timestamp_dir = os.path.join(log_dir, datetime_str)
|
|
460
569
|
cache_dir = os.path.join(timestamp_dir, "cache", "screens")
|
|
@@ -515,7 +624,7 @@ def main():
|
|
|
515
624
|
else:
|
|
516
625
|
logger.info("Web search functionality is ENABLED")
|
|
517
626
|
|
|
518
|
-
# Initialize hardware interface
|
|
627
|
+
# Initialize hardware interface with error handling
|
|
519
628
|
backend_kwargs = {"platform": platform_os}
|
|
520
629
|
if args.lybic_sid is not None:
|
|
521
630
|
backend_kwargs["precreate_sid"] = args.lybic_sid
|
|
@@ -523,7 +632,25 @@ def main():
|
|
|
523
632
|
else:
|
|
524
633
|
logger.info("Using Lybic SID from environment variable LYBIC_PRECREATE_SID")
|
|
525
634
|
|
|
526
|
-
|
|
635
|
+
try:
|
|
636
|
+
hwi = HardwareInterface(backend=args.backend, **backend_kwargs)
|
|
637
|
+
logger.info(f"Successfully initialized hardware interface with backend: {args.backend}")
|
|
638
|
+
except Exception as e:
|
|
639
|
+
logger.error(f"Failed to initialize hardware interface with backend '{args.backend}': {e}")
|
|
640
|
+
|
|
641
|
+
# If the backend failed and it's a GUI-dependent backend, suggest alternatives
|
|
642
|
+
if args.backend in incompatible_backends and not args.force_backend:
|
|
643
|
+
logger.info("Attempting to initialize with lybic backend as fallback...")
|
|
644
|
+
try:
|
|
645
|
+
hwi = HardwareInterface(backend="lybic", **backend_kwargs)
|
|
646
|
+
logger.info("Successfully initialized with lybic backend")
|
|
647
|
+
args.backend = "lybic"
|
|
648
|
+
except Exception as fallback_error:
|
|
649
|
+
logger.error(f"Fallback to lybic backend also failed: {fallback_error}")
|
|
650
|
+
sys.exit(1)
|
|
651
|
+
else:
|
|
652
|
+
logger.error("Hardware interface initialization failed. Please check your environment and backend configuration.")
|
|
653
|
+
sys.exit(1)
|
|
527
654
|
|
|
528
655
|
# if query is provided, run the agent on the query
|
|
529
656
|
if args.query:
|
|
@@ -547,6 +674,13 @@ def main():
|
|
|
547
674
|
|
|
548
675
|
if __name__ == "__main__":
|
|
549
676
|
"""
|
|
677
|
+
GUI Agent CLI Application with environment compatibility checking.
|
|
678
|
+
|
|
679
|
+
The application automatically detects the current environment and recommends compatible backends:
|
|
680
|
+
- In headless/containerized environments: uses 'lybic' or 'adb' backends
|
|
681
|
+
- In GUI environments: supports all backends including 'pyautogui' and 'pyautogui_vmware'
|
|
682
|
+
|
|
683
|
+
Examples:
|
|
550
684
|
python gui_agents/cli_app.py --backend lybic
|
|
551
685
|
python gui_agents/cli_app.py --backend pyautogui --mode fast
|
|
552
686
|
python gui_agents/cli_app.py --backend pyautogui_vmware
|
|
@@ -556,5 +690,6 @@ if __name__ == "__main__":
|
|
|
556
690
|
python gui_agents/cli_app.py --backend pyautogui --mode fast --disable-search
|
|
557
691
|
python gui_agents/cli_app.py --backend lybic --lybic-sid SBX-01K1X6ZKAERXAN73KTJ1XXJXAF
|
|
558
692
|
python gui_agents/cli_app.py --backend lybic --mode fast --lybic-sid SBX-01K1X6ZKAERXAN73KTJ1XXJXAF
|
|
693
|
+
python gui_agents/cli_app.py --backend pyautogui --force-backend # Force incompatible backend
|
|
559
694
|
"""
|
|
560
695
|
main()
|
gui_agents/core/engine.py
CHANGED
|
@@ -2,7 +2,6 @@ import os
|
|
|
2
2
|
import json
|
|
3
3
|
import backoff
|
|
4
4
|
import requests
|
|
5
|
-
from typing import List, Dict, Any, Optional, Union
|
|
6
5
|
import numpy as np
|
|
7
6
|
from anthropic import Anthropic
|
|
8
7
|
from openai import (
|
|
@@ -18,7 +17,6 @@ from google.genai import types
|
|
|
18
17
|
from zhipuai import ZhipuAI
|
|
19
18
|
from groq import Groq
|
|
20
19
|
import boto3
|
|
21
|
-
import exa_py
|
|
22
20
|
from typing import List, Dict, Any, Optional, Union, Tuple
|
|
23
21
|
|
|
24
22
|
class ModelPricing:
|
gui_agents/core/knowledge.py
CHANGED
|
@@ -9,8 +9,6 @@ from gui_agents.utils.common_utils import (
|
|
|
9
9
|
save_embeddings,
|
|
10
10
|
)
|
|
11
11
|
from gui_agents.tools.tools import Tools
|
|
12
|
-
from gui_agents.agents.global_state import GlobalState
|
|
13
|
-
from gui_agents.store.registry import Registry
|
|
14
12
|
from gui_agents.core.mllm import CostManager
|
|
15
13
|
|
|
16
14
|
def get_embedding_dim(model_name):
|
|
File without changes
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
from typing import Optional, Dict, Any
|
|
3
|
+
|
|
4
|
+
class LybicClient:
|
|
5
|
+
"""Light-weight async wrapper for Lybic REST API."""
|
|
6
|
+
|
|
7
|
+
# ---------- life-cycle ----------
|
|
8
|
+
def __init__(self, api_key: str, base_url: str, org_id: str) -> None:
|
|
9
|
+
self.base = base_url.rstrip("/")
|
|
10
|
+
self.org_id = org_id
|
|
11
|
+
self.http = httpx.AsyncClient(
|
|
12
|
+
headers={"X-Api-Key": api_key, "Content-Type": "application/json"},
|
|
13
|
+
timeout=30,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# runtime cache (set by create_sandbox)
|
|
17
|
+
self.sandbox: Optional[Dict[str, Any]] = None
|
|
18
|
+
# self.connect_details: Optional[Dict[str, Any]] = None
|
|
19
|
+
|
|
20
|
+
async def close(self) -> None:
|
|
21
|
+
await self.http.aclose()
|
|
22
|
+
|
|
23
|
+
# ---------- low-level ----------
|
|
24
|
+
async def _req(self, path: str, method: str = "GET", json: Any = None):
|
|
25
|
+
r = await self.http.request(method, f"{self.base}{path}", json=json)
|
|
26
|
+
# ▶ 打印调试信息
|
|
27
|
+
req = r.request # httpx.Request 对象
|
|
28
|
+
print(
|
|
29
|
+
"[HTTP]", req.method, req.url, # 完整 URL(含 querystring)
|
|
30
|
+
"json=", json,
|
|
31
|
+
"status=", r.status_code,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
r.raise_for_status()
|
|
35
|
+
return r.json()
|
|
36
|
+
|
|
37
|
+
# ---------- high-level ----------
|
|
38
|
+
async def create_sandbox(self, **opts) -> Dict[str, Any]:
|
|
39
|
+
"""
|
|
40
|
+
Create a new sandbox and cache its metadata / connectDetails.
|
|
41
|
+
Returns the full response dict.
|
|
42
|
+
"""
|
|
43
|
+
resp = await self._req(
|
|
44
|
+
f"/api/orgs/{self.org_id}/sandboxes", "POST", opts or {}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# cache
|
|
48
|
+
self.sandbox = resp
|
|
49
|
+
# self.connect_details = resp.get("connectDetails")
|
|
50
|
+
return resp
|
|
51
|
+
|
|
52
|
+
def _require_sandbox_id(self, sid: Optional[str]) -> str:
|
|
53
|
+
if sid:
|
|
54
|
+
return sid
|
|
55
|
+
if self.sandbox:
|
|
56
|
+
return self.sandbox["id"]
|
|
57
|
+
raise RuntimeError("No sandbox_id specified and none cached — "
|
|
58
|
+
"call create_sandbox() first.")
|
|
59
|
+
|
|
60
|
+
async def preview(self, sid: Optional[str] = None):
|
|
61
|
+
sid = self._require_sandbox_id(sid)
|
|
62
|
+
return await self._req(
|
|
63
|
+
f"/api/orgs/{self.org_id}/sandboxes/{sid}/preview", "POST"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
async def exec_action(self, action: dict, sid: Optional[str] = None):
|
|
67
|
+
"""
|
|
68
|
+
Execute a single GUI action. `sid` optional if sandbox already cached.
|
|
69
|
+
"""
|
|
70
|
+
sid = self._require_sandbox_id(sid)
|
|
71
|
+
return await self._req(
|
|
72
|
+
f"/api/orgs/{self.org_id}/sandboxes/{sid}/actions/computer-use",
|
|
73
|
+
"POST",
|
|
74
|
+
{"action": action},
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
async def parse_nl(self, text: str, model: str = "ui-tars"):
|
|
78
|
+
return await self._req(
|
|
79
|
+
"/api/computer-use/parse",
|
|
80
|
+
"POST",
|
|
81
|
+
{"model": model, "textContent": text},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# ---------- helpers ----------
|
|
85
|
+
@property
|
|
86
|
+
def sandbox_id(self) -> Optional[str]:
|
|
87
|
+
return self.sandbox["id"] if self.sandbox else None
|
|
88
|
+
|
|
File without changes
|