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.
193 lines
8.0 KiB
Python
193 lines
8.0 KiB
Python
"""
|
|
Subgraph Agent — Composition Template
|
|
|
|
Demonstrates two patterns for composing subgraphs within a parent graph:
|
|
|
|
Pattern A: Different state schemas (call inside a node)
|
|
- Parent and subgraph have no shared keys
|
|
- Use a wrapper function to transform state at the boundary
|
|
|
|
Pattern B: Shared state keys (add as node)
|
|
- Parent and subgraph share state keys (e.g., messages)
|
|
- Pass compiled subgraph directly to add_node — no wrapper needed
|
|
|
|
Requirements:
|
|
pip install langgraph langchain langchain-openai
|
|
"""
|
|
|
|
from typing_extensions import TypedDict
|
|
from langgraph.graph.state import StateGraph, MessagesState, START, END
|
|
from langgraph.checkpoint.memory import MemorySaver
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Pattern A: Different State Schemas
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
# Subgraph state — entirely different keys from parent
|
|
class ResearchState(TypedDict):
|
|
topic: str
|
|
findings: list[str]
|
|
|
|
def search_node(state: ResearchState):
|
|
"""Simulate searching for information on the topic."""
|
|
return {"findings": [f"Search results for: {state['topic']}"]}
|
|
|
|
def analyze_node(state: ResearchState):
|
|
"""Analyze the search findings."""
|
|
return {"findings": state["findings"] + ["Analysis complete."]}
|
|
|
|
# Build and compile subgraph
|
|
research_builder = StateGraph(ResearchState)
|
|
research_builder.add_node("search", search_node)
|
|
research_builder.add_node("analyze", analyze_node)
|
|
research_builder.add_edge(START, "search")
|
|
research_builder.add_edge("search", "analyze")
|
|
research_builder.add_edge("analyze", END)
|
|
research_subgraph = research_builder.compile()
|
|
|
|
# Parent graph with different state
|
|
class QueryState(TypedDict):
|
|
question: str
|
|
answer: str
|
|
|
|
def call_research_team(state: QueryState):
|
|
"""Wrapper: transforms parent state to subgraph state and back."""
|
|
# Transform parent → subgraph
|
|
subgraph_input = {"topic": state["question"], "findings": []}
|
|
subgraph_output = research_subgraph.invoke(subgraph_input)
|
|
# Transform subgraph → parent
|
|
return {"answer": subgraph_output["findings"][-1]}
|
|
|
|
parent_a = StateGraph(QueryState)
|
|
parent_a.add_node("research", call_research_team)
|
|
parent_a.add_edge(START, "research")
|
|
parent_a.add_edge("research", END)
|
|
pattern_a_graph = parent_a.compile()
|
|
|
|
# Test Pattern A
|
|
result_a = pattern_a_graph.invoke({"question": "LangGraph subgraphs", "answer": ""})
|
|
print(f"Pattern A result: {result_a['answer']}")
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Pattern B: Shared State Keys (Add Subgraph as Node)
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
# Subgraph that operates on shared MessagesState
|
|
def sub_agent_node(state: MessagesState):
|
|
"""A simple agent node that responds to user messages."""
|
|
last_msg = state["messages"][-1].content if state["messages"] else ""
|
|
return {
|
|
"messages": [{"role": "assistant", "content": f"Subgraph processed: {last_msg[:50]}..."}]
|
|
}
|
|
|
|
sub_builder = StateGraph(MessagesState)
|
|
sub_builder.add_node("sub_agent", sub_agent_node)
|
|
sub_builder.add_edge(START, "sub_agent")
|
|
sub_builder.add_edge("sub_agent", END)
|
|
subgraph_b = sub_builder.compile()
|
|
|
|
# Parent graph — add compiled subgraph as a node directly
|
|
parent_b = StateGraph(MessagesState)
|
|
parent_b.add_node("entry", lambda s: {"messages": s["messages"]})
|
|
parent_b.add_node("subgraph_node", subgraph_b) # <-- compiled graph as node
|
|
parent_b.add_edge(START, "entry")
|
|
parent_b.add_edge("entry", "subgraph_node")
|
|
parent_b.add_edge("subgraph_node", END)
|
|
pattern_b_graph = parent_b.compile()
|
|
|
|
# Test Pattern B
|
|
result_b = pattern_b_graph.invoke(
|
|
{"messages": [{"role": "user", "content": "Hello from the parent graph!"}]}
|
|
)
|
|
print(f"Pattern B result: {result_b['messages'][-1].content}")
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Pattern C: Per-Thread Subgraph with Namespace Isolation
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
from langchain.agents import create_agent
|
|
from langchain_core.tools import tool
|
|
from langchain_openai import ChatOpenAI
|
|
|
|
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
|
|
|
|
@tool
|
|
def fruit_info(fruit_name: str) -> str:
|
|
"""Look up fruit info."""
|
|
return f"Info about {fruit_name}: fresh and delicious."
|
|
|
|
@tool
|
|
def veggie_info(veggie_name: str) -> str:
|
|
"""Look up veggie info."""
|
|
return f"Info about {veggie_name}: healthy and green."
|
|
|
|
def create_sub_agent(model, *, name, **kwargs):
|
|
"""Wrap an agent with a unique node name for namespace isolation."""
|
|
agent = create_agent(model=model, name=name, **kwargs)
|
|
return (
|
|
StateGraph(MessagesState)
|
|
.add_node(name, agent) # unique name → stable namespace
|
|
.add_edge("__start__", name)
|
|
.compile()
|
|
)
|
|
|
|
fruit_agent = create_sub_agent(
|
|
"gpt-4o-mini", name="fruit_agent",
|
|
tools=[fruit_info],
|
|
prompt="You are a fruit expert. Use the fruit_info tool.",
|
|
checkpointer=True,
|
|
)
|
|
|
|
veggie_agent = create_sub_agent(
|
|
"gpt-4o-mini", name="veggie_agent",
|
|
tools=[veggie_info],
|
|
prompt="You are a veggie expert. Use the veggie_info tool.",
|
|
checkpointer=True,
|
|
)
|
|
|
|
@tool
|
|
def ask_fruit_expert(question: str) -> str:
|
|
"""Ask the fruit expert. Use for ALL fruit questions."""
|
|
response = fruit_agent.invoke(
|
|
{"messages": [{"role": "user", "content": question}]},
|
|
)
|
|
return response["messages"][-1].content
|
|
|
|
@tool
|
|
def ask_veggie_expert(question: str) -> str:
|
|
"""Ask the veggie expert. Use for ALL veggie questions."""
|
|
response = veggie_agent.invoke(
|
|
{"messages": [{"role": "user", "content": question}]},
|
|
)
|
|
return response["messages"][-1].content
|
|
|
|
# Outer agent with checkpointer
|
|
from langchain.agents.middleware import ToolCallLimitMiddleware
|
|
|
|
orchestrator = create_agent(
|
|
llm,
|
|
tools=[ask_fruit_expert, ask_veggie_expert],
|
|
prompt=(
|
|
"You have two experts: ask_fruit_expert and ask_veggie_expert. "
|
|
"ALWAYS delegate questions to the appropriate expert."
|
|
),
|
|
middleware=[
|
|
ToolCallLimitMiddleware(tool_name="ask_fruit_expert", run_limit=1),
|
|
ToolCallLimitMiddleware(tool_name="ask_veggie_expert", run_limit=1),
|
|
],
|
|
checkpointer=MemorySaver(),
|
|
)
|
|
|
|
print("Pattern C: Namespace-isolated per-thread subagents ready.")
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Usage Notes
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
#
|
|
# - Use Pattern A when parent and subgraph have different data models
|
|
# - Use Pattern B when both operate on shared state (e.g., messages)
|
|
# - Use Pattern C when subagents need per-thread memory AND namespace isolation
|
|
# - Per-thread subgraphs (checkpointer=True) cannot run in parallel —
|
|
# use ToolCallLimitMiddleware to prevent parallel tool calls
|
|
# - Per-invocation (default) is the right choice for most multi-agent systems
|