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
@@ -0,0 +1,19 @@
1
+ # Service layer for GUI Agent
2
+
3
+ # Import order matters due to dependencies
4
+ from .exceptions import AgentServiceError, ConfigurationError, TaskExecutionError
5
+ from .api_models import TaskRequest, TaskResult, TaskStatus, ExecutionStats
6
+ from .config import ServiceConfig
7
+ from .agent_service import AgentService
8
+
9
+ __all__ = [
10
+ "ServiceConfig",
11
+ "TaskRequest",
12
+ "TaskResult",
13
+ "TaskStatus",
14
+ "ExecutionStats",
15
+ "AgentService",
16
+ "AgentServiceError",
17
+ "ConfigurationError",
18
+ "TaskExecutionError"
19
+ ]
@@ -0,0 +1,527 @@
1
+ """Core Agent Service implementation"""
2
+
3
+ import logging
4
+ import threading
5
+ import time
6
+ import uuid
7
+ import datetime
8
+ from concurrent.futures import ThreadPoolExecutor, Future
9
+ from typing import Dict, Optional, Any, Union
10
+ from pathlib import Path
11
+
12
+ from .api_models import (
13
+ TaskRequest, TaskResult, TaskStatus, ExecutionStats,
14
+ AsyncTaskHandle, Backend, AgentMode
15
+ )
16
+ from .config import ServiceConfig
17
+ from .exceptions import (
18
+ AgentServiceError, TaskExecutionError, TaskTimeoutError,
19
+ ConfigurationError, BackendError
20
+ )
21
+
22
+ # Import existing agent classes
23
+ from ..agents.agent_s import AgentS2, AgentSFast
24
+ from ..agents.hardware_interface import HardwareInterface
25
+ from ..store.registry import Registry
26
+ from ..agents.global_state import GlobalState
27
+
28
+
29
+ class AgentService:
30
+ """
31
+ Core service class that provides a unified interface for GUI automation tasks.
32
+
33
+ This service wraps the existing Agent-S functionality and provides:
34
+ - Synchronous and asynchronous task execution
35
+ - Configuration management with multi-level API key support
36
+ - Task lifecycle management
37
+ - Execution statistics and monitoring
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ config: Optional[ServiceConfig] = None,
43
+ **kwargs
44
+ ):
45
+ """
46
+ Initialize the Agent Service
47
+
48
+ Args:
49
+ config: Service configuration. If None, will create from environment
50
+ **kwargs: Override configuration parameters
51
+ """
52
+ # Initialize configuration
53
+ if config is None:
54
+ config = ServiceConfig.from_env()
55
+
56
+ # Apply kwargs overrides
57
+ for key, value in kwargs.items():
58
+ if hasattr(config, key):
59
+ setattr(config, key, value)
60
+
61
+ # Validate configuration
62
+ config.validate()
63
+
64
+ self.config = config
65
+ self.logger = self._setup_logging()
66
+
67
+ # Task management
68
+ self._tasks: Dict[str, TaskResult] = {}
69
+ self._task_futures: Dict[str, Future] = {}
70
+ self._task_lock = threading.RLock()
71
+
72
+ # Thread pool for async execution
73
+ self._executor = ThreadPoolExecutor(
74
+ max_workers=config.max_concurrent_tasks,
75
+ thread_name_prefix="AgentService"
76
+ )
77
+
78
+ # Agent instances cache
79
+ self._agents: Dict[str, Union[AgentS2, AgentSFast]] = {}
80
+ self._hwi_instances: Dict[str, HardwareInterface] = {}
81
+
82
+ self.logger.info(f"AgentService initialized with config: {config.to_dict()}")
83
+
84
+ def _setup_logging(self) -> logging.Logger:
85
+ """Setup logging for the service"""
86
+ logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
87
+ logger.setLevel(getattr(logging, self.config.log_level.upper()))
88
+
89
+ # Create log directory if it doesn't exist
90
+ log_dir = Path(self.config.log_dir)
91
+ log_dir.mkdir(parents=True, exist_ok=True)
92
+
93
+ return logger
94
+
95
+ def _get_or_create_agent(self, mode: str, **kwargs) -> Union[AgentS2, AgentSFast]:
96
+ """Get or create agent instance based on mode"""
97
+ cache_key = f"{mode}_{hash(str(sorted(kwargs.items())))}"
98
+
99
+ if cache_key not in self._agents:
100
+ agent_kwargs = {
101
+ 'platform': kwargs.get('platform', self.config.default_platform),
102
+ 'enable_takeover': kwargs.get('enable_takeover', self.config.enable_takeover),
103
+ 'enable_search': kwargs.get('enable_search', self.config.enable_search),
104
+ }
105
+
106
+ if mode == AgentMode.FAST.value:
107
+ self._agents[cache_key] = AgentSFast(**agent_kwargs)
108
+ else:
109
+ self._agents[cache_key] = AgentS2(**agent_kwargs)
110
+
111
+ self.logger.debug(f"Created new agent: {mode} with kwargs: {agent_kwargs}")
112
+
113
+ return self._agents[cache_key]
114
+
115
+ def _get_or_create_hwi(self, backend: str, **kwargs) -> HardwareInterface:
116
+ """Get or create hardware interface instance"""
117
+ cache_key = f"{backend}_{hash(str(sorted(kwargs.items())))}"
118
+
119
+ if cache_key not in self._hwi_instances:
120
+ # Get backend-specific config
121
+ backend_config = self.config.get_backend_config(backend)
122
+ backend_config.update(kwargs)
123
+
124
+ # Add platform info
125
+ backend_config.setdefault('platform', self.config.default_platform)
126
+
127
+ self._hwi_instances[cache_key] = HardwareInterface(
128
+ backend=backend,
129
+ **backend_config
130
+ )
131
+
132
+ self.logger.debug(f"Created new HWI: {backend} with config: {backend_config}")
133
+
134
+ return self._hwi_instances[cache_key]
135
+
136
+ def _setup_global_state(self, task_id: str) -> str:
137
+ """Setup global state for task execution"""
138
+ # Create timestamp-based directory structure like cli_app.py
139
+ datetime_str = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
140
+ timestamp_dir = Path(self.config.log_dir) / datetime_str
141
+ cache_dir = timestamp_dir / "cache" / "screens"
142
+ state_dir = timestamp_dir / "state"
143
+
144
+ cache_dir.mkdir(parents=True, exist_ok=True)
145
+ state_dir.mkdir(parents=True, exist_ok=True)
146
+
147
+ # Register global state for this task
148
+ global_state = GlobalState(
149
+ screenshot_dir=str(cache_dir),
150
+ tu_path=str(state_dir / "tu.json"),
151
+ search_query_path=str(state_dir / "search_query.json"),
152
+ completed_subtasks_path=str(state_dir / "completed_subtasks.json"),
153
+ failed_subtasks_path=str(state_dir / "failed_subtasks.json"),
154
+ remaining_subtasks_path=str(state_dir / "remaining_subtasks.json"),
155
+ termination_flag_path=str(state_dir / "termination_flag.json"),
156
+ running_state_path=str(state_dir / "running_state.json"),
157
+ display_info_path=str(timestamp_dir / "display.json"),
158
+ agent_log_path=str(timestamp_dir / "agent_log.json")
159
+ )
160
+
161
+ # Use task-specific registry key to avoid conflicts
162
+ registry_key = "GlobalStateStore"
163
+ Registry.register(registry_key, global_state)
164
+
165
+ return str(timestamp_dir)
166
+
167
+ def _execute_task_internal(self, request: TaskRequest, task_result: TaskResult) -> TaskResult:
168
+ """Internal task execution method"""
169
+ try:
170
+ task_result.mark_started()
171
+ self.logger.info(f"Starting task {task_result.task_id}: {request.instruction}")
172
+
173
+ # Setup global state
174
+ task_dir = self._setup_global_state(task_result.task_id)
175
+
176
+ # Create agent and hardware interface
177
+ agent = self._get_or_create_agent(
178
+ request.mode,
179
+ platform=self.config.default_platform,
180
+ enable_takeover=request.enable_takeover,
181
+ enable_search=request.enable_search
182
+ )
183
+
184
+ hwi = self._get_or_create_hwi(
185
+ request.backend,
186
+ **(request.config or {})
187
+ )
188
+
189
+ # Reset agent state
190
+ agent.reset()
191
+
192
+ # Execute task using existing run_agent logic
193
+ start_time = time.time()
194
+
195
+ if request.mode == AgentMode.FAST.value:
196
+ self._run_agent_fast_internal(
197
+ agent, request.instruction, hwi,
198
+ request.max_steps, request.enable_takeover,
199
+ task_result.task_id
200
+ )
201
+ else:
202
+ self._run_agent_normal_internal(
203
+ agent, request.instruction, hwi,
204
+ request.max_steps, request.enable_takeover,
205
+ task_result.task_id
206
+ )
207
+
208
+ end_time = time.time()
209
+
210
+ # Create execution stats
211
+ stats = ExecutionStats(
212
+ total_duration=end_time - start_time,
213
+ steps_count=0, # Will be populated from global state if available
214
+ tokens_used={"input": 0, "output": 0, "total": 0}
215
+ )
216
+
217
+ # Try to get more detailed stats from display.json
218
+ try:
219
+ display_json_path = Path(task_dir) / "display.json"
220
+ if display_json_path.exists():
221
+ # Import here to avoid circular imports
222
+ # Use dynamic import to handle packaging issues
223
+ try:
224
+ from gui_agents.utils.analyze_display import analyze_display_json
225
+ except ImportError:
226
+ try:
227
+ from ..utils.analyze_display import analyze_display_json
228
+ except ImportError:
229
+ # Fallback for packaged version
230
+ import importlib
231
+ utils_module = importlib.import_module('gui_agents.utils')
232
+ analyze_display_json = getattr(utils_module.analyze_display, 'analyze_display_json')
233
+ analysis_result = analyze_display_json(str(display_json_path))
234
+ if analysis_result:
235
+ stats.steps_count = analysis_result.get('steps', 0)
236
+ stats.tokens_used = {
237
+ "input": analysis_result.get('input_tokens', 0),
238
+ "output": analysis_result.get('output_tokens', 0),
239
+ "total": analysis_result.get('total_tokens', 0)
240
+ }
241
+ stats.cost = analysis_result.get('cost', 0.0)
242
+ except Exception as e:
243
+ self.logger.warning(f"Failed to analyze execution stats: {e}")
244
+
245
+ # Mark as completed
246
+ task_result.mark_completed(
247
+ result={"message": "Task completed successfully"},
248
+ stats=stats
249
+ )
250
+
251
+ self.logger.info(
252
+ f"Task {task_result.task_id} completed in {stats.total_duration:.2f}s "
253
+ f"with {stats.steps_count} steps"
254
+ )
255
+
256
+ except Exception as e:
257
+ error_msg = f"Task execution failed: {str(e)}"
258
+ self.logger.error(error_msg, exc_info=True)
259
+ task_result.mark_failed(error_msg)
260
+
261
+ finally:
262
+ # Cleanup global state registry
263
+ registry_key = f"GlobalStateStore"
264
+ try:
265
+ # Registry doesn't have unregister method, we'll use clear or manual removal
266
+ if hasattr(Registry, '_services') and registry_key in Registry._services:
267
+ del Registry._services[registry_key]
268
+ except:
269
+ pass
270
+
271
+ return task_result
272
+
273
+ def _run_agent_normal_internal(self, agent, instruction: str, hwi, max_steps: int,
274
+ enable_takeover: bool, task_id: str):
275
+ """Run agent in normal mode (adapted from cli_app.py)"""
276
+ # This is a simplified version - you may want to adapt the full logic from cli_app.py
277
+ global_state: GlobalState = Registry.get(f"GlobalStateStore") # type: ignore
278
+ global_state.set_Tu(instruction)
279
+ global_state.set_running_state("running")
280
+
281
+ # Use dynamic import to handle packaging issues
282
+ try:
283
+ from gui_agents.agents.Action import Screenshot
284
+ except ImportError:
285
+ try:
286
+ from ..agents.Action import Screenshot
287
+ except ImportError:
288
+ # Fallback for packaged version
289
+ import importlib
290
+ agents_module = importlib.import_module('gui_agents.agents')
291
+ Screenshot = getattr(agents_module.Action, 'Screenshot')
292
+ from PIL import Image
293
+
294
+ for step in range(max_steps):
295
+ # Take screenshot
296
+ screenshot: Image.Image = hwi.dispatch(Screenshot())
297
+ global_state.set_screenshot(screenshot)
298
+ obs = global_state.get_obs_for_manager()
299
+
300
+ # Get agent prediction
301
+ info, code = agent.predict(instruction=instruction, observation=obs)
302
+
303
+ # Check for completion
304
+ if "done" in code[0]["type"].lower() or "fail" in code[0]["type"].lower():
305
+ agent.update_narrative_memory(f"Task: {instruction}")
306
+ break
307
+
308
+ if "next" in code[0]["type"].lower():
309
+ continue
310
+
311
+ if "wait" in code[0]["type"].lower():
312
+ time.sleep(5)
313
+ continue
314
+
315
+ # Execute action
316
+ hwi.dispatchDict(code[0])
317
+ time.sleep(1.0)
318
+
319
+ def _run_agent_fast_internal(self, agent, instruction: str, hwi, max_steps: int,
320
+ enable_takeover: bool, task_id: str):
321
+ """Run agent in fast mode (adapted from cli_app.py)"""
322
+ global_state: GlobalState = Registry.get(f"GlobalStateStore") # type: ignore
323
+ global_state.set_Tu(instruction)
324
+ global_state.set_running_state("running")
325
+
326
+ # Use dynamic import to handle packaging issues
327
+ try:
328
+ from gui_agents.agents.Action import Screenshot
329
+ except ImportError:
330
+ try:
331
+ from ..agents.Action import Screenshot
332
+ except ImportError:
333
+ # Fallback for packaged version
334
+ import importlib
335
+ agents_module = importlib.import_module('gui_agents.agents')
336
+ Screenshot = getattr(agents_module.Action, 'Screenshot')
337
+ from PIL import Image
338
+
339
+ for step in range(max_steps):
340
+ # Take screenshot
341
+ screenshot: Image.Image = hwi.dispatch(Screenshot())
342
+ global_state.set_screenshot(screenshot)
343
+ obs = global_state.get_obs_for_manager()
344
+
345
+ # Get agent prediction
346
+ info, code = agent.predict(instruction=instruction, observation=obs)
347
+
348
+ # Check for completion
349
+ if "done" in code[0]["type"].lower() or "fail" in code[0]["type"].lower():
350
+ break
351
+
352
+ if "wait" in code[0]["type"].lower():
353
+ wait_duration = code[0].get("duration", 5000) / 1000
354
+ time.sleep(wait_duration)
355
+ continue
356
+
357
+ # Execute action
358
+ hwi.dispatchDict(code[0])
359
+ time.sleep(0.5)
360
+
361
+ def execute_task(
362
+ self,
363
+ instruction: str,
364
+ backend: str | None = None,
365
+ mode: str | None = None,
366
+ max_steps: int | None = None,
367
+ enable_takeover: bool | None = None,
368
+ enable_search: bool | None = None,
369
+ timeout: int | None = None,
370
+ **kwargs
371
+ ) -> TaskResult:
372
+ """
373
+ Execute a task synchronously
374
+
375
+ Args:
376
+ instruction: Task instruction in natural language
377
+ backend: Backend to use (overrides config default)
378
+ mode: Agent mode ('normal' or 'fast', overrides config default)
379
+ max_steps: Maximum steps (overrides config default)
380
+ enable_takeover: Enable user takeover (overrides config default)
381
+ enable_search: Enable web search (overrides config default)
382
+ timeout: Task timeout in seconds (overrides config default)
383
+ **kwargs: Additional configuration parameters
384
+
385
+ Returns:
386
+ TaskResult with execution details
387
+ """
388
+ # Create task request with defaults from config
389
+ request = TaskRequest(
390
+ instruction=instruction,
391
+ backend=backend or self.config.default_backend,
392
+ mode=mode or self.config.default_mode,
393
+ max_steps=max_steps or self.config.default_max_steps,
394
+ enable_takeover=enable_takeover if enable_takeover is not None else self.config.enable_takeover,
395
+ enable_search=enable_search if enable_search is not None else self.config.enable_search,
396
+ timeout=timeout or self.config.task_timeout,
397
+ config=kwargs
398
+ )
399
+
400
+ # Create task result
401
+ task_result = TaskResult.create_pending(instruction)
402
+
403
+ # Store task
404
+ with self._task_lock:
405
+ self._tasks[task_result.task_id] = task_result
406
+
407
+ # Execute task
408
+ try:
409
+ return self._execute_task_internal(request, task_result)
410
+ finally:
411
+ # Cleanup task future if exists
412
+ with self._task_lock:
413
+ self._task_futures.pop(task_result.task_id, None)
414
+
415
+ def execute_task_async(
416
+ self,
417
+ instruction: str,
418
+ **kwargs
419
+ ) -> AsyncTaskHandle:
420
+ """
421
+ Execute a task asynchronously
422
+
423
+ Args:
424
+ instruction: Task instruction
425
+ **kwargs: Same as execute_task
426
+
427
+ Returns:
428
+ AsyncTaskHandle for monitoring the task
429
+ """
430
+ # Create task request
431
+ request = TaskRequest(
432
+ instruction=instruction,
433
+ backend=kwargs.get('backend', self.config.default_backend),
434
+ mode=kwargs.get('mode', self.config.default_mode),
435
+ max_steps=kwargs.get('max_steps', self.config.default_max_steps),
436
+ enable_takeover=kwargs.get('enable_takeover', self.config.enable_takeover),
437
+ enable_search=kwargs.get('enable_search', self.config.enable_search),
438
+ timeout=kwargs.get('timeout', self.config.task_timeout),
439
+ config={k: v for k, v in kwargs.items() if k not in [
440
+ 'backend', 'mode', 'max_steps', 'enable_takeover',
441
+ 'enable_search', 'timeout'
442
+ ]}
443
+ )
444
+
445
+ # Create task result
446
+ task_result = TaskResult.create_pending(instruction)
447
+
448
+ # Store task and submit to executor
449
+ with self._task_lock:
450
+ self._tasks[task_result.task_id] = task_result
451
+ future = self._executor.submit(self._execute_task_internal, request, task_result)
452
+ self._task_futures[task_result.task_id] = future
453
+
454
+ return AsyncTaskHandle(task_id=task_result.task_id, status=TaskStatus.PENDING)
455
+
456
+ def get_task_status(self, task_id: str) -> Optional[TaskResult]:
457
+ """Get task status and result"""
458
+ with self._task_lock:
459
+ return self._tasks.get(task_id)
460
+
461
+ def cancel_task(self, task_id: str) -> bool:
462
+ """Cancel a running task"""
463
+ with self._task_lock:
464
+ # Cancel future if exists
465
+ future = self._task_futures.get(task_id)
466
+ if future:
467
+ cancelled = future.cancel()
468
+ if cancelled:
469
+ # Mark task as cancelled
470
+ task = self._tasks.get(task_id)
471
+ if task:
472
+ task.mark_cancelled()
473
+ return True
474
+ return False
475
+
476
+ def list_tasks(self, status: Optional[TaskStatus] = None) -> Dict[str, TaskResult]:
477
+ """List all tasks, optionally filtered by status"""
478
+ with self._task_lock:
479
+ if status is None:
480
+ return self._tasks.copy()
481
+ else:
482
+ return {
483
+ task_id: task for task_id, task in self._tasks.items()
484
+ if task.status == status
485
+ }
486
+
487
+ def cleanup_finished_tasks(self, max_age_seconds: int = 3600):
488
+ """Clean up finished tasks older than max_age_seconds"""
489
+ current_time = time.time()
490
+ to_remove = []
491
+
492
+ with self._task_lock:
493
+ for task_id, task in self._tasks.items():
494
+ if (task.is_finished and task.completed_at and
495
+ current_time - task.completed_at > max_age_seconds):
496
+ to_remove.append(task_id)
497
+
498
+ for task_id in to_remove:
499
+ self._tasks.pop(task_id, None)
500
+ self._task_futures.pop(task_id, None)
501
+
502
+ if to_remove:
503
+ self.logger.info(f"Cleaned up {len(to_remove)} finished tasks")
504
+
505
+ def shutdown(self):
506
+ """Shutdown the service and cleanup resources"""
507
+ self.logger.info("Shutting down AgentService...")
508
+
509
+ # Cancel all running tasks
510
+ with self._task_lock:
511
+ for task_id in list(self._task_futures.keys()):
512
+ self.cancel_task(task_id)
513
+
514
+ # Shutdown executor
515
+ self._executor.shutdown(wait=True)
516
+
517
+ # Clear caches
518
+ self._agents.clear()
519
+ self._hwi_instances.clear()
520
+
521
+ self.logger.info("AgentService shutdown complete")
522
+
523
+ def __enter__(self):
524
+ return self
525
+
526
+ def __exit__(self, exc_type, exc_val, exc_tb):
527
+ self.shutdown()
@@ -0,0 +1,136 @@
1
+ """Data models for the Agent Service API"""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Optional, Dict, Any, List, Union
5
+ from enum import Enum
6
+ import uuid
7
+ import time
8
+
9
+
10
+ class TaskStatus(Enum):
11
+ """Task execution status"""
12
+ PENDING = "pending"
13
+ RUNNING = "running"
14
+ COMPLETED = "completed"
15
+ FAILED = "failed"
16
+ CANCELLED = "cancelled"
17
+
18
+
19
+ class AgentMode(Enum):
20
+ """Agent execution mode"""
21
+ NORMAL = "normal"
22
+ FAST = "fast"
23
+
24
+
25
+ class Backend(Enum):
26
+ """Available backends"""
27
+ LYBIC = "lybic"
28
+ PYAUTOGUI = "pyautogui"
29
+ PYAUTOGUI_VMWARE = "pyautogui_vmware"
30
+ ADB = "adb"
31
+ LYBIC_SDK = "lybic_sdk"
32
+
33
+
34
+ @dataclass
35
+ class TaskRequest:
36
+ """Request to execute a task"""
37
+ instruction: str
38
+ backend: str = Backend.LYBIC.value
39
+ mode: str = AgentMode.NORMAL.value
40
+ max_steps: int = 50
41
+ enable_takeover: bool = False
42
+ enable_search: bool = True
43
+ timeout: int = 3600 # 1 hour default timeout
44
+ config: Optional[Dict[str, Any]] = None
45
+
46
+ def __post_init__(self):
47
+ """Validate request parameters"""
48
+ if self.max_steps <= 0:
49
+ raise ValueError("max_steps must be positive")
50
+ if self.timeout <= 0:
51
+ raise ValueError("timeout must be positive")
52
+
53
+
54
+ @dataclass
55
+ class ExecutionStats:
56
+ """Execution statistics"""
57
+ total_duration: float
58
+ steps_count: int
59
+ tokens_used: Dict[str, int] = field(default_factory=lambda: {
60
+ "input": 0, "output": 0, "total": 0
61
+ })
62
+ cost: Optional[float] = None
63
+ avg_step_duration: Optional[float] = None
64
+
65
+ def __post_init__(self):
66
+ if self.steps_count > 0:
67
+ self.avg_step_duration = self.total_duration / self.steps_count
68
+
69
+
70
+ @dataclass
71
+ class TaskResult:
72
+ """Result of task execution"""
73
+ task_id: str
74
+ status: TaskStatus
75
+ instruction: str
76
+ result: Optional[Dict[str, Any]] = None
77
+ error: Optional[str] = None
78
+ execution_stats: Optional[ExecutionStats] = None
79
+ created_at: float = field(default_factory=time.time)
80
+ started_at: Optional[float] = None
81
+ completed_at: Optional[float] = None
82
+
83
+ @classmethod
84
+ def create_pending(cls, instruction: str) -> 'TaskResult':
85
+ """Create a pending task result"""
86
+ return cls(
87
+ task_id=str(uuid.uuid4()),
88
+ status=TaskStatus.PENDING,
89
+ instruction=instruction
90
+ )
91
+
92
+ def mark_started(self):
93
+ """Mark task as started"""
94
+ self.status = TaskStatus.RUNNING
95
+ self.started_at = time.time()
96
+
97
+ def mark_completed(self, result: Optional[Dict[str, Any]] = None, stats: Optional[ExecutionStats] = None):
98
+ """Mark task as completed"""
99
+ self.status = TaskStatus.COMPLETED
100
+ self.completed_at = time.time()
101
+ self.result = result
102
+ self.execution_stats = stats
103
+
104
+ def mark_failed(self, error: str):
105
+ """Mark task as failed"""
106
+ self.status = TaskStatus.FAILED
107
+ self.completed_at = time.time()
108
+ self.error = error
109
+
110
+ def mark_cancelled(self):
111
+ """Mark task as cancelled"""
112
+ self.status = TaskStatus.CANCELLED
113
+ self.completed_at = time.time()
114
+
115
+ @property
116
+ def is_finished(self) -> bool:
117
+ """Check if task is finished (completed, failed, or cancelled)"""
118
+ return self.status in [TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.CANCELLED]
119
+
120
+ @property
121
+ def execution_duration(self) -> Optional[float]:
122
+ """Get execution duration if available"""
123
+ if self.started_at and self.completed_at:
124
+ return self.completed_at - self.started_at
125
+ return None
126
+
127
+
128
+ @dataclass
129
+ class AsyncTaskHandle:
130
+ """Handle for asynchronous task execution"""
131
+ task_id: str
132
+ status: TaskStatus = TaskStatus.PENDING
133
+
134
+ def is_finished(self) -> bool:
135
+ """Check if task is finished"""
136
+ return self.status in [TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.CANCELLED]