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.
@@ -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