Executive Briefing: An enterprise e-commerce platform requires a tier-1 support agent that handles order status queries, delivery tracking, and returns. If a customer problem cannot be solved autonomously or an adversarial user attempts to trap the agent in an infinite loop, the agent must cap its resolution cycles at 3 turns, route to an
escalate_to_humannode, and terminate gracefully with a ticket summary.
1. Enterprise Scenario & Requirements
Linear chains break when agents face non-deterministic conversations or failed tool runs. Using LangGraph:
- Shared State Schema: Create a
SupportStateTypedDict containingmessages: Annotated[list[BaseMessage], operator.add],customer_id: str,iteration_count: int, andescalated: bool. - Conditional Router: Implement a conditional edge function
route_next_stepthat inspects the latest message. If tools are requested anditeration_count < 3, route toToolNode. Ifiteration_count >= 3, route toescalate. - Loop Defense: Guarantee that under no circumstances does the graph loop infinitely.
- State Compilation: Compile the
StateGraphand demonstrate clean transitions using mock order database tools.
2. Explicit Deliverables
state.py: Typed state schema with message reducers and iteration counters.graph.py: CompleteStateGraphcompilation with nodes (agent,tools,escalate) and conditional edges.test_support_agent.py: Automated tests demonstrating: (a) instant 1-turn resolution, (b) 2-turn resolution requiring follow-up tool execution, and (c) adversarial prompt triggering 3-cycle cap and human escalation.trace_visualization.png/graph_ascii.txt: Rendered LangGraph topology.
3. Starter Code & Scaffold
from typing import TypedDict, Annotated, Literal
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
import operator
from langgraph.graph import StateGraph, END
from langgraph.prebuilt import ToolNode
class SupportState(TypedDict):
messages: Annotated[list[BaseMessage], operator.add]
customer_id: str
iteration: int
escalated: bool
def agent_node(state: SupportState):
# LLM reasoning step: increment iteration counter
new_iter = state.get("iteration", 0) + 1
# Mock LLM call or real ChatOpenAI call
return {"iteration": new_iter}
def escalate_node(state: SupportState):
msg = AIMessage(content="Your request has been escalated to a human tier-2 supervisor. Ticket #7192 created.")
return {"messages": [msg], "escalated": True}
def router_condition(state: SupportState) -> Literal["tools", "escalate", "__end__"]:
if state["iteration"] >= 3:
return "escalate"
last_message = state["messages"][-1]
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
return "tools"
return "__end__"
4. 100-Point Grading Rubric
| Criterion | Points | Excellent (25 pts) | Acceptable (15 pts) | Needs Work (5 pts) |
|---|---|---|---|---|
| State Reducers & Schema | 25 pts | Annotated[list, operator.add] correctly preserves conversation history across nodes. | State updates work but overwrite previous messages. | Untyped state dictionary without schema validation. |
| Conditional Edge Routing | 25 pts | Routing function deterministically maps state attributes to correct destination nodes. | Routing functions correctly for happy path only. | Hardcoded static edges instead of conditional routing. |
| Cycle Capping Defense | 25 pts | Hard iteration limit (3 turns) strictly enforced; zero infinite loop vulnerabilities. | Loop limit exists but fails under edge-case prompts. | No loop protection; graph hangs on circular tasks. |
| Escalation Delivery | 25 pts | Escalation node produces structured ticket metadata and user-friendly handover message. | Escalation terminates without recording ticket context. | Graph terminates abruptly without notifying user. |