Skip to main content

LangChain / LangGraph Integration

Installation

pip install comply54 langchain-core langgraph langchain-anthropic

Patterns at a glance

comply54 ships three LangGraph primitives:

ImportTypeWhen to use
Comply54Guard + comply54_routeGuard nodeRecommended — intercepts tool calls in ReAct agents
compliance_node(compliance)State nodeCompliance check as explicit step in a pipeline graph
comply54_tool(compliance)StructuredToolThe LLM decides when to call the compliance check

For all regulated sectors (fintech, healthcare, insurance), use Comply54Guard — compliance must intercept every tool call automatically.

Comply54Guard sits between the agent node and the tools node. When the LLM emits tool_calls, the guard evaluates each against all compliance packs. Blocked calls receive a ToolMessage error; the LLM reads it and explains the refusal to the user.

from langchain_anthropic import ChatAnthropic
from langgraph.graph import StateGraph, MessagesState
from langgraph.prebuilt import ToolNode
from comply54.sectors import NigeriaFintechCompliance
from comply54.langchain import Comply54Guard, comply54_route

compliance = NigeriaFintechCompliance()
tools = [transfer_funds_tool, check_balance_tool]

def agent_node(state):
llm = ChatAnthropic(model="claude-haiku-4-5-20251001").bind_tools(tools)
return {"messages": [llm.invoke(state["messages"])]}

guard = Comply54Guard(compliance, tools)

def build_graph():
graph = StateGraph(MessagesState)

graph.add_node("agent", agent_node)
graph.add_node("guard", guard)
graph.add_node("tools", ToolNode(tools))

graph.set_entry_point("agent")
graph.add_conditional_edges("agent", comply54_route, {"guard": "guard", "__end__": "__end__"})
graph.add_edge("guard", "tools")
graph.add_edge("tools", "agent")

return graph.compile()


app = build_graph()
result = app.invoke({"messages": [("user", "Transfer ₦15,000,000 to account 0123456789")]})
# Last AIMessage: "I can't process this transfer — it exceeds the ₦10,000,000 CBN NIP cap."

Passing context to the guard: Set state["compliance_context"] before the agent runs:

result = app.invoke({
"messages": [("user", "Transfer ₦500,000 to 0123456789")],
"compliance_context": {"kyc_tier": 2},
})

Pattern 2: compliance_node (pipeline graphs)

Use compliance_node when compliance is a named, explicit step in a non-ReAct pipeline:

from comply54.sectors import NigeriaFintechCompliance
from comply54.langchain import compliance_node
from typing import TypedDict, Any
from langgraph.graph import StateGraph


class AgentState(TypedDict):
action: str
params: dict
context: dict
output: str
blocked: bool
compliance_result: Any
messages: list


compliance = NigeriaFintechCompliance()

def agent_node(state: AgentState) -> AgentState:
return {**state, "action": "transfer_funds", "params": {"amount": 15_000_000}}

def execute_node(state: AgentState) -> AgentState:
result = execute_transfer(state["params"])
return {**state, "output": result}

def rejection_node(state: AgentState) -> AgentState:
reason = state["compliance_result"].primary_violation.messages[0]
return {**state, "output": f"Blocked: {reason}"}


def build_graph():
graph = StateGraph(AgentState)

graph.add_node("agent", agent_node)
graph.add_node("check_compliance", compliance_node(compliance))
graph.add_node("execute", execute_node)
graph.add_node("reject", rejection_node)

graph.set_entry_point("agent")
graph.add_edge("agent", "check_compliance")

graph.add_conditional_edges(
"check_compliance",
lambda state: "reject" if state.get("blocked") else "execute",
{"execute": "execute", "reject": "reject"},
)
graph.add_edge("execute", "__end__")
graph.add_edge("reject", "__end__")

return graph.compile()


app = build_graph()
result = app.invoke({
"action": "transfer_funds",
"params": {"amount": 15_000_000, "currency": "NGN"},
"context": {"kyc_tier": 3},
"messages": [],
})
print(result["output"])
# Blocked: CBN NIP Framework §4.2: Transaction exceeds ₦10,000,000 cap

Pattern 3: comply54_tool

The compliance check becomes a tool the LLM calls before taking actions. Best for exploratory agents where the model needs to reason about whether an action is compliant before deciding to execute it.

from comply54.langchain import comply54_tool
from langchain_anthropic import ChatAnthropic

compliance = NigeriaFintechCompliance()
tools = [comply54_tool(compliance)]

llm = ChatAnthropic(model="claude-sonnet-4-6").bind_tools(tools)

response = llm.invoke(
"Check if I can transfer ₦12,000,000 to account 0123456789. My KYC tier is 3."
)
# The LLM calls check_compliance tool → receives "deny" with CBN message
# → responds explaining the block

Output guard pattern

Check the agent's output before sending it to the user — catches BVN/NIN leakage regardless of where in the graph it occurs.

def output_guard_node(state: AgentState) -> AgentState:
result = compliance.check(
action="respond_to_user",
output=state["output"],
)

if result.blocked:
# Redact and return a safe message
safe_output = "[Response contained sensitive data and was redacted]"
return {**state, "output": safe_output, "pii_blocked": True}

return state

Add it as the last node before __end__:

graph.add_node("output_guard", output_guard_node)
graph.add_edge("execute", "output_guard")
graph.add_edge("output_guard", "__end__")

Three-stage guard (production pattern)

Production Nigerian fintech agents should gate compliance at three points:

User input → [Input guard] → Agent reasoning → [Tool call guard] → Execute → [Output guard] → User
# Stage 1: Input guard — check what the user is asking to do
def input_guard(state):
result = compliance.check(
action=extract_intent(state["messages"]),
params=extract_params(state["messages"]),
context=state["context"],
)
return {**state, "input_check": result}

# Stage 2: Tool call guard — check before every tool execution
def before_tool(tool_call, context):
result = compliance.check(
action=tool_call["name"],
params=tool_call["args"],
context=context,
)
if result.blocked:
raise ToolCallBlocked(result.primary_violation.messages[0])

# Stage 3: Output guard — check before response is sent to user
def output_guard(state):
result = compliance.check(
action="respond_to_user",
output=state["output"],
)
if result.blocked:
return {**state, "output": redact(state["output"])}
return state

Streaming with compliance

comply54 evaluates synchronously in microseconds — it does not interfere with LangChain streaming:

async for chunk in app.astream(input_state):
# compliance evaluation happens synchronously inside nodes
# streaming is unaffected
yield chunk