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,160 @@
|
|
|
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 Any
|
|
10
|
+
|
|
11
|
+
from .models import Action, Todo, TodoHistory, TodoStatus
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PlannerMemory:
|
|
15
|
+
"""In-memory state management for the planner agent.
|
|
16
|
+
|
|
17
|
+
This class manages the hierarchical task execution state for TaskerAgent.
|
|
18
|
+
It provides methods for:
|
|
19
|
+
- Task/todo management
|
|
20
|
+
- Execution history tracking
|
|
21
|
+
- Memory state serialization
|
|
22
|
+
|
|
23
|
+
Context formatting for backend API calls is handled by the backend.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
"""Initialize empty memory."""
|
|
28
|
+
self.task_description: str = ""
|
|
29
|
+
self.todos: list[Todo] = []
|
|
30
|
+
self.history: list[TodoHistory] = []
|
|
31
|
+
self.task_execution_summary: str = ""
|
|
32
|
+
self.todo_execution_summaries: dict[int, str] = {}
|
|
33
|
+
|
|
34
|
+
def set_task(
|
|
35
|
+
self,
|
|
36
|
+
task_description: str,
|
|
37
|
+
todos: list[str] | list[Todo],
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Set the task and todos.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
task_description: Overall task description
|
|
43
|
+
todos: List of todo items (strings or Todo objects)
|
|
44
|
+
"""
|
|
45
|
+
self.task_description = task_description
|
|
46
|
+
|
|
47
|
+
# Convert todos
|
|
48
|
+
self.todos = []
|
|
49
|
+
for todo in todos:
|
|
50
|
+
if isinstance(todo, str):
|
|
51
|
+
self.todos.append(Todo(description=todo))
|
|
52
|
+
else:
|
|
53
|
+
self.todos.append(todo)
|
|
54
|
+
|
|
55
|
+
def get_current_todo(self) -> tuple[Todo | None, int]:
|
|
56
|
+
"""Get the next pending or in-progress todo.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Tuple of (Todo object, index) or (None, -1) if no todos remain
|
|
60
|
+
"""
|
|
61
|
+
for idx, todo in enumerate(self.todos):
|
|
62
|
+
if todo.status in [TodoStatus.PENDING, TodoStatus.IN_PROGRESS]:
|
|
63
|
+
return todo, idx
|
|
64
|
+
return None, -1
|
|
65
|
+
|
|
66
|
+
def update_todo(
|
|
67
|
+
self,
|
|
68
|
+
index: int,
|
|
69
|
+
status: TodoStatus | str,
|
|
70
|
+
summary: str | None = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Update a todo's status and optionally its summary.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
index: Index of the todo to update
|
|
76
|
+
status: New status for the todo
|
|
77
|
+
summary: Optional execution summary
|
|
78
|
+
"""
|
|
79
|
+
if 0 <= index < len(self.todos):
|
|
80
|
+
if isinstance(status, str):
|
|
81
|
+
status = TodoStatus(status)
|
|
82
|
+
self.todos[index].status = status
|
|
83
|
+
if summary:
|
|
84
|
+
self.todo_execution_summaries[index] = summary
|
|
85
|
+
|
|
86
|
+
def add_history(
|
|
87
|
+
self,
|
|
88
|
+
todo_index: int,
|
|
89
|
+
actions: list[Action],
|
|
90
|
+
summary: str | None = None,
|
|
91
|
+
completed: bool = False,
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Add execution history for a todo.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
todo_index: Index of the todo
|
|
97
|
+
actions: List of actions taken
|
|
98
|
+
summary: Optional execution summary
|
|
99
|
+
completed: Whether the todo was completed
|
|
100
|
+
"""
|
|
101
|
+
if 0 <= todo_index < len(self.todos):
|
|
102
|
+
self.history.append(
|
|
103
|
+
TodoHistory(
|
|
104
|
+
todo_index=todo_index,
|
|
105
|
+
todo=self.todos[todo_index].description,
|
|
106
|
+
actions=actions,
|
|
107
|
+
summary=summary,
|
|
108
|
+
completed=completed,
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def get_context(self) -> dict[str, Any]:
|
|
113
|
+
"""Get the full context for planning/reflection.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Dictionary containing all memory state
|
|
117
|
+
"""
|
|
118
|
+
return {
|
|
119
|
+
"task_description": self.task_description,
|
|
120
|
+
"todos": [
|
|
121
|
+
{"index": i, "description": t.description, "status": t.status}
|
|
122
|
+
for i, t in enumerate(self.todos)
|
|
123
|
+
],
|
|
124
|
+
"history": [
|
|
125
|
+
{
|
|
126
|
+
"todo_index": h.todo_index,
|
|
127
|
+
"todo": h.todo,
|
|
128
|
+
"action_count": len(h.actions),
|
|
129
|
+
"summary": h.summary,
|
|
130
|
+
"completed": h.completed,
|
|
131
|
+
}
|
|
132
|
+
for h in self.history
|
|
133
|
+
],
|
|
134
|
+
"task_execution_summary": self.task_execution_summary,
|
|
135
|
+
"todo_execution_summaries": self.todo_execution_summaries,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
def get_todo_status_summary(self) -> dict[str, int]:
|
|
139
|
+
"""Get a summary of todo statuses.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Dictionary with counts for each status
|
|
143
|
+
"""
|
|
144
|
+
summary = {
|
|
145
|
+
TodoStatus.PENDING: 0,
|
|
146
|
+
TodoStatus.IN_PROGRESS: 0,
|
|
147
|
+
TodoStatus.COMPLETED: 0,
|
|
148
|
+
TodoStatus.SKIPPED: 0,
|
|
149
|
+
}
|
|
150
|
+
for todo in self.todos:
|
|
151
|
+
summary[todo.status] += 1
|
|
152
|
+
return summary
|
|
153
|
+
|
|
154
|
+
def append_todo(self, description: str) -> None:
|
|
155
|
+
"""Append a new todo to the list.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
description: Description of the new todo
|
|
159
|
+
"""
|
|
160
|
+
self.todos.append(Todo(description=description))
|
|
@@ -0,0 +1,77 @@
|
|
|
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 enum import Enum
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TodoStatus(str, Enum):
|
|
16
|
+
"""Status of a todo item in the workflow."""
|
|
17
|
+
|
|
18
|
+
PENDING = "pending"
|
|
19
|
+
IN_PROGRESS = "in_progress"
|
|
20
|
+
COMPLETED = "completed"
|
|
21
|
+
SKIPPED = "skipped"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Todo(BaseModel):
|
|
25
|
+
"""A single todo item in the workflow."""
|
|
26
|
+
|
|
27
|
+
description: str
|
|
28
|
+
status: TodoStatus = TodoStatus.PENDING
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Action(BaseModel):
|
|
32
|
+
"""An action taken during execution."""
|
|
33
|
+
|
|
34
|
+
timestamp: str
|
|
35
|
+
action_type: str # "plan", "reflect", "click", "type", "scroll", etc.
|
|
36
|
+
target: str | None = None
|
|
37
|
+
details: dict[str, Any] = Field(default_factory=dict)
|
|
38
|
+
reasoning: str | None = None
|
|
39
|
+
result: str | None = None
|
|
40
|
+
screenshot_uuid: str | None = None # UUID of uploaded screenshot for this action
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TodoHistory(BaseModel):
|
|
44
|
+
"""Execution history for a specific todo."""
|
|
45
|
+
|
|
46
|
+
todo_index: int
|
|
47
|
+
todo: str
|
|
48
|
+
actions: list[Action]
|
|
49
|
+
summary: str | None = None
|
|
50
|
+
completed: bool = False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class PlannerOutput(BaseModel):
|
|
54
|
+
"""Output from the LLM planner's initial planning."""
|
|
55
|
+
|
|
56
|
+
instruction: str # Clear instruction for the todo
|
|
57
|
+
reasoning: str # Planner's reasoning
|
|
58
|
+
subtodos: list[str] = Field(default_factory=list) # Optional subtasks
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ReflectionOutput(BaseModel):
|
|
62
|
+
"""Output from the LLM planner's reflection."""
|
|
63
|
+
|
|
64
|
+
continue_current: bool # Whether to continue with current approach
|
|
65
|
+
new_instruction: str | None = None # New instruction if pivoting
|
|
66
|
+
reasoning: str # Reflection reasoning
|
|
67
|
+
success_assessment: bool = False # Whether the task appears successful
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ExecutionResult(BaseModel):
|
|
71
|
+
"""Result from executing a single todo."""
|
|
72
|
+
|
|
73
|
+
success: bool
|
|
74
|
+
actions: list[Action]
|
|
75
|
+
summary: str
|
|
76
|
+
error: str | None = None
|
|
77
|
+
total_steps: int = 0
|
|
@@ -0,0 +1,408 @@
|
|
|
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 json
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from ...client import AsyncClient
|
|
13
|
+
from ...types import URL, Image
|
|
14
|
+
from .memory import PlannerMemory
|
|
15
|
+
from .models import Action, PlannerOutput, ReflectionOutput
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Planner:
|
|
19
|
+
"""Planner for task decomposition and reflection.
|
|
20
|
+
|
|
21
|
+
This class provides planning and reflection capabilities using OAGI workers.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
client: AsyncClient | None = None,
|
|
27
|
+
api_key: str | None = None,
|
|
28
|
+
base_url: str | None = None,
|
|
29
|
+
):
|
|
30
|
+
"""Initialize the planner.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
client: AsyncClient for OAGI API calls. If None, one will be created when needed.
|
|
34
|
+
api_key: API key for creating internal client
|
|
35
|
+
base_url: Base URL for creating internal client
|
|
36
|
+
"""
|
|
37
|
+
self.client = client
|
|
38
|
+
self.api_key = api_key
|
|
39
|
+
self.base_url = base_url
|
|
40
|
+
self._owns_client = False # Track if we created the client
|
|
41
|
+
|
|
42
|
+
def _ensure_client(self) -> AsyncClient:
|
|
43
|
+
"""Ensure we have a client, creating one if needed."""
|
|
44
|
+
if not self.client:
|
|
45
|
+
self.client = AsyncClient(api_key=self.api_key, base_url=self.base_url)
|
|
46
|
+
self._owns_client = True
|
|
47
|
+
return self.client
|
|
48
|
+
|
|
49
|
+
async def close(self):
|
|
50
|
+
"""Close the client if we own it."""
|
|
51
|
+
if self._owns_client and self.client:
|
|
52
|
+
await self.client.close()
|
|
53
|
+
|
|
54
|
+
async def __aenter__(self):
|
|
55
|
+
return self
|
|
56
|
+
|
|
57
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
58
|
+
await self.close()
|
|
59
|
+
|
|
60
|
+
def _extract_memory_data(
|
|
61
|
+
self,
|
|
62
|
+
memory: PlannerMemory | None,
|
|
63
|
+
context: dict[str, Any],
|
|
64
|
+
todo_index: int | None = None,
|
|
65
|
+
) -> tuple[str, list, list, str | None, str]:
|
|
66
|
+
"""Extract memory data for API calls.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
memory: Optional PlannerMemory instance
|
|
70
|
+
context: Fallback context dictionary
|
|
71
|
+
todo_index: Optional todo index for extracting overall_todo
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Tuple of (task_description, todos, history,
|
|
75
|
+
task_execution_summary, overall_todo)
|
|
76
|
+
"""
|
|
77
|
+
if memory and todo_index is not None:
|
|
78
|
+
# Use memory data
|
|
79
|
+
task_description = memory.task_description
|
|
80
|
+
todos = [
|
|
81
|
+
{
|
|
82
|
+
"index": i,
|
|
83
|
+
"description": t.description,
|
|
84
|
+
"status": t.status.value,
|
|
85
|
+
"execution_summary": memory.todo_execution_summaries.get(i),
|
|
86
|
+
}
|
|
87
|
+
for i, t in enumerate(memory.todos)
|
|
88
|
+
]
|
|
89
|
+
history = [
|
|
90
|
+
{
|
|
91
|
+
"todo_index": h.todo_index,
|
|
92
|
+
"todo_description": h.todo,
|
|
93
|
+
"action_count": len(h.actions),
|
|
94
|
+
"summary": h.summary,
|
|
95
|
+
"completed": h.completed,
|
|
96
|
+
}
|
|
97
|
+
for h in memory.history
|
|
98
|
+
]
|
|
99
|
+
task_execution_summary = memory.task_execution_summary or None
|
|
100
|
+
overall_todo = memory.todos[todo_index].description if memory.todos else ""
|
|
101
|
+
else:
|
|
102
|
+
# Fallback to basic context
|
|
103
|
+
task_description = context.get("task_description", "")
|
|
104
|
+
todos = context.get("todos", [])
|
|
105
|
+
history = context.get("history", [])
|
|
106
|
+
task_execution_summary = None
|
|
107
|
+
overall_todo = context.get("current_todo", "")
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
task_description,
|
|
111
|
+
todos,
|
|
112
|
+
history,
|
|
113
|
+
task_execution_summary,
|
|
114
|
+
overall_todo,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
async def initial_plan(
|
|
118
|
+
self,
|
|
119
|
+
todo: str,
|
|
120
|
+
context: dict[str, Any],
|
|
121
|
+
screenshot: Image | URL | None = None,
|
|
122
|
+
memory: PlannerMemory | None = None,
|
|
123
|
+
todo_index: int | None = None,
|
|
124
|
+
) -> PlannerOutput:
|
|
125
|
+
"""Generate initial plan for a todo.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
todo: The todo description to plan for
|
|
129
|
+
context: Full context including task, todos, deliverables, and history
|
|
130
|
+
screenshot: Optional screenshot for visual context
|
|
131
|
+
memory: Optional PlannerMemory for formatting contexts
|
|
132
|
+
todo_index: Optional todo index for formatting internal context
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
PlannerOutput with instruction, reasoning, and optional subtodos
|
|
136
|
+
"""
|
|
137
|
+
# Ensure we have a client
|
|
138
|
+
client = self._ensure_client()
|
|
139
|
+
|
|
140
|
+
# Upload screenshot if provided
|
|
141
|
+
screenshot_uuid = None
|
|
142
|
+
if screenshot:
|
|
143
|
+
upload_response = await client.put_s3_presigned_url(screenshot)
|
|
144
|
+
screenshot_uuid = upload_response.uuid
|
|
145
|
+
|
|
146
|
+
# Extract memory data if provided
|
|
147
|
+
(
|
|
148
|
+
task_description,
|
|
149
|
+
todos,
|
|
150
|
+
history,
|
|
151
|
+
task_execution_summary,
|
|
152
|
+
_, # overall_todo not needed here, we use the `todo` parameter
|
|
153
|
+
) = self._extract_memory_data(memory, context, todo_index)
|
|
154
|
+
|
|
155
|
+
# Call OAGI worker
|
|
156
|
+
response = await client.call_worker(
|
|
157
|
+
worker_id="oagi_first",
|
|
158
|
+
overall_todo=todo,
|
|
159
|
+
task_description=task_description,
|
|
160
|
+
todos=todos,
|
|
161
|
+
history=history,
|
|
162
|
+
current_todo_index=todo_index,
|
|
163
|
+
task_execution_summary=task_execution_summary,
|
|
164
|
+
current_screenshot=screenshot_uuid,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Parse response
|
|
168
|
+
return self._parse_planner_output(response.response)
|
|
169
|
+
|
|
170
|
+
async def reflect(
|
|
171
|
+
self,
|
|
172
|
+
actions: list[Action],
|
|
173
|
+
context: dict[str, Any],
|
|
174
|
+
screenshot: Image | URL | None = None,
|
|
175
|
+
memory: PlannerMemory | None = None,
|
|
176
|
+
todo_index: int | None = None,
|
|
177
|
+
current_instruction: str | None = None,
|
|
178
|
+
reflection_interval: int = 4,
|
|
179
|
+
) -> ReflectionOutput:
|
|
180
|
+
"""Reflect on recent actions and progress.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
actions: Recent actions to reflect on
|
|
184
|
+
context: Full context including task, todos, deliverables, and history
|
|
185
|
+
screenshot: Optional current screenshot
|
|
186
|
+
memory: Optional PlannerMemory for formatting contexts
|
|
187
|
+
todo_index: Optional todo index for formatting internal context
|
|
188
|
+
current_instruction: Current subtask instruction being executed
|
|
189
|
+
reflection_interval: Window size for recent actions/screenshots
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
ReflectionOutput with continuation decision and reasoning
|
|
193
|
+
"""
|
|
194
|
+
# Ensure we have a client
|
|
195
|
+
client = self._ensure_client()
|
|
196
|
+
|
|
197
|
+
# Upload screenshot if provided
|
|
198
|
+
result_screenshot_uuid = None
|
|
199
|
+
if screenshot:
|
|
200
|
+
upload_response = await client.put_s3_presigned_url(screenshot)
|
|
201
|
+
result_screenshot_uuid = upload_response.uuid
|
|
202
|
+
|
|
203
|
+
# Extract memory data if provided
|
|
204
|
+
(
|
|
205
|
+
task_description,
|
|
206
|
+
todos,
|
|
207
|
+
history,
|
|
208
|
+
task_execution_summary,
|
|
209
|
+
overall_todo,
|
|
210
|
+
) = self._extract_memory_data(memory, context, todo_index)
|
|
211
|
+
|
|
212
|
+
# Get window of recent actions based on reflection_interval
|
|
213
|
+
window_actions = actions[-reflection_interval:]
|
|
214
|
+
|
|
215
|
+
# Convert actions to window_steps format
|
|
216
|
+
window_steps = [
|
|
217
|
+
{
|
|
218
|
+
"step_number": i + 1,
|
|
219
|
+
"action_type": action.action_type,
|
|
220
|
+
"target": action.target or "",
|
|
221
|
+
"reasoning": action.reasoning or "",
|
|
222
|
+
}
|
|
223
|
+
for i, action in enumerate(window_actions)
|
|
224
|
+
]
|
|
225
|
+
|
|
226
|
+
# Extract screenshot UUIDs from window actions
|
|
227
|
+
window_screenshots = [
|
|
228
|
+
action.screenshot_uuid
|
|
229
|
+
for action in window_actions
|
|
230
|
+
if action.screenshot_uuid
|
|
231
|
+
]
|
|
232
|
+
|
|
233
|
+
# Format prior notes from context (still needed as a simple string summary)
|
|
234
|
+
prior_notes = self._format_execution_notes(context)
|
|
235
|
+
|
|
236
|
+
# Call OAGI worker
|
|
237
|
+
response = await client.call_worker(
|
|
238
|
+
worker_id="oagi_follow",
|
|
239
|
+
overall_todo=overall_todo,
|
|
240
|
+
task_description=task_description,
|
|
241
|
+
todos=todos,
|
|
242
|
+
history=history,
|
|
243
|
+
current_todo_index=todo_index,
|
|
244
|
+
task_execution_summary=task_execution_summary,
|
|
245
|
+
current_subtask_instruction=current_instruction or "",
|
|
246
|
+
window_steps=window_steps,
|
|
247
|
+
window_screenshots=window_screenshots,
|
|
248
|
+
result_screenshot=result_screenshot_uuid,
|
|
249
|
+
prior_notes=prior_notes,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Parse response
|
|
253
|
+
return self._parse_reflection_output(response.response)
|
|
254
|
+
|
|
255
|
+
async def summarize(
|
|
256
|
+
self,
|
|
257
|
+
execution_history: list[Action],
|
|
258
|
+
context: dict[str, Any],
|
|
259
|
+
memory: PlannerMemory | None = None,
|
|
260
|
+
todo_index: int | None = None,
|
|
261
|
+
) -> str:
|
|
262
|
+
"""Generate execution summary.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
execution_history: Complete execution history
|
|
266
|
+
context: Full context including task, todos, deliverables
|
|
267
|
+
memory: Optional PlannerMemory for formatting contexts
|
|
268
|
+
todo_index: Optional todo index for formatting internal context
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
String summary of the execution
|
|
272
|
+
"""
|
|
273
|
+
# Ensure we have a client
|
|
274
|
+
client = self._ensure_client()
|
|
275
|
+
|
|
276
|
+
# Extract memory data if provided
|
|
277
|
+
(
|
|
278
|
+
task_description,
|
|
279
|
+
todos,
|
|
280
|
+
history,
|
|
281
|
+
task_execution_summary,
|
|
282
|
+
overall_todo,
|
|
283
|
+
) = self._extract_memory_data(memory, context, todo_index)
|
|
284
|
+
|
|
285
|
+
# Extract latest_todo_summary (specific to summarize method)
|
|
286
|
+
if memory and todo_index is not None:
|
|
287
|
+
latest_todo_summary = memory.todo_execution_summaries.get(todo_index, "")
|
|
288
|
+
else:
|
|
289
|
+
latest_todo_summary = ""
|
|
290
|
+
|
|
291
|
+
# Call OAGI worker
|
|
292
|
+
response = await client.call_worker(
|
|
293
|
+
worker_id="oagi_task_summary",
|
|
294
|
+
overall_todo=overall_todo,
|
|
295
|
+
task_description=task_description,
|
|
296
|
+
todos=todos,
|
|
297
|
+
history=history,
|
|
298
|
+
current_todo_index=todo_index,
|
|
299
|
+
task_execution_summary=task_execution_summary,
|
|
300
|
+
latest_todo_summary=latest_todo_summary,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
# Parse response and extract summary
|
|
304
|
+
try:
|
|
305
|
+
result = json.loads(response.response)
|
|
306
|
+
return result.get("task_summary", response.response)
|
|
307
|
+
except json.JSONDecodeError:
|
|
308
|
+
return response.response
|
|
309
|
+
|
|
310
|
+
def _format_execution_notes(self, context: dict[str, Any]) -> str:
|
|
311
|
+
"""Format execution history notes.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
context: Context dictionary
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
Formatted execution notes
|
|
318
|
+
"""
|
|
319
|
+
if not context.get("history"):
|
|
320
|
+
return ""
|
|
321
|
+
|
|
322
|
+
parts = []
|
|
323
|
+
for hist in context["history"]:
|
|
324
|
+
parts.append(
|
|
325
|
+
f"Todo {hist['todo_index']}: {hist['action_count']} actions, "
|
|
326
|
+
f"completed: {hist['completed']}"
|
|
327
|
+
)
|
|
328
|
+
if hist.get("summary"):
|
|
329
|
+
parts.append(f"Summary: {hist['summary']}")
|
|
330
|
+
|
|
331
|
+
return "\n".join(parts)
|
|
332
|
+
|
|
333
|
+
def _parse_planner_output(self, response: str) -> PlannerOutput:
|
|
334
|
+
"""Parse OAGI worker response into structured planner output.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
response: Raw string response from OAGI worker (oagi_first)
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
Structured PlannerOutput
|
|
341
|
+
"""
|
|
342
|
+
try:
|
|
343
|
+
# Try to parse as JSON (oagi_first format)
|
|
344
|
+
# Extract JSON string to handle Markdown code blocks
|
|
345
|
+
json_response = self._extract_json_str(response)
|
|
346
|
+
data = json.loads(json_response)
|
|
347
|
+
# oagi_first returns: {"reasoning": "...", "subtask": "..."}
|
|
348
|
+
return PlannerOutput(
|
|
349
|
+
instruction=data.get("subtask", data.get("instruction", "")),
|
|
350
|
+
reasoning=data.get("reasoning", ""),
|
|
351
|
+
subtodos=data.get(
|
|
352
|
+
"subtodos", []
|
|
353
|
+
), # Not typically returned by oagi_first
|
|
354
|
+
)
|
|
355
|
+
except (json.JSONDecodeError, KeyError):
|
|
356
|
+
# Fallback: use the entire response as instruction
|
|
357
|
+
return PlannerOutput(
|
|
358
|
+
instruction="",
|
|
359
|
+
reasoning="Failed to parse structured response",
|
|
360
|
+
subtodos=[],
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
def _parse_reflection_output(self, response: str) -> ReflectionOutput:
|
|
364
|
+
"""Parse reflection response into structured output.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
response: Raw string response from OAGI worker (oagi_follow)
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
Structured ReflectionOutput
|
|
371
|
+
"""
|
|
372
|
+
try:
|
|
373
|
+
# Try to parse as JSON (oagi_follow format)
|
|
374
|
+
json_response = self._extract_json_str(response)
|
|
375
|
+
data = json.loads(json_response)
|
|
376
|
+
# oagi_follow returns:
|
|
377
|
+
# {"assessment": "...", "summary": "...", "reflection": "...",
|
|
378
|
+
# "success": "yes" | "no", "subtask_instruction": "..."}
|
|
379
|
+
|
|
380
|
+
# Determine if we should continue or pivot
|
|
381
|
+
success = data.get("success", "no") == "yes"
|
|
382
|
+
new_subtask = data.get("subtask_instruction", "").strip()
|
|
383
|
+
|
|
384
|
+
# Continue current if success is not achieved and no new subtask provided
|
|
385
|
+
# Pivot if a new subtask instruction is provided
|
|
386
|
+
continue_current = not success and not new_subtask
|
|
387
|
+
|
|
388
|
+
return ReflectionOutput(
|
|
389
|
+
continue_current=continue_current,
|
|
390
|
+
new_instruction=new_subtask if new_subtask else None,
|
|
391
|
+
reasoning=data.get("reflection", data.get("reasoning", "")),
|
|
392
|
+
success_assessment=success,
|
|
393
|
+
)
|
|
394
|
+
except (json.JSONDecodeError, KeyError):
|
|
395
|
+
# Fallback: continue with current approach
|
|
396
|
+
return ReflectionOutput(
|
|
397
|
+
continue_current=True,
|
|
398
|
+
new_instruction=None,
|
|
399
|
+
reasoning="Failed to parse reflection response, continuing current approach",
|
|
400
|
+
success_assessment=False,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
def _extract_json_str(self, text: str) -> str:
|
|
404
|
+
start = text.find("{")
|
|
405
|
+
end = text.rfind("}") + 1
|
|
406
|
+
if start < 0 or end <= start:
|
|
407
|
+
return ""
|
|
408
|
+
return text[start:end]
|