mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-15 13:36:35 +03:00
Comprehensive LangGraph skill covering: - Core architecture: Graph API, Functional API, state management, agent loops - Three multi-agent patterns: supervisor (~94% accuracy), swarm (~40% fewer LLM calls), hierarchical teams (subgraphs with nested state) - Persistence: checkpointers vs stores, per-invocation/per-thread/stateless modes - Production: Agent Server deployment, LangSmith observability, 8 failure modes - Evals: routing accuracy, resolution coverage, LLM-as-judge methodology - Troubleshooting: symptom→cause→fix tables per pattern - 3 Python scripts: supervisor scaffold, swarm scaffold, eval generator - 3 runnable templates: supervisor, swarm, subgraph composition Ships 8 reference files, 3 scripts, and 3 templates.
292 lines
9.4 KiB
Python
292 lines
9.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
LangGraph Swarm Pattern Scaffold Generator.
|
|
|
|
Generates a complete swarm-based multi-agent project with:
|
|
- State definitions with handoff tracking
|
|
- Handoff tool factory (Command-based)
|
|
- Triage agent for initial routing
|
|
- Specialist agents with domain tools + handoff tools
|
|
- Conditional routing with recursion guard
|
|
- Node wrappers for each agent
|
|
|
|
Usage:
|
|
python lg-swarm-scaffold.py --name support --agents billing,tech,account
|
|
python lg-swarm-scaffold.py --name triage --agents search,summarize --triage-only
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
from typing import List
|
|
|
|
|
|
def snake_case(name: str) -> str:
|
|
return name.replace("-", "_").replace(" ", "_").lower()
|
|
|
|
|
|
def pascal_case(name: str) -> str:
|
|
return "".join(word.capitalize() for word in name.replace("-", " ").replace("_", " ").split())
|
|
|
|
|
|
SWARM_HANDOFF_TOOLS = """\
|
|
from langgraph.types import Command
|
|
from langchain_core.tools import tool
|
|
|
|
|
|
def make_handoff_tool(target_agent: str, description: str):
|
|
\"\"\"Factory that creates a handoff tool for transferring to another agent.
|
|
|
|
The tool returns a Command that tells LangGraph to navigate to a different
|
|
node in the parent graph, updating current_agent and the handoff counter.
|
|
\"\"\"
|
|
|
|
@tool(f"transfer_to_{target_agent}")
|
|
def handoff(reason: str) -> Command:
|
|
\"\"\"Transfer the conversation to another specialist agent.\"\"\"
|
|
return Command(
|
|
goto=target_agent,
|
|
update={"current_agent": target_agent},
|
|
graph=Command.PARENT,
|
|
)
|
|
|
|
handoff.__doc__ = description
|
|
return handoff
|
|
"""
|
|
|
|
|
|
def generate_swarm_project(project_name: str, agents: List[str], output_dir: str, triage_only: bool = False):
|
|
pname = snake_case(project_name)
|
|
state_class = pascal_case(project_name) + "State"
|
|
dir_path = os.path.join(output_dir, pname)
|
|
os.makedirs(dir_path, exist_ok=True)
|
|
|
|
agent_names = [snake_case(a) for a in agents]
|
|
agent_labels = [a.replace("-", " ").title() for a in agents]
|
|
|
|
# state.py
|
|
state_code = f'''\
|
|
\"\"\"State definitions for {project_name} swarm multi-agent system.\"\"\"
|
|
from typing import Annotated, TypedDict
|
|
from langgraph.graph import MessagesState
|
|
import operator
|
|
|
|
|
|
class {state_class}(MessagesState):
|
|
"""Shared state across all agents in the {project_name} swarm."""
|
|
|
|
current_agent: str = ""
|
|
"""Which specialist agent is currently active. Empty = triage phase."""
|
|
|
|
resolution_notes: Annotated[list[str], operator.add]
|
|
"""Audit trail of what each agent resolved."""
|
|
|
|
handoff_count: int = 0
|
|
"""Recursion guard — incremented on each handoff. Hard limit at 3."""
|
|
'''
|
|
|
|
# handoff_tools.py
|
|
handoff_tool_defs = ""
|
|
handoff_tool_imports = "\n".join(
|
|
f'transfer_to_{name} = make_handoff_tool(\n'
|
|
f' "{name}",\n'
|
|
f' "Transfer to the {label.lower()} specialist for {label.lower()} issues.",\n'
|
|
f')'
|
|
for name, label in zip(agent_names, agent_labels)
|
|
)
|
|
handoff_code = SWARM_HANDOFF_TOOLS + "\n\n" + handoff_tool_imports
|
|
|
|
# agents.py
|
|
if triage_only:
|
|
agent_code = f'''\
|
|
\"\"\"Agent definitions for {project_name} swarm.
|
|
|
|
Triage-only mode: the triage agent routes to specialists who handle the request.
|
|
Specialists may or may not have handoff tools depending on the use case.
|
|
\"\"\"
|
|
from langchain.agents import create_agent
|
|
from langchain_openai import ChatOpenAI
|
|
from .handoff_tools import (
|
|
{", ".join(f"transfer_to_{n}" for n in agent_names)}
|
|
)
|
|
|
|
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
|
|
|
|
# Triage agent — only routes, never answers
|
|
triage_agent = create_agent(
|
|
llm,
|
|
tools=[{", ".join(f"transfer_to_{n}" for n in agent_names)}],
|
|
system_prompt=(
|
|
"You are a triage agent. Analyze the request and transfer "
|
|
"to the appropriate specialist using the transfer tools. "
|
|
"Do NOT try to answer questions yourself — always transfer."
|
|
),
|
|
)
|
|
'''
|
|
else:
|
|
# Full swarm: each specialist gets handoff tools for all OTHER agents
|
|
agent_handoff_imports = "\n".join(
|
|
f'from .handoff_tools import transfer_to_{n}'
|
|
for n in agent_names
|
|
)
|
|
agent_code = f'''\
|
|
\"\"\"Agent definitions for {project_name} swarm.\"\"\"
|
|
from langchain.agents import create_agent
|
|
from langchain_openai import ChatOpenAI
|
|
{agent_handoff_imports}
|
|
|
|
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
|
|
|
|
# Triage agent — only routes, never answers
|
|
triage_agent = create_agent(
|
|
llm,
|
|
tools=[{", ".join(f"transfer_to_{n}" for n in agent_names)}],
|
|
system_prompt=(
|
|
"You are a triage agent. Analyze the request and transfer "
|
|
"to the appropriate specialist. Do NOT answer questions "
|
|
"yourself — always transfer. If multiple issues exist, "
|
|
"transfer to the most urgent one first."
|
|
),
|
|
)
|
|
'''
|
|
|
|
for name, label in zip(agent_names, agent_labels):
|
|
other_handoffs = [f"transfer_to_{n}" for n in agent_names if n != name]
|
|
handoff_str = ",\n ".join(other_handoffs)
|
|
|
|
agent_code += f'''
|
|
|
|
# {label} specialist
|
|
{name}_agent = create_agent(
|
|
llm,
|
|
tools=[
|
|
# Add domain tools here
|
|
# e.g., lookup_{name}_info, do_{name}_action,
|
|
{handoff_str},
|
|
],
|
|
system_prompt=(
|
|
"You are a {label.lower()} specialist. "
|
|
"Help with {label.lower()} issues. "
|
|
"If the customer has issues outside your domain, "
|
|
"transfer to the appropriate specialist."
|
|
),
|
|
)
|
|
'''
|
|
|
|
# graph.py
|
|
route_code = f'''\
|
|
\"\"\"Graph assembly for {project_name} swarm multi-agent system.\"\"\"
|
|
from typing import Literal
|
|
|
|
from langchain_core.messages import AIMessage
|
|
from langgraph.graph import StateGraph, START, END
|
|
from langgraph.checkpoint.memory import MemorySaver
|
|
|
|
from .state import {state_class}
|
|
from .agents import triage_agent, {", ".join(f"{name}_agent" for name in agent_names)}
|
|
'''
|
|
|
|
route_code += f"""
|
|
|
|
# Node wrappers
|
|
def triage_node(state: {state_class}) -> Command:
|
|
result = triage_agent.invoke({{"messages": state["messages"]}})
|
|
return result # Command from handoff tool
|
|
|
|
"""
|
|
|
|
for name, label in zip(agent_names, agent_labels):
|
|
route_code += f"""
|
|
def {name}_node(state: {state_class}) -> dict:
|
|
result = {name}_agent.invoke({{"messages": state["messages"]}})
|
|
return {{
|
|
"messages": result["messages"][-1:],
|
|
"resolution_notes": [f"{label}: {{result['messages'][-1].content[:200]}}"],
|
|
}}
|
|
|
|
"""
|
|
|
|
route_code += f"""
|
|
def route_after_agent(
|
|
state: {state_class},
|
|
) -> Literal[{', '.join(f'"{n}"' for n in agent_names)}, "__end__"]:
|
|
\"\"\"Route to the next agent or end based on state and handoff count.\"\"\"
|
|
|
|
# Recursion guard
|
|
if state.get("handoff_count", 0) >= 3:
|
|
return "__end__"
|
|
|
|
messages = state.get("messages", [])
|
|
if messages:
|
|
last_msg = messages[-1]
|
|
if isinstance(last_msg, AIMessage) and not last_msg.tool_calls:
|
|
return "__end__" # No tool calls = done
|
|
|
|
current = state.get("current_agent", "")
|
|
if current in ({', '.join(f'"{n}"' for n in agent_names)}):
|
|
return current
|
|
return "__end__"
|
|
|
|
|
|
def build_graph() -> StateGraph:
|
|
\"\"\"Assemble and compile the swarm multi-agent graph.\"\"\"
|
|
builder = StateGraph({state_class})
|
|
|
|
# Add nodes
|
|
builder.add_node("triage", triage_node)
|
|
{chr(10) + ' '.join(f'builder.add_node("{n}", {n}_node)' for n in agent_names)}
|
|
|
|
# Wire edges
|
|
builder.add_edge(START, "triage")
|
|
|
|
# Each specialist can route to any other specialist or end
|
|
for node in [{', '.join(f'"{n}"' for n in agent_names)}]:
|
|
builder.add_conditional_edges(
|
|
node,
|
|
route_after_agent,
|
|
[{', '.join(f'"{n}"' for n in agent_names)}, END],
|
|
)
|
|
|
|
checkpointer = MemorySaver()
|
|
return builder.compile(checkpointer=checkpointer)
|
|
"""
|
|
|
|
# Write files
|
|
with open(os.path.join(dir_path, "state.py"), "w") as f:
|
|
f.write(state_code)
|
|
with open(os.path.join(dir_path, "handoff_tools.py"), "w") as f:
|
|
f.write(handoff_code)
|
|
with open(os.path.join(dir_path, "agents.py"), "w") as f:
|
|
f.write(agent_code)
|
|
with open(os.path.join(dir_path, "graph.py"), "w") as f:
|
|
f.write(route_code)
|
|
|
|
# Add Command import to graph.py
|
|
graph_path = os.path.join(dir_path, "graph.py")
|
|
with open(graph_path) as f:
|
|
content = f.read()
|
|
content = content.replace(
|
|
"from .agents import",
|
|
"from langgraph.types import Command\n\nfrom .agents import"
|
|
)
|
|
with open(graph_path, "w") as f:
|
|
f.write(content)
|
|
|
|
print(f"Swarm project generated at: {dir_path}")
|
|
print(f"Files: state.py, handoff_tools.py, agents.py, graph.py")
|
|
print(f"Agents: {', '.join(agent_names)}")
|
|
if triage_only:
|
|
print("Mode: triage-only (specialists handle requests without further handoffs)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(description="Generate LangGraph swarm pattern scaffold")
|
|
parser.add_argument("--name", required=True, help="Project name (e.g., support)")
|
|
parser.add_argument("--agents", required=True, help="Comma-separated agent names (e.g., billing,tech,account)")
|
|
parser.add_argument("--output", default=".", help="Output directory (default: current)")
|
|
parser.add_argument("--triage-only", action="store_true",
|
|
help="Triage routes to specialists who handle without further handoffs")
|
|
args = parser.parse_args()
|
|
|
|
agents = [a.strip() for a in args.agents.split(",")]
|
|
generate_swarm_project(args.name, agents, args.output, args.triage_only)
|