lybic-guiagents 0.3.0__py3-none-any.whl → 0.4.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 +1 -1
- gui_agents/agents/agent_s.py +28 -17
- gui_agents/agents/grounding.py +14 -4
- gui_agents/agents/manager.py +8 -1
- gui_agents/agents/worker.py +8 -2
- gui_agents/cli_app.py +236 -220
- gui_agents/grpc_app.py +118 -50
- gui_agents/service/agent_service.py +51 -34
- gui_agents/store/registry.py +114 -6
- {lybic_guiagents-0.3.0.dist-info → lybic_guiagents-0.4.0.dist-info}/METADATA +6 -6
- {lybic_guiagents-0.3.0.dist-info → lybic_guiagents-0.4.0.dist-info}/RECORD +15 -15
- {lybic_guiagents-0.3.0.dist-info → lybic_guiagents-0.4.0.dist-info}/WHEEL +0 -0
- {lybic_guiagents-0.3.0.dist-info → lybic_guiagents-0.4.0.dist-info}/entry_points.txt +0 -0
- {lybic_guiagents-0.3.0.dist-info → lybic_guiagents-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {lybic_guiagents-0.3.0.dist-info → lybic_guiagents-0.4.0.dist-info}/top_level.txt +0 -0
gui_agents/grpc_app.py
CHANGED
|
@@ -29,6 +29,7 @@ import platform
|
|
|
29
29
|
from concurrent import futures
|
|
30
30
|
import grpc
|
|
31
31
|
import uuid
|
|
32
|
+
import datetime
|
|
32
33
|
|
|
33
34
|
from lybic import LybicClient, LybicAuth, Sandbox
|
|
34
35
|
import gui_agents.cli_app as app
|
|
@@ -44,12 +45,13 @@ class AgentServicer(agent_pb2_grpc.AgentServicer):
|
|
|
44
45
|
Implements the Agent gRPC service.
|
|
45
46
|
"""
|
|
46
47
|
|
|
47
|
-
def __init__(self, max_concurrent_task_num = 1):
|
|
48
|
+
def __init__(self, max_concurrent_task_num: int = 1, log_dir: str = "runtime"):
|
|
48
49
|
"""
|
|
49
50
|
Initialize the AgentServicer with concurrency and runtime state.
|
|
50
51
|
|
|
51
52
|
Parameters:
|
|
52
53
|
max_concurrent_task_num (int): Maximum number of agent tasks allowed to run concurrently; defaults to 1.
|
|
54
|
+
log_dir (str): Directory for logging and task-related files.
|
|
53
55
|
"""
|
|
54
56
|
self.lybic_auth: LybicAuth | None = None
|
|
55
57
|
self.max_concurrent_task_num = max_concurrent_task_num
|
|
@@ -58,6 +60,7 @@ class AgentServicer(agent_pb2_grpc.AgentServicer):
|
|
|
58
60
|
self.task_lock = asyncio.Lock()
|
|
59
61
|
self.lybic_client: LybicClient | None = None
|
|
60
62
|
self.sandbox: Sandbox | None = None
|
|
63
|
+
self.log_dir = log_dir
|
|
61
64
|
|
|
62
65
|
async def GetAgentTaskStream(self, request, context):
|
|
63
66
|
"""
|
|
@@ -110,6 +113,42 @@ class AgentServicer(agent_pb2_grpc.AgentServicer):
|
|
|
110
113
|
domain=platform.node(),
|
|
111
114
|
)
|
|
112
115
|
|
|
116
|
+
async def _setup_task_state(self, task_id: str) -> Registry:
|
|
117
|
+
"""Setup global state and registry for task execution with task isolation"""
|
|
118
|
+
# Create timestamp-based directory structure like cli_app.py
|
|
119
|
+
datetime_str = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
120
|
+
timestamp_dir = Path(self.log_dir) / f"{datetime_str}_{task_id[:8]}" # Include task_id prefix
|
|
121
|
+
cache_dir = timestamp_dir / "cache" / "screens"
|
|
122
|
+
state_dir = timestamp_dir / "state"
|
|
123
|
+
|
|
124
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
125
|
+
state_dir.mkdir(parents=True, exist_ok=True)
|
|
126
|
+
|
|
127
|
+
# Create task-specific registry
|
|
128
|
+
task_registry = Registry()
|
|
129
|
+
|
|
130
|
+
# Register global state for this task in task-specific registry
|
|
131
|
+
global_state = GlobalState(
|
|
132
|
+
screenshot_dir=str(cache_dir),
|
|
133
|
+
tu_path=str(state_dir / "tu.json"),
|
|
134
|
+
search_query_path=str(state_dir / "search_query.json"),
|
|
135
|
+
completed_subtasks_path=str(state_dir / "completed_subtasks.json"),
|
|
136
|
+
failed_subtasks_path=str(state_dir / "failed_subtasks.json"),
|
|
137
|
+
remaining_subtasks_path=str(state_dir / "remaining_subtasks.json"),
|
|
138
|
+
termination_flag_path=str(state_dir / "termination_flag.json"),
|
|
139
|
+
running_state_path=str(state_dir / "running_state.json"),
|
|
140
|
+
display_info_path=str(timestamp_dir / "display.json"),
|
|
141
|
+
agent_log_path=str(timestamp_dir / "agent_log.json")
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Register in task-specific registry using instance method
|
|
145
|
+
registry_key = "GlobalStateStore"
|
|
146
|
+
task_registry.register_instance(registry_key, global_state)
|
|
147
|
+
|
|
148
|
+
logger.info(f"Created task-specific registry for task {task_id}")
|
|
149
|
+
|
|
150
|
+
return task_registry
|
|
151
|
+
|
|
113
152
|
async def _run_task(self, task_id: str, backend_kwargs):
|
|
114
153
|
"""
|
|
115
154
|
Run the lifecycle of a single agent task: mark it running, execute the agent, record final state, emit stream messages, and unregister the task.
|
|
@@ -136,23 +175,29 @@ class AgentServicer(agent_pb2_grpc.AgentServicer):
|
|
|
136
175
|
# Send message through stream manager
|
|
137
176
|
await stream_manager.add_message(task_id, "starting", "Task starting")
|
|
138
177
|
|
|
139
|
-
#
|
|
178
|
+
# Create task-specific registry
|
|
179
|
+
task_registry = await self._setup_task_state(task_id)
|
|
180
|
+
|
|
181
|
+
# Set task_id for the agent. This is needed so that agent.reset() can find the right components.
|
|
140
182
|
if hasattr(agent, 'set_task_id'):
|
|
141
183
|
agent.set_task_id(task_id)
|
|
142
184
|
|
|
143
185
|
hwi = HardwareInterface(backend='lybic', **backend_kwargs)
|
|
144
186
|
|
|
187
|
+
# We need to set the registry for the main thread context before reset
|
|
188
|
+
Registry.set_task_registry(task_id, task_registry)
|
|
145
189
|
agent.reset()
|
|
190
|
+
Registry.remove_task_registry(task_id) # Clean up main thread's local
|
|
146
191
|
|
|
147
|
-
# Run the blocking function in a separate thread
|
|
148
|
-
mode: InstanceMode | None = backend_kwargs
|
|
192
|
+
# Run the blocking function in a separate thread, passing the context
|
|
193
|
+
mode: InstanceMode | None = backend_kwargs.get("mode")
|
|
149
194
|
if mode and mode == InstanceMode.NORMAL:
|
|
150
|
-
await asyncio.to_thread(app.run_agent_normal,agent, query, hwi, steps, False)
|
|
195
|
+
await asyncio.to_thread(app.run_agent_normal, agent, query, hwi, steps, False, task_id=task_id, task_registry=task_registry)
|
|
151
196
|
else:
|
|
152
|
-
await asyncio.to_thread(app.run_agent_fast, agent, query, hwi, steps, False)
|
|
197
|
+
await asyncio.to_thread(app.run_agent_fast, agent, query, hwi, steps, False, task_id=task_id, task_registry=task_registry)
|
|
153
198
|
|
|
154
|
-
|
|
155
|
-
final_state =
|
|
199
|
+
# The final state is now determined inside the thread. We'll assume success if no exception.
|
|
200
|
+
final_state = "completed"
|
|
156
201
|
|
|
157
202
|
async with self.task_lock:
|
|
158
203
|
self.tasks[task_id]["final_state"] = final_state
|
|
@@ -171,6 +216,7 @@ class AgentServicer(agent_pb2_grpc.AgentServicer):
|
|
|
171
216
|
await stream_manager.add_message(task_id, "error", f"An error occurred: {e}")
|
|
172
217
|
finally:
|
|
173
218
|
logger.info(f"Task {task_id} processing finished.")
|
|
219
|
+
# Registry cleanup is now handled within the worker thread
|
|
174
220
|
await stream_manager.unregister_task(task_id)
|
|
175
221
|
|
|
176
222
|
async def _make_backend_kwargs(self, request):
|
|
@@ -213,23 +259,30 @@ class AgentServicer(agent_pb2_grpc.AgentServicer):
|
|
|
213
259
|
|
|
214
260
|
platform_str = platform.system()
|
|
215
261
|
sid = ''
|
|
262
|
+
sandbox_pb = None
|
|
263
|
+
|
|
216
264
|
if backend == 'lybic':
|
|
217
265
|
if request.HasField("sandbox"):
|
|
218
266
|
shape = request.sandbox.shapeName
|
|
219
267
|
sid = request.sandbox.id
|
|
220
268
|
if sid:
|
|
221
269
|
logger.info(f"Using existing sandbox with id: {sid}")
|
|
270
|
+
sandbox_pb = await self._get_sandbox_pb(sid) # if not exist raise NotFound
|
|
271
|
+
platform_str = sandbox_pb.os
|
|
222
272
|
else:
|
|
223
|
-
|
|
273
|
+
sandbox_pb = await self._create_sandbox(shape)
|
|
274
|
+
sid, platform_str = sandbox_pb.id, sandbox_pb.os
|
|
224
275
|
|
|
225
276
|
if request.sandbox.os != agent_pb2.SandboxOS.OSUNDEFINED:
|
|
226
277
|
platform_str = platform_map.get(request.sandbox.os, platform.system())
|
|
227
278
|
else:
|
|
228
|
-
|
|
229
|
-
|
|
279
|
+
sandbox_pb = await self._create_sandbox(shape)
|
|
280
|
+
sid, platform_str = sandbox_pb.id, sandbox_pb.os
|
|
230
281
|
else:
|
|
231
|
-
|
|
282
|
+
if request.HasField("sandbox") and request.sandbox.os != agent_pb2.SandboxOS.OSUNDEFINED:
|
|
283
|
+
platform_str = platform_map.get(request.sandbox.os, platform.system())
|
|
232
284
|
|
|
285
|
+
backend_kwargs["sandbox"] = sandbox_pb
|
|
233
286
|
backend_kwargs["platform"] = platform_str
|
|
234
287
|
backend_kwargs["precreate_sid"] = sid
|
|
235
288
|
|
|
@@ -250,12 +303,6 @@ class AgentServicer(agent_pb2_grpc.AgentServicer):
|
|
|
250
303
|
return backend_kwargs
|
|
251
304
|
|
|
252
305
|
async def _make_agent(self,request):
|
|
253
|
-
# todo: add max_steps support
|
|
254
|
-
# max_steps = 50
|
|
255
|
-
# if request.HasField("runningConfig") and request.runningConfig.steps:
|
|
256
|
-
# max_steps = request.runningConfig.steps
|
|
257
|
-
|
|
258
|
-
# Dynamically build tools_dict based on global config
|
|
259
306
|
"""
|
|
260
307
|
Builds and returns an AgentS2 configured for the incoming request by applying model and provider overrides to the tool configurations.
|
|
261
308
|
|
|
@@ -345,7 +392,6 @@ class AgentServicer(agent_pb2_grpc.AgentServicer):
|
|
|
345
392
|
tools_config=tools_config,
|
|
346
393
|
)
|
|
347
394
|
|
|
348
|
-
|
|
349
395
|
async def RunAgentInstruction(self, request, context):
|
|
350
396
|
"""
|
|
351
397
|
Stream task progress for a newly created instruction-run agent while managing task lifecycle and concurrency.
|
|
@@ -390,8 +436,13 @@ class AgentServicer(agent_pb2_grpc.AgentServicer):
|
|
|
390
436
|
"query": request.instruction,
|
|
391
437
|
"agent": agent,
|
|
392
438
|
"max_steps": max_steps,
|
|
439
|
+
"sandbox": backend_kwargs["sandbox"],
|
|
393
440
|
}
|
|
394
441
|
|
|
442
|
+
# This property is used to pass sandbox information.
|
|
443
|
+
# It has now completed its mission and needs to be deleted, otherwise other backends may crash.
|
|
444
|
+
del backend_kwargs["sandbox"]
|
|
445
|
+
|
|
395
446
|
task_future = asyncio.create_task(self._run_task(task_id, backend_kwargs))
|
|
396
447
|
self.tasks[task_id]["future"] = task_future
|
|
397
448
|
try:
|
|
@@ -449,6 +500,7 @@ class AgentServicer(agent_pb2_grpc.AgentServicer):
|
|
|
449
500
|
"query": request.instruction,
|
|
450
501
|
"agent": agent,
|
|
451
502
|
"max_steps": max_steps,
|
|
503
|
+
"sandbox": backend_kwargs["sandbox"],
|
|
452
504
|
}
|
|
453
505
|
|
|
454
506
|
# Start the task in background
|
|
@@ -510,7 +562,7 @@ class AgentServicer(agent_pb2_grpc.AgentServicer):
|
|
|
510
562
|
status=task_status,
|
|
511
563
|
message=message,
|
|
512
564
|
result=result,
|
|
513
|
-
sandbox=task_info["
|
|
565
|
+
sandbox=task_info["sandbox"]
|
|
514
566
|
)
|
|
515
567
|
|
|
516
568
|
def _mask_config_secrets(self, config: CommonConfig) -> CommonConfig:
|
|
@@ -700,7 +752,7 @@ class AgentServicer(agent_pb2_grpc.AgentServicer):
|
|
|
700
752
|
logger.info(f"Global embedding LLM config updated to: {request.llmConfig.modelName}")
|
|
701
753
|
return request.llmConfig
|
|
702
754
|
|
|
703
|
-
async def _create_sandbox(self,shape:str):
|
|
755
|
+
async def _create_sandbox(self,shape:str)->agent_pb2.Sandbox:
|
|
704
756
|
"""
|
|
705
757
|
Create a sandbox with the given shape via the Lybic service and return its identifier and operating system.
|
|
706
758
|
|
|
@@ -721,7 +773,49 @@ class AgentServicer(agent_pb2_grpc.AgentServicer):
|
|
|
721
773
|
self.sandbox = Sandbox(self.lybic_client)
|
|
722
774
|
result = await self.sandbox.create(shape=shape)
|
|
723
775
|
sandbox = await self.sandbox.get(result.id)
|
|
724
|
-
|
|
776
|
+
|
|
777
|
+
return agent_pb2.Sandbox(
|
|
778
|
+
id=sandbox.sandbox.id,
|
|
779
|
+
os=sandbox.sandbox.shape.os.upper(),
|
|
780
|
+
shapeName=sandbox.sandbox.shapeName,
|
|
781
|
+
hardwareAcceleratedEncoding=sandbox.sandbox.shape.hardwareAcceleratedEncoding,
|
|
782
|
+
virtualization=sandbox.sandbox.shape.virtualization,
|
|
783
|
+
architecture=sandbox.sandbox.shape.architecture,
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
async def _get_sandbox_pb(self, sid: str) -> agent_pb2.Sandbox:
|
|
787
|
+
"""
|
|
788
|
+
Retrieves sandbox details for a given sandbox ID and returns them as a protobuf message.
|
|
789
|
+
"""
|
|
790
|
+
if not self.lybic_auth:
|
|
791
|
+
raise ValueError("Lybic client not initialized. Please call SetGlobalCommonConfig before")
|
|
792
|
+
|
|
793
|
+
if not self.lybic_client:
|
|
794
|
+
await self._new_lybic_client()
|
|
795
|
+
if not self.sandbox:
|
|
796
|
+
self.sandbox = Sandbox(self.lybic_client)
|
|
797
|
+
|
|
798
|
+
sandbox_details = await self.sandbox.get(sid)
|
|
799
|
+
|
|
800
|
+
os_raw = getattr(sandbox_details.sandbox.shape, "os", "") or ""
|
|
801
|
+
os_upper = str(os_raw).upper()
|
|
802
|
+
if "WIN" in os_upper:
|
|
803
|
+
os_enum = agent_pb2.SandboxOS.WINDOWS
|
|
804
|
+
elif "LINUX" in os_upper or "UBUNTU" in os_upper:
|
|
805
|
+
os_enum = agent_pb2.SandboxOS.LINUX
|
|
806
|
+
elif "ANDROID" in os_upper:
|
|
807
|
+
os_enum = agent_pb2.SandboxOS.ANDROID
|
|
808
|
+
else:
|
|
809
|
+
os_enum = agent_pb2.SandboxOS.OSUNDEFINED
|
|
810
|
+
|
|
811
|
+
return agent_pb2.Sandbox(
|
|
812
|
+
id=sandbox_details.sandbox.id,
|
|
813
|
+
os=os_enum,
|
|
814
|
+
shapeName=sandbox_details.sandbox.shapeName,
|
|
815
|
+
hardwareAcceleratedEncoding=sandbox_details.sandbox.shape.hardwareAcceleratedEncoding,
|
|
816
|
+
virtualization=sandbox_details.sandbox.shape.virtualization,
|
|
817
|
+
architecture=sandbox_details.sandbox.shape.architecture,
|
|
818
|
+
)
|
|
725
819
|
|
|
726
820
|
async def serve():
|
|
727
821
|
"""
|
|
@@ -735,8 +829,8 @@ async def serve():
|
|
|
735
829
|
"""
|
|
736
830
|
port = os.environ.get("GRPC_PORT", 50051)
|
|
737
831
|
max_workers = int(os.environ.get("GRPC_MAX_WORKER_THREADS", 100))
|
|
738
|
-
|
|
739
|
-
servicer = AgentServicer(max_concurrent_task_num=
|
|
832
|
+
task_num = int(os.environ.get("TASK_MAX_TASKS", 5))
|
|
833
|
+
servicer = AgentServicer(max_concurrent_task_num=task_num, log_dir=app.log_dir)
|
|
740
834
|
server = grpc.aio.server(futures.ThreadPoolExecutor(max_workers))
|
|
741
835
|
agent_pb2_grpc.add_AgentServicer_to_server(servicer, server)
|
|
742
836
|
server.add_insecure_port(f'[::]:{port}')
|
|
@@ -752,32 +846,6 @@ def main():
|
|
|
752
846
|
has_display, pyautogui_available, _ = app.check_display_environment()
|
|
753
847
|
compatible_backends, incompatible_backends = app.get_compatible_backends(has_display, pyautogui_available)
|
|
754
848
|
app.validate_backend_compatibility('lybic', compatible_backends, incompatible_backends)
|
|
755
|
-
timestamp_dir = os.path.join(app.log_dir, app.datetime_str)
|
|
756
|
-
cache_dir = os.path.join(timestamp_dir, "cache", "screens")
|
|
757
|
-
state_dir = os.path.join(timestamp_dir, "state")
|
|
758
|
-
|
|
759
|
-
os.makedirs(cache_dir, exist_ok=True)
|
|
760
|
-
os.makedirs(state_dir, exist_ok=True)
|
|
761
|
-
|
|
762
|
-
Registry.register(
|
|
763
|
-
"GlobalStateStore",
|
|
764
|
-
GlobalState(
|
|
765
|
-
screenshot_dir=cache_dir,
|
|
766
|
-
tu_path=os.path.join(state_dir, "tu.json"),
|
|
767
|
-
search_query_path=os.path.join(state_dir, "search_query.json"),
|
|
768
|
-
completed_subtasks_path=os.path.join(state_dir,
|
|
769
|
-
"completed_subtasks.json"),
|
|
770
|
-
failed_subtasks_path=os.path.join(state_dir,
|
|
771
|
-
"failed_subtasks.json"),
|
|
772
|
-
remaining_subtasks_path=os.path.join(state_dir,
|
|
773
|
-
"remaining_subtasks.json"),
|
|
774
|
-
termination_flag_path=os.path.join(state_dir,
|
|
775
|
-
"termination_flag.json"),
|
|
776
|
-
running_state_path=os.path.join(state_dir, "running_state.json"),
|
|
777
|
-
display_info_path=os.path.join(timestamp_dir, "display.json"),
|
|
778
|
-
agent_log_path=os.path.join(timestamp_dir, "agent_log.json")
|
|
779
|
-
)
|
|
780
|
-
)
|
|
781
849
|
asyncio.run(serve())
|
|
782
850
|
|
|
783
851
|
if __name__ == '__main__':
|
|
@@ -92,29 +92,41 @@ class AgentService:
|
|
|
92
92
|
|
|
93
93
|
return logger
|
|
94
94
|
|
|
95
|
-
def _get_or_create_agent(self, mode: str, **kwargs) -> Union[AgentS2, AgentSFast]:
|
|
95
|
+
def _get_or_create_agent(self, mode: str, task_id: Optional[str] = None, **kwargs) -> Union[AgentS2, AgentSFast]:
|
|
96
96
|
"""Get or create agent instance based on mode"""
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
# Include task_id in cache key for task isolation when task_id is provided
|
|
98
|
+
if task_id:
|
|
99
|
+
cache_key = f"{mode}_{task_id}_{hash(str(sorted(kwargs.items())))}"
|
|
100
|
+
else:
|
|
101
|
+
cache_key = f"{mode}_{hash(str(sorted(kwargs.items())))}"
|
|
102
|
+
|
|
99
103
|
if cache_key not in self._agents:
|
|
100
104
|
agent_kwargs = {
|
|
101
105
|
'platform': kwargs.get('platform', self.config.default_platform),
|
|
102
106
|
'enable_takeover': kwargs.get('enable_takeover', self.config.enable_takeover),
|
|
103
107
|
'enable_search': kwargs.get('enable_search', self.config.enable_search),
|
|
104
108
|
}
|
|
105
|
-
|
|
109
|
+
|
|
106
110
|
if mode == AgentMode.FAST.value:
|
|
107
111
|
self._agents[cache_key] = AgentSFast(**agent_kwargs)
|
|
108
112
|
else:
|
|
109
113
|
self._agents[cache_key] = AgentS2(**agent_kwargs)
|
|
110
|
-
|
|
114
|
+
|
|
111
115
|
self.logger.debug(f"Created new agent: {mode} with kwargs: {agent_kwargs}")
|
|
112
|
-
|
|
113
|
-
|
|
116
|
+
|
|
117
|
+
# Set task_id on the agent for task-specific operations
|
|
118
|
+
agent = self._agents[cache_key]
|
|
119
|
+
if task_id and hasattr(agent, 'set_task_id'):
|
|
120
|
+
agent.set_task_id(task_id)
|
|
121
|
+
|
|
122
|
+
return agent
|
|
114
123
|
|
|
115
|
-
def _get_or_create_hwi(self, backend: str, **kwargs) -> HardwareInterface:
|
|
124
|
+
def _get_or_create_hwi(self, backend: str, task_id: Optional[str] = None, **kwargs) -> HardwareInterface:
|
|
116
125
|
"""Get or create hardware interface instance"""
|
|
117
|
-
|
|
126
|
+
if task_id:
|
|
127
|
+
cache_key = f"{backend}_{task_id}_{hash(str(sorted(kwargs.items())))}"
|
|
128
|
+
else:
|
|
129
|
+
cache_key = f"{backend}_{hash(str(sorted(kwargs.items())))}"
|
|
118
130
|
|
|
119
131
|
if cache_key not in self._hwi_instances:
|
|
120
132
|
# Get backend-specific config
|
|
@@ -134,17 +146,20 @@ class AgentService:
|
|
|
134
146
|
return self._hwi_instances[cache_key]
|
|
135
147
|
|
|
136
148
|
def _setup_global_state(self, task_id: str) -> str:
|
|
137
|
-
"""Setup global state for task execution"""
|
|
149
|
+
"""Setup global state for task execution with task isolation"""
|
|
138
150
|
# Create timestamp-based directory structure like cli_app.py
|
|
139
151
|
datetime_str = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
140
|
-
timestamp_dir = Path(self.config.log_dir) / datetime_str
|
|
152
|
+
timestamp_dir = Path(self.config.log_dir) / f"{datetime_str}_{task_id[:8]}" # Include task_id prefix
|
|
141
153
|
cache_dir = timestamp_dir / "cache" / "screens"
|
|
142
154
|
state_dir = timestamp_dir / "state"
|
|
143
|
-
|
|
155
|
+
|
|
144
156
|
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
145
157
|
state_dir.mkdir(parents=True, exist_ok=True)
|
|
146
|
-
|
|
147
|
-
#
|
|
158
|
+
|
|
159
|
+
# Create task-specific registry
|
|
160
|
+
task_registry = Registry()
|
|
161
|
+
|
|
162
|
+
# Register global state for this task in task-specific registry
|
|
148
163
|
global_state = GlobalState(
|
|
149
164
|
screenshot_dir=str(cache_dir),
|
|
150
165
|
tu_path=str(state_dir / "tu.json"),
|
|
@@ -157,11 +172,16 @@ class AgentService:
|
|
|
157
172
|
display_info_path=str(timestamp_dir / "display.json"),
|
|
158
173
|
agent_log_path=str(timestamp_dir / "agent_log.json")
|
|
159
174
|
)
|
|
160
|
-
|
|
161
|
-
#
|
|
175
|
+
|
|
176
|
+
# Register in task-specific registry using instance method
|
|
162
177
|
registry_key = "GlobalStateStore"
|
|
163
|
-
|
|
164
|
-
|
|
178
|
+
task_registry.register_instance(registry_key, global_state)
|
|
179
|
+
|
|
180
|
+
# Set task registry in thread-local storage
|
|
181
|
+
Registry.set_task_registry(task_id, task_registry)
|
|
182
|
+
|
|
183
|
+
self.logger.info(f"Setup task-specific registry for task {task_id}")
|
|
184
|
+
|
|
165
185
|
return str(timestamp_dir)
|
|
166
186
|
|
|
167
187
|
def _execute_task_internal(self, request: TaskRequest, task_result: TaskResult) -> TaskResult:
|
|
@@ -173,19 +193,21 @@ class AgentService:
|
|
|
173
193
|
# Setup global state
|
|
174
194
|
task_dir = self._setup_global_state(task_result.task_id)
|
|
175
195
|
|
|
176
|
-
# Create agent and hardware interface
|
|
196
|
+
# Create agent and hardware interface with task_id
|
|
177
197
|
agent = self._get_or_create_agent(
|
|
178
198
|
request.mode,
|
|
199
|
+
task_id=task_result.task_id,
|
|
179
200
|
platform=self.config.default_platform,
|
|
180
201
|
enable_takeover=request.enable_takeover,
|
|
181
202
|
enable_search=request.enable_search
|
|
182
203
|
)
|
|
183
|
-
|
|
204
|
+
|
|
184
205
|
hwi = self._get_or_create_hwi(
|
|
185
206
|
request.backend,
|
|
207
|
+
task_id=task_result.task_id,
|
|
186
208
|
**(request.config or {})
|
|
187
209
|
)
|
|
188
|
-
|
|
210
|
+
|
|
189
211
|
# Reset agent state
|
|
190
212
|
agent.reset()
|
|
191
213
|
|
|
@@ -259,22 +281,17 @@ class AgentService:
|
|
|
259
281
|
task_result.mark_failed(error_msg)
|
|
260
282
|
|
|
261
283
|
finally:
|
|
262
|
-
# Cleanup
|
|
263
|
-
|
|
264
|
-
|
|
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
|
|
284
|
+
# Cleanup task-specific registry
|
|
285
|
+
Registry.remove_task_registry(task_result.task_id)
|
|
286
|
+
self.logger.info(f"Cleaned up task-specific registry for task {task_result.task_id}")
|
|
270
287
|
|
|
271
288
|
return task_result
|
|
272
289
|
|
|
273
|
-
def _run_agent_normal_internal(self, agent, instruction: str, hwi, max_steps: int,
|
|
290
|
+
def _run_agent_normal_internal(self, agent, instruction: str, hwi, max_steps: int,
|
|
274
291
|
enable_takeover: bool, task_id: str):
|
|
275
292
|
"""Run agent in normal mode (adapted from cli_app.py)"""
|
|
276
293
|
# This is a simplified version - you may want to adapt the full logic from cli_app.py
|
|
277
|
-
global_state: GlobalState = Registry.
|
|
294
|
+
global_state: GlobalState = Registry.get_from_context("GlobalStateStore", task_id) # type: ignore
|
|
278
295
|
global_state.set_Tu(instruction)
|
|
279
296
|
global_state.set_running_state("running")
|
|
280
297
|
|
|
@@ -316,10 +333,10 @@ class AgentService:
|
|
|
316
333
|
hwi.dispatchDict(code[0])
|
|
317
334
|
time.sleep(1.0)
|
|
318
335
|
|
|
319
|
-
def _run_agent_fast_internal(self, agent, instruction: str, hwi, max_steps: int,
|
|
320
|
-
|
|
336
|
+
def _run_agent_fast_internal(self, agent, instruction: str, hwi, max_steps: int,
|
|
337
|
+
enable_takeover: bool, task_id: str):
|
|
321
338
|
"""Run agent in fast mode (adapted from cli_app.py)"""
|
|
322
|
-
global_state: GlobalState = Registry.
|
|
339
|
+
global_state: GlobalState = Registry.get_from_context("GlobalStateStore", task_id) # type: ignore
|
|
323
340
|
global_state.set_Tu(instruction)
|
|
324
341
|
global_state.set_running_state("running")
|
|
325
342
|
|
gui_agents/store/registry.py
CHANGED
|
@@ -4,19 +4,127 @@
|
|
|
4
4
|
# from gui_agents.store.registry import Registry
|
|
5
5
|
# GlobalStateStore = Registry.get("GlobalStateStore")
|
|
6
6
|
|
|
7
|
+
import threading
|
|
8
|
+
from typing import Optional, ClassVar
|
|
9
|
+
|
|
10
|
+
|
|
7
11
|
class Registry:
|
|
8
|
-
|
|
12
|
+
"""
|
|
13
|
+
Registry class that supports both global singleton and task-specific instances.
|
|
14
|
+
It uses a process-wide dictionary for task registries to ensure visibility
|
|
15
|
+
across threads, making it compatible with asyncio.to_thread.
|
|
16
|
+
"""
|
|
17
|
+
# For global singletons (backward compatibility)
|
|
18
|
+
_global_services: ClassVar[dict[str, object]] = {}
|
|
19
|
+
_global_lock: ClassVar[threading.RLock] = threading.RLock()
|
|
20
|
+
|
|
21
|
+
# Process-wide storage for task-specific registries, protected by a lock
|
|
22
|
+
_task_registries: ClassVar[dict[str, 'Registry']] = {}
|
|
23
|
+
_task_registries_lock: ClassVar[threading.RLock] = threading.RLock()
|
|
24
|
+
|
|
25
|
+
# Thread-local storage can be used as a cache for faster access
|
|
26
|
+
_thread_local: ClassVar[threading.local] = threading.local()
|
|
27
|
+
|
|
28
|
+
def __init__(self):
|
|
29
|
+
"""Create a new registry instance (for a specific task)."""
|
|
30
|
+
self._services: dict[str, object] = {}
|
|
31
|
+
self._lock = threading.RLock()
|
|
9
32
|
|
|
33
|
+
# ========== Instance methods (for a single registry) ==========
|
|
34
|
+
def register_instance(self, name: str, obj: object):
|
|
35
|
+
"""Register an object in this registry instance."""
|
|
36
|
+
with self._lock:
|
|
37
|
+
self._services[name] = obj
|
|
38
|
+
|
|
39
|
+
def get_instance(self, name: str) -> object:
|
|
40
|
+
"""Get an object from this registry instance."""
|
|
41
|
+
with self._lock:
|
|
42
|
+
if name not in self._services:
|
|
43
|
+
raise KeyError(f"{name!r} not registered in this Registry instance")
|
|
44
|
+
return self._services[name]
|
|
45
|
+
|
|
46
|
+
def clear_instance(self):
|
|
47
|
+
"""Clear all objects in this registry instance."""
|
|
48
|
+
with self._lock:
|
|
49
|
+
self._services.clear()
|
|
50
|
+
|
|
51
|
+
# ========== Class methods for global registry (backward compatibility) ==========
|
|
10
52
|
@classmethod
|
|
11
53
|
def register(cls, name: str, obj: object):
|
|
12
|
-
|
|
54
|
+
"""Register an object in the global registry."""
|
|
55
|
+
with cls._global_lock:
|
|
56
|
+
cls._global_services[name] = obj
|
|
13
57
|
|
|
14
58
|
@classmethod
|
|
15
59
|
def get(cls, name: str) -> object:
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
60
|
+
"""Get an object from the global registry."""
|
|
61
|
+
with cls._global_lock:
|
|
62
|
+
if name not in cls._global_services:
|
|
63
|
+
raise KeyError(f"{name!r} not registered in global Registry")
|
|
64
|
+
return cls._global_services[name]
|
|
19
65
|
|
|
20
66
|
@classmethod
|
|
21
67
|
def clear(cls):
|
|
22
|
-
|
|
68
|
+
"""Clear all objects in the global registry."""
|
|
69
|
+
with cls._global_lock:
|
|
70
|
+
cls._global_services.clear()
|
|
71
|
+
|
|
72
|
+
# ========== Task-specific registry management (Process-wide) ==========
|
|
73
|
+
@classmethod
|
|
74
|
+
def set_task_registry(cls, task_id: str, registry: 'Registry'):
|
|
75
|
+
"""Set a task-specific registry, making it visible process-wide."""
|
|
76
|
+
with cls._task_registries_lock:
|
|
77
|
+
cls._task_registries[task_id] = registry
|
|
78
|
+
|
|
79
|
+
# Also set it in thread-local for faster access within the current thread
|
|
80
|
+
if not hasattr(cls._thread_local, 'task_cache'):
|
|
81
|
+
cls._thread_local.task_cache = {}
|
|
82
|
+
cls._thread_local.task_cache[task_id] = registry
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def get_task_registry(cls, task_id: str) -> Optional['Registry']:
|
|
86
|
+
"""Get a task-specific registry, checking thread-local cache first."""
|
|
87
|
+
# Check thread-local cache first for performance
|
|
88
|
+
if hasattr(cls._thread_local, 'task_cache'):
|
|
89
|
+
cached_registry = cls._thread_local.task_cache.get(task_id)
|
|
90
|
+
if cached_registry:
|
|
91
|
+
return cached_registry
|
|
92
|
+
|
|
93
|
+
# If not in cache, check the process-wide dictionary
|
|
94
|
+
with cls._task_registries_lock:
|
|
95
|
+
registry = cls._task_registries.get(task_id)
|
|
96
|
+
if registry:
|
|
97
|
+
# Populate cache for subsequent calls in the same thread
|
|
98
|
+
if not hasattr(cls._thread_local, 'task_cache'):
|
|
99
|
+
cls._thread_local.task_cache = {}
|
|
100
|
+
cls._thread_local.task_cache[task_id] = registry
|
|
101
|
+
return registry
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def remove_task_registry(cls, task_id: str):
|
|
105
|
+
"""Remove a task-specific registry from process-wide and thread-local storage."""
|
|
106
|
+
# Remove from the main process-wide storage
|
|
107
|
+
with cls._task_registries_lock:
|
|
108
|
+
cls._task_registries.pop(task_id, None)
|
|
109
|
+
|
|
110
|
+
# Remove from the current thread's local cache, if it exists
|
|
111
|
+
if hasattr(cls._thread_local, 'task_cache'):
|
|
112
|
+
cls._thread_local.task_cache.pop(task_id, None)
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def get_from_context(cls, name: str, task_id: Optional[str] = None) -> object:
|
|
116
|
+
"""
|
|
117
|
+
Get an object, trying task-specific registry first, then global registry.
|
|
118
|
+
This is now thread-safe across different threads for the same task_id.
|
|
119
|
+
"""
|
|
120
|
+
# Try task-specific registry first
|
|
121
|
+
if task_id:
|
|
122
|
+
task_registry = cls.get_task_registry(task_id)
|
|
123
|
+
if task_registry:
|
|
124
|
+
try:
|
|
125
|
+
return task_registry.get_instance(name)
|
|
126
|
+
except KeyError:
|
|
127
|
+
pass # Fall back to global registry
|
|
128
|
+
|
|
129
|
+
# Fall back to global registry for CLI mode or if not in task registry
|
|
130
|
+
return cls.get(name)
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lybic-guiagents
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: An open-source agentic framework that enables AI to use computers like humans and can provide a multi-agent runtime environment as an infrastructure capability
|
|
5
5
|
Author: Lybic Development Team
|
|
6
6
|
Author-email: Lybic Development Team <lybic@tingyutech.com>
|
|
7
7
|
License-Expression: Apache-2.0
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
|
-
Requires-Python: >=3.12, <3.
|
|
9
|
+
Requires-Python: >=3.12, <3.15
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
11
|
License-File: LICENSE
|
|
12
12
|
Requires-Dist: dotenv
|
|
@@ -281,11 +281,11 @@ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.8.5/install.ps
|
|
|
281
281
|
# testing uv installation, version should be 0.8.5
|
|
282
282
|
uv --version
|
|
283
283
|
|
|
284
|
-
# 2. Install the python 3.
|
|
285
|
-
uv python install 3.
|
|
284
|
+
# 2. Install the python 3.14
|
|
285
|
+
uv python install 3.14
|
|
286
286
|
|
|
287
287
|
# 3. Create a virtual environment
|
|
288
|
-
uv venv -p 3.
|
|
288
|
+
uv venv -p 3.14
|
|
289
289
|
|
|
290
290
|
# 4. Activate the virtual environment
|
|
291
291
|
# macOS and Linux
|
|
@@ -525,7 +525,7 @@ USE_PRECREATE_VM=Ubuntu
|
|
|
525
525
|
**Problem**: `ModuleNotFoundError` or package import errors.
|
|
526
526
|
|
|
527
527
|
**Solution**:
|
|
528
|
-
- Ensure you're using Python 3.12
|
|
528
|
+
- Ensure you're using Python >= 3.12
|
|
529
529
|
- Activate the virtual environment:
|
|
530
530
|
```bash
|
|
531
531
|
# macOS/Linux
|