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.

Files changed (38) hide show
  1. gui_agents/__init__.py +63 -0
  2. gui_agents/agents/Action.py +3 -3
  3. gui_agents/agents/Backend/ADBBackend.py +62 -0
  4. gui_agents/agents/Backend/Backend.py +28 -0
  5. gui_agents/agents/Backend/LybicBackend.py +354 -0
  6. gui_agents/agents/Backend/PyAutoGUIBackend.py +183 -0
  7. gui_agents/agents/Backend/PyAutoGUIVMwareBackend.py +250 -0
  8. gui_agents/agents/Backend/__init__.py +0 -0
  9. gui_agents/agents/agent_s.py +0 -2
  10. gui_agents/agents/grounding.py +1 -6
  11. gui_agents/agents/hardware_interface.py +24 -7
  12. gui_agents/agents/manager.py +0 -3
  13. gui_agents/agents/translator.py +1 -1
  14. gui_agents/agents/worker.py +1 -2
  15. gui_agents/cli_app.py +143 -8
  16. gui_agents/core/engine.py +0 -2
  17. gui_agents/core/knowledge.py +0 -2
  18. gui_agents/lybic_client/__init__.py +0 -0
  19. gui_agents/lybic_client/lybic_client.py +88 -0
  20. gui_agents/prompts/__init__.py +0 -0
  21. gui_agents/prompts/prompts.py +869 -0
  22. gui_agents/service/__init__.py +19 -0
  23. gui_agents/service/agent_service.py +527 -0
  24. gui_agents/service/api_models.py +136 -0
  25. gui_agents/service/config.py +241 -0
  26. gui_agents/service/exceptions.py +35 -0
  27. gui_agents/store/__init__.py +0 -0
  28. gui_agents/store/registry.py +22 -0
  29. gui_agents/tools/tools.py +0 -4
  30. gui_agents/unit_test/test_manager.py +0 -2
  31. gui_agents/unit_test/test_worker.py +0 -2
  32. gui_agents/utils/analyze_display.py +1 -1
  33. gui_agents/utils/common_utils.py +0 -2
  34. {lybic_guiagents-0.1.0.dist-info → lybic_guiagents-0.2.1.dist-info}/METADATA +203 -75
  35. {lybic_guiagents-0.1.0.dist-info → lybic_guiagents-0.2.1.dist-info}/RECORD +38 -21
  36. {lybic_guiagents-0.1.0.dist-info → lybic_guiagents-0.2.1.dist-info}/WHEEL +0 -0
  37. {lybic_guiagents-0.1.0.dist-info → lybic_guiagents-0.2.1.dist-info}/licenses/LICENSE +0 -0
  38. {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
- if isinstance(hwi_para.backend, PyAutoGUIBackend):
176
- screen_width, screen_height = pyautogui.size()
177
- if screen_width != screenshot_width or screen_height != screenshot_high:
178
- screenshot = screenshot.resize((screen_width, screen_height), Image.Resampling.LANCZOS)
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
- hwi = HardwareInterface(backend=args.backend, **backend_kwargs)
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:
@@ -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