LangGraph Basics for CitadelMesh
LangGraph powers CitadelMesh agents with state machines that orchestrate complex building operations. This guide introduces LangGraph concepts and how they're used in CitadelMesh.
What is LangGraph?
LangGraph is a framework for building stateful, multi-step agents using state machines. In CitadelMesh:
- Agents are state machines that process events and make decisions
- States represent stages in building control workflows
- Edges define transitions between states based on conditions
- Nodes execute logic for each state (API calls, policy checks, etc.)
Why LangGraph for Building Automation?
✅ Deterministic - Predictable state transitions ✅ Debuggable - Clear execution flow ✅ Testable - Mock states and transitions ✅ Visualizable - Generate state machine diagrams ✅ Resumable - Persist and resume long-running processes
Core Concepts
1. State
State is the data that flows through your agent:
from dataclasses import dataclass, field
from typing import List, Dict, Any
from enum import Enum
class ThreatLevel(Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
@dataclass
class SecurityState:
"""State for security agent"""
events: List[Dict] = field(default_factory=list)
threat_level: ThreatLevel = ThreatLevel.LOW
response_plan: List[str] = field(default_factory=list)
context: Dict[str, Any] = field(default_factory=dict)
next_action: str = None
State is immutable - each node returns a new state.
2. Nodes
Nodes are functions that process state:
async def analyze_threat(state: SecurityState) -> SecurityState:
"""Analyze security events to determine threat level"""
# Process events
threat_score = 0
for event in state.events:
if event["severity"] == "high":
threat_score += 10
# Update threat level
if threat_score >= 20:
state.threat_level = ThreatLevel.CRITICAL
elif threat_score >= 10:
state.threat_level = ThreatLevel.HIGH
else:
state.threat_level = ThreatLevel.MEDIUM
return state
3. Edges
Edges connect nodes and define transitions:
from langgraph.graph import StateGraph, END
# Create graph
workflow = StateGraph(SecurityState)
# Add nodes
workflow.add_node("monitor", monitor_events)
workflow.add_node("analyze", analyze_threat)
workflow.add_node("respond", execute_response)
# Add edges
workflow.add_edge("monitor", "analyze") # Always go to analyze
workflow.add_edge("respond", END) # End after response
4. Conditional Edges
Route based on state:
def determine_response(state: SecurityState) -> str:
"""Decide next action based on threat level"""
if state.threat_level == ThreatLevel.CRITICAL:
return "escalate"
elif state.threat_level == ThreatLevel.HIGH:
return "respond"
else:
return "monitor"
# Add conditional routing
workflow.add_conditional_edges(
"analyze", # From node
determine_response, # Decision function
{
"escalate": "escalate", # Route to escalate node
"respond": "respond", # Route to respond node
"monitor": "monitor" # Route back to monitor
}
)
5. Entry Point
Define where execution starts:
# Set entry point
workflow.set_entry_point("monitor")
# Compile graph
graph = workflow.compile()
Building a Simple Agent
Let's build a temperature control agent step-by-step.
Step 1: Define State
from dataclasses import dataclass
from typing import Optional
@dataclass
class TemperatureState:
"""State for temperature control agent"""
zone_id: str
current_temp: float
target_temp: float
action: Optional[str] = None
error: Optional[str] = None
Step 2: Create Nodes
async def read_temperature(state: TemperatureState) -> TemperatureState:
"""Read current temperature from sensor"""
# Simulate reading from sensor
state.current_temp = 72.0
print(f"Current temperature: {state.current_temp}°F")
return state
async def calculate_action(state: TemperatureState) -> TemperatureState:
"""Determine heating/cooling action"""
diff = state.target_temp - state.current_temp
if abs(diff) < 1.0:
state.action = "maintain"
elif diff > 0:
state.action = "heat"
else:
state.action = "cool"
print(f"Action: {state.action}")
return state
async def execute_action(state: TemperatureState) -> TemperatureState:
"""Execute HVAC command"""
print(f"Executing: {state.action} in {state.zone_id}")
# In production: call MCP adapter
return state
Step 3: Build Graph
from langgraph.graph import StateGraph, END
def build_temperature_agent():
"""Build temperature control state machine"""
# Create graph
workflow = StateGraph(TemperatureState)
# Add nodes
workflow.add_node("read_temp", read_temperature)
workflow.add_node("calculate", calculate_action)
workflow.add_node("execute", execute_action)
# Define flow
workflow.add_edge("read_temp", "calculate")
# Conditional routing after calculation
workflow.add_conditional_edges(
"calculate",
lambda state: "execute" if state.action != "maintain" else "done",
{
"execute": "execute",
"done": END
}
)
workflow.add_edge("execute", END)
# Set entry point
workflow.set_entry_point("read_temp")
return workflow.compile()
Step 4: Run Agent
import asyncio
async def main():
# Build agent
agent = build_temperature_agent()
# Initial state
initial_state = TemperatureState(
zone_id="zone-lobby",
current_temp=0.0, # Will be read
target_temp=72.0
)
# Run agent
final_state = await agent.ainvoke(initial_state)
print(f"\nFinal state: {final_state}")
# Run
asyncio.run(main())
Output:
Current temperature: 72.0°F
Action: maintain
Final state: TemperatureState(
zone_id='zone-lobby',
current_temp=72.0,
target_temp=72.0,
action='maintain',
error=None
)
CitadelMesh Agent Pattern
All CitadelMesh agents follow this pattern:
from src.agents.runtime.base_agent import BaseAgent, AgentConfig, AgentState
from langgraph.graph import StateGraph, END
class MyBuildingAgent(BaseAgent):
"""Custom building control agent"""
def __init__(self, config: AgentConfig):
super().__init__(config)
def build_graph(self) -> StateGraph:
"""Build the agent state machine"""
workflow = StateGraph(AgentState)
# Add your nodes
workflow.add_node("monitor", self._monitor)
workflow.add_node("analyze", self._analyze)
workflow.add_node("execute", self._execute)
# Define transitions
workflow.add_edge("monitor", "analyze")
workflow.add_conditional_edges(
"analyze",
self._decide_action,
{
"execute": "execute",
"wait": "monitor"
}
)
workflow.add_edge("execute", END)
workflow.set_entry_point("monitor")
return workflow.compile()
async def _monitor(self, state: AgentState) -> AgentState:
"""Monitor building systems"""
# Your logic here
return state
async def _analyze(self, state: AgentState) -> AgentState:
"""Analyze data and make decisions"""
# Your logic here
return state
async def _execute(self, state: AgentState) -> AgentState:
"""Execute building control actions"""
# Your logic here
return state
def _decide_action(self, state: AgentState) -> str:
"""Routing logic"""
# Your decision logic
return "execute"
async def process_event(self, event):
"""Handle incoming events"""
# Implement event processing
pass
Debugging State Machines
1. Print State Transitions
async def debug_node(state: SecurityState) -> SecurityState:
"""Node with debug output"""
print(f"[DEBUG] Entering node with state: {state}")
# Process...
print(f"[DEBUG] Exiting node with state: {state}")
return state
2. Visualize Graph
from langgraph.graph import StateGraph
# Build graph
graph = build_temperature_agent()
# Generate mermaid diagram
print(graph.get_graph().draw_mermaid())
Output:
graph TD
read_temp --> calculate
calculate --> execute
calculate --> END
execute --> END
3. Step Through Execution
# Run step-by-step
agent = build_temperature_agent()
state = initial_state
# Execute one step at a time
for step in agent.stream(state):
print(f"Step: {step}")
input("Press Enter for next step...")
4. Add Breakpoints
async def analyze_with_breakpoint(state: SecurityState) -> SecurityState:
"""Node with breakpoint for debugging"""
import pdb; pdb.set_trace() # Breakpoint here
# Continue debugging...
return state
Advanced Patterns
1. Parallel Execution
Execute multiple nodes in parallel:
from langgraph.graph import StateGraph
workflow = StateGraph(SecurityState)
# These can run in parallel
workflow.add_node("check_cameras", check_cameras)
workflow.add_node("check_doors", check_doors)
workflow.add_node("check_sensors", check_sensors)
# Converge results
workflow.add_node("merge_results", merge_results)
# Parallel edges
workflow.add_edge("start", "check_cameras")
workflow.add_edge("start", "check_doors")
workflow.add_edge("start", "check_sensors")
# Wait for all to complete
workflow.add_edge("check_cameras", "merge_results")
workflow.add_edge("check_doors", "merge_results")
workflow.add_edge("check_sensors", "merge_results")
2. Looping
Create feedback loops:
async def retry_logic(state: MyState) -> str:
"""Retry up to 3 times"""
if state.retry_count < 3:
return "retry"
else:
return "failed"
workflow.add_conditional_edges(
"execute",
retry_logic,
{
"retry": "execute", # Loop back
"failed": "handle_failure"
}
)
3. Subgraphs
Compose complex agents from smaller graphs:
# Sub-agent for door control
door_agent = build_door_control_graph()
# Main security agent
workflow = StateGraph(SecurityState)
workflow.add_node("door_control", door_agent)
workflow.add_edge("analyze", "door_control")
Common Pitfalls
Issue: State not updating
Problem: Mutations don't propagate
# ❌ Wrong - mutating state
async def bad_node(state: MyState) -> MyState:
state.value = 10 # Mutation might not persist
return state
Solution: Return new state or use dataclasses properly
# ✅ Correct
from dataclasses import replace
async def good_node(state: MyState) -> MyState:
return replace(state, value=10)
Issue: Infinite loops
Problem: No exit condition
# ❌ Wrong - can loop forever
workflow.add_edge("process", "process")
Solution: Add loop counter and exit condition
def check_exit(state: MyState) -> str:
if state.iterations >= 10:
return "exit"
return "continue"
workflow.add_conditional_edges("process", check_exit, {
"continue": "process",
"exit": END
})
Testing State Machines
import pytest
@pytest.mark.asyncio
async def test_temperature_agent():
"""Test temperature control agent"""
agent = build_temperature_agent()
# Test heating scenario
state = TemperatureState(
zone_id="test-zone",
current_temp=65.0,
target_temp=72.0
)
result = await agent.ainvoke(state)
assert result.action == "heat"
assert result.error is None
Next Steps
- Agent Template - Create your first agent
- Policy Integration - Connect to OPA
- Testing Agents - Comprehensive testing
Resources
Master state machines and build intelligent building agents! Continue to Agent Template.