strands-swarms 0.1.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.
- strands_swarms/__init__.py +88 -0
- strands_swarms/events.py +564 -0
- strands_swarms/orchestrator.py +265 -0
- strands_swarms/py.typed +0 -0
- strands_swarms/swarm.py +738 -0
- strands_swarms-0.1.0.dist-info/METADATA +386 -0
- strands_swarms-0.1.0.dist-info/RECORD +9 -0
- strands_swarms-0.1.0.dist-info/WHEEL +4 -0
- strands_swarms-0.1.0.dist-info/licenses/LICENSE +176 -0
strands_swarms/swarm.py
ADDED
|
@@ -0,0 +1,738 @@
|
|
|
1
|
+
"""DynamicSwarm - automatically construct and execute multi-agent workflows.
|
|
2
|
+
|
|
3
|
+
This module provides the core swarm functionality:
|
|
4
|
+
- Agent and task definitions
|
|
5
|
+
- SwarmConfig for collecting definitions and building the swarm graph
|
|
6
|
+
- DynamicSwarm class that uses an orchestrator for:
|
|
7
|
+
1. Planning and creating subagents
|
|
8
|
+
2. Assigning tasks
|
|
9
|
+
3. Generating final response
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from textwrap import dedent
|
|
17
|
+
from typing import Any, Callable, TYPE_CHECKING
|
|
18
|
+
|
|
19
|
+
from strands import Agent
|
|
20
|
+
from strands.multiagent.base import Status, MultiAgentResult
|
|
21
|
+
from strands.multiagent.graph import Graph, GraphBuilder, GraphResult
|
|
22
|
+
from strands.hooks import HookProvider, HookRegistry
|
|
23
|
+
|
|
24
|
+
from .events import (
|
|
25
|
+
AGENT_COLORS,
|
|
26
|
+
SwarmStartedEvent,
|
|
27
|
+
PlanningStartedEvent,
|
|
28
|
+
ExecutionStartedEvent,
|
|
29
|
+
TaskStartedEvent,
|
|
30
|
+
TaskCompletedEvent,
|
|
31
|
+
ExecutionCompletedEvent,
|
|
32
|
+
SwarmCompletedEvent,
|
|
33
|
+
SwarmFailedEvent,
|
|
34
|
+
PrintingHookProvider,
|
|
35
|
+
create_colored_callback_handler,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if TYPE_CHECKING:
|
|
39
|
+
from strands.models import Model
|
|
40
|
+
from strands.session import SessionManager
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# =============================================================================
|
|
44
|
+
# Agent and Task Definitions
|
|
45
|
+
# =============================================================================
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class AgentDefinition:
|
|
50
|
+
"""Definition for a dynamically spawned agent.
|
|
51
|
+
|
|
52
|
+
Attributes:
|
|
53
|
+
name: Unique identifier for this agent.
|
|
54
|
+
role: What this agent does (used in system prompt).
|
|
55
|
+
instructions: Additional instructions for the agent.
|
|
56
|
+
tools: List of tool names from the available pool.
|
|
57
|
+
model: Model name from the available pool.
|
|
58
|
+
color: Display color for this agent (auto-assigned on registration).
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
name: str
|
|
62
|
+
role: str
|
|
63
|
+
instructions: str | None = None
|
|
64
|
+
tools: list[str] = field(default_factory=list)
|
|
65
|
+
model: str | None = None
|
|
66
|
+
color: str | None = None
|
|
67
|
+
|
|
68
|
+
def build_system_prompt(self) -> str:
|
|
69
|
+
"""Build the system prompt for this agent."""
|
|
70
|
+
parts = [f"You are a {self.role}."]
|
|
71
|
+
if self.instructions:
|
|
72
|
+
parts.append(f"\n\nInstructions:\n{self.instructions}")
|
|
73
|
+
return "\n".join(parts)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class TaskDefinition:
|
|
78
|
+
"""Definition for a task in the dynamic workflow.
|
|
79
|
+
|
|
80
|
+
Attributes:
|
|
81
|
+
name: Unique identifier for this task.
|
|
82
|
+
agent: Name of the agent assigned to this task.
|
|
83
|
+
description: What this task should accomplish.
|
|
84
|
+
depends_on: List of task names this task depends on.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
name: str
|
|
88
|
+
agent: str
|
|
89
|
+
description: str | None = None
|
|
90
|
+
depends_on: list[str] = field(default_factory=list)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# =============================================================================
|
|
94
|
+
# Swarm Configuration
|
|
95
|
+
# =============================================================================
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class SwarmConfig:
|
|
99
|
+
"""Configuration for a dynamic swarm.
|
|
100
|
+
|
|
101
|
+
Collects agent and task definitions during planning. Call build() to
|
|
102
|
+
create a complete Graph ready for execution.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
def __init__(
|
|
106
|
+
self,
|
|
107
|
+
available_tools: dict[str, Callable[..., Any]],
|
|
108
|
+
available_models: dict[str, Model],
|
|
109
|
+
default_model: str | None = None,
|
|
110
|
+
) -> None:
|
|
111
|
+
"""Initialize the swarm configuration.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
available_tools: Pool of tools that can be assigned to agents.
|
|
115
|
+
available_models: Pool of models (name -> Model instance) that can be used by agents.
|
|
116
|
+
default_model: Default model name to use if none specified.
|
|
117
|
+
"""
|
|
118
|
+
self._available_tools = available_tools
|
|
119
|
+
self._available_models = available_models
|
|
120
|
+
self._default_model = default_model or next(iter(available_models.keys()), None)
|
|
121
|
+
|
|
122
|
+
self._agents: dict[str, AgentDefinition] = {}
|
|
123
|
+
self._tasks: dict[str, TaskDefinition] = {}
|
|
124
|
+
self._entry_task: str | None = None
|
|
125
|
+
self._color_index = 0
|
|
126
|
+
|
|
127
|
+
def register_agent(self, definition: AgentDefinition) -> None:
|
|
128
|
+
"""Register a new agent definition.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
definition: The agent definition to register.
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
ValueError: If agent name already exists or references invalid tools/models.
|
|
135
|
+
"""
|
|
136
|
+
if definition.name in self._agents:
|
|
137
|
+
raise ValueError(f"Agent '{definition.name}' already exists")
|
|
138
|
+
|
|
139
|
+
# Validate tools
|
|
140
|
+
for tool_name in definition.tools:
|
|
141
|
+
if tool_name not in self._available_tools:
|
|
142
|
+
available = list(self._available_tools.keys())
|
|
143
|
+
raise ValueError(
|
|
144
|
+
f"Tool '{tool_name}' not in available tools: {available}"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Validate model
|
|
148
|
+
if definition.model and definition.model not in self._available_models:
|
|
149
|
+
available = list(self._available_models.keys())
|
|
150
|
+
raise ValueError(
|
|
151
|
+
f"Model '{definition.model}' not in available models: {available}"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Assign a color to this agent (used by events for display)
|
|
155
|
+
definition.color = AGENT_COLORS[self._color_index % len(AGENT_COLORS)]
|
|
156
|
+
self._color_index += 1
|
|
157
|
+
|
|
158
|
+
self._agents[definition.name] = definition
|
|
159
|
+
|
|
160
|
+
def register_task(self, definition: TaskDefinition) -> None:
|
|
161
|
+
"""Register a new task definition.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
definition: The task definition to register.
|
|
165
|
+
|
|
166
|
+
Raises:
|
|
167
|
+
ValueError: If task name exists or references unknown agent/dependencies.
|
|
168
|
+
"""
|
|
169
|
+
if definition.name in self._tasks:
|
|
170
|
+
raise ValueError(f"Task '{definition.name}' already exists")
|
|
171
|
+
|
|
172
|
+
if definition.agent not in self._agents:
|
|
173
|
+
available = list(self._agents.keys())
|
|
174
|
+
raise ValueError(
|
|
175
|
+
f"Agent '{definition.agent}' not found. Available: {available}"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
for dep in definition.depends_on:
|
|
179
|
+
if dep not in self._tasks:
|
|
180
|
+
available = list(self._tasks.keys())
|
|
181
|
+
raise ValueError(
|
|
182
|
+
f"Dependency '{dep}' not found. Available: {available}"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
self._tasks[definition.name] = definition
|
|
186
|
+
|
|
187
|
+
def set_entry_task(self, task_name: str) -> None:
|
|
188
|
+
"""Set the entry point task."""
|
|
189
|
+
if task_name not in self._tasks:
|
|
190
|
+
raise ValueError(f"Task '{task_name}' not found")
|
|
191
|
+
self._entry_task = task_name
|
|
192
|
+
|
|
193
|
+
def clear(self) -> None:
|
|
194
|
+
"""Clear all registered agents and tasks."""
|
|
195
|
+
self._agents.clear()
|
|
196
|
+
self._tasks.clear()
|
|
197
|
+
self._color_index = 0
|
|
198
|
+
self._entry_task = None
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def agents(self) -> dict[str, AgentDefinition]:
|
|
202
|
+
"""Get all registered agent definitions."""
|
|
203
|
+
return dict(self._agents)
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def tasks(self) -> dict[str, TaskDefinition]:
|
|
207
|
+
"""Get all registered task definitions."""
|
|
208
|
+
return dict(self._tasks)
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def entry_task(self) -> str | None:
|
|
212
|
+
"""Get the entry task name."""
|
|
213
|
+
return self._entry_task
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def available_tools(self) -> dict[str, Callable[..., Any]]:
|
|
217
|
+
"""Get available tools mapping."""
|
|
218
|
+
return self._available_tools
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def available_models(self) -> dict[str, Model]:
|
|
222
|
+
"""Get available models mapping."""
|
|
223
|
+
return self._available_models
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def default_model(self) -> str | None:
|
|
227
|
+
"""Get the default model name."""
|
|
228
|
+
return self._default_model
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def available_tool_names(self) -> list[str]:
|
|
232
|
+
"""Get list of available tool names."""
|
|
233
|
+
return list(self._available_tools.keys())
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def available_model_names(self) -> list[str]:
|
|
237
|
+
"""Get list of available model names."""
|
|
238
|
+
return list(self._available_models.keys())
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def agent_colors(self) -> dict[str, str]:
|
|
242
|
+
"""Get agent name to color mapping."""
|
|
243
|
+
return {name: d.color for name, d in self._agents.items() if d.color}
|
|
244
|
+
|
|
245
|
+
def get_summary(self) -> str:
|
|
246
|
+
"""Get a summary of the current swarm configuration."""
|
|
247
|
+
lines = [
|
|
248
|
+
f"Agents ({len(self._agents)}):",
|
|
249
|
+
*[f" - {name}: {d.role}" for name, d in self._agents.items()],
|
|
250
|
+
f"\nTasks ({len(self._tasks)}):",
|
|
251
|
+
*[
|
|
252
|
+
f" - {name} -> {d.agent}" + (f" (depends: {d.depends_on})" if d.depends_on else "")
|
|
253
|
+
for name, d in self._tasks.items()
|
|
254
|
+
],
|
|
255
|
+
f"\nEntry: {self._entry_task or 'auto'}",
|
|
256
|
+
]
|
|
257
|
+
return "\n".join(lines)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def build_swarm(
|
|
261
|
+
config: SwarmConfig,
|
|
262
|
+
*,
|
|
263
|
+
use_colored_output: bool = False,
|
|
264
|
+
execution_timeout: float = 900.0,
|
|
265
|
+
task_timeout: float = 300.0,
|
|
266
|
+
session_manager: SessionManager | None = None,
|
|
267
|
+
) -> Graph:
|
|
268
|
+
"""Build a complete Graph from a swarm configuration.
|
|
269
|
+
|
|
270
|
+
Creates all agents from registered definitions and wires up the
|
|
271
|
+
task dependency graph.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
config: The swarm configuration containing agent and task definitions.
|
|
275
|
+
use_colored_output: Whether to use colored output for agents.
|
|
276
|
+
execution_timeout: Overall execution timeout in seconds.
|
|
277
|
+
task_timeout: Per-task timeout in seconds.
|
|
278
|
+
session_manager: Optional session manager for persistence.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Configured Graph ready for execution.
|
|
282
|
+
|
|
283
|
+
Raises:
|
|
284
|
+
ValueError: If no tasks are registered.
|
|
285
|
+
"""
|
|
286
|
+
def build_agent(agent_name: str) -> Agent:
|
|
287
|
+
"""Build an Agent from a registered definition."""
|
|
288
|
+
definition = config.agents.get(agent_name)
|
|
289
|
+
if not definition:
|
|
290
|
+
raise ValueError(f"Agent '{agent_name}' not found")
|
|
291
|
+
|
|
292
|
+
tools = [config.available_tools[t] for t in definition.tools]
|
|
293
|
+
model_name = definition.model or config.default_model
|
|
294
|
+
model = config.available_models.get(model_name) if model_name else None
|
|
295
|
+
|
|
296
|
+
callback_handler = None
|
|
297
|
+
if use_colored_output and definition.color:
|
|
298
|
+
callback_handler = create_colored_callback_handler(definition.color, agent_name)
|
|
299
|
+
|
|
300
|
+
return Agent(
|
|
301
|
+
name=definition.name,
|
|
302
|
+
system_prompt=definition.build_system_prompt(),
|
|
303
|
+
model=model,
|
|
304
|
+
tools=tools if tools else None,
|
|
305
|
+
callback_handler=callback_handler,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
if not config.tasks:
|
|
309
|
+
raise ValueError("No tasks registered - cannot build swarm")
|
|
310
|
+
|
|
311
|
+
builder = GraphBuilder()
|
|
312
|
+
|
|
313
|
+
# Build agents and add as nodes
|
|
314
|
+
for task_name, task_def in config.tasks.items():
|
|
315
|
+
builder.add_node(build_agent(task_def.agent), task_name)
|
|
316
|
+
|
|
317
|
+
# Add edges based on dependencies
|
|
318
|
+
for task_name, task_def in config.tasks.items():
|
|
319
|
+
for dep_name in task_def.depends_on:
|
|
320
|
+
if dep_name in config.tasks:
|
|
321
|
+
builder.add_edge(dep_name, task_name)
|
|
322
|
+
|
|
323
|
+
# Set entry point
|
|
324
|
+
if config.entry_task:
|
|
325
|
+
builder.set_entry_point(config.entry_task)
|
|
326
|
+
|
|
327
|
+
# Configure execution
|
|
328
|
+
builder.set_execution_timeout(execution_timeout)
|
|
329
|
+
builder.set_node_timeout(task_timeout)
|
|
330
|
+
|
|
331
|
+
if session_manager:
|
|
332
|
+
builder.set_session_manager(session_manager)
|
|
333
|
+
|
|
334
|
+
return builder.build()
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# =============================================================================
|
|
338
|
+
# DynamicSwarm Result
|
|
339
|
+
# =============================================================================
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
@dataclass
|
|
343
|
+
class DynamicSwarmResult:
|
|
344
|
+
"""Result from DynamicSwarm execution.
|
|
345
|
+
|
|
346
|
+
Attributes:
|
|
347
|
+
status: Overall execution status.
|
|
348
|
+
planning_output: Output from the planning phase.
|
|
349
|
+
execution_result: Result from the execution phase (GraphResult).
|
|
350
|
+
final_response: Final response from the planner after all tasks completed.
|
|
351
|
+
agents_spawned: Number of agents that were dynamically spawned.
|
|
352
|
+
tasks_created: Number of tasks that were created.
|
|
353
|
+
"""
|
|
354
|
+
|
|
355
|
+
status: Status
|
|
356
|
+
planning_output: str | None = None
|
|
357
|
+
execution_result: GraphResult | None = None
|
|
358
|
+
final_response: str | None = None
|
|
359
|
+
agents_spawned: int = 0
|
|
360
|
+
tasks_created: int = 0
|
|
361
|
+
error: str | None = None
|
|
362
|
+
|
|
363
|
+
def get_output(self, task_name: str) -> Any | None:
|
|
364
|
+
"""Get output from a specific task."""
|
|
365
|
+
if self.execution_result and hasattr(self.execution_result, "results"):
|
|
366
|
+
node_result = self.execution_result.results.get(task_name)
|
|
367
|
+
if node_result:
|
|
368
|
+
return str(node_result.result)
|
|
369
|
+
return None
|
|
370
|
+
|
|
371
|
+
def __bool__(self) -> bool:
|
|
372
|
+
"""Return True if completed successfully."""
|
|
373
|
+
return self.status == Status.COMPLETED
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
# =============================================================================
|
|
377
|
+
# DynamicSwarm Orchestrator
|
|
378
|
+
# =============================================================================
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class DynamicSwarm:
|
|
382
|
+
"""Dynamically construct and execute multi-agent workflows.
|
|
383
|
+
|
|
384
|
+
DynamicSwarm uses an orchestrator agent to analyze user queries and coordinate
|
|
385
|
+
a multi-agent workflow. The orchestrator has three main responsibilities:
|
|
386
|
+
|
|
387
|
+
1. **Planning and Creating Subagents** - Analyze the task and spawn specialized
|
|
388
|
+
agents with appropriate tools and models.
|
|
389
|
+
|
|
390
|
+
2. **Assigning Tasks** - Create tasks and assign them to the spawned agents,
|
|
391
|
+
defining dependencies between tasks when needed.
|
|
392
|
+
|
|
393
|
+
3. **Generating Final Response** - After all tasks complete, synthesize the
|
|
394
|
+
results into a cohesive final response.
|
|
395
|
+
|
|
396
|
+
All sub-agents run in parallel unless they have dependencies declared.
|
|
397
|
+
Tasks with dependencies will wait for their dependencies to complete first.
|
|
398
|
+
|
|
399
|
+
This is the unique value of this package - dynamic LLM-driven workflow orchestration.
|
|
400
|
+
For static multi-agent workflows, use the Strands SDK directly:
|
|
401
|
+
- strands.multiagent.graph.Graph for dependency-based execution
|
|
402
|
+
|
|
403
|
+
Example:
|
|
404
|
+
from strands_swarms import DynamicSwarm
|
|
405
|
+
from strands import tool
|
|
406
|
+
|
|
407
|
+
@tool
|
|
408
|
+
def search_web(query: str) -> str:
|
|
409
|
+
'''Search the web.'''
|
|
410
|
+
return f"Results for: {query}"
|
|
411
|
+
|
|
412
|
+
swarm = DynamicSwarm(
|
|
413
|
+
available_tools={"search_web": search_web},
|
|
414
|
+
verbose=True,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
result = swarm.execute("Research AI trends and summarize")
|
|
418
|
+
|
|
419
|
+
Customization:
|
|
420
|
+
Prompt templates can be overridden via subclassing:
|
|
421
|
+
|
|
422
|
+
class CustomSwarm(DynamicSwarm):
|
|
423
|
+
ORCHESTRATION_PROMPT_TEMPLATE = "Your custom template with {query}, {available_tools}, {available_models}"
|
|
424
|
+
"""
|
|
425
|
+
|
|
426
|
+
ORCHESTRATION_PROMPT_TEMPLATE: str = dedent("""\
|
|
427
|
+
Analyze this request and design a workflow:
|
|
428
|
+
|
|
429
|
+
{query}
|
|
430
|
+
|
|
431
|
+
Available tools: {available_tools}
|
|
432
|
+
Available models: {available_models}
|
|
433
|
+
|
|
434
|
+
Create the necessary agents and tasks, then call execute_swarm() when ready.""")
|
|
435
|
+
|
|
436
|
+
COMPLETION_PROMPT_TEMPLATE: str = dedent("""\
|
|
437
|
+
The tasks you designed have completed. Here are the outputs from each agent:
|
|
438
|
+
|
|
439
|
+
{task_outputs}
|
|
440
|
+
|
|
441
|
+
Now synthesize these results into a final, cohesive response to the original query:
|
|
442
|
+
{query}
|
|
443
|
+
|
|
444
|
+
Be direct and deliver the result. Don't explain the process or mention the sub-agents.""")
|
|
445
|
+
|
|
446
|
+
def __init__(
|
|
447
|
+
self,
|
|
448
|
+
available_tools: dict[str, Callable[..., Any]] | None = None,
|
|
449
|
+
available_models: dict[str, Model] | None = None,
|
|
450
|
+
*,
|
|
451
|
+
orchestrator_model: Model | None = None,
|
|
452
|
+
default_agent_model: str | None = None,
|
|
453
|
+
max_iterations: int = 20,
|
|
454
|
+
execution_timeout: float = 900.0,
|
|
455
|
+
task_timeout: float = 300.0,
|
|
456
|
+
session_manager: SessionManager | None = None,
|
|
457
|
+
hooks: list[HookProvider] | None = None,
|
|
458
|
+
verbose: bool = False,
|
|
459
|
+
) -> None:
|
|
460
|
+
"""Initialize DynamicSwarm.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
available_tools: Pool of tools that can be assigned to spawned agents.
|
|
464
|
+
Keys are tool names used by the orchestrator.
|
|
465
|
+
available_models: Pool of Model instances that can be used by spawned agents.
|
|
466
|
+
Keys are friendly names (e.g., "fast", "powerful"),
|
|
467
|
+
values are Model instances.
|
|
468
|
+
orchestrator_model: Model instance to use for the orchestrator agent.
|
|
469
|
+
default_agent_model: Default model name (key in available_models) for spawned agents.
|
|
470
|
+
max_iterations: Maximum iterations for execution.
|
|
471
|
+
execution_timeout: Overall execution timeout in seconds.
|
|
472
|
+
task_timeout: Per-task timeout in seconds.
|
|
473
|
+
session_manager: Optional session manager for persistence.
|
|
474
|
+
hooks: List of HookProvider instances for event callbacks.
|
|
475
|
+
Use PrintingHookProvider() for CLI output.
|
|
476
|
+
verbose: Shorthand for hooks=[PrintingHookProvider()].
|
|
477
|
+
"""
|
|
478
|
+
self._available_tools = available_tools or {}
|
|
479
|
+
self._available_models = available_models or {}
|
|
480
|
+
self._orchestrator_model = orchestrator_model
|
|
481
|
+
self._default_agent_model = default_agent_model
|
|
482
|
+
self._max_iterations = max_iterations
|
|
483
|
+
self._execution_timeout = execution_timeout
|
|
484
|
+
self._task_timeout = task_timeout
|
|
485
|
+
self._session_manager = session_manager
|
|
486
|
+
|
|
487
|
+
# Build hook registry
|
|
488
|
+
self._hook_registry = HookRegistry()
|
|
489
|
+
|
|
490
|
+
# Add verbose hook if requested
|
|
491
|
+
if verbose:
|
|
492
|
+
self._hook_registry.add_hook(PrintingHookProvider())
|
|
493
|
+
|
|
494
|
+
# Add user-provided hooks
|
|
495
|
+
if hooks:
|
|
496
|
+
for hook in hooks:
|
|
497
|
+
self._hook_registry.add_hook(hook)
|
|
498
|
+
|
|
499
|
+
def execute(self, query: str) -> DynamicSwarmResult:
|
|
500
|
+
"""Execute a query by dynamically building and running a workflow.
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
query: The user's request to process.
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
DynamicSwarmResult containing planning and execution results.
|
|
507
|
+
"""
|
|
508
|
+
return asyncio.get_event_loop().run_until_complete(self.execute_async(query))
|
|
509
|
+
|
|
510
|
+
def _emit(self, event: Any) -> None:
|
|
511
|
+
"""Emit an event to all registered hooks."""
|
|
512
|
+
if self._hook_registry.has_callbacks():
|
|
513
|
+
self._hook_registry.invoke_callbacks(event)
|
|
514
|
+
|
|
515
|
+
async def execute_async(self, query: str) -> DynamicSwarmResult:
|
|
516
|
+
"""Execute asynchronously.
|
|
517
|
+
|
|
518
|
+
Args:
|
|
519
|
+
query: The user's request to process.
|
|
520
|
+
|
|
521
|
+
Returns:
|
|
522
|
+
DynamicSwarmResult containing planning and execution results.
|
|
523
|
+
"""
|
|
524
|
+
# Import here to avoid circular import
|
|
525
|
+
from .orchestrator import set_swarm_config, create_orchestrator_agent
|
|
526
|
+
|
|
527
|
+
# Create swarm config for this execution
|
|
528
|
+
config = SwarmConfig(
|
|
529
|
+
available_tools=self._available_tools,
|
|
530
|
+
available_models=self._available_models,
|
|
531
|
+
default_model=self._default_agent_model,
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
# Set config for planning tools (with hook registry)
|
|
535
|
+
set_swarm_config(config, hook_registry=self._hook_registry)
|
|
536
|
+
|
|
537
|
+
try:
|
|
538
|
+
# Emit swarm started event
|
|
539
|
+
self._emit(SwarmStartedEvent(
|
|
540
|
+
query=query,
|
|
541
|
+
available_tools=list(self._available_tools.keys()),
|
|
542
|
+
available_models=list(self._available_models.keys()),
|
|
543
|
+
))
|
|
544
|
+
|
|
545
|
+
# =================================================================
|
|
546
|
+
# Orchestrator Phase 1 & 2: Planning, Creating Subagents, Assigning Tasks
|
|
547
|
+
# =================================================================
|
|
548
|
+
self._emit(PlanningStartedEvent())
|
|
549
|
+
|
|
550
|
+
planning_result = await self._run_planning(query, config)
|
|
551
|
+
if not planning_result.success:
|
|
552
|
+
self._emit(SwarmFailedEvent(
|
|
553
|
+
error=planning_result.error or "Orchestration failed"
|
|
554
|
+
))
|
|
555
|
+
return DynamicSwarmResult(
|
|
556
|
+
status=Status.FAILED,
|
|
557
|
+
planning_output=planning_result.output,
|
|
558
|
+
error=planning_result.error,
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
# Validate we have tasks
|
|
562
|
+
if not config.tasks:
|
|
563
|
+
self._emit(SwarmFailedEvent(
|
|
564
|
+
error="Orchestration completed but no tasks were created"
|
|
565
|
+
))
|
|
566
|
+
return DynamicSwarmResult(
|
|
567
|
+
status=Status.FAILED,
|
|
568
|
+
planning_output=planning_result.output,
|
|
569
|
+
error="Orchestration completed but no tasks were created",
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
# Build the swarm graph (create sub-agents, wire up task dependencies)
|
|
573
|
+
use_colored_output = self._hook_registry.has_callbacks()
|
|
574
|
+
graph = build_swarm(
|
|
575
|
+
config,
|
|
576
|
+
use_colored_output=use_colored_output,
|
|
577
|
+
execution_timeout=self._execution_timeout,
|
|
578
|
+
task_timeout=self._task_timeout,
|
|
579
|
+
session_manager=self._session_manager,
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
# =================================================================
|
|
583
|
+
# Execute Tasks
|
|
584
|
+
# =================================================================
|
|
585
|
+
self._emit(ExecutionStartedEvent(
|
|
586
|
+
tasks=list(config.tasks.keys()),
|
|
587
|
+
))
|
|
588
|
+
|
|
589
|
+
execution_result = await self._execute_graph(query, graph, config)
|
|
590
|
+
|
|
591
|
+
# Emit execution completion event
|
|
592
|
+
self._emit(ExecutionCompletedEvent(
|
|
593
|
+
status=str(execution_result.status) if execution_result else "FAILED",
|
|
594
|
+
agent_count=len(config.agents),
|
|
595
|
+
task_count=len(config.tasks),
|
|
596
|
+
))
|
|
597
|
+
|
|
598
|
+
# =================================================================
|
|
599
|
+
# Orchestrator Phase 3: Generate Final Response
|
|
600
|
+
# Uses the SAME orchestrator agent from planning (continued conversation)
|
|
601
|
+
# =================================================================
|
|
602
|
+
final_response = await self._run_completion(
|
|
603
|
+
query, config, execution_result,
|
|
604
|
+
orchestrator=planning_result.orchestrator
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
self._emit(SwarmCompletedEvent())
|
|
608
|
+
|
|
609
|
+
return DynamicSwarmResult(
|
|
610
|
+
status=execution_result.status if execution_result else Status.FAILED,
|
|
611
|
+
planning_output=planning_result.output,
|
|
612
|
+
execution_result=execution_result,
|
|
613
|
+
final_response=final_response,
|
|
614
|
+
agents_spawned=len(config.agents),
|
|
615
|
+
tasks_created=len(config.tasks),
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
finally:
|
|
619
|
+
# Clear config
|
|
620
|
+
set_swarm_config(None)
|
|
621
|
+
|
|
622
|
+
async def _run_planning(
|
|
623
|
+
self, query: str, state: SwarmConfig
|
|
624
|
+
) -> _PlanningResult:
|
|
625
|
+
"""Run the orchestration phase: planning and creating subagents, assigning tasks.
|
|
626
|
+
|
|
627
|
+
Returns the orchestrator agent along with the result so it can be reused
|
|
628
|
+
for the completion phase (same agent, continued conversation).
|
|
629
|
+
"""
|
|
630
|
+
from .orchestrator import create_orchestrator_agent
|
|
631
|
+
|
|
632
|
+
orchestrator = create_orchestrator_agent(model=self._orchestrator_model)
|
|
633
|
+
|
|
634
|
+
orchestration_prompt = self.ORCHESTRATION_PROMPT_TEMPLATE.format(
|
|
635
|
+
query=query,
|
|
636
|
+
available_tools=state.available_tool_names or ['none'],
|
|
637
|
+
available_models=state.available_model_names or ['default only'],
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
try:
|
|
641
|
+
result = orchestrator(orchestration_prompt)
|
|
642
|
+
|
|
643
|
+
output = None
|
|
644
|
+
if hasattr(result, "message") and result.message:
|
|
645
|
+
content = result.message.get("content", [])
|
|
646
|
+
if content and isinstance(content[0], dict):
|
|
647
|
+
output = content[0].get("text", "")
|
|
648
|
+
|
|
649
|
+
return _PlanningResult(success=True, output=output, orchestrator=orchestrator)
|
|
650
|
+
|
|
651
|
+
except Exception as e:
|
|
652
|
+
return _PlanningResult(success=False, error=str(e))
|
|
653
|
+
|
|
654
|
+
async def _run_completion(
|
|
655
|
+
self,
|
|
656
|
+
query: str,
|
|
657
|
+
state: SwarmConfig,
|
|
658
|
+
execution_result: MultiAgentResult | None,
|
|
659
|
+
orchestrator: Agent,
|
|
660
|
+
) -> str | None:
|
|
661
|
+
"""Run the final response generation phase - synthesize task outputs into a cohesive response.
|
|
662
|
+
|
|
663
|
+
This is the third responsibility of the orchestrator: generating the final response
|
|
664
|
+
by combining outputs from all sub-agents.
|
|
665
|
+
|
|
666
|
+
Uses the SAME orchestrator agent that did the planning, continuing the conversation
|
|
667
|
+
so it has full context about why agents were spawned and what tasks were meant to do.
|
|
668
|
+
|
|
669
|
+
Args:
|
|
670
|
+
query: Original user query.
|
|
671
|
+
state: Swarm configuration with task definitions.
|
|
672
|
+
execution_result: Results from task execution.
|
|
673
|
+
orchestrator: The orchestrator agent from planning phase (same agent, continued conversation).
|
|
674
|
+
"""
|
|
675
|
+
if not execution_result or not hasattr(execution_result, "results"):
|
|
676
|
+
return None
|
|
677
|
+
|
|
678
|
+
# Collect all task outputs
|
|
679
|
+
task_outputs: list[str] = []
|
|
680
|
+
for task_name in state.tasks:
|
|
681
|
+
node_result = execution_result.results.get(task_name)
|
|
682
|
+
if node_result:
|
|
683
|
+
task_outputs.append(f"[{task_name}]:\n{node_result.result}")
|
|
684
|
+
|
|
685
|
+
if not task_outputs:
|
|
686
|
+
return None
|
|
687
|
+
|
|
688
|
+
completion_prompt = self.COMPLETION_PROMPT_TEMPLATE.format(
|
|
689
|
+
query=query,
|
|
690
|
+
task_outputs="\n\n".join(task_outputs),
|
|
691
|
+
)
|
|
692
|
+
try:
|
|
693
|
+
result = orchestrator(completion_prompt)
|
|
694
|
+
|
|
695
|
+
if hasattr(result, "message") and result.message:
|
|
696
|
+
content = result.message.get("content", [])
|
|
697
|
+
if content and isinstance(content[0], dict):
|
|
698
|
+
return content[0].get("text", "")
|
|
699
|
+
return None
|
|
700
|
+
except Exception:
|
|
701
|
+
return None
|
|
702
|
+
|
|
703
|
+
async def _execute_graph(
|
|
704
|
+
self, query: str, graph: Graph, state: SwarmConfig
|
|
705
|
+
) -> GraphResult:
|
|
706
|
+
"""Execute a pre-built Graph with dependency-based execution."""
|
|
707
|
+
if self._hook_registry.has_callbacks():
|
|
708
|
+
result = None
|
|
709
|
+
current_task = None
|
|
710
|
+
async for event in graph.stream_async(query):
|
|
711
|
+
task_name = event.get("node_id")
|
|
712
|
+
if task_name and task_name != current_task:
|
|
713
|
+
if current_task:
|
|
714
|
+
self._emit(TaskCompletedEvent(name=current_task))
|
|
715
|
+
agent_def = state.agents.get(task_name)
|
|
716
|
+
self._emit(TaskStartedEvent(
|
|
717
|
+
name=task_name,
|
|
718
|
+
agent_role=agent_def.role if agent_def else None,
|
|
719
|
+
))
|
|
720
|
+
current_task = task_name
|
|
721
|
+
if "result" in event:
|
|
722
|
+
result = event["result"]
|
|
723
|
+
if current_task:
|
|
724
|
+
self._emit(TaskCompletedEvent(name=current_task))
|
|
725
|
+
return result if result else await graph.invoke_async(query)
|
|
726
|
+
else:
|
|
727
|
+
return await graph.invoke_async(query)
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
@dataclass
|
|
731
|
+
class _PlanningResult:
|
|
732
|
+
"""Internal result from planning phase."""
|
|
733
|
+
|
|
734
|
+
success: bool
|
|
735
|
+
output: str | None = None
|
|
736
|
+
error: str | None = None
|
|
737
|
+
orchestrator: Agent | None = None # Preserved for completion phase
|
|
738
|
+
|