@elizaos/python 2.0.0-alpha.10
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.
- package/LICENSE +26 -0
- package/README.md +239 -0
- package/elizaos/__init__.py +280 -0
- package/elizaos/action_docs.py +149 -0
- package/elizaos/advanced_capabilities/__init__.py +85 -0
- package/elizaos/advanced_capabilities/actions/__init__.py +54 -0
- package/elizaos/advanced_capabilities/actions/add_contact.py +139 -0
- package/elizaos/advanced_capabilities/actions/follow_room.py +151 -0
- package/elizaos/advanced_capabilities/actions/image_generation.py +148 -0
- package/elizaos/advanced_capabilities/actions/mute_room.py +164 -0
- package/elizaos/advanced_capabilities/actions/remove_contact.py +145 -0
- package/elizaos/advanced_capabilities/actions/roles.py +207 -0
- package/elizaos/advanced_capabilities/actions/schedule_follow_up.py +154 -0
- package/elizaos/advanced_capabilities/actions/search_contacts.py +145 -0
- package/elizaos/advanced_capabilities/actions/send_message.py +187 -0
- package/elizaos/advanced_capabilities/actions/settings.py +151 -0
- package/elizaos/advanced_capabilities/actions/unfollow_room.py +164 -0
- package/elizaos/advanced_capabilities/actions/unmute_room.py +164 -0
- package/elizaos/advanced_capabilities/actions/update_contact.py +164 -0
- package/elizaos/advanced_capabilities/actions/update_entity.py +161 -0
- package/elizaos/advanced_capabilities/evaluators/__init__.py +18 -0
- package/elizaos/advanced_capabilities/evaluators/reflection.py +134 -0
- package/elizaos/advanced_capabilities/evaluators/relationship_extraction.py +203 -0
- package/elizaos/advanced_capabilities/providers/__init__.py +36 -0
- package/elizaos/advanced_capabilities/providers/agent_settings.py +60 -0
- package/elizaos/advanced_capabilities/providers/contacts.py +77 -0
- package/elizaos/advanced_capabilities/providers/facts.py +82 -0
- package/elizaos/advanced_capabilities/providers/follow_ups.py +113 -0
- package/elizaos/advanced_capabilities/providers/knowledge.py +83 -0
- package/elizaos/advanced_capabilities/providers/relationships.py +112 -0
- package/elizaos/advanced_capabilities/providers/roles.py +97 -0
- package/elizaos/advanced_capabilities/providers/settings.py +51 -0
- package/elizaos/advanced_capabilities/services/__init__.py +18 -0
- package/elizaos/advanced_capabilities/services/follow_up.py +138 -0
- package/elizaos/advanced_capabilities/services/rolodex.py +244 -0
- package/elizaos/advanced_memory/__init__.py +3 -0
- package/elizaos/advanced_memory/evaluators.py +97 -0
- package/elizaos/advanced_memory/memory_service.py +556 -0
- package/elizaos/advanced_memory/plugin.py +30 -0
- package/elizaos/advanced_memory/prompts.py +12 -0
- package/elizaos/advanced_memory/providers.py +90 -0
- package/elizaos/advanced_memory/types.py +65 -0
- package/elizaos/advanced_planning/__init__.py +10 -0
- package/elizaos/advanced_planning/actions.py +145 -0
- package/elizaos/advanced_planning/message_classifier.py +127 -0
- package/elizaos/advanced_planning/planning_service.py +712 -0
- package/elizaos/advanced_planning/plugin.py +40 -0
- package/elizaos/advanced_planning/prompts.py +4 -0
- package/elizaos/basic_capabilities/__init__.py +66 -0
- package/elizaos/basic_capabilities/actions/__init__.py +24 -0
- package/elizaos/basic_capabilities/actions/choice.py +140 -0
- package/elizaos/basic_capabilities/actions/ignore.py +66 -0
- package/elizaos/basic_capabilities/actions/none.py +56 -0
- package/elizaos/basic_capabilities/actions/reply.py +120 -0
- package/elizaos/basic_capabilities/providers/__init__.py +54 -0
- package/elizaos/basic_capabilities/providers/action_state.py +113 -0
- package/elizaos/basic_capabilities/providers/actions.py +263 -0
- package/elizaos/basic_capabilities/providers/attachments.py +76 -0
- package/elizaos/basic_capabilities/providers/capabilities.py +62 -0
- package/elizaos/basic_capabilities/providers/character.py +113 -0
- package/elizaos/basic_capabilities/providers/choice.py +73 -0
- package/elizaos/basic_capabilities/providers/context_bench.py +44 -0
- package/elizaos/basic_capabilities/providers/current_time.py +58 -0
- package/elizaos/basic_capabilities/providers/entities.py +99 -0
- package/elizaos/basic_capabilities/providers/evaluators.py +54 -0
- package/elizaos/basic_capabilities/providers/providers_list.py +55 -0
- package/elizaos/basic_capabilities/providers/recent_messages.py +85 -0
- package/elizaos/basic_capabilities/providers/time.py +45 -0
- package/elizaos/basic_capabilities/providers/world.py +93 -0
- package/elizaos/basic_capabilities/services/__init__.py +18 -0
- package/elizaos/basic_capabilities/services/embedding.py +122 -0
- package/elizaos/basic_capabilities/services/task.py +178 -0
- package/elizaos/bootstrap/__init__.py +12 -0
- package/elizaos/bootstrap/actions/__init__.py +68 -0
- package/elizaos/bootstrap/actions/add_contact.py +149 -0
- package/elizaos/bootstrap/actions/choice.py +147 -0
- package/elizaos/bootstrap/actions/follow_room.py +151 -0
- package/elizaos/bootstrap/actions/ignore.py +80 -0
- package/elizaos/bootstrap/actions/image_generation.py +135 -0
- package/elizaos/bootstrap/actions/mute_room.py +151 -0
- package/elizaos/bootstrap/actions/none.py +71 -0
- package/elizaos/bootstrap/actions/remove_contact.py +159 -0
- package/elizaos/bootstrap/actions/reply.py +140 -0
- package/elizaos/bootstrap/actions/roles.py +193 -0
- package/elizaos/bootstrap/actions/schedule_follow_up.py +164 -0
- package/elizaos/bootstrap/actions/search_contacts.py +159 -0
- package/elizaos/bootstrap/actions/send_message.py +173 -0
- package/elizaos/bootstrap/actions/settings.py +165 -0
- package/elizaos/bootstrap/actions/unfollow_room.py +151 -0
- package/elizaos/bootstrap/actions/unmute_room.py +151 -0
- package/elizaos/bootstrap/actions/update_contact.py +178 -0
- package/elizaos/bootstrap/actions/update_entity.py +175 -0
- package/elizaos/bootstrap/autonomy/__init__.py +18 -0
- package/elizaos/bootstrap/autonomy/action.py +197 -0
- package/elizaos/bootstrap/autonomy/providers.py +165 -0
- package/elizaos/bootstrap/autonomy/routes.py +171 -0
- package/elizaos/bootstrap/autonomy/service.py +562 -0
- package/elizaos/bootstrap/autonomy/types.py +18 -0
- package/elizaos/bootstrap/evaluators/__init__.py +19 -0
- package/elizaos/bootstrap/evaluators/reflection.py +118 -0
- package/elizaos/bootstrap/evaluators/relationship_extraction.py +192 -0
- package/elizaos/bootstrap/plugin.py +140 -0
- package/elizaos/bootstrap/providers/__init__.py +80 -0
- package/elizaos/bootstrap/providers/action_state.py +71 -0
- package/elizaos/bootstrap/providers/actions.py +256 -0
- package/elizaos/bootstrap/providers/agent_settings.py +63 -0
- package/elizaos/bootstrap/providers/attachments.py +76 -0
- package/elizaos/bootstrap/providers/capabilities.py +66 -0
- package/elizaos/bootstrap/providers/character.py +128 -0
- package/elizaos/bootstrap/providers/choice.py +77 -0
- package/elizaos/bootstrap/providers/contacts.py +78 -0
- package/elizaos/bootstrap/providers/context_bench.py +49 -0
- package/elizaos/bootstrap/providers/current_time.py +56 -0
- package/elizaos/bootstrap/providers/entities.py +99 -0
- package/elizaos/bootstrap/providers/evaluators.py +58 -0
- package/elizaos/bootstrap/providers/facts.py +86 -0
- package/elizaos/bootstrap/providers/follow_ups.py +116 -0
- package/elizaos/bootstrap/providers/knowledge.py +73 -0
- package/elizaos/bootstrap/providers/providers_list.py +59 -0
- package/elizaos/bootstrap/providers/recent_messages.py +85 -0
- package/elizaos/bootstrap/providers/relationships.py +106 -0
- package/elizaos/bootstrap/providers/roles.py +95 -0
- package/elizaos/bootstrap/providers/settings.py +55 -0
- package/elizaos/bootstrap/providers/time.py +45 -0
- package/elizaos/bootstrap/providers/world.py +97 -0
- package/elizaos/bootstrap/services/__init__.py +26 -0
- package/elizaos/bootstrap/services/embedding.py +122 -0
- package/elizaos/bootstrap/services/follow_up.py +138 -0
- package/elizaos/bootstrap/services/rolodex.py +244 -0
- package/elizaos/bootstrap/services/task.py +585 -0
- package/elizaos/bootstrap/types.py +54 -0
- package/elizaos/bootstrap/utils/__init__.py +7 -0
- package/elizaos/bootstrap/utils/xml.py +69 -0
- package/elizaos/character.py +149 -0
- package/elizaos/logger.py +179 -0
- package/elizaos/media/__init__.py +45 -0
- package/elizaos/media/mime.py +315 -0
- package/elizaos/media/search.py +161 -0
- package/elizaos/media/tests/__init__.py +1 -0
- package/elizaos/media/tests/test_mime.py +117 -0
- package/elizaos/media/tests/test_search.py +156 -0
- package/elizaos/plugin.py +191 -0
- package/elizaos/prompts.py +1071 -0
- package/elizaos/py.typed +0 -0
- package/elizaos/runtime.py +2572 -0
- package/elizaos/services/__init__.py +49 -0
- package/elizaos/services/hook_service.py +511 -0
- package/elizaos/services/message_service.py +1248 -0
- package/elizaos/settings.py +182 -0
- package/elizaos/streaming_context.py +159 -0
- package/elizaos/trajectory_context.py +18 -0
- package/elizaos/types/__init__.py +512 -0
- package/elizaos/types/agent.py +31 -0
- package/elizaos/types/components.py +208 -0
- package/elizaos/types/database.py +64 -0
- package/elizaos/types/environment.py +46 -0
- package/elizaos/types/events.py +47 -0
- package/elizaos/types/memory.py +45 -0
- package/elizaos/types/model.py +393 -0
- package/elizaos/types/plugin.py +188 -0
- package/elizaos/types/primitives.py +100 -0
- package/elizaos/types/runtime.py +460 -0
- package/elizaos/types/service.py +113 -0
- package/elizaos/types/service_interfaces.py +244 -0
- package/elizaos/types/state.py +188 -0
- package/elizaos/types/task.py +29 -0
- package/elizaos/utils/__init__.py +108 -0
- package/elizaos/utils/spec_examples.py +48 -0
- package/elizaos/utils/streaming.py +426 -0
- package/elizaos_atropos_shared/__init__.py +1 -0
- package/elizaos_atropos_shared/canonical_eliza.py +282 -0
- package/package.json +19 -0
- package/pyproject.toml +143 -0
- package/requirements-dev.in +11 -0
- package/requirements-dev.lock +134 -0
- package/requirements.in +9 -0
- package/requirements.lock +64 -0
- package/tests/__init__.py +0 -0
- package/tests/test_action_parameters.py +154 -0
- package/tests/test_actions_provider_examples.py +39 -0
- package/tests/test_advanced_memory_behavior.py +96 -0
- package/tests/test_advanced_memory_flag.py +30 -0
- package/tests/test_advanced_planning_behavior.py +225 -0
- package/tests/test_advanced_planning_flag.py +26 -0
- package/tests/test_autonomy.py +445 -0
- package/tests/test_bootstrap_initialize.py +37 -0
- package/tests/test_character.py +163 -0
- package/tests/test_character_provider.py +231 -0
- package/tests/test_dynamic_prompt_exec.py +561 -0
- package/tests/test_logger_redaction.py +43 -0
- package/tests/test_plugin.py +117 -0
- package/tests/test_runtime.py +422 -0
- package/tests/test_salt_production_enforcement.py +22 -0
- package/tests/test_settings_crypto.py +118 -0
- package/tests/test_streaming.py +295 -0
- package/tests/test_types.py +221 -0
- package/tests/test_uuid_parity.py +46 -0
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
import time
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from uuid import UUID, uuid4
|
|
9
|
+
|
|
10
|
+
from google.protobuf.json_format import MessageToDict
|
|
11
|
+
|
|
12
|
+
from elizaos.logger import Logger
|
|
13
|
+
from elizaos.types.components import ActionContext, ActionResult, HandlerCallback, HandlerOptions
|
|
14
|
+
from elizaos.types.memory import Memory
|
|
15
|
+
from elizaos.types.primitives import Content
|
|
16
|
+
from elizaos.types.service import Service
|
|
17
|
+
from elizaos.types.state import State
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class RetryPolicy:
|
|
22
|
+
max_retries: int = 2
|
|
23
|
+
backoff_ms: int = 1000
|
|
24
|
+
backoff_multiplier: int = 2
|
|
25
|
+
on_error: str = "abort" # abort | continue | skip
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ActionStep:
|
|
30
|
+
id: UUID
|
|
31
|
+
action_name: str
|
|
32
|
+
parameters: dict[str, object]
|
|
33
|
+
dependencies: list[UUID]
|
|
34
|
+
retry_policy: RetryPolicy = field(default_factory=RetryPolicy)
|
|
35
|
+
on_error: str | None = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class PlanState:
|
|
40
|
+
status: str = "pending" # pending | running | completed | failed | cancelled
|
|
41
|
+
start_time: float | None = None
|
|
42
|
+
end_time: float | None = None
|
|
43
|
+
current_step_index: int = 0
|
|
44
|
+
error: str | None = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class ActionPlan:
|
|
49
|
+
id: UUID
|
|
50
|
+
goal: str
|
|
51
|
+
thought: str
|
|
52
|
+
total_steps: int
|
|
53
|
+
current_step: int
|
|
54
|
+
steps: list[ActionStep]
|
|
55
|
+
execution_model: str = "sequential"
|
|
56
|
+
state: PlanState = field(default_factory=PlanState)
|
|
57
|
+
metadata: dict[str, object] | None = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class PlanExecutionResult:
|
|
62
|
+
plan_id: UUID
|
|
63
|
+
success: bool
|
|
64
|
+
completed_steps: int
|
|
65
|
+
total_steps: int
|
|
66
|
+
results: list[ActionResult]
|
|
67
|
+
errors: list[str] | None = None
|
|
68
|
+
duration_ms: float | None = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class PlanExecution:
|
|
73
|
+
state: PlanState
|
|
74
|
+
working_memory: dict[str, object]
|
|
75
|
+
results: list[ActionResult]
|
|
76
|
+
abort_event: asyncio.Event = field(default_factory=asyncio.Event)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class PlanningService(Service):
|
|
80
|
+
service_type = "planning"
|
|
81
|
+
|
|
82
|
+
def __init__(self, runtime=None) -> None:
|
|
83
|
+
super().__init__(runtime=runtime)
|
|
84
|
+
self._active_plans: dict[UUID, ActionPlan] = {}
|
|
85
|
+
self._executions: dict[UUID, PlanExecution] = {}
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def capability_description(self) -> str:
|
|
89
|
+
return "Planning and action coordination"
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
async def start(cls, runtime):
|
|
93
|
+
service = cls(runtime=runtime)
|
|
94
|
+
runtime.logger.info("PlanningService started successfully", src="service:planning")
|
|
95
|
+
return service
|
|
96
|
+
|
|
97
|
+
async def stop(self) -> None:
|
|
98
|
+
for execution in self._executions.values():
|
|
99
|
+
execution.abort_event.set()
|
|
100
|
+
execution.state.status = "cancelled"
|
|
101
|
+
execution.state.end_time = time.time()
|
|
102
|
+
self._active_plans.clear()
|
|
103
|
+
self._executions.clear()
|
|
104
|
+
|
|
105
|
+
async def create_simple_plan(
|
|
106
|
+
self,
|
|
107
|
+
message: Memory,
|
|
108
|
+
state: State | None = None,
|
|
109
|
+
response_content: Content | None = None,
|
|
110
|
+
) -> ActionPlan | None:
|
|
111
|
+
_ = state
|
|
112
|
+
|
|
113
|
+
text = (message.content.text or "").lower()
|
|
114
|
+
actions: list[str]
|
|
115
|
+
if response_content and response_content.actions:
|
|
116
|
+
actions = [a for a in response_content.actions if isinstance(a, str)]
|
|
117
|
+
elif "email" in text:
|
|
118
|
+
actions = ["SEND_EMAIL"]
|
|
119
|
+
elif "research" in text and ("send" in text or "summary" in text):
|
|
120
|
+
actions = ["SEARCH", "REPLY"]
|
|
121
|
+
elif any(word in text for word in ["search", "find", "research"]):
|
|
122
|
+
actions = ["SEARCH"]
|
|
123
|
+
elif "analyze" in text:
|
|
124
|
+
actions = ["THINK", "REPLY"]
|
|
125
|
+
else:
|
|
126
|
+
actions = ["REPLY"]
|
|
127
|
+
|
|
128
|
+
plan_id = uuid4()
|
|
129
|
+
steps: list[ActionStep] = []
|
|
130
|
+
prev: UUID | None = None
|
|
131
|
+
for action_name in actions:
|
|
132
|
+
step_id = uuid4()
|
|
133
|
+
deps = [prev] if prev else []
|
|
134
|
+
steps.append(
|
|
135
|
+
ActionStep(
|
|
136
|
+
id=step_id,
|
|
137
|
+
action_name=action_name,
|
|
138
|
+
parameters={
|
|
139
|
+
"message": response_content.text
|
|
140
|
+
if response_content
|
|
141
|
+
else (message.content.text or ""),
|
|
142
|
+
"thought": response_content.thought if response_content else None,
|
|
143
|
+
"providers": response_content.providers if response_content else [],
|
|
144
|
+
},
|
|
145
|
+
dependencies=[d for d in deps if d is not None],
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
prev = step_id
|
|
149
|
+
|
|
150
|
+
plan = ActionPlan(
|
|
151
|
+
id=plan_id,
|
|
152
|
+
goal=response_content.text
|
|
153
|
+
if response_content and response_content.text
|
|
154
|
+
else (message.content.text or "Execute plan"),
|
|
155
|
+
thought=response_content.thought
|
|
156
|
+
if response_content and response_content.thought
|
|
157
|
+
else f"Executing {len(steps)} action(s)",
|
|
158
|
+
total_steps=len(steps),
|
|
159
|
+
current_step=0,
|
|
160
|
+
steps=steps,
|
|
161
|
+
execution_model="sequential",
|
|
162
|
+
state=PlanState(status="pending"),
|
|
163
|
+
metadata={"createdAt": int(time.time() * 1000)},
|
|
164
|
+
)
|
|
165
|
+
self._active_plans[plan_id] = plan
|
|
166
|
+
return plan
|
|
167
|
+
|
|
168
|
+
def _build_planning_prompt(
|
|
169
|
+
self,
|
|
170
|
+
context: dict[str, object],
|
|
171
|
+
message: Memory | None,
|
|
172
|
+
state: State | None,
|
|
173
|
+
) -> str:
|
|
174
|
+
goal = str(context.get("goal") or "")
|
|
175
|
+
available_actions = (
|
|
176
|
+
context.get("available_actions") or context.get("availableActions") or []
|
|
177
|
+
)
|
|
178
|
+
available_providers = (
|
|
179
|
+
context.get("available_providers") or context.get("availableProviders") or []
|
|
180
|
+
)
|
|
181
|
+
constraints_obj = context.get("constraints") or []
|
|
182
|
+
preferences = (
|
|
183
|
+
context.get("preferences") if isinstance(context.get("preferences"), dict) else {}
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
execution_model = "sequential"
|
|
187
|
+
max_steps = 10
|
|
188
|
+
if isinstance(preferences, dict):
|
|
189
|
+
execution_model = str(
|
|
190
|
+
preferences.get("execution_model")
|
|
191
|
+
or preferences.get("executionModel")
|
|
192
|
+
or "sequential"
|
|
193
|
+
)
|
|
194
|
+
max_steps = int(preferences.get("max_steps") or preferences.get("maxSteps") or 10)
|
|
195
|
+
|
|
196
|
+
if isinstance(available_actions, list):
|
|
197
|
+
actions_text = ", ".join(str(a) for a in available_actions)
|
|
198
|
+
else:
|
|
199
|
+
actions_text = ""
|
|
200
|
+
|
|
201
|
+
providers_text = (
|
|
202
|
+
", ".join(str(p) for p in available_providers)
|
|
203
|
+
if isinstance(available_providers, list)
|
|
204
|
+
else ""
|
|
205
|
+
)
|
|
206
|
+
constraints_text = ""
|
|
207
|
+
if isinstance(constraints_obj, list):
|
|
208
|
+
parts: list[str] = []
|
|
209
|
+
for c in constraints_obj:
|
|
210
|
+
if isinstance(c, dict):
|
|
211
|
+
c_type = str(c.get("type") or "custom")
|
|
212
|
+
c_desc = c.get("description")
|
|
213
|
+
c_val = c.get("value")
|
|
214
|
+
parts.append(f"{c_type}: {c_desc or c_val}")
|
|
215
|
+
constraints_text = ", ".join(parts)
|
|
216
|
+
|
|
217
|
+
msg_text = (
|
|
218
|
+
f"CONTEXT MESSAGE: {message.content.text}" if message and message.content.text else ""
|
|
219
|
+
)
|
|
220
|
+
state_text = f"CURRENT STATE: {json.dumps(state.values)}" if state else ""
|
|
221
|
+
|
|
222
|
+
return f"""You are an expert AI planning system. Create a comprehensive action plan to achieve the following goal.
|
|
223
|
+
|
|
224
|
+
GOAL: {goal}
|
|
225
|
+
|
|
226
|
+
AVAILABLE ACTIONS: {actions_text}
|
|
227
|
+
AVAILABLE PROVIDERS: {providers_text}
|
|
228
|
+
CONSTRAINTS: {constraints_text}
|
|
229
|
+
|
|
230
|
+
EXECUTION MODEL: {execution_model}
|
|
231
|
+
MAX STEPS: {max_steps}
|
|
232
|
+
|
|
233
|
+
{msg_text}
|
|
234
|
+
{state_text}
|
|
235
|
+
|
|
236
|
+
Create a detailed plan with the following structure:
|
|
237
|
+
<plan>
|
|
238
|
+
<goal>{goal}</goal>
|
|
239
|
+
<execution_model>{execution_model}</execution_model>
|
|
240
|
+
<steps>
|
|
241
|
+
<step>
|
|
242
|
+
<id>step_1</id>
|
|
243
|
+
<action>ACTION_NAME</action>
|
|
244
|
+
<parameters>{{"key": "value"}}</parameters>
|
|
245
|
+
<dependencies>[]</dependencies>
|
|
246
|
+
</step>
|
|
247
|
+
</steps>
|
|
248
|
+
</plan>
|
|
249
|
+
|
|
250
|
+
Focus on:
|
|
251
|
+
1. Breaking down the goal into logical, executable steps
|
|
252
|
+
2. Ensuring each step uses available actions
|
|
253
|
+
3. Managing dependencies between steps
|
|
254
|
+
4. Providing realistic time estimates
|
|
255
|
+
5. Including error handling considerations"""
|
|
256
|
+
|
|
257
|
+
def _parse_plan(self, response: str, goal: str) -> ActionPlan:
|
|
258
|
+
plan_id = uuid4()
|
|
259
|
+
steps: list[ActionStep] = []
|
|
260
|
+
step_id_map: dict[str, UUID] = {}
|
|
261
|
+
|
|
262
|
+
step_matches = re.findall(r"<step>(.*?)</step>", response, flags=re.DOTALL)
|
|
263
|
+
for step_match in step_matches:
|
|
264
|
+
id_match = re.search(r"<id>(.*?)</id>", step_match)
|
|
265
|
+
action_match = re.search(r"<action>(.*?)</action>", step_match)
|
|
266
|
+
params_match = re.search(r"<parameters>(.*?)</parameters>", step_match)
|
|
267
|
+
deps_match = re.search(r"<dependencies>(.*?)</dependencies>", step_match)
|
|
268
|
+
if not id_match or not action_match:
|
|
269
|
+
continue
|
|
270
|
+
|
|
271
|
+
orig_id = id_match.group(1).strip()
|
|
272
|
+
actual_id = uuid4()
|
|
273
|
+
step_id_map[orig_id] = actual_id
|
|
274
|
+
|
|
275
|
+
parameters: dict[str, object] = {}
|
|
276
|
+
if params_match:
|
|
277
|
+
try:
|
|
278
|
+
parsed = json.loads(params_match.group(1))
|
|
279
|
+
if isinstance(parsed, dict):
|
|
280
|
+
parameters = parsed
|
|
281
|
+
except Exception:
|
|
282
|
+
parameters = {}
|
|
283
|
+
|
|
284
|
+
dep_strings: list[str] = []
|
|
285
|
+
if deps_match:
|
|
286
|
+
try:
|
|
287
|
+
parsed_deps = json.loads(deps_match.group(1))
|
|
288
|
+
if isinstance(parsed_deps, list):
|
|
289
|
+
dep_strings = [str(d).strip() for d in parsed_deps if str(d).strip()]
|
|
290
|
+
except Exception:
|
|
291
|
+
dep_strings = []
|
|
292
|
+
|
|
293
|
+
steps.append(
|
|
294
|
+
ActionStep(
|
|
295
|
+
id=actual_id,
|
|
296
|
+
action_name=action_match.group(1).strip(),
|
|
297
|
+
parameters=parameters,
|
|
298
|
+
dependencies=[],
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
# stash original dependency strings on the parameters dict (private key)
|
|
302
|
+
steps[-1].parameters["_depStrings"] = dep_strings
|
|
303
|
+
|
|
304
|
+
# resolve dependencies
|
|
305
|
+
for step in steps:
|
|
306
|
+
dep_strings_raw = step.parameters.pop("_depStrings", [])
|
|
307
|
+
step_dep_strings: list[str] = []
|
|
308
|
+
if isinstance(dep_strings_raw, list):
|
|
309
|
+
step_dep_strings = [str(d).strip() for d in dep_strings_raw if str(d).strip()]
|
|
310
|
+
deps: list[UUID] = []
|
|
311
|
+
for d in step_dep_strings:
|
|
312
|
+
if d in step_id_map:
|
|
313
|
+
deps.append(step_id_map[d])
|
|
314
|
+
step.dependencies = deps
|
|
315
|
+
|
|
316
|
+
if not steps:
|
|
317
|
+
steps = [
|
|
318
|
+
ActionStep(
|
|
319
|
+
id=uuid4(),
|
|
320
|
+
action_name="REPLY",
|
|
321
|
+
parameters={"text": "I will help you with this request step by step."},
|
|
322
|
+
dependencies=[],
|
|
323
|
+
)
|
|
324
|
+
]
|
|
325
|
+
|
|
326
|
+
plan = ActionPlan(
|
|
327
|
+
id=plan_id,
|
|
328
|
+
goal=goal,
|
|
329
|
+
thought=f"Plan to achieve: {goal}",
|
|
330
|
+
total_steps=len(steps),
|
|
331
|
+
current_step=0,
|
|
332
|
+
steps=steps,
|
|
333
|
+
execution_model="sequential",
|
|
334
|
+
state=PlanState(status="pending"),
|
|
335
|
+
metadata={"createdAt": int(time.time() * 1000)},
|
|
336
|
+
)
|
|
337
|
+
self._active_plans[plan_id] = plan
|
|
338
|
+
return plan
|
|
339
|
+
|
|
340
|
+
def _normalize_action_name(self, name: str) -> str:
|
|
341
|
+
return re.sub(r"[_\s]+", "", name.strip()).lower()
|
|
342
|
+
|
|
343
|
+
def _build_action_lookup(self) -> dict[str, object]:
|
|
344
|
+
lookup: dict[str, object] = {}
|
|
345
|
+
for action in self.runtime.actions:
|
|
346
|
+
name_norm = self._normalize_action_name(action.name)
|
|
347
|
+
lookup.setdefault(name_norm, action)
|
|
348
|
+
if action.similes:
|
|
349
|
+
for s in action.similes:
|
|
350
|
+
simile_norm = self._normalize_action_name(s)
|
|
351
|
+
lookup.setdefault(simile_norm, action)
|
|
352
|
+
return lookup
|
|
353
|
+
|
|
354
|
+
def _find_action(self, action_name: str, action_lookup: dict[str, object] | None = None):
|
|
355
|
+
target_norm = self._normalize_action_name(action_name)
|
|
356
|
+
if action_lookup is not None:
|
|
357
|
+
return action_lookup.get(target_norm)
|
|
358
|
+
|
|
359
|
+
for action in self.runtime.actions:
|
|
360
|
+
name_norm = self._normalize_action_name(action.name)
|
|
361
|
+
if name_norm == target_norm:
|
|
362
|
+
return action
|
|
363
|
+
if action.similes:
|
|
364
|
+
for s in action.similes:
|
|
365
|
+
if self._normalize_action_name(s) == target_norm:
|
|
366
|
+
return action
|
|
367
|
+
return None
|
|
368
|
+
|
|
369
|
+
async def create_comprehensive_plan(
|
|
370
|
+
self,
|
|
371
|
+
context: dict[str, object],
|
|
372
|
+
message: Memory | None = None,
|
|
373
|
+
state: State | None = None,
|
|
374
|
+
) -> ActionPlan:
|
|
375
|
+
goal = str(context.get("goal") or "")
|
|
376
|
+
if not goal.strip():
|
|
377
|
+
raise ValueError("Planning context must have a non-empty goal")
|
|
378
|
+
|
|
379
|
+
prompt = self._build_planning_prompt(context, message, state)
|
|
380
|
+
action_lookup = self._build_action_lookup()
|
|
381
|
+
try:
|
|
382
|
+
response = await self.runtime.use_model(
|
|
383
|
+
"TEXT_LARGE",
|
|
384
|
+
{"prompt": prompt, "temperature": 0.3, "maxTokens": 2000},
|
|
385
|
+
)
|
|
386
|
+
plan = self._parse_plan(str(response), goal=goal)
|
|
387
|
+
# enhance plan by downgrading unknown actions to REPLY
|
|
388
|
+
for step in plan.steps:
|
|
389
|
+
if not self._find_action(step.action_name, action_lookup):
|
|
390
|
+
missing = step.action_name
|
|
391
|
+
step.action_name = "REPLY"
|
|
392
|
+
step.parameters = {"text": f"Unable to find action: {missing}"}
|
|
393
|
+
return plan
|
|
394
|
+
except Exception as e:
|
|
395
|
+
logger: Logger = self.runtime.logger
|
|
396
|
+
logger.error(f"Failed to create comprehensive plan: {e}", src="service:planning")
|
|
397
|
+
return self._parse_plan("", goal=goal)
|
|
398
|
+
|
|
399
|
+
async def validate_plan(self, plan: ActionPlan) -> tuple[bool, list[str], list[str]]:
|
|
400
|
+
errors: list[str] = []
|
|
401
|
+
warnings: list[str] = []
|
|
402
|
+
|
|
403
|
+
if not plan.goal or not plan.steps:
|
|
404
|
+
errors.append("Plan missing required fields (goal or steps)")
|
|
405
|
+
if len(plan.steps) == 0:
|
|
406
|
+
errors.append("Plan has no steps")
|
|
407
|
+
|
|
408
|
+
step_ids = {s.id for s in plan.steps}
|
|
409
|
+
action_lookup = self._build_action_lookup()
|
|
410
|
+
for step in plan.steps:
|
|
411
|
+
if not step.action_name:
|
|
412
|
+
errors.append(f"Step {step.id} missing action_name")
|
|
413
|
+
continue
|
|
414
|
+
if self._find_action(step.action_name, action_lookup) is None:
|
|
415
|
+
errors.append(f"Action '{step.action_name}' not found in runtime")
|
|
416
|
+
for dep in step.dependencies:
|
|
417
|
+
if dep not in step_ids:
|
|
418
|
+
errors.append(f"Step '{step.id}' has invalid dependency '{dep}'")
|
|
419
|
+
|
|
420
|
+
if plan.execution_model == "dag":
|
|
421
|
+
if self._detect_cycles(plan.steps):
|
|
422
|
+
errors.append("Plan has circular dependencies")
|
|
423
|
+
|
|
424
|
+
return (len(errors) == 0, errors, warnings)
|
|
425
|
+
|
|
426
|
+
def _detect_cycles(self, steps: list[ActionStep]) -> bool:
|
|
427
|
+
visited: set[UUID] = set()
|
|
428
|
+
stack: set[UUID] = set()
|
|
429
|
+
|
|
430
|
+
by_id = {s.id: s for s in steps}
|
|
431
|
+
|
|
432
|
+
def dfs(step_id: UUID) -> bool:
|
|
433
|
+
if step_id in stack:
|
|
434
|
+
return True
|
|
435
|
+
if step_id in visited:
|
|
436
|
+
return False
|
|
437
|
+
visited.add(step_id)
|
|
438
|
+
stack.add(step_id)
|
|
439
|
+
step = by_id.get(step_id)
|
|
440
|
+
if step:
|
|
441
|
+
for dep in step.dependencies:
|
|
442
|
+
if dfs(dep):
|
|
443
|
+
return True
|
|
444
|
+
stack.discard(step_id)
|
|
445
|
+
return False
|
|
446
|
+
|
|
447
|
+
return any(dfs(s.id) for s in steps)
|
|
448
|
+
|
|
449
|
+
async def execute_plan(
|
|
450
|
+
self,
|
|
451
|
+
plan: ActionPlan,
|
|
452
|
+
message: Memory,
|
|
453
|
+
state: State | None = None,
|
|
454
|
+
callback: HandlerCallback | None = None,
|
|
455
|
+
) -> PlanExecutionResult:
|
|
456
|
+
start = time.time()
|
|
457
|
+
working_memory: dict[str, object] = {}
|
|
458
|
+
results: list[ActionResult] = []
|
|
459
|
+
errors: list[str] = []
|
|
460
|
+
|
|
461
|
+
execution_state = PlanState(status="running", start_time=start, current_step_index=0)
|
|
462
|
+
execution = PlanExecution(
|
|
463
|
+
state=execution_state, working_memory=working_memory, results=results
|
|
464
|
+
)
|
|
465
|
+
action_lookup = self._build_action_lookup()
|
|
466
|
+
self._executions[plan.id] = execution
|
|
467
|
+
|
|
468
|
+
try:
|
|
469
|
+
if plan.execution_model == "parallel":
|
|
470
|
+
await self._execute_parallel(
|
|
471
|
+
plan, message, state, callback, execution, action_lookup
|
|
472
|
+
)
|
|
473
|
+
elif plan.execution_model == "dag":
|
|
474
|
+
await self._execute_dag(plan, message, state, callback, execution, action_lookup)
|
|
475
|
+
else:
|
|
476
|
+
await self._execute_sequential(
|
|
477
|
+
plan, message, state, callback, execution, action_lookup
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
execution_state.status = "failed" if errors else "completed"
|
|
481
|
+
execution_state.end_time = time.time()
|
|
482
|
+
return PlanExecutionResult(
|
|
483
|
+
plan_id=plan.id,
|
|
484
|
+
success=len(errors) == 0,
|
|
485
|
+
completed_steps=len(results),
|
|
486
|
+
total_steps=len(plan.steps),
|
|
487
|
+
results=results,
|
|
488
|
+
errors=errors if errors else None,
|
|
489
|
+
duration_ms=(time.time() - start) * 1000,
|
|
490
|
+
)
|
|
491
|
+
except Exception as e:
|
|
492
|
+
execution_state.status = "failed"
|
|
493
|
+
execution_state.end_time = time.time()
|
|
494
|
+
execution_state.error = str(e)
|
|
495
|
+
return PlanExecutionResult(
|
|
496
|
+
plan_id=plan.id,
|
|
497
|
+
success=False,
|
|
498
|
+
completed_steps=len(results),
|
|
499
|
+
total_steps=len(plan.steps),
|
|
500
|
+
results=results,
|
|
501
|
+
errors=[str(e), *errors],
|
|
502
|
+
duration_ms=(time.time() - start) * 1000,
|
|
503
|
+
)
|
|
504
|
+
finally:
|
|
505
|
+
self._executions.pop(plan.id, None)
|
|
506
|
+
|
|
507
|
+
async def _execute_sequential(
|
|
508
|
+
self,
|
|
509
|
+
plan: ActionPlan,
|
|
510
|
+
message: Memory,
|
|
511
|
+
state: State | None,
|
|
512
|
+
callback: HandlerCallback | None,
|
|
513
|
+
execution: PlanExecution,
|
|
514
|
+
action_lookup: dict[str, object],
|
|
515
|
+
) -> None:
|
|
516
|
+
for i, step in enumerate(plan.steps):
|
|
517
|
+
if execution.abort_event.is_set():
|
|
518
|
+
raise RuntimeError("Plan execution aborted")
|
|
519
|
+
result = await self._execute_step(
|
|
520
|
+
step, message, state, callback, execution, action_lookup
|
|
521
|
+
)
|
|
522
|
+
if result is not None:
|
|
523
|
+
execution.results.append(result)
|
|
524
|
+
execution.state.current_step_index = i + 1
|
|
525
|
+
|
|
526
|
+
async def _execute_parallel(
|
|
527
|
+
self,
|
|
528
|
+
plan: ActionPlan,
|
|
529
|
+
message: Memory,
|
|
530
|
+
state: State | None,
|
|
531
|
+
callback: HandlerCallback | None,
|
|
532
|
+
execution: PlanExecution,
|
|
533
|
+
action_lookup: dict[str, object],
|
|
534
|
+
) -> None:
|
|
535
|
+
tasks = [
|
|
536
|
+
self._execute_step(step, message, state, callback, execution, action_lookup)
|
|
537
|
+
for step in plan.steps
|
|
538
|
+
]
|
|
539
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
540
|
+
for r in results:
|
|
541
|
+
if isinstance(r, ActionResult):
|
|
542
|
+
execution.results.append(r)
|
|
543
|
+
|
|
544
|
+
async def _execute_dag(
|
|
545
|
+
self,
|
|
546
|
+
plan: ActionPlan,
|
|
547
|
+
message: Memory,
|
|
548
|
+
state: State | None,
|
|
549
|
+
callback: HandlerCallback | None,
|
|
550
|
+
execution: PlanExecution,
|
|
551
|
+
action_lookup: dict[str, object],
|
|
552
|
+
) -> None:
|
|
553
|
+
import heapq
|
|
554
|
+
|
|
555
|
+
by_id: dict[UUID, ActionStep] = {s.id: s for s in plan.steps}
|
|
556
|
+
in_degree: dict[UUID, int] = {s.id: len(s.dependencies) for s in plan.steps}
|
|
557
|
+
dependents: dict[UUID, list[UUID]] = {}
|
|
558
|
+
index_by_id: dict[UUID, int] = {}
|
|
559
|
+
for idx, step in enumerate(plan.steps):
|
|
560
|
+
index_by_id[step.id] = idx
|
|
561
|
+
for dep in step.dependencies:
|
|
562
|
+
dependents.setdefault(dep, []).append(step.id)
|
|
563
|
+
|
|
564
|
+
ready_heap: list[tuple[int, UUID]] = [
|
|
565
|
+
(index_by_id[sid], sid) for sid, count in in_degree.items() if count == 0
|
|
566
|
+
]
|
|
567
|
+
heapq.heapify(ready_heap)
|
|
568
|
+
|
|
569
|
+
completed_count = 0
|
|
570
|
+
while ready_heap and not execution.abort_event.is_set():
|
|
571
|
+
ready_batch: list[ActionStep] = []
|
|
572
|
+
while ready_heap:
|
|
573
|
+
_, sid = heapq.heappop(ready_heap)
|
|
574
|
+
ready_batch.append(by_id[sid])
|
|
575
|
+
|
|
576
|
+
if not ready_batch:
|
|
577
|
+
break
|
|
578
|
+
|
|
579
|
+
results = await asyncio.gather(
|
|
580
|
+
*[
|
|
581
|
+
self._execute_step(step, message, state, callback, execution, action_lookup)
|
|
582
|
+
for step in ready_batch
|
|
583
|
+
],
|
|
584
|
+
return_exceptions=True,
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
for step, r in zip(ready_batch, results, strict=False):
|
|
588
|
+
completed_count += 1
|
|
589
|
+
if isinstance(r, ActionResult):
|
|
590
|
+
execution.results.append(r)
|
|
591
|
+
|
|
592
|
+
for nxt in dependents.get(step.id, []):
|
|
593
|
+
remaining = in_degree.get(nxt, 0)
|
|
594
|
+
if remaining > 0:
|
|
595
|
+
remaining -= 1
|
|
596
|
+
in_degree[nxt] = remaining
|
|
597
|
+
if remaining == 0:
|
|
598
|
+
heapq.heappush(ready_heap, (index_by_id[nxt], nxt))
|
|
599
|
+
|
|
600
|
+
if completed_count != len(plan.steps):
|
|
601
|
+
raise RuntimeError("No steps ready to execute - possible circular dependency")
|
|
602
|
+
|
|
603
|
+
async def _execute_step(
|
|
604
|
+
self,
|
|
605
|
+
step: ActionStep,
|
|
606
|
+
message: Memory,
|
|
607
|
+
state: State | None,
|
|
608
|
+
callback: HandlerCallback | None,
|
|
609
|
+
execution: PlanExecution,
|
|
610
|
+
action_lookup: dict[str, object],
|
|
611
|
+
) -> ActionResult | None:
|
|
612
|
+
action = self._find_action(step.action_name, action_lookup)
|
|
613
|
+
if action is None:
|
|
614
|
+
raise RuntimeError(f"Action '{step.action_name}' not found")
|
|
615
|
+
|
|
616
|
+
previous_results = execution.results
|
|
617
|
+
action_context = ActionContext(previous_results=previous_results)
|
|
618
|
+
|
|
619
|
+
retries = 0
|
|
620
|
+
max_retries = step.retry_policy.max_retries if step.retry_policy else 0
|
|
621
|
+
while retries <= max_retries:
|
|
622
|
+
if execution.abort_event.is_set():
|
|
623
|
+
raise RuntimeError("Plan execution aborted")
|
|
624
|
+
try:
|
|
625
|
+
options = HandlerOptions(
|
|
626
|
+
action_context=action_context,
|
|
627
|
+
parameters=step.parameters,
|
|
628
|
+
)
|
|
629
|
+
# Attach extra execution context (allowed by extra="allow")
|
|
630
|
+
options.previous_results = previous_results # type: ignore[attr-defined]
|
|
631
|
+
options.context = {"workingMemory": execution.working_memory} # type: ignore[attr-defined]
|
|
632
|
+
|
|
633
|
+
validate_fn = getattr(action, "validate", None) or getattr(
|
|
634
|
+
action, "validate_fn", None
|
|
635
|
+
)
|
|
636
|
+
ok = await validate_fn(self.runtime, message, state) if validate_fn else True
|
|
637
|
+
if not ok:
|
|
638
|
+
return None
|
|
639
|
+
|
|
640
|
+
result = await action.handler(self.runtime, message, state, options, callback, None)
|
|
641
|
+
if result is None:
|
|
642
|
+
return None
|
|
643
|
+
|
|
644
|
+
if result.data is None:
|
|
645
|
+
result.data = {}
|
|
646
|
+
if isinstance(result.data, dict):
|
|
647
|
+
result.data["stepId"] = str(step.id)
|
|
648
|
+
result.data["actionName"] = step.action_name
|
|
649
|
+
result.data["executedAt"] = int(time.time() * 1000)
|
|
650
|
+
return result
|
|
651
|
+
except Exception as e:
|
|
652
|
+
retries += 1
|
|
653
|
+
if retries > max_retries:
|
|
654
|
+
raise e
|
|
655
|
+
backoff = step.retry_policy.backoff_ms * (
|
|
656
|
+
step.retry_policy.backoff_multiplier ** (retries - 1)
|
|
657
|
+
)
|
|
658
|
+
await asyncio.sleep(backoff / 1000.0)
|
|
659
|
+
|
|
660
|
+
return None
|
|
661
|
+
|
|
662
|
+
async def get_plan_status(self, plan_id: UUID) -> PlanState | None:
|
|
663
|
+
execution = self._executions.get(plan_id)
|
|
664
|
+
return execution.state if execution else None
|
|
665
|
+
|
|
666
|
+
async def cancel_plan(self, plan_id: UUID) -> bool:
|
|
667
|
+
execution = self._executions.get(plan_id)
|
|
668
|
+
if not execution:
|
|
669
|
+
return False
|
|
670
|
+
execution.abort_event.set()
|
|
671
|
+
execution.state.status = "cancelled"
|
|
672
|
+
execution.state.end_time = time.time()
|
|
673
|
+
return True
|
|
674
|
+
|
|
675
|
+
async def adapt_plan(
|
|
676
|
+
self,
|
|
677
|
+
plan: ActionPlan,
|
|
678
|
+
current_step_index: int,
|
|
679
|
+
results: list[ActionResult],
|
|
680
|
+
error: Exception | None = None,
|
|
681
|
+
) -> ActionPlan:
|
|
682
|
+
# For now, keep parity with TS structure by asking the model to return new steps.
|
|
683
|
+
prompt = f"""You are an expert AI adaptation system. A plan execution has encountered an issue and needs adaptation.
|
|
684
|
+
|
|
685
|
+
ORIGINAL PLAN: {json.dumps({"id": str(plan.id), "goal": plan.goal, "steps": [{"id": str(s.id), "action": s.action_name} for s in plan.steps]}, indent=2)}
|
|
686
|
+
CURRENT STEP INDEX: {current_step_index}
|
|
687
|
+
COMPLETED RESULTS: {json.dumps([MessageToDict(r, preserving_proto_field_name=False) for r in results], indent=2)}
|
|
688
|
+
{f"ERROR: {str(error)}" if error else ""}
|
|
689
|
+
|
|
690
|
+
Return the adapted plan in the same XML format as the original planning response."""
|
|
691
|
+
|
|
692
|
+
try:
|
|
693
|
+
response = await self.runtime.use_model(
|
|
694
|
+
"TEXT_LARGE",
|
|
695
|
+
{"prompt": prompt, "temperature": 0.4, "maxTokens": 1500},
|
|
696
|
+
)
|
|
697
|
+
adapted = self._parse_plan(str(response), goal=plan.goal)
|
|
698
|
+
new_steps = plan.steps[:current_step_index] + adapted.steps
|
|
699
|
+
plan.steps = new_steps
|
|
700
|
+
plan.total_steps = len(new_steps)
|
|
701
|
+
return plan
|
|
702
|
+
except Exception:
|
|
703
|
+
# Fallback: append a REPLY step
|
|
704
|
+
fallback = ActionStep(
|
|
705
|
+
id=uuid4(),
|
|
706
|
+
action_name="REPLY",
|
|
707
|
+
parameters={"text": "Plan adaptation completed successfully"},
|
|
708
|
+
dependencies=[],
|
|
709
|
+
)
|
|
710
|
+
plan.steps = plan.steps[:current_step_index] + [fallback]
|
|
711
|
+
plan.total_steps = len(plan.steps)
|
|
712
|
+
return plan
|