soorma-core 0.3.0__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.
- soorma/__init__.py +138 -0
- soorma/agents/__init__.py +17 -0
- soorma/agents/base.py +523 -0
- soorma/agents/planner.py +391 -0
- soorma/agents/tool.py +373 -0
- soorma/agents/worker.py +385 -0
- soorma/ai/event_toolkit.py +281 -0
- soorma/ai/tools.py +280 -0
- soorma/cli/__init__.py +7 -0
- soorma/cli/commands/__init__.py +3 -0
- soorma/cli/commands/dev.py +780 -0
- soorma/cli/commands/init.py +717 -0
- soorma/cli/main.py +52 -0
- soorma/context.py +832 -0
- soorma/events.py +496 -0
- soorma/models.py +24 -0
- soorma/registry/client.py +186 -0
- soorma/utils/schema_utils.py +209 -0
- soorma_core-0.3.0.dist-info/METADATA +454 -0
- soorma_core-0.3.0.dist-info/RECORD +23 -0
- soorma_core-0.3.0.dist-info/WHEEL +4 -0
- soorma_core-0.3.0.dist-info/entry_points.txt +3 -0
- soorma_core-0.3.0.dist-info/licenses/LICENSE.txt +21 -0
soorma/agents/planner.py
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Planner Agent - Strategic reasoning engine.
|
|
3
|
+
|
|
4
|
+
The Planner is the "brain" of the DisCo architecture. It:
|
|
5
|
+
- Receives high-level goals from clients
|
|
6
|
+
- Decomposes goals into actionable tasks
|
|
7
|
+
- Assigns tasks to Worker agents
|
|
8
|
+
- Monitors plan execution progress
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
from soorma.agents import Planner
|
|
12
|
+
|
|
13
|
+
planner = Planner(
|
|
14
|
+
name="research-planner",
|
|
15
|
+
description="Plans research tasks",
|
|
16
|
+
capabilities=["research_planning", "task_decomposition"],
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
@planner.on_goal("research.goal")
|
|
20
|
+
async def plan_research(goal, context):
|
|
21
|
+
# Decompose goal into tasks
|
|
22
|
+
tasks = [
|
|
23
|
+
Task(
|
|
24
|
+
name="search_papers",
|
|
25
|
+
assigned_to="research-worker",
|
|
26
|
+
data={"query": goal.data["topic"]},
|
|
27
|
+
),
|
|
28
|
+
Task(
|
|
29
|
+
name="summarize_findings",
|
|
30
|
+
assigned_to="summarizer-worker",
|
|
31
|
+
depends_on=["search_papers"],
|
|
32
|
+
),
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
# Create and track the plan
|
|
36
|
+
return Plan(
|
|
37
|
+
goal=goal,
|
|
38
|
+
tasks=tasks,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
planner.run()
|
|
42
|
+
"""
|
|
43
|
+
import logging
|
|
44
|
+
from dataclasses import dataclass, field
|
|
45
|
+
from typing import Any, Awaitable, Callable, Dict, List, Optional
|
|
46
|
+
from uuid import uuid4
|
|
47
|
+
|
|
48
|
+
from .base import Agent
|
|
49
|
+
from ..context import PlatformContext
|
|
50
|
+
|
|
51
|
+
logger = logging.getLogger(__name__)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class Task:
|
|
56
|
+
"""
|
|
57
|
+
A single task in a plan.
|
|
58
|
+
|
|
59
|
+
Tasks are atomic units of work assigned to Worker agents.
|
|
60
|
+
|
|
61
|
+
Attributes:
|
|
62
|
+
name: Task identifier (e.g., "search_papers")
|
|
63
|
+
assigned_to: Capability or agent name to execute this task
|
|
64
|
+
data: Task input data
|
|
65
|
+
depends_on: List of task names this depends on
|
|
66
|
+
timeout: Timeout in seconds (None = no timeout)
|
|
67
|
+
priority: Task priority (higher = more urgent)
|
|
68
|
+
"""
|
|
69
|
+
name: str
|
|
70
|
+
assigned_to: str
|
|
71
|
+
data: Dict[str, Any] = field(default_factory=dict)
|
|
72
|
+
depends_on: List[str] = field(default_factory=list)
|
|
73
|
+
timeout: Optional[float] = None
|
|
74
|
+
priority: int = 0
|
|
75
|
+
|
|
76
|
+
# Runtime fields (set by Planner)
|
|
77
|
+
task_id: str = field(default_factory=lambda: str(uuid4()))
|
|
78
|
+
status: str = "pending" # pending, running, completed, failed
|
|
79
|
+
result: Optional[Dict[str, Any]] = None
|
|
80
|
+
error: Optional[str] = None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class Goal:
|
|
85
|
+
"""
|
|
86
|
+
A goal submitted by a client.
|
|
87
|
+
|
|
88
|
+
Goals are high-level objectives that the Planner decomposes into tasks.
|
|
89
|
+
|
|
90
|
+
Attributes:
|
|
91
|
+
goal_type: Type of goal (e.g., "research.goal")
|
|
92
|
+
data: Goal parameters
|
|
93
|
+
correlation_id: Tracking ID
|
|
94
|
+
session_id: Client session
|
|
95
|
+
tenant_id: Multi-tenant isolation
|
|
96
|
+
"""
|
|
97
|
+
goal_type: str
|
|
98
|
+
data: Dict[str, Any]
|
|
99
|
+
goal_id: str = field(default_factory=lambda: str(uuid4()))
|
|
100
|
+
correlation_id: Optional[str] = None
|
|
101
|
+
session_id: Optional[str] = None
|
|
102
|
+
tenant_id: Optional[str] = None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class Plan:
|
|
107
|
+
"""
|
|
108
|
+
A plan to achieve a goal.
|
|
109
|
+
|
|
110
|
+
Plans contain ordered lists of tasks with dependencies.
|
|
111
|
+
The Planner creates plans, and the platform tracks their execution.
|
|
112
|
+
|
|
113
|
+
Attributes:
|
|
114
|
+
goal: The goal this plan achieves
|
|
115
|
+
tasks: Ordered list of tasks
|
|
116
|
+
plan_id: Unique plan identifier
|
|
117
|
+
metadata: Additional plan metadata
|
|
118
|
+
"""
|
|
119
|
+
goal: Goal
|
|
120
|
+
tasks: List[Task]
|
|
121
|
+
plan_id: str = field(default_factory=lambda: str(uuid4()))
|
|
122
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
123
|
+
|
|
124
|
+
# Runtime state
|
|
125
|
+
status: str = "pending" # pending, running, completed, failed, paused
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# Type alias for goal handlers
|
|
129
|
+
GoalHandler = Callable[[Goal, PlatformContext], Awaitable[Plan]]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class Planner(Agent):
|
|
133
|
+
"""
|
|
134
|
+
Strategic reasoning engine that breaks goals into tasks.
|
|
135
|
+
|
|
136
|
+
The Planner is responsible for:
|
|
137
|
+
1. Receiving high-level goals from clients
|
|
138
|
+
2. Understanding available Worker capabilities (via Registry)
|
|
139
|
+
3. Decomposing goals into executable tasks
|
|
140
|
+
4. Creating execution plans with dependencies
|
|
141
|
+
5. Publishing tasks as action-requests
|
|
142
|
+
6. Monitoring plan progress (via Tracker)
|
|
143
|
+
|
|
144
|
+
Planners typically use LLMs for reasoning about goal decomposition.
|
|
145
|
+
|
|
146
|
+
Attributes:
|
|
147
|
+
All Agent attributes, plus:
|
|
148
|
+
on_goal: Decorator for registering goal handlers
|
|
149
|
+
|
|
150
|
+
Usage:
|
|
151
|
+
planner = Planner(
|
|
152
|
+
name="fleet-planner",
|
|
153
|
+
description="Plans fleet maintenance workflows",
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
@planner.on_goal("maintenance.goal")
|
|
157
|
+
async def plan_maintenance(goal: Goal, context: PlatformContext) -> Plan:
|
|
158
|
+
# Query available workers
|
|
159
|
+
mechanics = await context.registry.find_all("vehicle_maintenance")
|
|
160
|
+
|
|
161
|
+
# Create plan
|
|
162
|
+
return Plan(
|
|
163
|
+
goal=goal,
|
|
164
|
+
tasks=[
|
|
165
|
+
Task(name="diagnose", assigned_to="diagnostic-worker", data=goal.data),
|
|
166
|
+
Task(name="repair", assigned_to="repair-worker", depends_on=["diagnose"]),
|
|
167
|
+
Task(name="verify", assigned_to="qa-worker", depends_on=["repair"]),
|
|
168
|
+
],
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
planner.run()
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
def __init__(
|
|
175
|
+
self,
|
|
176
|
+
name: str,
|
|
177
|
+
description: str = "",
|
|
178
|
+
version: str = "0.1.0",
|
|
179
|
+
capabilities: Optional[List[str]] = None,
|
|
180
|
+
**kwargs,
|
|
181
|
+
):
|
|
182
|
+
"""
|
|
183
|
+
Initialize the Planner.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
name: Planner name
|
|
187
|
+
description: What this planner does
|
|
188
|
+
version: Version string
|
|
189
|
+
capabilities: Planning capabilities offered
|
|
190
|
+
**kwargs: Additional Agent arguments
|
|
191
|
+
"""
|
|
192
|
+
# Planners always produce action-requests
|
|
193
|
+
events_produced = kwargs.pop("events_produced", [])
|
|
194
|
+
if "action.request" not in events_produced:
|
|
195
|
+
events_produced.append("action.request")
|
|
196
|
+
|
|
197
|
+
super().__init__(
|
|
198
|
+
name=name,
|
|
199
|
+
description=description,
|
|
200
|
+
version=version,
|
|
201
|
+
agent_type="planner",
|
|
202
|
+
capabilities=capabilities or ["planning"],
|
|
203
|
+
events_produced=events_produced,
|
|
204
|
+
**kwargs,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Goal handlers: goal_type -> handler
|
|
208
|
+
self._goal_handlers: Dict[str, GoalHandler] = {}
|
|
209
|
+
|
|
210
|
+
def on_goal(self, goal_type: str) -> Callable[[GoalHandler], GoalHandler]:
|
|
211
|
+
"""
|
|
212
|
+
Decorator to register a goal handler.
|
|
213
|
+
|
|
214
|
+
Goal handlers receive a Goal and PlatformContext, and return a Plan.
|
|
215
|
+
The Planner automatically publishes tasks as action-requests.
|
|
216
|
+
|
|
217
|
+
Usage:
|
|
218
|
+
@planner.on_goal("research.goal")
|
|
219
|
+
async def plan_research(goal: Goal, context: PlatformContext) -> Plan:
|
|
220
|
+
return Plan(
|
|
221
|
+
goal=goal,
|
|
222
|
+
tasks=[...],
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
goal_type: The goal type to handle (e.g., "research.goal")
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Decorator function
|
|
230
|
+
"""
|
|
231
|
+
def decorator(func: GoalHandler) -> GoalHandler:
|
|
232
|
+
self._goal_handlers[goal_type] = func
|
|
233
|
+
|
|
234
|
+
# Register as event handler for this goal type
|
|
235
|
+
@self.on_event(goal_type)
|
|
236
|
+
async def goal_event_handler(event: Dict[str, Any], context: PlatformContext) -> None:
|
|
237
|
+
await self._handle_goal_event(goal_type, event, context)
|
|
238
|
+
|
|
239
|
+
logger.debug(f"Registered goal handler: {goal_type}")
|
|
240
|
+
return func
|
|
241
|
+
return decorator
|
|
242
|
+
|
|
243
|
+
async def _handle_goal_event(
|
|
244
|
+
self,
|
|
245
|
+
goal_type: str,
|
|
246
|
+
event: Dict[str, Any],
|
|
247
|
+
context: PlatformContext,
|
|
248
|
+
) -> None:
|
|
249
|
+
"""Handle an incoming goal event."""
|
|
250
|
+
handler = self._goal_handlers.get(goal_type)
|
|
251
|
+
if not handler:
|
|
252
|
+
logger.warning(f"No handler for goal type: {goal_type}")
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
# Create Goal from event
|
|
256
|
+
goal = Goal(
|
|
257
|
+
goal_type=goal_type,
|
|
258
|
+
data=event.get("data", {}),
|
|
259
|
+
goal_id=event.get("id", str(uuid4())),
|
|
260
|
+
correlation_id=event.get("correlation_id"),
|
|
261
|
+
session_id=event.get("session_id"),
|
|
262
|
+
tenant_id=event.get("tenant_id"),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
# Generate plan
|
|
267
|
+
plan = await handler(goal, context)
|
|
268
|
+
|
|
269
|
+
# Track plan in Tracker service
|
|
270
|
+
await context.tracker.start_plan(
|
|
271
|
+
plan_id=plan.plan_id,
|
|
272
|
+
agent_id=self.agent_id,
|
|
273
|
+
goal=goal_type,
|
|
274
|
+
tasks=[
|
|
275
|
+
{
|
|
276
|
+
"task_id": t.task_id,
|
|
277
|
+
"name": t.name,
|
|
278
|
+
"assigned_to": t.assigned_to,
|
|
279
|
+
"depends_on": t.depends_on,
|
|
280
|
+
}
|
|
281
|
+
for t in plan.tasks
|
|
282
|
+
],
|
|
283
|
+
metadata={
|
|
284
|
+
"goal_id": goal.goal_id,
|
|
285
|
+
"correlation_id": goal.correlation_id,
|
|
286
|
+
**plan.metadata,
|
|
287
|
+
},
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Publish tasks as action-requests
|
|
291
|
+
await self._publish_tasks(plan, context)
|
|
292
|
+
|
|
293
|
+
logger.info(f"Created plan {plan.plan_id} with {len(plan.tasks)} tasks")
|
|
294
|
+
|
|
295
|
+
except Exception as e:
|
|
296
|
+
logger.error(f"Goal handling failed: {e}")
|
|
297
|
+
# Emit failure event
|
|
298
|
+
await context.bus.publish(
|
|
299
|
+
event_type=f"{goal_type}.failed",
|
|
300
|
+
data={
|
|
301
|
+
"goal_id": goal.goal_id,
|
|
302
|
+
"error": str(e),
|
|
303
|
+
},
|
|
304
|
+
correlation_id=goal.correlation_id,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
async def _publish_tasks(self, plan: Plan, context: PlatformContext) -> None:
|
|
308
|
+
"""Publish tasks as action-request events."""
|
|
309
|
+
for task in plan.tasks:
|
|
310
|
+
# Only publish tasks with no unmet dependencies
|
|
311
|
+
if not task.depends_on:
|
|
312
|
+
await self._publish_task(plan, task, context)
|
|
313
|
+
|
|
314
|
+
async def _publish_task(
|
|
315
|
+
self,
|
|
316
|
+
plan: Plan,
|
|
317
|
+
task: Task,
|
|
318
|
+
context: PlatformContext,
|
|
319
|
+
) -> None:
|
|
320
|
+
"""Publish a single task as an action-request."""
|
|
321
|
+
await context.bus.publish(
|
|
322
|
+
event_type="action.request",
|
|
323
|
+
data={
|
|
324
|
+
"task_id": task.task_id,
|
|
325
|
+
"task_name": task.name,
|
|
326
|
+
"assigned_to": task.assigned_to,
|
|
327
|
+
"plan_id": plan.plan_id,
|
|
328
|
+
"goal_id": plan.goal.goal_id,
|
|
329
|
+
"data": task.data,
|
|
330
|
+
"timeout": task.timeout,
|
|
331
|
+
"priority": task.priority,
|
|
332
|
+
},
|
|
333
|
+
topic="action-requests",
|
|
334
|
+
correlation_id=plan.goal.correlation_id,
|
|
335
|
+
)
|
|
336
|
+
logger.debug(f"Published task: {task.name} ({task.task_id})")
|
|
337
|
+
|
|
338
|
+
async def create_plan(
|
|
339
|
+
self,
|
|
340
|
+
goal_type: str,
|
|
341
|
+
goal_data: Dict[str, Any],
|
|
342
|
+
correlation_id: Optional[str] = None,
|
|
343
|
+
) -> Plan:
|
|
344
|
+
"""
|
|
345
|
+
Programmatically create and execute a plan.
|
|
346
|
+
|
|
347
|
+
This method allows creating plans without going through the event bus,
|
|
348
|
+
useful for integrating with existing systems.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
goal_type: Type of goal
|
|
352
|
+
goal_data: Goal parameters
|
|
353
|
+
correlation_id: Optional correlation ID
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
The created Plan
|
|
357
|
+
|
|
358
|
+
Raises:
|
|
359
|
+
ValueError: If no handler for goal_type
|
|
360
|
+
"""
|
|
361
|
+
handler = self._goal_handlers.get(goal_type)
|
|
362
|
+
if not handler:
|
|
363
|
+
raise ValueError(f"No handler for goal type: {goal_type}")
|
|
364
|
+
|
|
365
|
+
goal = Goal(
|
|
366
|
+
goal_type=goal_type,
|
|
367
|
+
data=goal_data,
|
|
368
|
+
correlation_id=correlation_id or str(uuid4()),
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
plan = await handler(goal, self.context)
|
|
372
|
+
|
|
373
|
+
# Track and publish
|
|
374
|
+
await self.context.tracker.start_plan(
|
|
375
|
+
plan_id=plan.plan_id,
|
|
376
|
+
agent_id=self.agent_id,
|
|
377
|
+
goal=goal_type,
|
|
378
|
+
tasks=[
|
|
379
|
+
{
|
|
380
|
+
"task_id": t.task_id,
|
|
381
|
+
"name": t.name,
|
|
382
|
+
"assigned_to": t.assigned_to,
|
|
383
|
+
"depends_on": t.depends_on,
|
|
384
|
+
}
|
|
385
|
+
for t in plan.tasks
|
|
386
|
+
],
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
await self._publish_tasks(plan, self.context)
|
|
390
|
+
|
|
391
|
+
return plan
|