lybic-guiagents 0.1.0__py3-none-any.whl → 0.2.0__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 +67 -0
- gui_agents/agents/Backend/ADBBackend.py +62 -0
- gui_agents/agents/Backend/Backend.py +28 -0
- gui_agents/agents/Backend/LybicBackend.py +355 -0
- gui_agents/agents/Backend/PyAutoGUIBackend.py +186 -0
- gui_agents/agents/Backend/PyAutoGUIVMwareBackend.py +250 -0
- gui_agents/agents/Backend/__init__.py +0 -0
- gui_agents/agents/hardware_interface.py +4 -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
- {lybic_guiagents-0.1.0.dist-info → lybic_guiagents-0.2.0.dist-info}/METADATA +69 -4
- {lybic_guiagents-0.1.0.dist-info → lybic_guiagents-0.2.0.dist-info}/RECORD +24 -7
- {lybic_guiagents-0.1.0.dist-info → lybic_guiagents-0.2.0.dist-info}/WHEEL +0 -0
- {lybic_guiagents-0.1.0.dist-info → lybic_guiagents-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {lybic_guiagents-0.1.0.dist-info → lybic_guiagents-0.2.0.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]
|