-
Notifications
You must be signed in to change notification settings - Fork 0
Home
Complete reference for every YAML field, node type, edge type, and generated output. Read this to understand exactly what GraphForge compiles and why.
- How GraphForge Works
- YAML Spec — Top-Level Structure
- graph — Graph Configuration
- State — TypedDict vs Pydantic
- nodes — Node Types
- chains — LLM Chain Configuration
- edges — Edge Types
- tools — Tool Configuration
- Memory & Checkpointing
- Hierarchical Sub-Agents (
graphRef) - Generated Output — File by File
- CLI Reference
- Global Configuration (
configs/config.yaml) - Complete End-to-End Example
- Roadmap
- How AgentGraph Compiler Works
- CLI Usage & Workflows
- YAML Specification Reference
- Architecture Patterns & Scaffolding
- State Schemas & Reducers
- Sub-Agent & Parallel Fan-Out Deep Dive
- Vector Store & Retrieval Architecture
- Generated Code Anatomy
┌───────────────────────────────┐
│ agent.yaml │
│ (YAML Graph Specification) │
└───────────────┬───────────────┘
│
▼
┌───────────────────────────────┐
│ agentgraph-compiler │
│ (Validation & Code Gen) │
└───────────────┬───────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
graphs/state.py graphs/graph.py graphs/nodes/*
AgentGraph Compiler reads a declarative YAML file and generates a complete, runnable Python codebase wired to LangGraph. You never write StateGraph, add_node, or add_edge by hand.
- Parse YAML → Python dict
- Validate schema (Pydantic) → check edge targets, supervisor route fields, fan_out params
- Create directories → graphs/, nodes/, chains/, tools/, routers/
-
Generate
graphs/state.py→ TypedDict or Pydantic state class -
Scaffold nodes → one
.pyfile per node with typed function stub -
Scaffold chains → one
.pyfile per chain with LLM binding comments -
Scaffold tools → one
.pyfile per tool (or MCP client stub) - Scaffold routers → conditional edge router function stubs
-
Generate
graphs/graph.py→ fully-wiredStateGraphwith all edges,ToolNode,Sendfan-outs, checkpointer -
Generate
main.py→ runnable entrypoint withapp.stream() -
Generate
flowchart.md→ Mermaid diagram of the compiled graph
Every agent YAML can have the following top-level keys:
version: "1.0" # Optional. Schema version for forward-compat tracking.
graph: # Required. Graph metadata and state configuration.
...
nodes: # Required. All nodes in the graph.
...
chains: # Optional. LLM chain / runnable definitions.
...
edges: # Required. Graph topology (connections between nodes).
...
tools: # Optional. LangChain tools or MCP client stubs.
...
retriever: # Optional. Vector store retriever configuration.
...The graph block controls the output directory, state schema, shared fields, and persistence.
graph:
source_dir: "my_agent" # (str) Output directory for all generated Python files.
# Sub-agents are nested under this directory.
state_type: typeddict # (str) "typeddict" (default) | "pydantic"
# Controls what base class state.py generates.
state_schema: "graphs.state.AgentState" # (str) Dotted Python path to the state class.
# Format: "<module_path>.<ClassName>"
state_fields: # (dict) Field name → Python type string.
messages: "List[BaseMessage]" # → auto-wired with add_messages reducer
next: "str" # Simple scalar field
context: "dict" # Dict field
documents: "List[str]" # List of strings
score: "float" # Numeric field
approved: "bool" # Boolean field
shared_fields: [messages, context] # (list) Fields readable/writable by sub-agents.
# Annotated in state.py docstring for clarity.
memory: # (dict) Checkpointing configuration.
type: sqlite # "sqlite" | "in_memory" | omit for stateless
db_path: ".checkpoints/agent.db" # Only used for sqlite.| YAML type string | Generated TypedDict field | Generated Pydantic field |
|---|---|---|
"List[BaseMessage]" |
Annotated[List[BaseMessage], add_messages] |
Annotated[List[BaseMessage], add_messages] = Field(default_factory=list) |
"List[str]" |
List[str] |
List[str] = Field(default_factory=list) |
"str" |
str |
str = Field(default='') |
"Optional[str]" |
Optional[str] |
Optional[str] = Field(default='') |
"dict" |
dict |
dict = Field(default_factory=dict) |
"int" / "float"
|
int / float
|
int = Field(default=0) |
"bool" |
bool |
bool = Field(default=False) |
"Any" |
Any |
Any = Field(...) |
graph:
state_type: typeddict
state_fields:
messages: "List[BaseMessage]"
question: "str"
generation: "str"Generated graphs/state.py:
# Auto-generated state schema — TypedDict (LangGraph-native)
from typing import TypedDict, List, Annotated
from langchain_core.messages import BaseMessage
from langgraph.graph.message import add_messages
class AgentState(TypedDict):
messages: Annotated[List[BaseMessage], add_messages]
question: str
generation: strTypedDict is the preferred LangGraph state type. Annotated[..., add_messages] means each update appends messages rather than overwriting the list — critical for multi-turn conversation history.
graph:
state_type: pydantic
state_fields:
question: "str"
documents: "List[str]"Generated graphs/state.py:
# Auto-generated state schema — Pydantic BaseModel
from pydantic import BaseModel, Field
from typing import List
class AgentState(BaseModel):
question: str = Field(default='', description='Generated field')
documents: List[str] = Field(default_factory=list, description='Generated field')Use pydantic when you need custom validators, serializers, or integration with Pydantic-aware systems.
The most common type. Calls a Python function that takes state and returns a dict of updates.
nodes:
retrieve:
module: "graphs.nodes.retrieve" # (str) Python module path (dotted).
function: "retrieve" # (str) Function name inside the module.
chains: ["retrieval_grader"] # (list) Chain names to import into this node.
description: "Pulls document chunks from the vector store."Generated graphs/nodes/retrieve.py:
from my_agent.graphs.state import AgentState
from my_agent.graphs.chains import retrieval_grader
def retrieve(state: AgentState) -> dict:
"""
Pulls document chunks from the vector store.
"""
print("---RUNNING NODE: retrieve---")
# Placeholder: implement node logic here and return a state update dict.
return {}You implement:
def retrieve(state: AgentState) -> dict:
docs = retriever.invoke(state["question"])
return {"documents": [d.page_content for d in docs]}Use when the node performs async I/O (concurrent HTTP calls, async LLM clients, database queries).
nodes:
async_retriever:
module: "graphs.nodes.async_retriever"
function: "async_retriever"
async_fn: true # ← scaffolds `async def`
description: "Async concurrent document retrieval."Generated graphs/nodes/async_retriever.py:
from my_agent.graphs.state import AgentState
async def async_retriever(state: AgentState) -> dict:
"""
Async concurrent document retrieval.
"""
# This is an async node — use `await` for async LLM/tool calls.
print("---RUNNING ASYNC NODE: async_retriever---")
return {}You implement:
async def async_retriever(state: AgentState) -> dict:
results = await asyncio.gather(
vector_store.asimilarity_search(state["question"]),
web_client.aget(state["question"]),
)
return {"documents": [doc for r in results for doc in r]}Note: Run async graphs with
app.astream()(notapp.stream()).
An LLM-driven router that reads the user request and writes a routing decision to state[route_field].
nodes:
supervisor:
type: supervisor # Required. Triggers supervisor generator.
provider: openai # openai | anthropic | ollama | groq
llm: gpt-4o # Model name for the LLM.
route_field: next # (str) State field to write routing decision to. Default: "next"
system_prompt: "templates/supervisor.prompt" # Path to a .prompt template file.
routes_to: [rag_agent, code_agent] # (list) Node names this supervisor can dispatch to.
description: "Routes requests to the appropriate sub-agent."Generated graphs/nodes/supervisor.py:
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from my_agent.graphs.state import AgentState
llm = ChatOpenAI(model="gpt-4o", temperature=0)
SYSTEM_PROMPT = """You are a supervisor routing requests to: rag_agent, code_agent.
Respond with JSON: {"next": "<agent_name>"}"""
prompt = ChatPromptTemplate.from_messages([
("system", SYSTEM_PROMPT),
("human", "{messages}"),
])
chain = prompt | llm
def supervisor(state: AgentState) -> dict:
"""LLM-driven routing supervisor."""
result = chain.invoke({"messages": state["messages"]})
# TODO: parse result to extract routing decision
return {"next": "rag_agent"} # Replace with parsed resultHow routing works:
The supervisor_route edge reads state["next"] (or your custom route_field) to dispatch:
supervisor → state["next"] == "rag_agent" → rag_agent node
→ state["next"] == "code_agent" → code_agent node
→ state["next"] == "__end__" → post_process (convergence)
References another YAML file. GraphForge compiles it recursively and nests the output under the parent's source_dir.
nodes:
rag_agent:
graphRef: "sub-agents/adaptive-rag.yaml" # Relative path from this YAML's directory.
description: "Adaptive RAG sub-agent for knowledge-base questions."The sub-agent YAML is compiled independently and its compiled app is imported into the parent graph as a node:
# Auto-generated in parent graphs/graph.py:
from my_agent.adaptive_rag.graphs.graph import app as rag_agent_app
workflow.add_node('rag_agent', rag_agent_app)See Section 10 for full sub-agent documentation.
When an edge has interrupt_before: true, GraphForge scaffolds a human_review node:
# Auto-generated graphs/nodes/human_review.py
from my_agent.graphs.state import AgentState
def human_review(state: AgentState) -> dict:
"""
Pauses graph execution for human review/approval.
Resume by calling: app.invoke(None, config={"configurable": {"thread_id": "..."}})
Or update state first: app.update_state(config, {"approved": True})
"""
print("---HUMAN REVIEW NODE: awaiting review input---")
return {}Chains are LangChain runnables (prompt | llm | parser). GraphForge scaffolds the file with the correct provider imports and LLM binding comments.
chains:
retrieval_grader:
module: "graphs.chains.retrieval_grader" # (str) Python module path.
runnable: "retrieval_grader" # (str) Variable name exported from the module.
provider: anthropic # openai | anthropic | ollama | groq
llm: claude-3-5-haiku-20241022 # Model name.
temperature: 0.0 # LLM temperature.
prompt: "retrieval_grader" # Load from templates/retrieval_grader.prompt
tools: [web_search] # Tool names to import and bind to LLM.
description: "Binary relevance grader — returns 'yes' or 'no'."Generated graphs/chains/retrieval_grader.py:
"""
Binary relevance grader — returns 'yes' or 'no'.
"""
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
# --- LLM Configuration (Anthropic) ---
# TODO: Uncomment and configure your preferred LLM provider:
# from langchain_anthropic import ChatAnthropic
# llm = ChatAnthropic(model="claude-3-5-haiku-20241022", temperature=0.0)
# --- Prompt Template ---
prompt = ChatPromptTemplate.from_messages([
("system", """<content from templates/retrieval_grader.prompt>"""),
("human", "{input}"),
])
# --- Chain Definition ---
# If using an LLM: retrieval_grader = prompt | llm | StrOutputParser()
retrieval_grader = prompt | StrOutputParser()You implement:
from langchain_anthropic import ChatAnthropic
from langchain_core.output_parsers import JsonOutputParser
llm = ChatAnthropic(model="claude-3-5-haiku-20241022", temperature=0.0)
retrieval_grader = prompt | llm | JsonOutputParser()
provider value |
Import scaffolded | Class |
|---|---|---|
openai (default) |
from langchain_openai import ChatOpenAI |
ChatOpenAI |
anthropic |
from langchain_anthropic import ChatAnthropic |
ChatAnthropic |
ollama |
from langchain_ollama import ChatOllama |
ChatOllama |
groq |
from langchain_groq import ChatGroq |
ChatGroq |
A simple A → B connection with no routing logic.
- type: standard
start: retrieve # Source node name (or START)
end: grade_documents # Destination node name (or END)Generated:
workflow.add_edge('retrieve', 'grade_documents')Calls a Python function to decide which node to go to next.
- type: conditional
start: grade_documents
router:
module: "routers.router" # Module containing the router function.
function: "route_after_grading" # Function name.
mappings:
web_search: web_search # return value → node name
generate: generateGenerated routers/router.py (stub):
from my_agent.graphs.state import AgentState
def route_after_grading(state: AgentState) -> str:
"""
Conditional edge router. Return one of: ['web_search', 'generate']
"""
print("---RUNNING ROUTER: route_after_grading---")
# TODO: implement routing logic
# Example: return "web_search" if state["web_search"] else "generate"
return "generate"Generated in graphs/graph.py:
from my_agent.routers.router import route_after_grading
workflow.add_conditional_edges(
'grade_documents',
route_after_grading,
{'web_search': 'web_search', 'generate': 'generate'}
)You implement the router:
def route_after_grading(state: AgentState) -> str:
return "web_search" if state["web_search"] else "generate"Routes from a supervisor node based on the value of state[route_field]. Also wires return edges from each sub-agent back to the convergence node.
- type: supervisor_route
start: supervisor
routes_to: [rag_agent, code_agent] # Possible dispatch targets.
end: post_process # Convergence node all agents return to.Generated in graphs/graph.py:
# Supervisor routing: dispatches based on state['next']
workflow.add_conditional_edges(
'supervisor',
lambda state: state['next'] if isinstance(state, dict) else getattr(state, 'next'),
{'rag_agent': 'rag_agent', 'code_agent': 'code_agent', '__end__': 'post_process'}
)
# Return edges: sub-agents → convergence node
workflow.add_edge('rag_agent', 'post_process')
workflow.add_edge('code_agent', 'post_process')Compile-time validation: GraphForge raises
ValueErrorifroute_field(default:next) is not declared instate_fields.
Wires the ReAct pattern: LLM node → ToolNode → LLM node — looping until no tool calls remain.
- type: tool_loop
start: llm_agent # LLM node (must have tool_calls in state or as return value)
tool_node: tools # ToolNode name (auto-created by compiler)
end: END # Where to go when no more tool callsGenerated in graphs/graph.py:
from langgraph.prebuilt import ToolNode
from my_agent.graphs.tools import tools # list of LangChain tool functions
workflow.add_node('tools', ToolNode(tools=tools))
# Tool loop: llm_agent ↔ tools
workflow.add_edge('tools', 'llm_agent')
workflow.add_conditional_edges(
'llm_agent',
lambda s: 'tools' if (s.get('tool_calls') if isinstance(s, dict) else getattr(s, 'tool_calls', None)) else '__end__',
{'tools': 'tools', '__end__': END}
)Auto-generated graphs/tools/__init__.py:
# Export tools list for ToolNode
from my_agent.graphs.tools.web_search import web_search
from my_agent.graphs.tools.calculator import calculator
tools = [web_search, calculator]
__all__ = ['tools']Dispatches one node's output to multiple instances of another node simultaneously, using LangGraph's Send API.
- type: fan_out
start: coordinator # Node that produces the list to fan out over
target: worker # Node to run in parallel for each item
field: tasks # State field (must be a list) to iterate over
key: task # Key name in each Send payload dict
end: aggregator # Convergence node after all workers finishGenerated in graphs/graph.py:
from langgraph.types import Send
# Fan-out: Send each item in state['tasks'] to 'worker' in parallel
def _fan_out_coordinator_to_worker(state):
items = state['tasks'] if isinstance(state, dict) else getattr(state, 'tasks', [])
return [Send('worker', {'task': item}) for item in items]
workflow.add_conditional_edges(
'coordinator',
_fan_out_coordinator_to_worker,
['worker']
)
workflow.add_edge('worker', 'aggregator')How it works:
coordinator → [Send(worker, {task: "A"}), Send(worker, {task: "B"}), Send(worker, {task: "C"})]
↓ parallel execution ↓
worker(task="A") worker(task="B") worker(task="C")
↓ all complete ↓
aggregator
Pauses graph execution before a specified node. Requires a persistent checkpointer (sqlite).
- type: standard
start: drafter
end: human_review
interrupt_before: true # Pause before 'human_review' runsGenerated in graphs/graph.py:
app = workflow.compile(
checkpointer=_checkpointer,
interrupt_before=['human_review']
)Resume patterns:
thread_config = {"configurable": {"thread_id": "review-thread-1"}}
# Option 1: Resume without changes
for event in app.stream(None, config=thread_config):
print(event)
# Option 2: Inject feedback then resume
app.update_state(thread_config, {"feedback": "Shorten the intro.", "approved": True})
for event in app.stream(None, config=thread_config):
print(event)tools:
web_search:
module: "graphs.tools.web_search"
function: "web_search"
description: "Searches the web via Tavily for up-to-date information."Generated graphs/tools/web_search.py:
from langchain_core.tools import tool
@tool
def web_search(query: str) -> str:
"""
Searches the web via Tavily for up-to-date information.
"""
print("---RUNNING TOOL: web_search---")
# Placeholder: return string output
return "Placeholder response from tool: web_search"You implement:
from langchain_tavily import TavilySearch
search = TavilySearch(max_results=3)
@tool
def web_search(query: str) -> str:
"""Searches the web via Tavily for up-to-date information."""
results = search.invoke(query)
return "\n".join(r["content"] for r in results)tools:
knowledge_base:
module: "graphs.tools.knowledge_base"
function: "knowledge_base"
source: mcp # ← triggers MCP stub generation
server_url: "http://localhost:8000/mcp"
description: "Queries internal knowledge base via MCP server."Generated graphs/tools/knowledge_base.py:
"""
MCP Tool Client scaffold for knowledge_base.
Connects to external MCP Server at http://localhost:8000/mcp.
"""
from langchain_core.tools import tool
@tool
def knowledge_base(query: str) -> str:
"""
Queries internal knowledge base via MCP server. (MCP Tool Client)
"""
print("---RUNNING MCP TOOL: knowledge_base -> http://localhost:8000/mcp---")
# TODO: Initialize MCP Client session and call remote tool
return f"MCP Tool response from http://localhost:8000/mcp for: {query}"graph:
memory:
type: sqlite
db_path: ".checkpoints/agent.db"Generated in graphs/graph.py:
from langgraph.checkpoint.sqlite import SqliteSaver
import os
_DB_PATH = ".checkpoints/agent.db"
os.makedirs(os.path.dirname(_DB_PATH), exist_ok=True)
_checkpointer = SqliteSaver.from_conn_string(_DB_PATH)
app = workflow.compile(checkpointer=_checkpointer)graph:
memory:
type: in_memoryGenerated:
from langgraph.checkpoint.memory import MemorySaver
_checkpointer = MemorySaver()
app = workflow.compile(checkpointer=_checkpointer)Omit the memory block entirely:
graph:
source_dir: "my_agent"
# no memory: blockGenerated:
_checkpointer = None
app = workflow.compile()Sub-agents allow you to compose complex multi-agent systems from modular, independently-testable sub-graphs.
configs/agents/
├── agent.yaml ← parent: references sub-agents
└── sub-agents/
├── rag_agent.yaml ← compiled independently
└── code_agent.yaml ← compiled independently
Parent YAML:
nodes:
rag_agent:
graphRef: "sub-agents/rag_agent.yaml" # Path relative to THIS yaml file
description: "RAG sub-agent."What happens during compilation:
-
rag_agent.yamlis compiled first, generating code inmy_agent/rag_agent/ - The parent's
graphs/graph.pyimports the sub-agent's compiledapp:
from my_agent.rag_agent.graphs.graph import app as rag_agent_app
workflow.add_node('rag_agent', rag_agent_app)my_agent/ ← parent source_dir
├── graphs/graph.py ← parent graph (imports sub-agent apps)
├── graphs/state.py ← parent state
└── rag_agent/ ← sub-agent nested here (not at root!)
├── graphs/graph.py ← sub-agent graph
└── graphs/state.py ← sub-agent state
GraphForge automatically compares parent and sub-agent state fields and prints warnings for mismatches:
WARNING: State schema mismatch detected for sub-agent 'rag_agent':
- Missing on parent state: ['documents', 'web_search']
Add missing fields to parent state_fields to resolve.
The state schema for this graph. TypedDict by default.
class AgentState(TypedDict):
messages: Annotated[List[BaseMessage], add_messages]
question: str
generation: strPreservation rule: If all declared fields already exist in the file, the compiler skips regeneration — preserving any manual additions you made.
The fully-wired StateGraph. Do not edit manually — regenerate with forge compile.
# Auto-generated LangGraph workflow definition
from langgraph.graph import StateGraph, START, END
from my_agent.graphs.state import AgentState
from my_agent.graphs.nodes.retrieve import retrieve
from my_agent.graphs.nodes.generate import generate
workflow = StateGraph(AgentState)
# Add Nodes
workflow.add_node('retrieve', retrieve)
workflow.add_node('generate', generate)
# Add Edges
workflow.add_edge(START, 'retrieve')
workflow.add_edge('retrieve', 'generate')
workflow.add_edge('generate', END)
# Compile
app = workflow.compile(checkpointer=_checkpointer)One file per node. Edit these to implement your business logic.
from my_agent.graphs.state import AgentState
from my_agent.graphs.chains import retrieval_grader
def grade_documents(state: AgentState) -> dict:
"""
Filters retrieved document chunks and flags irrelevant results.
"""
print("---RUNNING NODE: grade_documents---")
# Placeholder: implement node logic here and return a state update dict.
return {}One file per chain. Edit to bind your LLM and prompt.
"""
Binary relevance grader.
"""
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
# --- LLM Configuration (Anthropic) ---
# from langchain_anthropic import ChatAnthropic
# llm = ChatAnthropic(model="claude-3-5-haiku-20241022", temperature=0.0)
prompt = ChatPromptTemplate.from_messages([
("system", "Grade the document relevance."),
("human", "{input}"),
])
retrieval_grader = prompt | StrOutputParser()The runnable entrypoint. Streams node events in real time.
import sys
from dotenv import load_dotenv
from my_agent.graphs.graph import app
load_dotenv()
def main():
question = sys.argv[1] if len(sys.argv) > 1 else "Hello!"
thread_config = {"configurable": {"thread_id": "main-thread"}}
inputs = {"question": question}
print(f"\n🚀 Running agent with: '{question}'\n{'─' * 50}")
for event in app.stream(inputs, config=thread_config, stream_mode="updates"):
for node_name, output in event.items():
print(f"\n📍 [{node_name}]")
print(output)
if __name__ == "__main__":
main()A Mermaid diagram of the compiled graph topology, auto-regenerated on every compile.
Compile a YAML spec into a Python scaffold.
uv run forge compile configs/agents/agent.yaml # Full compile
uv run forge compile configs/agents/agent.yaml --dry-run # Preview only (no files written)--dry-run runs full validation and prints what would be generated without touching the filesystem.
Validate YAML schema and edge targets without generating files.
uv run forge validate configs/agents/agent.yamlCatches:
- Pydantic schema errors (wrong field types, missing required fields)
- Edge targets referencing undefined nodes
-
supervisor_routeedges whereroute_fieldis not instate_fields -
fan_outedges missingtargetorfield
Scaffold starter config files in the current directory.
uv run forge init
uv run forge init ./my-projectCreates:
-
configs/config.yaml— global settings template -
configs/agents/agent.yaml— starter agent YAML
Ingest URLs from configs/config.yaml into a Qdrant vector store.
uv run forge ingest
uv run forge ingest --config configs/config.yamlCentralized defaults applied across all agents during compilation.
models:
chat:
name: gpt-4o # Default LLM used if node/chain doesn't declare one
temperature: 0.5
embedding:
name: text-embedding-3-small
provider: openai
vector_store:
name: qdrant
url: "http://localhost:6333"
collection: "my-collection"
ingestion:
urls:
- "https://example.com/docs"
- "https://example.com/blog"If present, GraphForge automatically injects retriever config into compiled agents:
config["retriever"] = {
"provider": "qdrant",
"collection": "my-collection",
"embedding_model": "text-embedding-3-small",
"top_k": 4
}Here is a full Adaptive RAG agent, YAML → generated code → implementation:
version: "1.0"
graph:
source_dir: "simple_rag_agent"
state_type: typeddict
state_schema: "graphs.state.RAGState"
state_fields:
question: "str"
generation: "str"
web_search: "bool"
documents: "List[str]"
memory:
type: in_memory
nodes:
retrieve:
module: "graphs.nodes.retrieve"
function: "retrieve"
description: "Pulls relevant document chunks from vector store."
grade_documents:
module: "graphs.nodes.grade_documents"
function: "grade_documents"
chains: ["retrieval_grader"]
description: "Grades retrieved docs for relevance."
web_search:
module: "graphs.nodes.web_search"
function: "web_search"
description: "Falls back to Tavily web search."
generate:
module: "graphs.nodes.generate"
function: "generate"
chains: ["generation_chain"]
description: "Generates final answer from context."
chains:
retrieval_grader:
provider: openai
llm: gpt-4o-mini
temperature: 0.0
generation_chain:
provider: openai
llm: gpt-4o
temperature: 0.2
edges:
- type: standard
start: START
end: retrieve
- type: standard
start: retrieve
end: grade_documents
- type: conditional
start: grade_documents
router:
module: "routers.router"
function: "route_after_grading"
mappings:
web_search: web_search
generate: generate
- type: standard
start: web_search
end: generate
- type: standard
start: generate
end: ENDuv run forge compile configs/agents/examples/simple_rag/agent.yaml# simple_rag_agent/graphs/nodes/retrieve.py
from simple_rag_agent.graphs.state import RAGState
from langchain_qdrant import QdrantVectorStore
retriever = QdrantVectorStore.from_existing_collection(...).as_retriever(k=4)
def retrieve(state: RAGState) -> dict:
docs = retriever.invoke(state["question"])
return {"documents": [d.page_content for d in docs]}# simple_rag_agent/routers/router.py
from simple_rag_agent.graphs.state import RAGState
def route_after_grading(state: RAGState) -> str:
return "web_search" if state["web_search"] else "generate"uv run python simple_rag_agent/main.py "What is self-RAG?"-
interrupt_after— pause graph after a node completes (mirror ofinterrupt_before) -
Custom state reducers — declare arbitrary reducer functions in YAML (beyond
add_messages) -
astream()entrypoint — generatedmain.pyusesasync def main()+app.astream()when async nodes are present -
Stream mode config —
stream_mode: values | updates | debug | messagesin graph config -
Commandprimitive — nodes can returnCommand(goto=..., update={})for dynamic in-node routing -
get_statehelper — generated utility for inspecting and replaying thread state history -
forge migrate— detect state schema drift and guide migration when YAML changes after compile -
docker-compose.yaml— agent + Qdrant + Redis sidecar example -
MessagesStateshorthand — auto-detect single-messages-field state and use built-inMessagesState