dao-ai 0.0.36__py3-none-any.whl → 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.
Files changed (59) hide show
  1. dao_ai/__init__.py +29 -0
  2. dao_ai/cli.py +195 -30
  3. dao_ai/config.py +770 -244
  4. dao_ai/genie/__init__.py +1 -22
  5. dao_ai/genie/cache/__init__.py +1 -2
  6. dao_ai/genie/cache/base.py +20 -70
  7. dao_ai/genie/cache/core.py +75 -0
  8. dao_ai/genie/cache/lru.py +44 -21
  9. dao_ai/genie/cache/semantic.py +390 -109
  10. dao_ai/genie/core.py +35 -0
  11. dao_ai/graph.py +27 -253
  12. dao_ai/hooks/__init__.py +9 -6
  13. dao_ai/hooks/core.py +22 -190
  14. dao_ai/memory/__init__.py +10 -0
  15. dao_ai/memory/core.py +23 -5
  16. dao_ai/memory/databricks.py +389 -0
  17. dao_ai/memory/postgres.py +2 -2
  18. dao_ai/messages.py +6 -4
  19. dao_ai/middleware/__init__.py +125 -0
  20. dao_ai/middleware/assertions.py +778 -0
  21. dao_ai/middleware/base.py +50 -0
  22. dao_ai/middleware/core.py +61 -0
  23. dao_ai/middleware/guardrails.py +415 -0
  24. dao_ai/middleware/human_in_the_loop.py +228 -0
  25. dao_ai/middleware/message_validation.py +554 -0
  26. dao_ai/middleware/summarization.py +192 -0
  27. dao_ai/models.py +1177 -108
  28. dao_ai/nodes.py +118 -161
  29. dao_ai/optimization.py +664 -0
  30. dao_ai/orchestration/__init__.py +52 -0
  31. dao_ai/orchestration/core.py +287 -0
  32. dao_ai/orchestration/supervisor.py +264 -0
  33. dao_ai/orchestration/swarm.py +226 -0
  34. dao_ai/prompts.py +126 -29
  35. dao_ai/providers/databricks.py +126 -381
  36. dao_ai/state.py +139 -21
  37. dao_ai/tools/__init__.py +8 -5
  38. dao_ai/tools/core.py +57 -4
  39. dao_ai/tools/email.py +280 -0
  40. dao_ai/tools/genie.py +47 -24
  41. dao_ai/tools/mcp.py +4 -3
  42. dao_ai/tools/memory.py +50 -0
  43. dao_ai/tools/python.py +4 -12
  44. dao_ai/tools/search.py +14 -0
  45. dao_ai/tools/slack.py +1 -1
  46. dao_ai/tools/unity_catalog.py +8 -6
  47. dao_ai/tools/vector_search.py +16 -9
  48. dao_ai/utils.py +72 -8
  49. dao_ai-0.1.0.dist-info/METADATA +1878 -0
  50. dao_ai-0.1.0.dist-info/RECORD +62 -0
  51. dao_ai/chat_models.py +0 -204
  52. dao_ai/guardrails.py +0 -112
  53. dao_ai/tools/genie/__init__.py +0 -236
  54. dao_ai/tools/human_in_the_loop.py +0 -100
  55. dao_ai-0.0.36.dist-info/METADATA +0 -951
  56. dao_ai-0.0.36.dist-info/RECORD +0 -47
  57. {dao_ai-0.0.36.dist-info → dao_ai-0.1.0.dist-info}/WHEEL +0 -0
  58. {dao_ai-0.0.36.dist-info → dao_ai-0.1.0.dist-info}/entry_points.txt +0 -0
  59. {dao_ai-0.0.36.dist-info → dao_ai-0.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,52 @@
1
+ """
2
+ Orchestration patterns for DAO AI multi-agent systems.
3
+
4
+ This package provides factory functions for creating LangGraph workflows
5
+ that orchestrate multiple agents using the supervisor and swarm patterns.
6
+
7
+ Supervisor Pattern:
8
+ A central supervisor coordinates specialized worker agents. The supervisor
9
+ hands off control to agents who then control the conversation. Agents can
10
+ hand back to the supervisor when done or hand off to other agents.
11
+
12
+ Swarm Pattern:
13
+ Agents can directly transfer control to each other using handoff tools.
14
+ The active agent changes, and the user may continue interacting with
15
+ the new agent. This provides decentralized, peer-to-peer collaboration.
16
+
17
+ Both patterns use Command(goto=...) for routing between agent nodes in
18
+ the workflow graph.
19
+
20
+ See: https://docs.langchain.com/oss/python/langchain/multi-agent
21
+ See: https://github.com/langchain-ai/langgraph-supervisor-py
22
+ See: https://github.com/langchain-ai/langgraph-swarm-py
23
+ """
24
+
25
+ from dao_ai.orchestration.core import (
26
+ SUPERVISOR_NODE,
27
+ OutputMode,
28
+ create_agent_node_handler,
29
+ create_checkpointer,
30
+ create_handoff_tool,
31
+ create_orchestration_graph,
32
+ create_store,
33
+ extract_agent_response,
34
+ filter_messages_for_agent,
35
+ get_handoff_description,
36
+ )
37
+
38
+ __all__ = [
39
+ # Constants
40
+ "SUPERVISOR_NODE",
41
+ "OutputMode",
42
+ # Core utilities
43
+ "create_store",
44
+ "create_checkpointer",
45
+ "filter_messages_for_agent",
46
+ "extract_agent_response",
47
+ "create_agent_node_handler",
48
+ "create_handoff_tool",
49
+ "get_handoff_description",
50
+ # Main factory
51
+ "create_orchestration_graph",
52
+ ]
@@ -0,0 +1,287 @@
1
+ """
2
+ Core orchestration utilities and infrastructure.
3
+
4
+ This module provides the foundational utilities for multi-agent orchestration:
5
+ - Memory and checkpointer creation
6
+ - Message filtering and extraction
7
+ - Agent node handlers
8
+ - Handoff tools
9
+ - Main orchestration graph factory
10
+ """
11
+
12
+ from typing import Awaitable, Callable, Literal
13
+
14
+ from langchain.tools import ToolRuntime, tool
15
+ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, ToolMessage
16
+ from langchain_core.tools import BaseTool
17
+ from langgraph.checkpoint.base import BaseCheckpointSaver
18
+ from langgraph.graph.state import CompiledStateGraph
19
+ from langgraph.runtime import Runtime
20
+ from langgraph.store.base import BaseStore
21
+ from langgraph.types import Command
22
+ from loguru import logger
23
+
24
+ from dao_ai.config import AgentModel, AppConfig, OrchestrationModel
25
+ from dao_ai.messages import last_ai_message
26
+ from dao_ai.state import AgentState, Context
27
+
28
+ # Constant for supervisor node name
29
+ SUPERVISOR_NODE = "supervisor"
30
+
31
+ # Output mode for agent responses
32
+ # - "full_history": Include all messages from the agent's execution
33
+ # - "last_message": Include only the final AI message from the agent
34
+ OutputMode = Literal["full_history", "last_message"]
35
+
36
+
37
+ def create_store(orchestration: OrchestrationModel) -> BaseStore | None:
38
+ """
39
+ Create a memory store from orchestration config.
40
+
41
+ Args:
42
+ orchestration: The orchestration configuration
43
+
44
+ Returns:
45
+ The configured store, or None if not configured
46
+ """
47
+ if orchestration.memory and orchestration.memory.store:
48
+ store = orchestration.memory.store.as_store()
49
+ logger.debug(f"Using memory store: {store}")
50
+ return store
51
+ return None
52
+
53
+
54
+ def create_checkpointer(
55
+ orchestration: OrchestrationModel,
56
+ ) -> BaseCheckpointSaver | None:
57
+ """
58
+ Create a checkpointer from orchestration config.
59
+
60
+ Args:
61
+ orchestration: The orchestration configuration
62
+
63
+ Returns:
64
+ The configured checkpointer, or None if not configured
65
+ """
66
+ if orchestration.memory and orchestration.memory.checkpointer:
67
+ checkpointer = orchestration.memory.checkpointer.as_checkpointer()
68
+ logger.debug(f"Using checkpointer: {checkpointer}")
69
+ return checkpointer
70
+ return None
71
+
72
+
73
+ def filter_messages_for_agent(messages: list[BaseMessage]) -> list[BaseMessage]:
74
+ """
75
+ Filter messages for a worker agent to avoid tool_use/tool_result pairing errors.
76
+
77
+ When the supervisor hands off to an agent, the agent should only see:
78
+ - HumanMessage (user queries)
79
+ - AIMessage with content (previous responses, but not tool calls)
80
+
81
+ This prevents the agent from seeing orphaned ToolMessages or AIMessages
82
+ with tool_calls that don't belong to the agent's context.
83
+
84
+ Args:
85
+ messages: The full message history from parent state
86
+
87
+ Returns:
88
+ Filtered messages safe for the agent to process
89
+ """
90
+ filtered: list[BaseMessage] = []
91
+ for msg in messages:
92
+ if isinstance(msg, HumanMessage):
93
+ # Always include user messages
94
+ filtered.append(msg)
95
+ elif isinstance(msg, AIMessage):
96
+ # Include AI messages but strip tool_calls to avoid confusion
97
+ if msg.content and not msg.tool_calls:
98
+ filtered.append(msg)
99
+ elif msg.content and msg.tool_calls:
100
+ # Include content but create clean AIMessage without tool_calls
101
+ filtered.append(AIMessage(content=msg.content, id=msg.id))
102
+ # Skip ToolMessages - they belong to the supervisor's context
103
+ return filtered
104
+
105
+
106
+ def extract_agent_response(
107
+ messages: list[BaseMessage],
108
+ output_mode: OutputMode = "last_message",
109
+ ) -> list[BaseMessage]:
110
+ """
111
+ Extract the agent's response based on the output mode.
112
+
113
+ Args:
114
+ messages: The agent's full message history after execution
115
+ output_mode: How to extract the response
116
+ - "full_history": Return all messages (may cause issues)
117
+ - "last_message": Return only the final AI message
118
+
119
+ Returns:
120
+ Messages to include in the parent state update
121
+ """
122
+ if output_mode == "full_history":
123
+ return messages
124
+
125
+ # Find the last AI message with content
126
+ final_response: AIMessage | None = last_ai_message(messages)
127
+
128
+ if final_response:
129
+ # Return clean AIMessage without tool_calls
130
+ if final_response.tool_calls:
131
+ return [AIMessage(content=final_response.content, id=final_response.id)]
132
+ return [final_response]
133
+
134
+ return []
135
+
136
+
137
+ def create_agent_node_handler(
138
+ agent_name: str,
139
+ agent: CompiledStateGraph,
140
+ output_mode: OutputMode = "last_message",
141
+ ) -> Callable[[AgentState, Runtime[Context]], Awaitable[AgentState]]:
142
+ """
143
+ Create a handler that wraps an agent subgraph with message filtering.
144
+
145
+ This filters messages before passing to the agent and extracts only
146
+ the relevant response, avoiding tool_use/tool_result pairing errors.
147
+
148
+ Used by both supervisor and swarm patterns to ensure consistent
149
+ message handling when agents are CompiledStateGraphs.
150
+
151
+ Based on langgraph-supervisor-py output_mode pattern.
152
+
153
+ Args:
154
+ agent_name: Name of the agent (for logging)
155
+ agent: The compiled agent subgraph
156
+ output_mode: How to extract response ("last_message" or "full_history")
157
+
158
+ Returns:
159
+ An async handler function for the workflow node
160
+ """
161
+
162
+ async def handler(state: AgentState, runtime: Runtime[Context]) -> AgentState:
163
+ # Filter messages to avoid tool_use/tool_result pairing errors
164
+ original_messages = state.get("messages", [])
165
+ filtered_messages = filter_messages_for_agent(original_messages)
166
+
167
+ logger.debug(
168
+ f"Agent '{agent_name}' receiving {len(filtered_messages)} filtered "
169
+ f"messages (from {len(original_messages)} total)"
170
+ )
171
+
172
+ # Create state with filtered messages for the agent
173
+ agent_state: AgentState = {
174
+ **state,
175
+ "messages": filtered_messages,
176
+ }
177
+
178
+ # Invoke the agent
179
+ result: AgentState = await agent.ainvoke(agent_state, context=runtime.context)
180
+
181
+ # Extract agent response based on output mode
182
+ result_messages = result.get("messages", [])
183
+ response_messages = extract_agent_response(result_messages, output_mode)
184
+
185
+ logger.debug(
186
+ f"Agent '{agent_name}' completed. Returning {len(response_messages)} "
187
+ f"messages (from {len(result_messages)} total, mode={output_mode})"
188
+ )
189
+
190
+ # Return state update with extracted response
191
+ return {
192
+ **result,
193
+ "messages": response_messages,
194
+ }
195
+
196
+ return handler
197
+
198
+
199
+ def create_handoff_tool(
200
+ target_agent_name: str,
201
+ description: str,
202
+ ) -> BaseTool:
203
+ """
204
+ Create a handoff tool that transfers control to another agent.
205
+
206
+ The tool returns a Command object with goto to directly route
207
+ to the target agent node in the parent graph.
208
+
209
+ Args:
210
+ target_agent_name: The name of the agent to hand off to
211
+ description: Description of what this agent handles
212
+
213
+ Returns:
214
+ A tool that triggers a handoff to the target agent via Command
215
+ """
216
+
217
+ @tool
218
+ def handoff_tool(runtime: ToolRuntime[Context, AgentState]) -> Command:
219
+ """Transfer control to another agent."""
220
+ tool_call_id: str = runtime.tool_call_id
221
+ logger.debug(f"Handoff to agent '{target_agent_name}'")
222
+
223
+ return Command(
224
+ update={
225
+ "active_agent": target_agent_name,
226
+ "messages": [
227
+ ToolMessage(
228
+ content=f"Transferred to {target_agent_name}",
229
+ tool_call_id=tool_call_id,
230
+ )
231
+ ],
232
+ },
233
+ goto=target_agent_name,
234
+ graph=Command.PARENT,
235
+ )
236
+
237
+ # Set the tool name and description
238
+ handoff_tool.name = f"handoff_to_{target_agent_name}"
239
+ handoff_tool.__doc__ = f"Transfer to {target_agent_name}: {description}"
240
+ handoff_tool.description = f"Transfer to {target_agent_name}: {description}"
241
+
242
+ return handoff_tool
243
+
244
+
245
+ def get_handoff_description(agent: AgentModel) -> str:
246
+ """
247
+ Get the handoff description for an agent.
248
+
249
+ Priority: handoff_prompt > description > default message
250
+
251
+ Args:
252
+ agent: The agent to get the handoff description for
253
+
254
+ Returns:
255
+ The handoff description string
256
+ """
257
+ return (
258
+ agent.handoff_prompt
259
+ or agent.description
260
+ or f"Handles {agent.name} related tasks and inquiries"
261
+ )
262
+
263
+
264
+ def create_orchestration_graph(config: AppConfig) -> CompiledStateGraph:
265
+ """
266
+ Create the main orchestration graph based on the configuration.
267
+
268
+ This factory function creates either a supervisor or swarm graph
269
+ depending on the configuration.
270
+
271
+ Args:
272
+ config: The application configuration
273
+
274
+ Returns:
275
+ A compiled LangGraph state machine
276
+ """
277
+ from dao_ai.orchestration.supervisor import create_supervisor_graph
278
+ from dao_ai.orchestration.swarm import create_swarm_graph
279
+
280
+ orchestration: OrchestrationModel = config.app.orchestration
281
+ if orchestration.supervisor:
282
+ return create_supervisor_graph(config)
283
+
284
+ if orchestration.swarm:
285
+ return create_swarm_graph(config)
286
+
287
+ raise ValueError("No valid orchestration model found in the configuration.")
@@ -0,0 +1,264 @@
1
+ """
2
+ Supervisor pattern for multi-agent orchestration.
3
+
4
+ The supervisor pattern uses a central supervisor agent that coordinates
5
+ specialized worker agents. The supervisor hands off control to agents
6
+ who then control the conversation. Agents can hand back to the supervisor
7
+ when done.
8
+
9
+ Based on: https://github.com/langchain-ai/langgraph-supervisor-py
10
+ """
11
+
12
+ from langchain.agents import create_agent
13
+ from langchain.agents.middleware import AgentMiddleware as LangchainAgentMiddleware
14
+ from langchain.tools import ToolRuntime, tool
15
+ from langchain_core.language_models import LanguageModelLike
16
+ from langchain_core.messages import ToolMessage
17
+ from langchain_core.tools import BaseTool
18
+ from langgraph.checkpoint.base import BaseCheckpointSaver
19
+ from langgraph.graph import StateGraph
20
+ from langgraph.graph.state import CompiledStateGraph
21
+ from langgraph.store.base import BaseStore
22
+ from langgraph.types import Command
23
+ from langmem import create_manage_memory_tool
24
+ from loguru import logger
25
+
26
+ from dao_ai.config import (
27
+ AppConfig,
28
+ MemoryModel,
29
+ OrchestrationModel,
30
+ SupervisorModel,
31
+ )
32
+ from dao_ai.middleware.base import AgentMiddleware
33
+ from dao_ai.middleware.core import create_factory_middleware
34
+ from dao_ai.nodes import create_agent_node
35
+ from dao_ai.orchestration import (
36
+ SUPERVISOR_NODE,
37
+ create_agent_node_handler,
38
+ create_checkpointer,
39
+ create_handoff_tool,
40
+ create_store,
41
+ get_handoff_description,
42
+ )
43
+ from dao_ai.prompts import make_prompt
44
+ from dao_ai.state import AgentState, Context
45
+ from dao_ai.tools import create_tools
46
+ from dao_ai.tools.memory import create_search_memory_tool
47
+
48
+
49
+ def _create_handoff_back_to_supervisor_tool() -> BaseTool:
50
+ """
51
+ Create a tool for agents to hand control back to the supervisor.
52
+
53
+ This is used in the supervisor pattern when an agent has completed
54
+ its task and wants to return control to the supervisor for further
55
+ coordination or to complete the conversation.
56
+
57
+ Returns:
58
+ A tool that routes back to the supervisor node
59
+ """
60
+
61
+ @tool
62
+ def handoff_to_supervisor(
63
+ summary: str,
64
+ runtime: ToolRuntime[Context, AgentState],
65
+ ) -> Command:
66
+ """
67
+ Hand control back to the supervisor.
68
+
69
+ Use this when you have completed your task and want to return
70
+ control to the supervisor for further coordination.
71
+
72
+ Args:
73
+ summary: A brief summary of what was accomplished
74
+ """
75
+ tool_call_id: str = runtime.tool_call_id
76
+ logger.debug(f"Handoff back to supervisor with summary: {summary[:100]}...")
77
+
78
+ return Command(
79
+ update={
80
+ "active_agent": None,
81
+ "messages": [
82
+ ToolMessage(
83
+ content=f"Task completed: {summary}",
84
+ tool_call_id=tool_call_id,
85
+ )
86
+ ],
87
+ },
88
+ goto=SUPERVISOR_NODE,
89
+ graph=Command.PARENT,
90
+ )
91
+
92
+ return handoff_to_supervisor
93
+
94
+
95
+ def _create_supervisor_agent(
96
+ config: AppConfig,
97
+ tools: list[BaseTool],
98
+ handoff_tools: list[BaseTool],
99
+ middlewares: list[AgentMiddleware],
100
+ ) -> CompiledStateGraph:
101
+ """
102
+ Create a supervisor agent with handoff tools for each worker agent.
103
+
104
+ The supervisor coordinates worker agents by handing off control.
105
+ Worker agents take over the conversation and can hand back to
106
+ the supervisor when done.
107
+
108
+ Args:
109
+ config: Application configuration
110
+ tools: Additional tools for the supervisor (e.g., memory tools)
111
+ handoff_tools: Handoff tools to route to worker agents
112
+ middlewares: Middleware to apply to the supervisor
113
+
114
+ Returns:
115
+ Compiled supervisor agent
116
+ """
117
+ orchestration: OrchestrationModel = config.app.orchestration
118
+ supervisor: SupervisorModel = orchestration.supervisor
119
+
120
+ all_tools: list[BaseTool] = list(tools) + list(handoff_tools)
121
+
122
+ model: LanguageModelLike = supervisor.model.as_chat_model()
123
+
124
+ # Get the prompt as middleware (always returns AgentMiddleware or None)
125
+ prompt_middleware: LangchainAgentMiddleware | None = make_prompt(supervisor.prompt)
126
+
127
+ # Add prompt middleware at the beginning for priority
128
+ if prompt_middleware is not None:
129
+ middlewares.insert(0, prompt_middleware)
130
+
131
+ # Create the supervisor agent
132
+ # Handoff tools route to worker agents in the parent workflow graph
133
+ supervisor_agent: CompiledStateGraph = create_agent(
134
+ name=SUPERVISOR_NODE,
135
+ model=model,
136
+ tools=all_tools,
137
+ middleware=middlewares,
138
+ state_schema=AgentState,
139
+ context_schema=Context,
140
+ )
141
+
142
+ return supervisor_agent
143
+
144
+
145
+ def create_supervisor_graph(config: AppConfig) -> CompiledStateGraph:
146
+ """
147
+ Create a supervisor-based multi-agent system using handoffs.
148
+
149
+ This implements a supervisor pattern where:
150
+ 1. Supervisor receives user input and decides which agent to hand off to
151
+ 2. Agent takes control of the conversation and interacts with user
152
+ 3. Agent can hand back to supervisor or complete the task
153
+
154
+ The supervisor and all worker agents are nodes in a workflow graph.
155
+ Handoff tools use Command(goto=..., graph=Command.PARENT) to route
156
+ between nodes.
157
+
158
+ Args:
159
+ config: The application configuration
160
+
161
+ Returns:
162
+ A compiled LangGraph state machine
163
+
164
+ Based on: https://github.com/langchain-ai/langgraph-supervisor-py
165
+ """
166
+ logger.debug("Creating supervisor graph (handoff pattern)")
167
+
168
+ orchestration: OrchestrationModel = config.app.orchestration
169
+ supervisor_config: SupervisorModel = orchestration.supervisor
170
+
171
+ # Create handoff tools for supervisor to route to agents
172
+ handoff_tools: list[BaseTool] = []
173
+ for registered_agent in config.app.agents:
174
+ description: str = get_handoff_description(registered_agent)
175
+ handoff_tool: BaseTool = create_handoff_tool(
176
+ target_agent_name=registered_agent.name,
177
+ description=description,
178
+ )
179
+ handoff_tools.append(handoff_tool)
180
+ logger.debug(f"Created handoff tool for supervisor: {registered_agent.name}")
181
+
182
+ # Create supervisor's own tools (e.g., memory tools)
183
+ supervisor_tools: list[BaseTool] = list(create_tools(supervisor_config.tools))
184
+
185
+ # Create middleware from configuration
186
+ middlewares: list[AgentMiddleware] = []
187
+ for middleware_config in supervisor_config.middleware:
188
+ middleware = create_factory_middleware(
189
+ function_name=middleware_config.name,
190
+ args=middleware_config.args,
191
+ )
192
+ if middleware is not None:
193
+ middlewares.append(middleware)
194
+ logger.debug(f"Created supervisor middleware: {middleware_config.name}")
195
+
196
+ # Set up memory store and checkpointer
197
+ store: BaseStore | None = create_store(orchestration)
198
+ checkpointer: BaseCheckpointSaver | None = create_checkpointer(orchestration)
199
+
200
+ # Add memory tools if store is configured with namespace
201
+ if (
202
+ orchestration.memory
203
+ and orchestration.memory.store
204
+ and orchestration.memory.store.namespace
205
+ ):
206
+ namespace: tuple[str, ...] = ("memory", orchestration.memory.store.namespace)
207
+ logger.debug(f"Memory store namespace: {namespace}")
208
+ # Use Databricks-compatible search_memory tool (omits problematic filter field)
209
+ supervisor_tools += [
210
+ create_manage_memory_tool(namespace=namespace),
211
+ create_search_memory_tool(namespace=namespace),
212
+ ]
213
+
214
+ # Create the supervisor agent
215
+ supervisor_agent: CompiledStateGraph = _create_supervisor_agent(
216
+ config=config,
217
+ tools=supervisor_tools,
218
+ handoff_tools=handoff_tools,
219
+ middlewares=middlewares,
220
+ )
221
+
222
+ # Create worker agent subgraphs
223
+ # Each worker gets a handoff_to_supervisor tool to return control
224
+ agent_subgraphs: dict[str, CompiledStateGraph] = {}
225
+ memory: MemoryModel | None = orchestration.memory
226
+ for registered_agent in config.app.agents:
227
+ # Create handoff back to supervisor tool
228
+ supervisor_handoff: BaseTool = _create_handoff_back_to_supervisor_tool()
229
+
230
+ # Create the worker agent with handoff back to supervisor
231
+ agent_subgraph: CompiledStateGraph = create_agent_node(
232
+ agent=registered_agent,
233
+ memory=memory,
234
+ chat_history=config.app.chat_history,
235
+ additional_tools=[supervisor_handoff],
236
+ )
237
+ agent_subgraphs[registered_agent.name] = agent_subgraph
238
+ logger.debug(f"Created worker agent subgraph: {registered_agent.name}")
239
+
240
+ # Build the workflow graph
241
+ # All agents are nodes, handoffs route between them via Command
242
+ workflow: StateGraph = StateGraph(
243
+ AgentState,
244
+ input=AgentState,
245
+ output=AgentState,
246
+ context_schema=Context,
247
+ )
248
+
249
+ # Add supervisor node
250
+ workflow.add_node(SUPERVISOR_NODE, supervisor_agent)
251
+
252
+ # Add worker agent nodes with message filtering handlers
253
+ for agent_name, agent_subgraph in agent_subgraphs.items():
254
+ handler = create_agent_node_handler(
255
+ agent_name=agent_name,
256
+ agent=agent_subgraph,
257
+ output_mode="last_message", # Only return final response to avoid issues
258
+ )
259
+ workflow.add_node(agent_name, handler)
260
+
261
+ # Supervisor is the entry point
262
+ workflow.set_entry_point(SUPERVISOR_NODE)
263
+
264
+ return workflow.compile(checkpointer=checkpointer, store=store)