oagi-core 0.9.2__py3-none-any.whl → 0.10.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.
- oagi/__init__.py +76 -33
- oagi/agent/__init__.py +2 -0
- oagi/agent/default.py +41 -8
- oagi/agent/factories.py +22 -3
- oagi/agent/observer/__init__.py +38 -0
- oagi/agent/observer/agent_observer.py +99 -0
- oagi/agent/observer/events.py +28 -0
- oagi/agent/observer/exporters.py +445 -0
- oagi/agent/observer/protocol.py +12 -0
- oagi/agent/registry.py +2 -2
- oagi/agent/tasker/models.py +1 -0
- oagi/agent/tasker/planner.py +30 -7
- oagi/agent/tasker/taskee_agent.py +171 -79
- oagi/agent/tasker/tasker_agent.py +20 -9
- oagi/cli/agent.py +42 -3
- oagi/cli/tracking.py +27 -17
- oagi/handler/pyautogui_action_handler.py +7 -0
- oagi/server/agent_wrappers.py +5 -5
- oagi/server/models.py +1 -1
- oagi/server/session_store.py +2 -2
- oagi/task/async_.py +11 -32
- oagi/task/async_short.py +1 -1
- oagi/task/base.py +41 -7
- oagi/task/short.py +1 -1
- oagi/task/sync.py +9 -32
- oagi/types/__init__.py +24 -4
- oagi/types/async_image_provider.py +3 -2
- oagi/types/image_provider.py +3 -2
- oagi/types/step_observer.py +75 -16
- oagi/types/url.py +3 -0
- {oagi_core-0.9.2.dist-info → oagi_core-0.10.0.dist-info}/METADATA +37 -25
- oagi_core-0.10.0.dist-info/RECORD +68 -0
- oagi/types/url_image.py +0 -47
- oagi_core-0.9.2.dist-info/RECORD +0 -63
- {oagi_core-0.9.2.dist-info → oagi_core-0.10.0.dist-info}/WHEEL +0 -0
- {oagi_core-0.9.2.dist-info → oagi_core-0.10.0.dist-info}/entry_points.txt +0 -0
- {oagi_core-0.9.2.dist-info → oagi_core-0.10.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -11,7 +11,16 @@ from datetime import datetime
|
|
|
11
11
|
from typing import Any
|
|
12
12
|
|
|
13
13
|
from oagi import AsyncActor
|
|
14
|
-
from oagi.types import
|
|
14
|
+
from oagi.types import (
|
|
15
|
+
URL,
|
|
16
|
+
ActionEvent,
|
|
17
|
+
AsyncActionHandler,
|
|
18
|
+
AsyncImageProvider,
|
|
19
|
+
AsyncObserver,
|
|
20
|
+
Image,
|
|
21
|
+
PlanEvent,
|
|
22
|
+
StepEvent,
|
|
23
|
+
)
|
|
15
24
|
|
|
16
25
|
from ..protocol import AsyncAgent
|
|
17
26
|
from .memory import PlannerMemory
|
|
@@ -21,6 +30,13 @@ from .planner import Planner
|
|
|
21
30
|
logger = logging.getLogger(__name__)
|
|
22
31
|
|
|
23
32
|
|
|
33
|
+
def _serialize_image(image: Image | str) -> bytes | str:
|
|
34
|
+
"""Convert an image to bytes or keep URL as string."""
|
|
35
|
+
if isinstance(image, str):
|
|
36
|
+
return image
|
|
37
|
+
return image.read()
|
|
38
|
+
|
|
39
|
+
|
|
24
40
|
class TaskeeAgent(AsyncAgent):
|
|
25
41
|
"""Executes a single todo with planning and reflection capabilities.
|
|
26
42
|
|
|
@@ -36,13 +52,13 @@ class TaskeeAgent(AsyncAgent):
|
|
|
36
52
|
api_key: str | None = None,
|
|
37
53
|
base_url: str | None = None,
|
|
38
54
|
model: str = "lux-actor-1",
|
|
39
|
-
|
|
55
|
+
max_steps: int = 20,
|
|
40
56
|
reflection_interval: int = 4,
|
|
41
57
|
temperature: float = 0.5,
|
|
42
58
|
planner: Planner | None = None,
|
|
43
59
|
external_memory: PlannerMemory | None = None,
|
|
44
60
|
todo_index: int | None = None,
|
|
45
|
-
step_observer:
|
|
61
|
+
step_observer: AsyncObserver | None = None,
|
|
46
62
|
):
|
|
47
63
|
"""Initialize the taskee agent.
|
|
48
64
|
|
|
@@ -50,7 +66,7 @@ class TaskeeAgent(AsyncAgent):
|
|
|
50
66
|
api_key: OAGI API key
|
|
51
67
|
base_url: OAGI API base URL
|
|
52
68
|
model: Model to use for vision tasks
|
|
53
|
-
|
|
69
|
+
max_steps: Maximum steps before reinitializing task
|
|
54
70
|
reflection_interval: Number of actions before triggering reflection
|
|
55
71
|
temperature: Sampling temperature
|
|
56
72
|
planner: Planner for planning and reflection
|
|
@@ -61,7 +77,7 @@ class TaskeeAgent(AsyncAgent):
|
|
|
61
77
|
self.api_key = api_key
|
|
62
78
|
self.base_url = base_url
|
|
63
79
|
self.model = model
|
|
64
|
-
self.
|
|
80
|
+
self.max_steps = max_steps
|
|
65
81
|
self.reflection_interval = reflection_interval
|
|
66
82
|
self.temperature = temperature
|
|
67
83
|
self.planner = planner or Planner(api_key=api_key, base_url=base_url)
|
|
@@ -101,17 +117,27 @@ class TaskeeAgent(AsyncAgent):
|
|
|
101
117
|
self.success = False
|
|
102
118
|
|
|
103
119
|
try:
|
|
120
|
+
self.actor = AsyncActor(
|
|
121
|
+
api_key=self.api_key,
|
|
122
|
+
base_url=self.base_url,
|
|
123
|
+
model=self.model,
|
|
124
|
+
temperature=self.temperature,
|
|
125
|
+
)
|
|
104
126
|
# Initial planning
|
|
105
127
|
await self._initial_plan(image_provider)
|
|
106
128
|
|
|
129
|
+
# Initialize the actor with the task
|
|
130
|
+
await self.actor.init_task(
|
|
131
|
+
self.current_instruction, max_steps=self.max_steps
|
|
132
|
+
)
|
|
133
|
+
|
|
107
134
|
# Main execution loop with reinitializations
|
|
108
|
-
|
|
109
|
-
remaining_steps = max_total_steps
|
|
135
|
+
remaining_steps = self.max_steps
|
|
110
136
|
|
|
111
137
|
while remaining_steps > 0 and not self.success:
|
|
112
138
|
# Execute subtask
|
|
113
139
|
steps_taken = await self._execute_subtask(
|
|
114
|
-
min(self.
|
|
140
|
+
min(self.max_steps, remaining_steps),
|
|
115
141
|
action_handler,
|
|
116
142
|
image_provider,
|
|
117
143
|
)
|
|
@@ -174,6 +200,17 @@ class TaskeeAgent(AsyncAgent):
|
|
|
174
200
|
result=plan_output.instruction,
|
|
175
201
|
)
|
|
176
202
|
|
|
203
|
+
# Emit plan event
|
|
204
|
+
if self.step_observer:
|
|
205
|
+
await self.step_observer.on_event(
|
|
206
|
+
PlanEvent(
|
|
207
|
+
phase="initial",
|
|
208
|
+
image=_serialize_image(screenshot),
|
|
209
|
+
reasoning=plan_output.reasoning,
|
|
210
|
+
result=plan_output.instruction,
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
|
|
177
214
|
# Set current instruction
|
|
178
215
|
self.current_instruction = plan_output.instruction
|
|
179
216
|
logger.info(f"Initial instruction: {self.current_instruction}")
|
|
@@ -201,89 +238,108 @@ class TaskeeAgent(AsyncAgent):
|
|
|
201
238
|
"""
|
|
202
239
|
logger.info(f"Executing subtask with max {max_steps} steps")
|
|
203
240
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
#
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
241
|
+
steps_taken = 0
|
|
242
|
+
client = self.planner._ensure_client()
|
|
243
|
+
|
|
244
|
+
for step_num in range(max_steps):
|
|
245
|
+
# Capture screenshot
|
|
246
|
+
screenshot = await image_provider()
|
|
247
|
+
|
|
248
|
+
# Upload screenshot first to get UUID (avoids re-upload in actor.step)
|
|
249
|
+
try:
|
|
250
|
+
upload_response = await client.put_s3_presigned_url(screenshot)
|
|
251
|
+
screenshot_uuid = upload_response.uuid
|
|
252
|
+
screenshot_url = upload_response.download_url
|
|
253
|
+
except Exception as e:
|
|
254
|
+
logger.error(f"Error uploading screenshot: {e}")
|
|
255
|
+
self._record_action(
|
|
256
|
+
action_type="error",
|
|
257
|
+
target="screenshot_upload",
|
|
258
|
+
reasoning=str(e),
|
|
259
|
+
)
|
|
260
|
+
break
|
|
261
|
+
|
|
262
|
+
# Get next step from OAGI using URL (avoids re-upload)
|
|
263
|
+
try:
|
|
264
|
+
step = await self.actor.step(URL(screenshot_url), instruction=None)
|
|
265
|
+
except Exception as e:
|
|
266
|
+
logger.error(f"Error getting step from OAGI: {e}")
|
|
267
|
+
self._record_action(
|
|
268
|
+
action_type="error",
|
|
269
|
+
target="oagi_step",
|
|
270
|
+
reasoning=str(e),
|
|
271
|
+
screenshot_uuid=screenshot_uuid,
|
|
272
|
+
)
|
|
273
|
+
break
|
|
274
|
+
|
|
275
|
+
# Log reasoning
|
|
276
|
+
if step.reason:
|
|
277
|
+
logger.info(f"Step {self.total_actions + 1}: {step.reason}")
|
|
278
|
+
|
|
279
|
+
# Emit step event
|
|
280
|
+
if self.step_observer:
|
|
281
|
+
await self.step_observer.on_event(
|
|
282
|
+
StepEvent(
|
|
283
|
+
step_num=self.total_actions + 1,
|
|
284
|
+
image=_serialize_image(screenshot),
|
|
285
|
+
step=step,
|
|
231
286
|
)
|
|
232
|
-
|
|
287
|
+
)
|
|
233
288
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
289
|
+
# Record OAGI actions
|
|
290
|
+
if step.actions:
|
|
291
|
+
# Log actions with details
|
|
292
|
+
logger.info(f"Actions ({len(step.actions)}):")
|
|
293
|
+
for action in step.actions:
|
|
294
|
+
count_suffix = (
|
|
295
|
+
f" x{action.count}" if action.count and action.count > 1 else ""
|
|
296
|
+
)
|
|
297
|
+
logger.info(
|
|
298
|
+
f" [{action.type.value}] {action.argument}{count_suffix}"
|
|
299
|
+
)
|
|
237
300
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
301
|
+
for action in step.actions:
|
|
302
|
+
self._record_action(
|
|
303
|
+
action_type=action.type.lower(),
|
|
304
|
+
target=action.argument,
|
|
305
|
+
reasoning=step.reason,
|
|
306
|
+
screenshot_uuid=screenshot_uuid,
|
|
242
307
|
)
|
|
243
308
|
|
|
244
|
-
#
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if action.count and action.count > 1
|
|
252
|
-
else ""
|
|
253
|
-
)
|
|
254
|
-
logger.info(
|
|
255
|
-
f" [{action.type.value}] {action.argument}{count_suffix}"
|
|
256
|
-
)
|
|
309
|
+
# Execute actions
|
|
310
|
+
error = None
|
|
311
|
+
try:
|
|
312
|
+
await action_handler(step.actions)
|
|
313
|
+
except Exception as e:
|
|
314
|
+
error = str(e)
|
|
315
|
+
raise
|
|
257
316
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
317
|
+
# Emit action event
|
|
318
|
+
if self.step_observer:
|
|
319
|
+
await self.step_observer.on_event(
|
|
320
|
+
ActionEvent(
|
|
321
|
+
step_num=self.total_actions + 1,
|
|
322
|
+
actions=step.actions,
|
|
323
|
+
error=error,
|
|
263
324
|
)
|
|
325
|
+
)
|
|
264
326
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
self.total_actions += len(step.actions)
|
|
268
|
-
self.since_reflection += len(step.actions)
|
|
327
|
+
self.total_actions += len(step.actions)
|
|
328
|
+
self.since_reflection += len(step.actions)
|
|
269
329
|
|
|
270
|
-
|
|
330
|
+
steps_taken += 1
|
|
271
331
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
break
|
|
332
|
+
# Check if task is complete
|
|
333
|
+
if step.stop:
|
|
334
|
+
logger.info("OAGI signaled task completion")
|
|
335
|
+
break
|
|
277
336
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
337
|
+
# Check if reflection is needed
|
|
338
|
+
if self.since_reflection >= self.reflection_interval:
|
|
339
|
+
logger.info("Reflection interval reached")
|
|
340
|
+
break
|
|
282
341
|
|
|
283
|
-
|
|
284
|
-
# Clear reference after context manager closes it
|
|
285
|
-
self.actor = None
|
|
286
|
-
return steps_taken
|
|
342
|
+
return steps_taken
|
|
287
343
|
|
|
288
344
|
async def _reflect_and_decide(self, image_provider: AsyncImageProvider) -> bool:
|
|
289
345
|
"""Reflect on progress and decide whether to continue.
|
|
@@ -314,6 +370,7 @@ class TaskeeAgent(AsyncAgent):
|
|
|
314
370
|
memory=self.external_memory,
|
|
315
371
|
todo_index=self.todo_index,
|
|
316
372
|
current_instruction=self.current_instruction,
|
|
373
|
+
reflection_interval=self.reflection_interval,
|
|
317
374
|
)
|
|
318
375
|
|
|
319
376
|
# Record reflection
|
|
@@ -324,6 +381,22 @@ class TaskeeAgent(AsyncAgent):
|
|
|
324
381
|
result=("continue" if reflection.continue_current else "pivot"),
|
|
325
382
|
)
|
|
326
383
|
|
|
384
|
+
# Emit plan event for reflection
|
|
385
|
+
if self.step_observer:
|
|
386
|
+
decision = (
|
|
387
|
+
"success"
|
|
388
|
+
if reflection.success_assessment
|
|
389
|
+
else ("continue" if reflection.continue_current else "pivot")
|
|
390
|
+
)
|
|
391
|
+
await self.step_observer.on_event(
|
|
392
|
+
PlanEvent(
|
|
393
|
+
phase="reflection",
|
|
394
|
+
image=_serialize_image(screenshot),
|
|
395
|
+
reasoning=reflection.reasoning,
|
|
396
|
+
result=decision,
|
|
397
|
+
)
|
|
398
|
+
)
|
|
399
|
+
|
|
327
400
|
# Update success assessment
|
|
328
401
|
if reflection.success_assessment:
|
|
329
402
|
self.success = True
|
|
@@ -337,6 +410,11 @@ class TaskeeAgent(AsyncAgent):
|
|
|
337
410
|
if not reflection.continue_current and reflection.new_instruction:
|
|
338
411
|
logger.info(f"Pivoting to new instruction: {reflection.new_instruction}")
|
|
339
412
|
self.current_instruction = reflection.new_instruction
|
|
413
|
+
|
|
414
|
+
# the following line create a new actor
|
|
415
|
+
await self.actor.init_task(
|
|
416
|
+
self.current_instruction, max_steps=self.max_steps
|
|
417
|
+
)
|
|
340
418
|
return True
|
|
341
419
|
|
|
342
420
|
return reflection.continue_current
|
|
@@ -362,6 +440,17 @@ class TaskeeAgent(AsyncAgent):
|
|
|
362
440
|
reasoning=summary,
|
|
363
441
|
)
|
|
364
442
|
|
|
443
|
+
# Emit plan event for summary
|
|
444
|
+
if self.step_observer:
|
|
445
|
+
await self.step_observer.on_event(
|
|
446
|
+
PlanEvent(
|
|
447
|
+
phase="summary",
|
|
448
|
+
image=None,
|
|
449
|
+
reasoning=summary,
|
|
450
|
+
result=None,
|
|
451
|
+
)
|
|
452
|
+
)
|
|
453
|
+
|
|
365
454
|
logger.info(f"Execution summary: {summary}")
|
|
366
455
|
|
|
367
456
|
def _record_action(
|
|
@@ -370,6 +459,7 @@ class TaskeeAgent(AsyncAgent):
|
|
|
370
459
|
target: str | None,
|
|
371
460
|
reasoning: str | None = None,
|
|
372
461
|
result: str | None = None,
|
|
462
|
+
screenshot_uuid: str | None = None,
|
|
373
463
|
) -> None:
|
|
374
464
|
"""Record an action to the history.
|
|
375
465
|
|
|
@@ -378,6 +468,7 @@ class TaskeeAgent(AsyncAgent):
|
|
|
378
468
|
target: Target of the action
|
|
379
469
|
reasoning: Reasoning for the action
|
|
380
470
|
result: Result of the action
|
|
471
|
+
screenshot_uuid: UUID of uploaded screenshot for this action
|
|
381
472
|
"""
|
|
382
473
|
action = Action(
|
|
383
474
|
timestamp=datetime.now().isoformat(),
|
|
@@ -386,6 +477,7 @@ class TaskeeAgent(AsyncAgent):
|
|
|
386
477
|
reasoning=reasoning,
|
|
387
478
|
result=result,
|
|
388
479
|
details={},
|
|
480
|
+
screenshot_uuid=screenshot_uuid,
|
|
389
481
|
)
|
|
390
482
|
self.actions.append(action)
|
|
391
483
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import logging
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
|
-
from oagi.types import AsyncActionHandler, AsyncImageProvider,
|
|
12
|
+
from oagi.types import AsyncActionHandler, AsyncImageProvider, AsyncObserver, SplitEvent
|
|
13
13
|
|
|
14
14
|
from ..protocol import AsyncAgent
|
|
15
15
|
from .memory import PlannerMemory
|
|
@@ -39,7 +39,7 @@ class TaskerAgent(AsyncAgent):
|
|
|
39
39
|
temperature: float = 0.5,
|
|
40
40
|
reflection_interval: int = 4,
|
|
41
41
|
planner: Planner | None = None,
|
|
42
|
-
step_observer:
|
|
42
|
+
step_observer: AsyncObserver | None = None,
|
|
43
43
|
):
|
|
44
44
|
"""Initialize the tasker agent.
|
|
45
45
|
|
|
@@ -100,18 +100,13 @@ class TaskerAgent(AsyncAgent):
|
|
|
100
100
|
or a failure occurs.
|
|
101
101
|
|
|
102
102
|
Args:
|
|
103
|
-
instruction:
|
|
103
|
+
instruction: Not used in TaskerAgent
|
|
104
104
|
action_handler: Handler for executing actions
|
|
105
105
|
image_provider: Provider for capturing screenshots
|
|
106
106
|
|
|
107
107
|
Returns:
|
|
108
108
|
True if all todos completed successfully, False otherwise
|
|
109
109
|
"""
|
|
110
|
-
# If task not set, use instruction as task description
|
|
111
|
-
if not self.memory.task_description:
|
|
112
|
-
logger.warning("Task not set, using instruction as task description")
|
|
113
|
-
self.memory.task_description = instruction
|
|
114
|
-
|
|
115
110
|
overall_success = True
|
|
116
111
|
|
|
117
112
|
# Execute todos until none remain
|
|
@@ -127,6 +122,14 @@ class TaskerAgent(AsyncAgent):
|
|
|
127
122
|
todo, todo_index = todo_info
|
|
128
123
|
logger.info(f"Executing todo {todo_index}: {todo.description}")
|
|
129
124
|
|
|
125
|
+
# Emit split event at the start of todo
|
|
126
|
+
if self.step_observer:
|
|
127
|
+
await self.step_observer.on_event(
|
|
128
|
+
SplitEvent(
|
|
129
|
+
label=f"Start of todo {todo_index + 1}: {todo.description}"
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
|
|
130
133
|
# Execute the todo
|
|
131
134
|
success = await self._execute_todo(
|
|
132
135
|
todo_index,
|
|
@@ -134,6 +137,14 @@ class TaskerAgent(AsyncAgent):
|
|
|
134
137
|
image_provider,
|
|
135
138
|
)
|
|
136
139
|
|
|
140
|
+
# Emit split event after each todo
|
|
141
|
+
if self.step_observer:
|
|
142
|
+
await self.step_observer.on_event(
|
|
143
|
+
SplitEvent(
|
|
144
|
+
label=f"End of todo {todo_index + 1}: {todo.description}"
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
|
|
137
148
|
if not success:
|
|
138
149
|
logger.warning(f"Todo {todo_index} failed")
|
|
139
150
|
overall_success = False
|
|
@@ -171,7 +182,7 @@ class TaskerAgent(AsyncAgent):
|
|
|
171
182
|
api_key=self.api_key,
|
|
172
183
|
base_url=self.base_url,
|
|
173
184
|
model=self.model,
|
|
174
|
-
|
|
185
|
+
max_steps=self.max_steps, # Smaller steps per subtask
|
|
175
186
|
reflection_interval=self.reflection_interval,
|
|
176
187
|
temperature=self.temperature,
|
|
177
188
|
planner=self.planner,
|
oagi/cli/agent.py
CHANGED
|
@@ -13,6 +13,7 @@ import sys
|
|
|
13
13
|
import time
|
|
14
14
|
import traceback
|
|
15
15
|
|
|
16
|
+
from oagi.agent.observer import AsyncAgentObserver
|
|
16
17
|
from oagi.exceptions import check_optional_dependency
|
|
17
18
|
|
|
18
19
|
from .display import display_step_table
|
|
@@ -53,6 +54,17 @@ def add_agent_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
|
53
54
|
type=str,
|
|
54
55
|
help="OAGI base URL (default: https://api.agiopen.org, or OAGI_BASE_URL env var)",
|
|
55
56
|
)
|
|
57
|
+
run_parser.add_argument(
|
|
58
|
+
"--export",
|
|
59
|
+
type=str,
|
|
60
|
+
choices=["markdown", "html", "json"],
|
|
61
|
+
help="Export execution history to file (markdown, html, or json)",
|
|
62
|
+
)
|
|
63
|
+
run_parser.add_argument(
|
|
64
|
+
"--export-file",
|
|
65
|
+
type=str,
|
|
66
|
+
help="Output file path for export (default: execution_report.[md|html|json])",
|
|
67
|
+
)
|
|
56
68
|
|
|
57
69
|
|
|
58
70
|
def handle_agent_command(args: argparse.Namespace) -> None:
|
|
@@ -85,11 +97,23 @@ def run_agent(args: argparse.Namespace) -> None:
|
|
|
85
97
|
max_steps = args.max_steps or 20
|
|
86
98
|
temperature = args.temperature if args.temperature is not None else 0.5
|
|
87
99
|
mode = args.mode or "actor"
|
|
100
|
+
export_format = args.export
|
|
101
|
+
export_file = args.export_file
|
|
88
102
|
|
|
89
|
-
# Create
|
|
103
|
+
# Create observers
|
|
90
104
|
step_tracker = StepTracker()
|
|
105
|
+
agent_observer = AsyncAgentObserver() if export_format else None
|
|
106
|
+
|
|
107
|
+
# Use a combined observer that forwards to both
|
|
108
|
+
class CombinedObserver:
|
|
109
|
+
async def on_event(self, event):
|
|
110
|
+
await step_tracker.on_event(event)
|
|
111
|
+
if agent_observer:
|
|
112
|
+
await agent_observer.on_event(event)
|
|
91
113
|
|
|
92
|
-
|
|
114
|
+
observer = CombinedObserver()
|
|
115
|
+
|
|
116
|
+
# Create agent with observer
|
|
93
117
|
agent = create_agent(
|
|
94
118
|
mode=mode,
|
|
95
119
|
api_key=api_key,
|
|
@@ -97,7 +121,7 @@ def run_agent(args: argparse.Namespace) -> None:
|
|
|
97
121
|
model=model,
|
|
98
122
|
max_steps=max_steps,
|
|
99
123
|
temperature=temperature,
|
|
100
|
-
step_observer=
|
|
124
|
+
step_observer=observer,
|
|
101
125
|
)
|
|
102
126
|
|
|
103
127
|
# Create handlers
|
|
@@ -137,6 +161,21 @@ def run_agent(args: argparse.Namespace) -> None:
|
|
|
137
161
|
else:
|
|
138
162
|
print("\nNo steps were executed.")
|
|
139
163
|
|
|
164
|
+
# Export if requested
|
|
165
|
+
if export_format and agent_observer:
|
|
166
|
+
# Determine output file path
|
|
167
|
+
if export_file:
|
|
168
|
+
output_path = export_file
|
|
169
|
+
else:
|
|
170
|
+
ext_map = {"markdown": "md", "html": "html", "json": "json"}
|
|
171
|
+
output_path = f"execution_report.{ext_map[export_format]}"
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
agent_observer.export(export_format, output_path)
|
|
175
|
+
print(f"\nExecution history exported to: {output_path}")
|
|
176
|
+
except Exception as e:
|
|
177
|
+
print(f"\nError exporting execution history: {e}", file=sys.stderr)
|
|
178
|
+
|
|
140
179
|
if interrupted:
|
|
141
180
|
sys.exit(130)
|
|
142
181
|
elif not success:
|
oagi/cli/tracking.py
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
from dataclasses import dataclass
|
|
10
10
|
from datetime import datetime
|
|
11
11
|
|
|
12
|
-
from oagi.types import Action
|
|
12
|
+
from oagi.types import Action, ActionEvent, ObserverEvent, StepEvent
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
@dataclass
|
|
@@ -23,23 +23,33 @@ class StepData:
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
class StepTracker:
|
|
26
|
-
"""Tracks agent step execution by implementing
|
|
26
|
+
"""Tracks agent step execution by implementing AsyncObserver protocol."""
|
|
27
27
|
|
|
28
28
|
def __init__(self):
|
|
29
29
|
self.steps: list[StepData] = []
|
|
30
30
|
|
|
31
|
-
async def
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
31
|
+
async def on_event(self, event: ObserverEvent) -> None:
|
|
32
|
+
"""Handle observer events.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
event: The observer event to handle.
|
|
36
|
+
"""
|
|
37
|
+
match event:
|
|
38
|
+
case StepEvent():
|
|
39
|
+
step_data = StepData(
|
|
40
|
+
step_num=event.step_num,
|
|
41
|
+
timestamp=event.timestamp,
|
|
42
|
+
reasoning=event.step.reason,
|
|
43
|
+
actions=event.step.actions,
|
|
44
|
+
action_count=len(event.step.actions),
|
|
45
|
+
status="running",
|
|
46
|
+
)
|
|
47
|
+
self.steps.append(step_data)
|
|
48
|
+
case ActionEvent():
|
|
49
|
+
# Update status of corresponding step
|
|
50
|
+
for step in self.steps:
|
|
51
|
+
if step.step_num == event.step_num:
|
|
52
|
+
step.status = "error" if event.error else "completed"
|
|
53
|
+
break
|
|
54
|
+
case _:
|
|
55
|
+
pass
|
|
@@ -80,6 +80,10 @@ class PyautoguiConfig(BaseModel):
|
|
|
80
80
|
default="session",
|
|
81
81
|
description="Caps lock handling mode: 'session' (internal state) or 'system' (OS-level)",
|
|
82
82
|
)
|
|
83
|
+
macos_ctrl_to_cmd: bool = Field(
|
|
84
|
+
default=True,
|
|
85
|
+
description="Replace 'ctrl' with 'command' in hotkey combinations on macOS",
|
|
86
|
+
)
|
|
83
87
|
|
|
84
88
|
|
|
85
89
|
class PyautoguiActionHandler:
|
|
@@ -169,6 +173,9 @@ class PyautoguiActionHandler:
|
|
|
169
173
|
# Normalize caps lock variations
|
|
170
174
|
if key in ["caps_lock", "caps", "capslock"]:
|
|
171
175
|
return "capslock"
|
|
176
|
+
# Remap ctrl to command on macOS if enabled
|
|
177
|
+
if self.config.macos_ctrl_to_cmd and sys.platform == "darwin" and key == "ctrl":
|
|
178
|
+
return "command"
|
|
172
179
|
return key
|
|
173
180
|
|
|
174
181
|
def _parse_hotkey(self, args_str: str) -> list[str]:
|
oagi/server/agent_wrappers.py
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import logging
|
|
10
10
|
from typing import TYPE_CHECKING
|
|
11
11
|
|
|
12
|
-
from ..types import
|
|
12
|
+
from ..types import URL
|
|
13
13
|
from ..types.models.action import Action
|
|
14
14
|
from .models import ScreenshotRequestData, ScreenshotResponseData
|
|
15
15
|
|
|
@@ -56,7 +56,7 @@ class SocketIOImageProvider:
|
|
|
56
56
|
self.oagi_client = oagi_client
|
|
57
57
|
self._last_url: str | None = None
|
|
58
58
|
|
|
59
|
-
async def __call__(self) ->
|
|
59
|
+
async def __call__(self) -> URL:
|
|
60
60
|
logger.debug("Requesting screenshot via Socket.IO")
|
|
61
61
|
|
|
62
62
|
# Get S3 presigned URL from OAGI
|
|
@@ -87,12 +87,12 @@ class SocketIOImageProvider:
|
|
|
87
87
|
self.session.current_screenshot_url = upload_response.download_url
|
|
88
88
|
|
|
89
89
|
logger.debug(f"Screenshot captured successfully: {upload_response.uuid}")
|
|
90
|
-
return
|
|
90
|
+
return URL(upload_response.download_url)
|
|
91
91
|
|
|
92
|
-
async def last_image(self) ->
|
|
92
|
+
async def last_image(self) -> URL:
|
|
93
93
|
if self._last_url:
|
|
94
94
|
logger.debug("Returning last captured screenshot")
|
|
95
|
-
return
|
|
95
|
+
return URL(self._last_url)
|
|
96
96
|
|
|
97
97
|
logger.debug("No previous screenshot, capturing new one")
|
|
98
98
|
return await self()
|
oagi/server/models.py
CHANGED
|
@@ -15,7 +15,7 @@ from pydantic import BaseModel, Field
|
|
|
15
15
|
class InitEventData(BaseModel):
|
|
16
16
|
instruction: str = Field(...)
|
|
17
17
|
mode: str | None = Field(default="actor")
|
|
18
|
-
model: str | None = Field(default="lux-
|
|
18
|
+
model: str | None = Field(default="lux-actor-1")
|
|
19
19
|
temperature: float | None = Field(default=0.1, ge=0.0, le=2.0)
|
|
20
20
|
|
|
21
21
|
|
oagi/server/session_store.py
CHANGED
|
@@ -18,7 +18,7 @@ class Session:
|
|
|
18
18
|
session_id: str,
|
|
19
19
|
instruction: str,
|
|
20
20
|
mode: str = "actor",
|
|
21
|
-
model: str = "lux-
|
|
21
|
+
model: str = "lux-actor-1",
|
|
22
22
|
temperature: float = 0.0,
|
|
23
23
|
):
|
|
24
24
|
self.session_id: str = session_id
|
|
@@ -54,7 +54,7 @@ class SessionStore:
|
|
|
54
54
|
self,
|
|
55
55
|
instruction: str,
|
|
56
56
|
mode: str = "actor",
|
|
57
|
-
model: str = "lux-
|
|
57
|
+
model: str = "lux-actor-1",
|
|
58
58
|
temperature: float = 0.0,
|
|
59
59
|
session_id: str | None = None,
|
|
60
60
|
) -> str:
|