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,445 @@
|
|
|
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 base64
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from ...types import (
|
|
14
|
+
ActionEvent,
|
|
15
|
+
ImageEvent,
|
|
16
|
+
LogEvent,
|
|
17
|
+
ObserverEvent,
|
|
18
|
+
PlanEvent,
|
|
19
|
+
SplitEvent,
|
|
20
|
+
StepEvent,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def export_to_markdown(
|
|
25
|
+
events: list[ObserverEvent],
|
|
26
|
+
path: str,
|
|
27
|
+
images_dir: str | None = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Export events to a Markdown file.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
events: List of events to export.
|
|
33
|
+
path: Path to the output Markdown file.
|
|
34
|
+
images_dir: Directory to save images. If None, images are not saved.
|
|
35
|
+
"""
|
|
36
|
+
output_path = Path(path)
|
|
37
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
|
|
39
|
+
if images_dir:
|
|
40
|
+
images_path = Path(images_dir)
|
|
41
|
+
images_path.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
|
|
43
|
+
lines: list[str] = ["# Agent Execution Report\n"]
|
|
44
|
+
image_counter = 0
|
|
45
|
+
|
|
46
|
+
for event in events:
|
|
47
|
+
timestamp = event.timestamp.strftime("%H:%M:%S")
|
|
48
|
+
|
|
49
|
+
match event:
|
|
50
|
+
case StepEvent():
|
|
51
|
+
lines.append(f"\n## Step {event.step_num}\n")
|
|
52
|
+
lines.append(f"**Time:** {timestamp}\n")
|
|
53
|
+
|
|
54
|
+
if isinstance(event.image, bytes):
|
|
55
|
+
if images_dir:
|
|
56
|
+
image_counter += 1
|
|
57
|
+
image_filename = f"step_{event.step_num}.png"
|
|
58
|
+
image_path = Path(images_dir) / image_filename
|
|
59
|
+
image_path.write_bytes(event.image)
|
|
60
|
+
rel_path = Path(images_dir).name / Path(image_filename)
|
|
61
|
+
lines.append(f"\n\n")
|
|
62
|
+
else:
|
|
63
|
+
lines.append(
|
|
64
|
+
f"\n*[Screenshot captured - {len(event.image)} bytes]*\n"
|
|
65
|
+
)
|
|
66
|
+
elif isinstance(event.image, str):
|
|
67
|
+
lines.append(f"\n**Screenshot URL:** {event.image}\n")
|
|
68
|
+
|
|
69
|
+
if event.step.reason:
|
|
70
|
+
lines.append(f"\n**Reasoning:**\n> {event.step.reason}\n")
|
|
71
|
+
|
|
72
|
+
if event.step.actions:
|
|
73
|
+
lines.append("\n**Planned Actions:**\n")
|
|
74
|
+
for action in event.step.actions:
|
|
75
|
+
count_str = (
|
|
76
|
+
f" (x{action.count})"
|
|
77
|
+
if action.count and action.count > 1
|
|
78
|
+
else ""
|
|
79
|
+
)
|
|
80
|
+
lines.append(
|
|
81
|
+
f"- `{action.type.value}`: {action.argument}{count_str}\n"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if event.step.stop:
|
|
85
|
+
lines.append("\n**Status:** Task Complete\n")
|
|
86
|
+
|
|
87
|
+
case ActionEvent():
|
|
88
|
+
lines.append(f"\n### Actions Executed ({timestamp})\n")
|
|
89
|
+
if event.error:
|
|
90
|
+
lines.append(f"\n**Error:** {event.error}\n")
|
|
91
|
+
else:
|
|
92
|
+
lines.append("\n**Result:** Success\n")
|
|
93
|
+
|
|
94
|
+
case LogEvent():
|
|
95
|
+
lines.append(f"\n> **Log ({timestamp}):** {event.message}\n")
|
|
96
|
+
|
|
97
|
+
case SplitEvent():
|
|
98
|
+
if event.label:
|
|
99
|
+
lines.append(f"\n---\n\n### {event.label}\n")
|
|
100
|
+
else:
|
|
101
|
+
lines.append("\n---\n")
|
|
102
|
+
|
|
103
|
+
case ImageEvent():
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
case PlanEvent():
|
|
107
|
+
phase_titles = {
|
|
108
|
+
"initial": "Initial Planning",
|
|
109
|
+
"reflection": "Reflection",
|
|
110
|
+
"summary": "Summary",
|
|
111
|
+
}
|
|
112
|
+
phase_title = phase_titles.get(event.phase, event.phase.capitalize())
|
|
113
|
+
lines.append(f"\n### {phase_title} ({timestamp})\n")
|
|
114
|
+
|
|
115
|
+
if event.image:
|
|
116
|
+
if isinstance(event.image, bytes):
|
|
117
|
+
if images_dir:
|
|
118
|
+
image_counter += 1
|
|
119
|
+
image_filename = f"plan_{event.phase}_{image_counter}.png"
|
|
120
|
+
image_path = Path(images_dir) / image_filename
|
|
121
|
+
image_path.write_bytes(event.image)
|
|
122
|
+
rel_path = Path(images_dir).name / Path(image_filename)
|
|
123
|
+
lines.append(f"\n\n")
|
|
124
|
+
else:
|
|
125
|
+
lines.append(
|
|
126
|
+
f"\n*[Screenshot captured - {len(event.image)} bytes]*\n"
|
|
127
|
+
)
|
|
128
|
+
elif isinstance(event.image, str):
|
|
129
|
+
lines.append(f"\n**Screenshot URL:** {event.image}\n")
|
|
130
|
+
|
|
131
|
+
if event.reasoning:
|
|
132
|
+
lines.append(f"\n**Reasoning:**\n> {event.reasoning}\n")
|
|
133
|
+
|
|
134
|
+
if event.result:
|
|
135
|
+
lines.append(f"\n**Result:** {event.result}\n")
|
|
136
|
+
|
|
137
|
+
output_path.write_text("".join(lines))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def export_to_html(events: list[ObserverEvent], path: str) -> None:
|
|
141
|
+
"""Export events to a self-contained HTML file.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
events: List of events to export.
|
|
145
|
+
path: Path to the output HTML file.
|
|
146
|
+
"""
|
|
147
|
+
output_path = Path(path)
|
|
148
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
149
|
+
|
|
150
|
+
html_parts: list[str] = [_get_html_header()]
|
|
151
|
+
|
|
152
|
+
for event in events:
|
|
153
|
+
timestamp = event.timestamp.strftime("%H:%M:%S")
|
|
154
|
+
|
|
155
|
+
match event:
|
|
156
|
+
case StepEvent():
|
|
157
|
+
html_parts.append('<div class="step">')
|
|
158
|
+
html_parts.append(f"<h2>Step {event.step_num}</h2>")
|
|
159
|
+
html_parts.append(f'<span class="timestamp">{timestamp}</span>')
|
|
160
|
+
|
|
161
|
+
if isinstance(event.image, bytes):
|
|
162
|
+
b64_image = base64.b64encode(event.image).decode("utf-8")
|
|
163
|
+
html_parts.append(
|
|
164
|
+
f'<img src="data:image/png;base64,{b64_image}" '
|
|
165
|
+
f'alt="Step {event.step_num}" class="screenshot"/>'
|
|
166
|
+
)
|
|
167
|
+
elif isinstance(event.image, str):
|
|
168
|
+
html_parts.append(
|
|
169
|
+
f'<p class="url">Screenshot URL: <a href="{event.image}">{event.image}</a></p>'
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if event.step.reason:
|
|
173
|
+
html_parts.append('<div class="reasoning">')
|
|
174
|
+
html_parts.append(
|
|
175
|
+
f"<strong>Reasoning:</strong><p>{_escape_html(event.step.reason)}</p>"
|
|
176
|
+
)
|
|
177
|
+
html_parts.append("</div>")
|
|
178
|
+
|
|
179
|
+
if event.step.actions:
|
|
180
|
+
html_parts.append('<div class="actions">')
|
|
181
|
+
html_parts.append("<strong>Planned Actions:</strong><ul>")
|
|
182
|
+
for action in event.step.actions:
|
|
183
|
+
count_str = (
|
|
184
|
+
f" (x{action.count})"
|
|
185
|
+
if action.count and action.count > 1
|
|
186
|
+
else ""
|
|
187
|
+
)
|
|
188
|
+
html_parts.append(
|
|
189
|
+
f"<li><code>{action.type.value}</code>: "
|
|
190
|
+
f"{_escape_html(action.argument)}{count_str}</li>"
|
|
191
|
+
)
|
|
192
|
+
html_parts.append("</ul></div>")
|
|
193
|
+
|
|
194
|
+
if event.step.stop:
|
|
195
|
+
html_parts.append('<div class="complete">Task Complete</div>')
|
|
196
|
+
|
|
197
|
+
html_parts.append("</div>")
|
|
198
|
+
|
|
199
|
+
case ActionEvent():
|
|
200
|
+
html_parts.append('<div class="action-result">')
|
|
201
|
+
html_parts.append(f'<span class="timestamp">{timestamp}</span>')
|
|
202
|
+
if event.error:
|
|
203
|
+
html_parts.append(
|
|
204
|
+
f'<div class="error">Error: {_escape_html(event.error)}</div>'
|
|
205
|
+
)
|
|
206
|
+
else:
|
|
207
|
+
html_parts.append(
|
|
208
|
+
'<div class="success">Actions executed successfully</div>'
|
|
209
|
+
)
|
|
210
|
+
html_parts.append("</div>")
|
|
211
|
+
|
|
212
|
+
case LogEvent():
|
|
213
|
+
html_parts.append('<div class="log">')
|
|
214
|
+
html_parts.append(f'<span class="timestamp">{timestamp}</span>')
|
|
215
|
+
html_parts.append(f"<p>{_escape_html(event.message)}</p>")
|
|
216
|
+
html_parts.append("</div>")
|
|
217
|
+
|
|
218
|
+
case SplitEvent():
|
|
219
|
+
if event.label:
|
|
220
|
+
html_parts.append(
|
|
221
|
+
f'<div class="split"><h3>{_escape_html(event.label)}</h3></div>'
|
|
222
|
+
)
|
|
223
|
+
else:
|
|
224
|
+
html_parts.append('<hr class="split-line"/>')
|
|
225
|
+
|
|
226
|
+
case ImageEvent():
|
|
227
|
+
pass
|
|
228
|
+
|
|
229
|
+
case PlanEvent():
|
|
230
|
+
phase_titles = {
|
|
231
|
+
"initial": "Initial Planning",
|
|
232
|
+
"reflection": "Reflection",
|
|
233
|
+
"summary": "Summary",
|
|
234
|
+
}
|
|
235
|
+
phase_title = phase_titles.get(event.phase, event.phase.capitalize())
|
|
236
|
+
html_parts.append('<div class="plan">')
|
|
237
|
+
html_parts.append(f"<h3>{phase_title}</h3>")
|
|
238
|
+
html_parts.append(f'<span class="timestamp">{timestamp}</span>')
|
|
239
|
+
|
|
240
|
+
if event.image:
|
|
241
|
+
if isinstance(event.image, bytes):
|
|
242
|
+
b64_image = base64.b64encode(event.image).decode("utf-8")
|
|
243
|
+
html_parts.append(
|
|
244
|
+
f'<img src="data:image/png;base64,{b64_image}" '
|
|
245
|
+
f'alt="{phase_title}" class="screenshot"/>'
|
|
246
|
+
)
|
|
247
|
+
elif isinstance(event.image, str):
|
|
248
|
+
html_parts.append(
|
|
249
|
+
f'<p class="url">Screenshot URL: '
|
|
250
|
+
f'<a href="{event.image}">{event.image}</a></p>'
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
if event.reasoning:
|
|
254
|
+
html_parts.append('<div class="reasoning">')
|
|
255
|
+
html_parts.append(
|
|
256
|
+
f"<strong>Reasoning:</strong><p>{_escape_html(event.reasoning)}</p>"
|
|
257
|
+
)
|
|
258
|
+
html_parts.append("</div>")
|
|
259
|
+
|
|
260
|
+
if event.result:
|
|
261
|
+
html_parts.append(
|
|
262
|
+
f'<div class="plan-result"><strong>Result:</strong> '
|
|
263
|
+
f"{_escape_html(event.result)}</div>"
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
html_parts.append("</div>")
|
|
267
|
+
|
|
268
|
+
html_parts.append(_get_html_footer())
|
|
269
|
+
output_path.write_text("".join(html_parts))
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _escape_html(text: str) -> str:
|
|
273
|
+
"""Escape HTML special characters."""
|
|
274
|
+
return (
|
|
275
|
+
text.replace("&", "&")
|
|
276
|
+
.replace("<", "<")
|
|
277
|
+
.replace(">", ">")
|
|
278
|
+
.replace('"', """)
|
|
279
|
+
.replace("'", "'")
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _get_html_header() -> str:
|
|
284
|
+
"""Get HTML document header with CSS styles."""
|
|
285
|
+
return """<!DOCTYPE html>
|
|
286
|
+
<html lang="en">
|
|
287
|
+
<head>
|
|
288
|
+
<meta charset="UTF-8">
|
|
289
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
290
|
+
<title>Agent Execution Report</title>
|
|
291
|
+
<style>
|
|
292
|
+
body {
|
|
293
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
294
|
+
max-width: 1200px;
|
|
295
|
+
margin: 0 auto;
|
|
296
|
+
padding: 20px;
|
|
297
|
+
background: #f5f5f5;
|
|
298
|
+
}
|
|
299
|
+
h1 {
|
|
300
|
+
color: #333;
|
|
301
|
+
border-bottom: 2px solid #007bff;
|
|
302
|
+
padding-bottom: 10px;
|
|
303
|
+
}
|
|
304
|
+
.step {
|
|
305
|
+
background: white;
|
|
306
|
+
border-radius: 8px;
|
|
307
|
+
padding: 20px;
|
|
308
|
+
margin: 20px 0;
|
|
309
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
310
|
+
}
|
|
311
|
+
.step h2 {
|
|
312
|
+
margin-top: 0;
|
|
313
|
+
color: #007bff;
|
|
314
|
+
}
|
|
315
|
+
.timestamp {
|
|
316
|
+
color: #666;
|
|
317
|
+
font-size: 0.9em;
|
|
318
|
+
}
|
|
319
|
+
.screenshot {
|
|
320
|
+
max-width: 100%;
|
|
321
|
+
border: 1px solid #ddd;
|
|
322
|
+
border-radius: 4px;
|
|
323
|
+
margin: 10px 0;
|
|
324
|
+
}
|
|
325
|
+
.reasoning {
|
|
326
|
+
background: #f8f9fa;
|
|
327
|
+
padding: 10px;
|
|
328
|
+
border-left: 3px solid #007bff;
|
|
329
|
+
margin: 10px 0;
|
|
330
|
+
}
|
|
331
|
+
.actions {
|
|
332
|
+
margin: 10px 0;
|
|
333
|
+
}
|
|
334
|
+
.actions ul {
|
|
335
|
+
margin: 5px 0;
|
|
336
|
+
padding-left: 20px;
|
|
337
|
+
}
|
|
338
|
+
.actions code {
|
|
339
|
+
background: #e9ecef;
|
|
340
|
+
padding: 2px 6px;
|
|
341
|
+
border-radius: 3px;
|
|
342
|
+
}
|
|
343
|
+
.complete {
|
|
344
|
+
background: #d4edda;
|
|
345
|
+
color: #155724;
|
|
346
|
+
padding: 10px;
|
|
347
|
+
border-radius: 4px;
|
|
348
|
+
margin-top: 10px;
|
|
349
|
+
}
|
|
350
|
+
.action-result {
|
|
351
|
+
padding: 10px;
|
|
352
|
+
margin: 5px 0;
|
|
353
|
+
}
|
|
354
|
+
.success {
|
|
355
|
+
color: #155724;
|
|
356
|
+
}
|
|
357
|
+
.error {
|
|
358
|
+
color: #721c24;
|
|
359
|
+
background: #f8d7da;
|
|
360
|
+
padding: 10px;
|
|
361
|
+
border-radius: 4px;
|
|
362
|
+
}
|
|
363
|
+
.log {
|
|
364
|
+
background: #fff3cd;
|
|
365
|
+
padding: 10px;
|
|
366
|
+
margin: 10px 0;
|
|
367
|
+
border-radius: 4px;
|
|
368
|
+
}
|
|
369
|
+
.split {
|
|
370
|
+
text-align: center;
|
|
371
|
+
margin: 30px 0;
|
|
372
|
+
}
|
|
373
|
+
.split h3 {
|
|
374
|
+
color: #666;
|
|
375
|
+
}
|
|
376
|
+
.split-line {
|
|
377
|
+
border: none;
|
|
378
|
+
border-top: 2px dashed #ccc;
|
|
379
|
+
margin: 30px 0;
|
|
380
|
+
}
|
|
381
|
+
.url {
|
|
382
|
+
word-break: break-all;
|
|
383
|
+
}
|
|
384
|
+
.plan {
|
|
385
|
+
background: #e7f3ff;
|
|
386
|
+
border-radius: 8px;
|
|
387
|
+
padding: 20px;
|
|
388
|
+
margin: 20px 0;
|
|
389
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
390
|
+
}
|
|
391
|
+
.plan h3 {
|
|
392
|
+
margin-top: 0;
|
|
393
|
+
color: #0056b3;
|
|
394
|
+
}
|
|
395
|
+
.plan-result {
|
|
396
|
+
background: #d1ecf1;
|
|
397
|
+
color: #0c5460;
|
|
398
|
+
padding: 10px;
|
|
399
|
+
border-radius: 4px;
|
|
400
|
+
margin-top: 10px;
|
|
401
|
+
}
|
|
402
|
+
</style>
|
|
403
|
+
</head>
|
|
404
|
+
<body>
|
|
405
|
+
<h1>Agent Execution Report</h1>
|
|
406
|
+
"""
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _get_html_footer() -> str:
|
|
410
|
+
"""Get HTML document footer."""
|
|
411
|
+
return """
|
|
412
|
+
</body>
|
|
413
|
+
</html>
|
|
414
|
+
"""
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def export_to_json(events: list[ObserverEvent], path: str) -> None:
|
|
418
|
+
"""Export events to a JSON file.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
events: List of events to export.
|
|
422
|
+
path: Path to the output JSON file.
|
|
423
|
+
"""
|
|
424
|
+
output_path = Path(path)
|
|
425
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
426
|
+
|
|
427
|
+
# Convert events to JSON-serializable format
|
|
428
|
+
json_events = []
|
|
429
|
+
for event in events:
|
|
430
|
+
# Handle bytes images before model_dump to avoid UTF-8 decode error
|
|
431
|
+
if isinstance(event, (StepEvent, ImageEvent, PlanEvent)) and isinstance(
|
|
432
|
+
getattr(event, "image", None), bytes
|
|
433
|
+
):
|
|
434
|
+
# Dump without json mode first, then handle bytes manually
|
|
435
|
+
event_dict = event.model_dump()
|
|
436
|
+
event_dict["image"] = base64.b64encode(event.image).decode("utf-8")
|
|
437
|
+
event_dict["image_encoding"] = "base64"
|
|
438
|
+
# Convert datetime to string
|
|
439
|
+
if "timestamp" in event_dict:
|
|
440
|
+
event_dict["timestamp"] = event_dict["timestamp"].isoformat()
|
|
441
|
+
else:
|
|
442
|
+
event_dict = event.model_dump(mode="json")
|
|
443
|
+
json_events.append(event_dict)
|
|
444
|
+
|
|
445
|
+
output_path.write_text(json.dumps(json_events, indent=2, default=str))
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
# Re-export from types for convenience
|
|
10
|
+
from ...types import AsyncObserver
|
|
11
|
+
|
|
12
|
+
__all__ = ["AsyncObserver"]
|
oagi/agent/protocol.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
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 typing import Protocol
|
|
10
|
+
|
|
11
|
+
from ..types import ActionHandler, AsyncActionHandler, AsyncImageProvider, ImageProvider
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Agent(Protocol):
|
|
15
|
+
"""Protocol for synchronous task execution agents."""
|
|
16
|
+
|
|
17
|
+
def execute(
|
|
18
|
+
self,
|
|
19
|
+
instruction: str,
|
|
20
|
+
action_handler: ActionHandler,
|
|
21
|
+
image_provider: ImageProvider,
|
|
22
|
+
) -> bool:
|
|
23
|
+
"""Execute a task with the given handlers.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
instruction: Task instruction to execute
|
|
27
|
+
action_handler: Handler for executing actions
|
|
28
|
+
image_provider: Provider for capturing images
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
True if task completed successfully, False otherwise
|
|
32
|
+
"""
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AsyncAgent(Protocol):
|
|
37
|
+
"""Protocol for asynchronous task execution agents."""
|
|
38
|
+
|
|
39
|
+
async def execute(
|
|
40
|
+
self,
|
|
41
|
+
instruction: str,
|
|
42
|
+
action_handler: AsyncActionHandler,
|
|
43
|
+
image_provider: AsyncImageProvider,
|
|
44
|
+
) -> bool:
|
|
45
|
+
"""Asynchronously execute a task with the given handlers.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
instruction: Task instruction to execute
|
|
49
|
+
action_handler: Handler for executing actions
|
|
50
|
+
image_provider: Provider for capturing images
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
True if task completed successfully, False otherwise
|
|
54
|
+
"""
|
|
55
|
+
...
|
oagi/agent/registry.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
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 inspect
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .protocol import AsyncAgent
|
|
14
|
+
|
|
15
|
+
# Type alias for agent factory functions
|
|
16
|
+
AgentFactory = Callable[..., AsyncAgent]
|
|
17
|
+
|
|
18
|
+
# Global registry mapping mode names to factory functions
|
|
19
|
+
_agent_registry: dict[str, AgentFactory] = {}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def async_agent_register(mode: str) -> Callable[[AgentFactory], AgentFactory]:
|
|
23
|
+
"""Decorator to register agent factory functions for specific modes.
|
|
24
|
+
|
|
25
|
+
The decorator performs the following:
|
|
26
|
+
1. Registers the factory function under the specified mode name
|
|
27
|
+
2. Validates that duplicate modes are not registered
|
|
28
|
+
3. Enables runtime validation of returned AsyncAgent instances
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
mode: The agent mode identifier (e.g., "actor", "planner", "todo")
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Decorator function that registers the factory
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
ValueError: If the mode is already registered
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def decorator(func: AgentFactory) -> AgentFactory:
|
|
41
|
+
# Check if mode is already registered
|
|
42
|
+
if mode in _agent_registry:
|
|
43
|
+
raise ValueError(
|
|
44
|
+
f"Agent mode '{mode}' is already registered. "
|
|
45
|
+
f"Cannot register the same mode twice."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Register the factory
|
|
49
|
+
_agent_registry[mode] = func
|
|
50
|
+
return func
|
|
51
|
+
|
|
52
|
+
return decorator
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_agent_factory(mode: str) -> AgentFactory:
|
|
56
|
+
"""Get the registered agent factory for a mode.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
mode: The agent mode identifier
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
The registered factory function
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
ValueError: If the mode is not registered
|
|
66
|
+
"""
|
|
67
|
+
if mode not in _agent_registry:
|
|
68
|
+
available_modes = list(_agent_registry.keys())
|
|
69
|
+
raise ValueError(
|
|
70
|
+
f"Unknown agent mode: '{mode}'. Available modes: {available_modes}"
|
|
71
|
+
)
|
|
72
|
+
return _agent_registry[mode]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def list_agent_modes() -> list[str]:
|
|
76
|
+
"""List all registered agent modes.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
List of registered mode names
|
|
80
|
+
"""
|
|
81
|
+
return list(_agent_registry.keys())
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def create_agent(mode: str, **kwargs: Any) -> AsyncAgent:
|
|
85
|
+
"""Create an agent instance using the registered factory for the given mode.
|
|
86
|
+
|
|
87
|
+
This function automatically introspects the factory's signature and only passes
|
|
88
|
+
parameters that the factory accepts. This allows factories to have flexible
|
|
89
|
+
signatures while callers can provide a standard set of parameters.
|
|
90
|
+
|
|
91
|
+
Standard parameters typically include:
|
|
92
|
+
- api_key: OAGI API key
|
|
93
|
+
- base_url: OAGI API base URL
|
|
94
|
+
- model: Model identifier (e.g., "lux-actor-1")
|
|
95
|
+
- max_steps: Maximum number of steps to execute
|
|
96
|
+
- temperature: Sampling temperature
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
mode: The agent mode identifier
|
|
100
|
+
**kwargs: Parameters to pass to the factory function
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
AsyncAgent instance created by the factory
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
ValueError: If the mode is not registered
|
|
107
|
+
TypeError: If the factory returns an object that doesn't implement AsyncAgent
|
|
108
|
+
|
|
109
|
+
Example:
|
|
110
|
+
agent = create_agent(
|
|
111
|
+
mode="actor",
|
|
112
|
+
api_key="...",
|
|
113
|
+
base_url="...",
|
|
114
|
+
model="lux-actor-1",
|
|
115
|
+
max_steps=30,
|
|
116
|
+
temperature=0.0,
|
|
117
|
+
)
|
|
118
|
+
"""
|
|
119
|
+
factory = get_agent_factory(mode)
|
|
120
|
+
|
|
121
|
+
# Introspect factory signature to determine which parameters it accepts
|
|
122
|
+
sig = inspect.signature(factory)
|
|
123
|
+
|
|
124
|
+
# Check if factory has **kwargs parameter (VAR_KEYWORD)
|
|
125
|
+
has_var_keyword = any(
|
|
126
|
+
param.kind == inspect.Parameter.VAR_KEYWORD for param in sig.parameters.values()
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if has_var_keyword:
|
|
130
|
+
# If factory has **kwargs, pass all parameters
|
|
131
|
+
filtered_kwargs = kwargs
|
|
132
|
+
else:
|
|
133
|
+
# Otherwise, filter kwargs to only include parameters the factory accepts
|
|
134
|
+
accepted_params = set(sig.parameters.keys())
|
|
135
|
+
filtered_kwargs = {
|
|
136
|
+
key: value for key, value in kwargs.items() if key in accepted_params
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
agent = factory(**filtered_kwargs)
|
|
140
|
+
|
|
141
|
+
if not hasattr(agent, "execute"):
|
|
142
|
+
raise TypeError(
|
|
143
|
+
f"Factory for mode '{mode}' returned an object that doesn't "
|
|
144
|
+
f"implement AsyncAgent protocol. Expected an object with an "
|
|
145
|
+
f"'execute' method, got {type(agent).__name__}"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if not inspect.iscoroutinefunction(agent.execute):
|
|
149
|
+
raise TypeError(
|
|
150
|
+
f"Factory for mode '{mode}' returned an object with a non-async "
|
|
151
|
+
f"'execute' method. AsyncAgent protocol requires 'execute' to be "
|
|
152
|
+
f"an async method."
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return agent
|
|
@@ -0,0 +1,33 @@
|
|
|
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 .memory import PlannerMemory
|
|
10
|
+
from .models import (
|
|
11
|
+
Action,
|
|
12
|
+
PlannerOutput,
|
|
13
|
+
ReflectionOutput,
|
|
14
|
+
Todo,
|
|
15
|
+
TodoHistory,
|
|
16
|
+
TodoStatus,
|
|
17
|
+
)
|
|
18
|
+
from .planner import Planner
|
|
19
|
+
from .taskee_agent import TaskeeAgent
|
|
20
|
+
from .tasker_agent import TaskerAgent
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"TaskerAgent",
|
|
24
|
+
"TaskeeAgent",
|
|
25
|
+
"PlannerMemory",
|
|
26
|
+
"Planner",
|
|
27
|
+
"Todo",
|
|
28
|
+
"TodoStatus",
|
|
29
|
+
"Action",
|
|
30
|
+
"TodoHistory",
|
|
31
|
+
"PlannerOutput",
|
|
32
|
+
"ReflectionOutput",
|
|
33
|
+
]
|