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.
Files changed (68) hide show
  1. oagi/__init__.py +148 -0
  2. oagi/agent/__init__.py +33 -0
  3. oagi/agent/default.py +124 -0
  4. oagi/agent/factories.py +74 -0
  5. oagi/agent/observer/__init__.py +38 -0
  6. oagi/agent/observer/agent_observer.py +99 -0
  7. oagi/agent/observer/events.py +28 -0
  8. oagi/agent/observer/exporters.py +445 -0
  9. oagi/agent/observer/protocol.py +12 -0
  10. oagi/agent/protocol.py +55 -0
  11. oagi/agent/registry.py +155 -0
  12. oagi/agent/tasker/__init__.py +33 -0
  13. oagi/agent/tasker/memory.py +160 -0
  14. oagi/agent/tasker/models.py +77 -0
  15. oagi/agent/tasker/planner.py +408 -0
  16. oagi/agent/tasker/taskee_agent.py +512 -0
  17. oagi/agent/tasker/tasker_agent.py +324 -0
  18. oagi/cli/__init__.py +11 -0
  19. oagi/cli/agent.py +281 -0
  20. oagi/cli/display.py +56 -0
  21. oagi/cli/main.py +77 -0
  22. oagi/cli/server.py +94 -0
  23. oagi/cli/tracking.py +55 -0
  24. oagi/cli/utils.py +89 -0
  25. oagi/client/__init__.py +12 -0
  26. oagi/client/async_.py +290 -0
  27. oagi/client/base.py +457 -0
  28. oagi/client/sync.py +293 -0
  29. oagi/exceptions.py +118 -0
  30. oagi/handler/__init__.py +24 -0
  31. oagi/handler/_macos.py +55 -0
  32. oagi/handler/async_pyautogui_action_handler.py +44 -0
  33. oagi/handler/async_screenshot_maker.py +47 -0
  34. oagi/handler/pil_image.py +102 -0
  35. oagi/handler/pyautogui_action_handler.py +291 -0
  36. oagi/handler/screenshot_maker.py +41 -0
  37. oagi/logging.py +55 -0
  38. oagi/server/__init__.py +13 -0
  39. oagi/server/agent_wrappers.py +98 -0
  40. oagi/server/config.py +46 -0
  41. oagi/server/main.py +157 -0
  42. oagi/server/models.py +98 -0
  43. oagi/server/session_store.py +116 -0
  44. oagi/server/socketio_server.py +405 -0
  45. oagi/task/__init__.py +21 -0
  46. oagi/task/async_.py +101 -0
  47. oagi/task/async_short.py +76 -0
  48. oagi/task/base.py +157 -0
  49. oagi/task/short.py +76 -0
  50. oagi/task/sync.py +99 -0
  51. oagi/types/__init__.py +50 -0
  52. oagi/types/action_handler.py +30 -0
  53. oagi/types/async_action_handler.py +30 -0
  54. oagi/types/async_image_provider.py +38 -0
  55. oagi/types/image.py +17 -0
  56. oagi/types/image_provider.py +35 -0
  57. oagi/types/models/__init__.py +32 -0
  58. oagi/types/models/action.py +33 -0
  59. oagi/types/models/client.py +68 -0
  60. oagi/types/models/image_config.py +47 -0
  61. oagi/types/models/step.py +17 -0
  62. oagi/types/step_observer.py +93 -0
  63. oagi/types/url.py +3 -0
  64. oagi_core-0.10.1.dist-info/METADATA +245 -0
  65. oagi_core-0.10.1.dist-info/RECORD +68 -0
  66. oagi_core-0.10.1.dist-info/WHEEL +4 -0
  67. oagi_core-0.10.1.dist-info/entry_points.txt +2 -0
  68. 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")