oagi-core 0.10.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.
- oagi/__init__.py +148 -0
- oagi/agent/__init__.py +33 -0
- oagi/agent/default.py +124 -0
- oagi/agent/factories.py +74 -0
- 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/protocol.py +55 -0
- oagi/agent/registry.py +155 -0
- oagi/agent/tasker/__init__.py +33 -0
- oagi/agent/tasker/memory.py +160 -0
- oagi/agent/tasker/models.py +77 -0
- oagi/agent/tasker/planner.py +408 -0
- oagi/agent/tasker/taskee_agent.py +512 -0
- oagi/agent/tasker/tasker_agent.py +324 -0
- oagi/cli/__init__.py +11 -0
- oagi/cli/agent.py +281 -0
- oagi/cli/display.py +56 -0
- oagi/cli/main.py +77 -0
- oagi/cli/server.py +94 -0
- oagi/cli/tracking.py +55 -0
- oagi/cli/utils.py +89 -0
- oagi/client/__init__.py +12 -0
- oagi/client/async_.py +290 -0
- oagi/client/base.py +457 -0
- oagi/client/sync.py +293 -0
- oagi/exceptions.py +118 -0
- oagi/handler/__init__.py +24 -0
- oagi/handler/_macos.py +55 -0
- oagi/handler/async_pyautogui_action_handler.py +44 -0
- oagi/handler/async_screenshot_maker.py +47 -0
- oagi/handler/pil_image.py +102 -0
- oagi/handler/pyautogui_action_handler.py +291 -0
- oagi/handler/screenshot_maker.py +41 -0
- oagi/logging.py +55 -0
- oagi/server/__init__.py +13 -0
- oagi/server/agent_wrappers.py +98 -0
- oagi/server/config.py +46 -0
- oagi/server/main.py +157 -0
- oagi/server/models.py +98 -0
- oagi/server/session_store.py +116 -0
- oagi/server/socketio_server.py +405 -0
- oagi/task/__init__.py +21 -0
- oagi/task/async_.py +101 -0
- oagi/task/async_short.py +76 -0
- oagi/task/base.py +157 -0
- oagi/task/short.py +76 -0
- oagi/task/sync.py +99 -0
- oagi/types/__init__.py +50 -0
- oagi/types/action_handler.py +30 -0
- oagi/types/async_action_handler.py +30 -0
- oagi/types/async_image_provider.py +38 -0
- oagi/types/image.py +17 -0
- oagi/types/image_provider.py +35 -0
- oagi/types/models/__init__.py +32 -0
- oagi/types/models/action.py +33 -0
- oagi/types/models/client.py +68 -0
- oagi/types/models/image_config.py +47 -0
- oagi/types/models/step.py +17 -0
- oagi/types/step_observer.py +93 -0
- oagi/types/url.py +3 -0
- oagi_core-0.10.1.dist-info/METADATA +245 -0
- oagi_core-0.10.1.dist-info/RECORD +68 -0
- oagi_core-0.10.1.dist-info/WHEEL +4 -0
- oagi_core-0.10.1.dist-info/entry_points.txt +2 -0
- oagi_core-0.10.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# -----------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) OpenAGI Foundation
|
|
3
|
+
# All rights reserved.
|
|
4
|
+
#
|
|
5
|
+
# This file is part of the official API project.
|
|
6
|
+
# Licensed under the MIT License.
|
|
7
|
+
# -----------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from oagi.types import AsyncActionHandler, AsyncImageProvider, AsyncObserver, SplitEvent
|
|
13
|
+
|
|
14
|
+
from ..protocol import AsyncAgent
|
|
15
|
+
from .memory import PlannerMemory
|
|
16
|
+
from .models import TodoStatus
|
|
17
|
+
from .planner import Planner
|
|
18
|
+
from .taskee_agent import TaskeeAgent
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TaskerAgent(AsyncAgent):
|
|
24
|
+
"""Hierarchical agent that manages multi-todo workflows.
|
|
25
|
+
|
|
26
|
+
This agent orchestrates the execution of multiple todos by:
|
|
27
|
+
1. Managing a workflow with todos and deliverables
|
|
28
|
+
2. Executing todos sequentially using TaskeeAgent
|
|
29
|
+
3. Tracking progress and updating memory
|
|
30
|
+
4. Sharing context between todos for informed execution
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
api_key: str | None = None,
|
|
36
|
+
base_url: str | None = None,
|
|
37
|
+
model: str = "lux-actor-1",
|
|
38
|
+
max_steps: int = 60,
|
|
39
|
+
temperature: float = 0.5,
|
|
40
|
+
reflection_interval: int = 4,
|
|
41
|
+
planner: Planner | None = None,
|
|
42
|
+
step_observer: AsyncObserver | None = None,
|
|
43
|
+
):
|
|
44
|
+
"""Initialize the tasker agent.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
api_key: OAGI API key
|
|
48
|
+
base_url: OAGI API base URL
|
|
49
|
+
model: Model to use for vision tasks
|
|
50
|
+
max_steps: Maximum steps per todo
|
|
51
|
+
temperature: Sampling temperature
|
|
52
|
+
reflection_interval: Actions before reflection
|
|
53
|
+
planner: Planner for planning and reflection
|
|
54
|
+
step_observer: Optional observer for step tracking
|
|
55
|
+
"""
|
|
56
|
+
self.api_key = api_key
|
|
57
|
+
self.base_url = base_url
|
|
58
|
+
self.model = model
|
|
59
|
+
self.max_steps = max_steps
|
|
60
|
+
self.temperature = temperature
|
|
61
|
+
self.reflection_interval = reflection_interval
|
|
62
|
+
self.planner = planner or Planner(api_key=api_key, base_url=base_url)
|
|
63
|
+
self.step_observer = step_observer
|
|
64
|
+
|
|
65
|
+
# Memory for tracking workflow
|
|
66
|
+
self.memory = PlannerMemory()
|
|
67
|
+
|
|
68
|
+
# Current execution state
|
|
69
|
+
self.current_taskee_agent: TaskeeAgent | None = None
|
|
70
|
+
self.current_todo_index: int = -1
|
|
71
|
+
|
|
72
|
+
def set_task(
|
|
73
|
+
self,
|
|
74
|
+
task: str,
|
|
75
|
+
todos: list[str],
|
|
76
|
+
) -> None:
|
|
77
|
+
"""Set the task and todos for the workflow.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
task: Overall task description
|
|
81
|
+
todos: List of todo descriptions
|
|
82
|
+
"""
|
|
83
|
+
self.memory.set_task(task, todos)
|
|
84
|
+
logger.info(f"Task set with {len(todos)} todos")
|
|
85
|
+
|
|
86
|
+
async def execute(
|
|
87
|
+
self,
|
|
88
|
+
instruction: str,
|
|
89
|
+
action_handler: AsyncActionHandler,
|
|
90
|
+
image_provider: AsyncImageProvider,
|
|
91
|
+
) -> bool:
|
|
92
|
+
"""Execute the multi-todo workflow.
|
|
93
|
+
|
|
94
|
+
This method will execute todos sequentially until all are complete
|
|
95
|
+
or a failure occurs.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
instruction: Not used in TaskerAgent
|
|
99
|
+
action_handler: Handler for executing actions
|
|
100
|
+
image_provider: Provider for capturing screenshots
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
True if all todos completed successfully, False otherwise
|
|
104
|
+
"""
|
|
105
|
+
overall_success = True
|
|
106
|
+
|
|
107
|
+
# Execute todos until none remain
|
|
108
|
+
while True:
|
|
109
|
+
# Prepare for next todo
|
|
110
|
+
todo_info = self._prepare()
|
|
111
|
+
|
|
112
|
+
if todo_info is None:
|
|
113
|
+
# No more todos to execute
|
|
114
|
+
logger.info("No more todos to execute")
|
|
115
|
+
break
|
|
116
|
+
|
|
117
|
+
todo, todo_index = todo_info
|
|
118
|
+
logger.info(f"Executing todo {todo_index}: {todo.description}")
|
|
119
|
+
|
|
120
|
+
# Emit split event at the start of todo
|
|
121
|
+
if self.step_observer:
|
|
122
|
+
await self.step_observer.on_event(
|
|
123
|
+
SplitEvent(
|
|
124
|
+
label=f"Start of todo {todo_index + 1}: {todo.description}"
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Execute the todo
|
|
129
|
+
success = await self._execute_todo(
|
|
130
|
+
todo_index,
|
|
131
|
+
action_handler,
|
|
132
|
+
image_provider,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Emit split event after each todo
|
|
136
|
+
if self.step_observer:
|
|
137
|
+
await self.step_observer.on_event(
|
|
138
|
+
SplitEvent(
|
|
139
|
+
label=f"End of todo {todo_index + 1}: {todo.description}"
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if not success:
|
|
144
|
+
logger.warning(f"Todo {todo_index} failed")
|
|
145
|
+
overall_success = False
|
|
146
|
+
# If todo failed due to exception, it stays IN_PROGRESS
|
|
147
|
+
# Break to avoid infinite loop re-attempting same todo
|
|
148
|
+
current_status = self.memory.todos[todo_index].status
|
|
149
|
+
if current_status == TodoStatus.IN_PROGRESS:
|
|
150
|
+
logger.error("Todo failed with exception, stopping execution")
|
|
151
|
+
break
|
|
152
|
+
# Otherwise continue with next todo
|
|
153
|
+
|
|
154
|
+
# Update task execution summary
|
|
155
|
+
self._update_task_summary()
|
|
156
|
+
|
|
157
|
+
# Log final status
|
|
158
|
+
status_summary = self.memory.get_todo_status_summary()
|
|
159
|
+
logger.info(f"Workflow complete. Status summary: {status_summary}")
|
|
160
|
+
|
|
161
|
+
return overall_success
|
|
162
|
+
|
|
163
|
+
def _prepare(self) -> tuple[Any, int] | None:
|
|
164
|
+
"""Prepare for the next todo execution.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Tuple of (todo, index) or None if no todos remain
|
|
168
|
+
"""
|
|
169
|
+
# Get current todo
|
|
170
|
+
todo, todo_index = self.memory.get_current_todo()
|
|
171
|
+
|
|
172
|
+
if todo is None:
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
# Create taskee agent with external memory
|
|
176
|
+
self.current_taskee_agent = TaskeeAgent(
|
|
177
|
+
api_key=self.api_key,
|
|
178
|
+
base_url=self.base_url,
|
|
179
|
+
model=self.model,
|
|
180
|
+
max_steps=self.max_steps, # Smaller steps per subtask
|
|
181
|
+
reflection_interval=self.reflection_interval,
|
|
182
|
+
temperature=self.temperature,
|
|
183
|
+
planner=self.planner,
|
|
184
|
+
external_memory=self.memory, # Share memory with child
|
|
185
|
+
todo_index=todo_index, # Pass the todo index
|
|
186
|
+
step_observer=self.step_observer, # Pass step observer
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
self.current_todo_index = todo_index
|
|
190
|
+
|
|
191
|
+
# Update todo status to in_progress if it was pending
|
|
192
|
+
if todo.status == TodoStatus.PENDING:
|
|
193
|
+
self.memory.update_todo(todo_index, TodoStatus.IN_PROGRESS)
|
|
194
|
+
|
|
195
|
+
logger.info(f"Prepared taskee agent for todo {todo_index}")
|
|
196
|
+
|
|
197
|
+
return todo, todo_index
|
|
198
|
+
|
|
199
|
+
async def _execute_todo(
|
|
200
|
+
self,
|
|
201
|
+
todo_index: int,
|
|
202
|
+
action_handler: AsyncActionHandler,
|
|
203
|
+
image_provider: AsyncImageProvider,
|
|
204
|
+
) -> bool:
|
|
205
|
+
"""Execute a single todo using the todo agent.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
todo_index: Index of the todo to execute
|
|
209
|
+
action_handler: Handler for executing actions
|
|
210
|
+
image_provider: Provider for capturing screenshots
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
True if successful, False otherwise
|
|
214
|
+
"""
|
|
215
|
+
if not self.current_taskee_agent or todo_index < 0:
|
|
216
|
+
logger.error("No taskee agent prepared")
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
todo = self.memory.todos[todo_index]
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
# Execute using taskee agent
|
|
223
|
+
success = await self.current_taskee_agent.execute(
|
|
224
|
+
todo.description,
|
|
225
|
+
action_handler,
|
|
226
|
+
image_provider,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Get execution results
|
|
230
|
+
results = self.current_taskee_agent.return_execution_results()
|
|
231
|
+
|
|
232
|
+
# Update memory with results
|
|
233
|
+
self._update_memory_from_execution(todo_index, results, success)
|
|
234
|
+
|
|
235
|
+
return success
|
|
236
|
+
|
|
237
|
+
except Exception as e:
|
|
238
|
+
logger.error(f"Error executing todo {todo_index}: {e}")
|
|
239
|
+
# Mark as in_progress (not completed)
|
|
240
|
+
self.memory.update_todo(
|
|
241
|
+
todo_index,
|
|
242
|
+
TodoStatus.IN_PROGRESS,
|
|
243
|
+
summary=f"Execution failed: {str(e)}",
|
|
244
|
+
)
|
|
245
|
+
return False
|
|
246
|
+
|
|
247
|
+
def _update_memory_from_execution(
|
|
248
|
+
self,
|
|
249
|
+
todo_index: int,
|
|
250
|
+
results: Any,
|
|
251
|
+
success: bool,
|
|
252
|
+
) -> None:
|
|
253
|
+
"""Update memory based on execution results.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
todo_index: Index of the executed todo
|
|
257
|
+
results: Execution results from todo agent
|
|
258
|
+
success: Whether execution was successful
|
|
259
|
+
"""
|
|
260
|
+
# Update todo status
|
|
261
|
+
status = TodoStatus.COMPLETED if success else TodoStatus.IN_PROGRESS
|
|
262
|
+
self.memory.update_todo(
|
|
263
|
+
todo_index,
|
|
264
|
+
status,
|
|
265
|
+
summary=results.summary,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Add to history
|
|
269
|
+
self.memory.add_history(
|
|
270
|
+
todo_index,
|
|
271
|
+
results.actions,
|
|
272
|
+
summary=results.summary,
|
|
273
|
+
completed=success,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Update task execution summary
|
|
277
|
+
if success:
|
|
278
|
+
if self.memory.task_execution_summary:
|
|
279
|
+
self.memory.task_execution_summary += (
|
|
280
|
+
f"\n- Completed todo {todo_index}: {results.summary}"
|
|
281
|
+
)
|
|
282
|
+
else:
|
|
283
|
+
self.memory.task_execution_summary = (
|
|
284
|
+
f"- Completed todo {todo_index}: {results.summary}"
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
logger.info(
|
|
288
|
+
f"Updated memory for todo {todo_index}: "
|
|
289
|
+
f"status={status}, actions={len(results.actions)}"
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
def _update_task_summary(self) -> None:
|
|
293
|
+
"""Update the overall task execution summary."""
|
|
294
|
+
status_summary = self.memory.get_todo_status_summary()
|
|
295
|
+
completed = status_summary.get(TodoStatus.COMPLETED, 0)
|
|
296
|
+
total = len(self.memory.todos)
|
|
297
|
+
|
|
298
|
+
summary_parts = [f"Progress: {completed}/{total} todos completed"]
|
|
299
|
+
|
|
300
|
+
# Add recent completions
|
|
301
|
+
for history in self.memory.history[-3:]: # Last 3 entries
|
|
302
|
+
if history.completed and history.summary:
|
|
303
|
+
summary_parts.append(
|
|
304
|
+
f"- Todo {history.todo_index}: {history.summary[:100]}"
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
self.memory.task_execution_summary = "\n".join(summary_parts)
|
|
308
|
+
|
|
309
|
+
def get_memory(self) -> PlannerMemory:
|
|
310
|
+
"""Get the current memory state.
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
Current PlannerMemory instance
|
|
314
|
+
"""
|
|
315
|
+
return self.memory
|
|
316
|
+
|
|
317
|
+
def append_todo(self, description: str) -> None:
|
|
318
|
+
"""Dynamically append a new todo to the workflow.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
description: Description of the new todo
|
|
322
|
+
"""
|
|
323
|
+
self.memory.append_todo(description)
|
|
324
|
+
logger.info(f"Appended new todo: {description}")
|
oagi/cli/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# -----------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) OpenAGI Foundation
|
|
3
|
+
# All rights reserved.
|
|
4
|
+
#
|
|
5
|
+
# This file is part of the official API project.
|
|
6
|
+
# Licensed under the MIT License.
|
|
7
|
+
# -----------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
from oagi.cli.main import main
|
|
10
|
+
|
|
11
|
+
__all__ = ["main"]
|
oagi/cli/agent.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# -----------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) OpenAGI Foundation
|
|
3
|
+
# All rights reserved.
|
|
4
|
+
#
|
|
5
|
+
# This file is part of the official API project.
|
|
6
|
+
# Licensed under the MIT License.
|
|
7
|
+
# -----------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import asyncio
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
import traceback
|
|
15
|
+
|
|
16
|
+
from oagi.agent.observer import AsyncAgentObserver
|
|
17
|
+
from oagi.exceptions import check_optional_dependency
|
|
18
|
+
|
|
19
|
+
from .display import display_step_table
|
|
20
|
+
from .tracking import StepTracker
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def add_agent_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
24
|
+
agent_parser = subparsers.add_parser("agent", help="Agent execution commands")
|
|
25
|
+
agent_subparsers = agent_parser.add_subparsers(dest="agent_command", required=True)
|
|
26
|
+
|
|
27
|
+
# agent run command
|
|
28
|
+
run_parser = agent_subparsers.add_parser(
|
|
29
|
+
"run", help="Run an agent with the given instruction"
|
|
30
|
+
)
|
|
31
|
+
run_parser.add_argument(
|
|
32
|
+
"instruction", type=str, help="Task instruction for the agent to execute"
|
|
33
|
+
)
|
|
34
|
+
run_parser.add_argument(
|
|
35
|
+
"--model", type=str, help="Model to use (default: lux-actor-1)"
|
|
36
|
+
)
|
|
37
|
+
run_parser.add_argument(
|
|
38
|
+
"--max-steps", type=int, help="Maximum number of steps (default: 20)"
|
|
39
|
+
)
|
|
40
|
+
run_parser.add_argument(
|
|
41
|
+
"--temperature", type=float, help="Sampling temperature (default: 0.5)"
|
|
42
|
+
)
|
|
43
|
+
run_parser.add_argument(
|
|
44
|
+
"--mode",
|
|
45
|
+
type=str,
|
|
46
|
+
default="actor",
|
|
47
|
+
help="Agent mode to use (default: actor). Available modes: actor, planner",
|
|
48
|
+
)
|
|
49
|
+
run_parser.add_argument(
|
|
50
|
+
"--oagi-api-key", type=str, help="OAGI API key (default: OAGI_API_KEY env var)"
|
|
51
|
+
)
|
|
52
|
+
run_parser.add_argument(
|
|
53
|
+
"--oagi-base-url",
|
|
54
|
+
type=str,
|
|
55
|
+
help="OAGI base URL (default: https://api.agiopen.org, or OAGI_BASE_URL env var)",
|
|
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
|
+
)
|
|
68
|
+
|
|
69
|
+
# agent permission command
|
|
70
|
+
agent_subparsers.add_parser(
|
|
71
|
+
"permission",
|
|
72
|
+
help="Check macOS permissions for screen recording and accessibility",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def handle_agent_command(args: argparse.Namespace) -> None:
|
|
77
|
+
if args.agent_command == "run":
|
|
78
|
+
run_agent(args)
|
|
79
|
+
elif args.agent_command == "permission":
|
|
80
|
+
check_permissions()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def check_permissions() -> None:
|
|
84
|
+
"""Check and request macOS permissions for screen recording and accessibility.
|
|
85
|
+
|
|
86
|
+
Guides the user through granting permissions one at a time.
|
|
87
|
+
"""
|
|
88
|
+
if sys.platform != "darwin":
|
|
89
|
+
print("Warning: Permission check is only applicable on macOS.")
|
|
90
|
+
print("On other platforms, no special permissions are required.")
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
check_optional_dependency("Quartz", "Permission check", "desktop")
|
|
94
|
+
check_optional_dependency("ApplicationServices", "Permission check", "desktop")
|
|
95
|
+
|
|
96
|
+
import subprocess # noqa: PLC0415
|
|
97
|
+
|
|
98
|
+
from ApplicationServices import AXIsProcessTrusted # noqa: PLC0415
|
|
99
|
+
from Quartz import ( # noqa: PLC0415
|
|
100
|
+
CGPreflightScreenCaptureAccess,
|
|
101
|
+
CGRequestScreenCaptureAccess,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Check all permissions first to show status
|
|
105
|
+
screen_recording_granted = CGPreflightScreenCaptureAccess()
|
|
106
|
+
accessibility_granted = AXIsProcessTrusted()
|
|
107
|
+
|
|
108
|
+
print("Checking permissions...")
|
|
109
|
+
print(f" {'[OK]' if screen_recording_granted else '[MISSING]'} Screen Recording")
|
|
110
|
+
print(f" {'[OK]' if accessibility_granted else '[MISSING]'} Accessibility")
|
|
111
|
+
|
|
112
|
+
# Guide user through missing permissions one at a time
|
|
113
|
+
if not screen_recording_granted:
|
|
114
|
+
CGRequestScreenCaptureAccess()
|
|
115
|
+
subprocess.run(
|
|
116
|
+
[
|
|
117
|
+
"open",
|
|
118
|
+
"x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture",
|
|
119
|
+
],
|
|
120
|
+
check=False,
|
|
121
|
+
)
|
|
122
|
+
print("\nPlease grant Screen Recording permission in System Preferences.")
|
|
123
|
+
print("After granting, run this command again to continue.")
|
|
124
|
+
print("Note: You may need to restart your terminal after granting permissions.")
|
|
125
|
+
sys.exit(1)
|
|
126
|
+
|
|
127
|
+
if not accessibility_granted:
|
|
128
|
+
subprocess.run(
|
|
129
|
+
[
|
|
130
|
+
"open",
|
|
131
|
+
"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility",
|
|
132
|
+
],
|
|
133
|
+
check=False,
|
|
134
|
+
)
|
|
135
|
+
print("\nPlease grant Accessibility permission in System Preferences.")
|
|
136
|
+
print("After granting, run this command again to continue.")
|
|
137
|
+
print("Note: You may need to restart your terminal after granting permissions.")
|
|
138
|
+
sys.exit(1)
|
|
139
|
+
|
|
140
|
+
print()
|
|
141
|
+
print("All permissions granted. You can run the agent.")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _warn_missing_permissions() -> None:
|
|
145
|
+
if sys.platform != "darwin":
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
if not check_optional_dependency(
|
|
149
|
+
"Quartz", "Permission check", "desktop", raise_error=False
|
|
150
|
+
):
|
|
151
|
+
return
|
|
152
|
+
if not check_optional_dependency(
|
|
153
|
+
"ApplicationServices", "Permission check", "desktop", raise_error=False
|
|
154
|
+
):
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
from ApplicationServices import AXIsProcessTrusted # noqa: PLC0415
|
|
158
|
+
from Quartz import CGPreflightScreenCaptureAccess # noqa: PLC0415
|
|
159
|
+
|
|
160
|
+
missing = []
|
|
161
|
+
if not CGPreflightScreenCaptureAccess():
|
|
162
|
+
missing.append("Screen Recording")
|
|
163
|
+
if not AXIsProcessTrusted():
|
|
164
|
+
missing.append("Accessibility")
|
|
165
|
+
|
|
166
|
+
if missing:
|
|
167
|
+
print(f"Warning: Missing macOS permissions: {', '.join(missing)}")
|
|
168
|
+
print("Run 'oagi agent permission' to configure permissions.\n")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def run_agent(args: argparse.Namespace) -> None:
|
|
172
|
+
# Check if desktop extras are installed
|
|
173
|
+
check_optional_dependency("pyautogui", "Agent execution", "desktop")
|
|
174
|
+
check_optional_dependency("PIL", "Agent execution", "desktop")
|
|
175
|
+
|
|
176
|
+
# Warn about missing macOS permissions (non-blocking)
|
|
177
|
+
_warn_missing_permissions()
|
|
178
|
+
|
|
179
|
+
from oagi import AsyncPyautoguiActionHandler, AsyncScreenshotMaker # noqa: PLC0415
|
|
180
|
+
from oagi.agent import create_agent # noqa: PLC0415
|
|
181
|
+
|
|
182
|
+
# Get configuration
|
|
183
|
+
api_key = args.oagi_api_key or os.getenv("OAGI_API_KEY")
|
|
184
|
+
if not api_key:
|
|
185
|
+
print(
|
|
186
|
+
"Error: OAGI API key not provided.\n"
|
|
187
|
+
"Set OAGI_API_KEY environment variable or use --oagi-api-key flag.",
|
|
188
|
+
file=sys.stderr,
|
|
189
|
+
)
|
|
190
|
+
sys.exit(1)
|
|
191
|
+
|
|
192
|
+
base_url = args.oagi_base_url or os.getenv(
|
|
193
|
+
"OAGI_BASE_URL", "https://api.agiopen.org"
|
|
194
|
+
)
|
|
195
|
+
model = args.model or "lux-actor-1"
|
|
196
|
+
max_steps = args.max_steps or 20
|
|
197
|
+
temperature = args.temperature if args.temperature is not None else 0.5
|
|
198
|
+
mode = args.mode or "actor"
|
|
199
|
+
export_format = args.export
|
|
200
|
+
export_file = args.export_file
|
|
201
|
+
|
|
202
|
+
# Create observers
|
|
203
|
+
step_tracker = StepTracker()
|
|
204
|
+
agent_observer = AsyncAgentObserver() if export_format else None
|
|
205
|
+
|
|
206
|
+
# Use a combined observer that forwards to both
|
|
207
|
+
class CombinedObserver:
|
|
208
|
+
async def on_event(self, event):
|
|
209
|
+
await step_tracker.on_event(event)
|
|
210
|
+
if agent_observer:
|
|
211
|
+
await agent_observer.on_event(event)
|
|
212
|
+
|
|
213
|
+
observer = CombinedObserver()
|
|
214
|
+
|
|
215
|
+
# Create agent with observer
|
|
216
|
+
agent = create_agent(
|
|
217
|
+
mode=mode,
|
|
218
|
+
api_key=api_key,
|
|
219
|
+
base_url=base_url,
|
|
220
|
+
model=model,
|
|
221
|
+
max_steps=max_steps,
|
|
222
|
+
temperature=temperature,
|
|
223
|
+
step_observer=observer,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Create handlers
|
|
227
|
+
action_handler = AsyncPyautoguiActionHandler()
|
|
228
|
+
image_provider = AsyncScreenshotMaker()
|
|
229
|
+
|
|
230
|
+
print(f"Starting agent with instruction: {args.instruction}")
|
|
231
|
+
print(
|
|
232
|
+
f"Mode: {mode}, Model: {model}, Max steps: {max_steps}, Temperature: {temperature}"
|
|
233
|
+
)
|
|
234
|
+
print("-" * 60)
|
|
235
|
+
|
|
236
|
+
start_time = time.time()
|
|
237
|
+
success = False
|
|
238
|
+
interrupted = False
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
success = asyncio.run(
|
|
242
|
+
agent.execute(
|
|
243
|
+
instruction=args.instruction,
|
|
244
|
+
action_handler=action_handler,
|
|
245
|
+
image_provider=image_provider,
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
except KeyboardInterrupt:
|
|
249
|
+
print("\nAgent execution interrupted by user (Ctrl+C)")
|
|
250
|
+
interrupted = True
|
|
251
|
+
except Exception as e:
|
|
252
|
+
print(f"\nError during agent execution: {e}", file=sys.stderr)
|
|
253
|
+
traceback.print_exc()
|
|
254
|
+
finally:
|
|
255
|
+
duration = time.time() - start_time
|
|
256
|
+
|
|
257
|
+
if step_tracker.steps:
|
|
258
|
+
print("\n" + "=" * 60)
|
|
259
|
+
display_step_table(step_tracker.steps, success, duration)
|
|
260
|
+
else:
|
|
261
|
+
print("\nNo steps were executed.")
|
|
262
|
+
|
|
263
|
+
# Export if requested
|
|
264
|
+
if export_format and agent_observer:
|
|
265
|
+
# Determine output file path
|
|
266
|
+
if export_file:
|
|
267
|
+
output_path = export_file
|
|
268
|
+
else:
|
|
269
|
+
ext_map = {"markdown": "md", "html": "html", "json": "json"}
|
|
270
|
+
output_path = f"execution_report.{ext_map[export_format]}"
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
agent_observer.export(export_format, output_path)
|
|
274
|
+
print(f"\nExecution history exported to: {output_path}")
|
|
275
|
+
except Exception as e:
|
|
276
|
+
print(f"\nError exporting execution history: {e}", file=sys.stderr)
|
|
277
|
+
|
|
278
|
+
if interrupted:
|
|
279
|
+
sys.exit(130)
|
|
280
|
+
elif not success:
|
|
281
|
+
sys.exit(1)
|
oagi/cli/display.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# -----------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) OpenAGI Foundation
|
|
3
|
+
# All rights reserved.
|
|
4
|
+
#
|
|
5
|
+
# This file is part of the official API project.
|
|
6
|
+
# Licensed under the MIT License.
|
|
7
|
+
# -----------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from .tracking import StepData
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def display_step_table(
|
|
16
|
+
steps: list[StepData], success: bool, duration: float | None = None
|
|
17
|
+
):
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
table = Table(title="Agent Execution Summary", show_lines=True)
|
|
21
|
+
table.add_column("Step", justify="center", style="cyan", width=6)
|
|
22
|
+
table.add_column("Reasoning", style="white")
|
|
23
|
+
table.add_column("Actions", style="yellow", width=35)
|
|
24
|
+
table.add_column("Status", justify="center", width=8)
|
|
25
|
+
|
|
26
|
+
for step in steps:
|
|
27
|
+
reason = step.reasoning or "N/A"
|
|
28
|
+
|
|
29
|
+
actions_display = []
|
|
30
|
+
for action in step.actions[:3]:
|
|
31
|
+
arg = action.argument[:20] if action.argument else ""
|
|
32
|
+
actions_display.append(f"{action.type.value}({arg})")
|
|
33
|
+
|
|
34
|
+
actions_str = ", ".join(actions_display)
|
|
35
|
+
if len(step.actions) > 3:
|
|
36
|
+
actions_str += f" (+{len(step.actions) - 3} more)"
|
|
37
|
+
|
|
38
|
+
status_display = "✓" if step.status == "complete" else "→"
|
|
39
|
+
|
|
40
|
+
table.add_row(
|
|
41
|
+
str(step.step_num),
|
|
42
|
+
reason,
|
|
43
|
+
actions_str,
|
|
44
|
+
status_display,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
console.print(table)
|
|
48
|
+
|
|
49
|
+
status_text = "Success" if success else "Failed/Interrupted"
|
|
50
|
+
console.print(
|
|
51
|
+
f"\nTotal Steps: {len(steps)} | Status: {status_text}",
|
|
52
|
+
style="bold",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if duration:
|
|
56
|
+
console.print(f"Duration: {duration:.2f}s")
|