195 lines
5.7 KiB
Python
195 lines
5.7 KiB
Python
"""Self‑correcting LangGraph agent.
|
||
|
||
The agent receives a *task* string. It executes the task via an
|
||
`unreliable_tool` that sometimes raises a ValueError. After execution
|
||
it asks the LLM (OpenAI or Ollama) to judge whether the *result* is
|
||
correct. If the judge says ``failed`` the agent retries until
|
||
``max_attempts`` is reached.
|
||
|
||
The implementation uses LangGraph's low‑level API: a StateGraph
|
||
with three nodes – execute_task, verify_result, handle_error – and a
|
||
simple loop.
|
||
|
||
Run the script with:
|
||
|
||
python agent.py
|
||
|
||
It will ask for a task, then show the attempts and final status.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import random
|
||
import sys
|
||
from typing import TypedDict
|
||
|
||
from langchain_openai import ChatOpenAI
|
||
from langgraph.graph import StateGraph, END
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 1. State definition
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class AgentState(TypedDict):
|
||
task: str
|
||
result: str | None
|
||
attempts: int
|
||
status: str # pending | success | failed | max_attempts
|
||
error: str | None
|
||
max_attempts: int
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 2. Unreliable tool – 30 % chance of raising ValueError
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def unreliable_tool(task: str) -> str:
|
||
"""Simulate a tool that sometimes fails.
|
||
|
||
The function simply returns ``task`` reversed (as a dummy result) but
|
||
raises a ValueError 30 % of the time.
|
||
"""
|
||
if random.random() < 0.3:
|
||
raise ValueError("Simulated tool failure")
|
||
return task[::-1] # dummy "computation"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 3. LLM judge – asks for "success" or "failed"
|
||
# ---------------------------------------------------------------------------
|
||
|
||
llm = ChatOpenAI(temperature=0, model="gpt-4o-mini") # or use Ollama
|
||
|
||
async def verify_result(state: AgentState) -> AgentState:
|
||
"""Ask the LLM whether the result is correct.
|
||
|
||
The prompt forces the model to answer only "success" or "failed".
|
||
"""
|
||
if state["result"] is None:
|
||
# Should not happen – guard
|
||
state["status"] = "failed"
|
||
return state
|
||
|
||
prompt = (
|
||
f"Task: {state['task']}\n"
|
||
f"Result: {state['result']}\n"
|
||
"Is this result correct? Respond with only 'success' or 'failed'."
|
||
)
|
||
response = llm.invoke(prompt)
|
||
verdict = response.content.strip().lower()
|
||
if verdict == "success":
|
||
state["status"] = "success"
|
||
else:
|
||
state["status"] = "failed"
|
||
return state
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 4. Execute task node
|
||
# ---------------------------------------------------------------------------
|
||
|
||
async def execute_task(state: AgentState) -> AgentState:
|
||
"""Run the unreliable tool and capture errors."""
|
||
try:
|
||
result = unreliable_tool(state["task"])
|
||
state["result"] = result
|
||
state["error"] = None
|
||
except Exception as exc: # catch ValueError
|
||
state["result"] = None
|
||
state["error"] = str(exc)
|
||
return state
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 5. Handle error / retry node
|
||
# ---------------------------------------------------------------------------
|
||
|
||
async def handle_error(state: AgentState) -> AgentState:
|
||
"""Increment attempt counter and decide whether to retry."""
|
||
state["attempts"] += 1
|
||
if state["attempts"] >= state["max_attempts"]:
|
||
state["status"] = "max_attempts"
|
||
else:
|
||
# Reset result and error for next try
|
||
state["result"] = None
|
||
state["error"] = None
|
||
return state
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 6. Build the graph
|
||
# ---------------------------------------------------------------------------
|
||
|
||
graph = StateGraph(AgentState)
|
||
|
||
# Add nodes
|
||
graph.add_node("execute_task", execute_task)
|
||
graph.add_node("verify_result", verify_result)
|
||
graph.add_node("handle_error", handle_error)
|
||
|
||
# Define edges
|
||
# Start → execute_task
|
||
graph.set_entry_point("execute_task")
|
||
|
||
# After execution, go to verification
|
||
graph.add_edge("execute_task", "verify_result")
|
||
|
||
# Verification outcomes
|
||
# success → END
|
||
# failed → check attempts
|
||
# max_attempts → END
|
||
|
||
# We use a conditional edge on the status field
|
||
|
||
def verify_cond(state: AgentState):
|
||
return state["status"]
|
||
|
||
# Map status to next node
|
||
graph.add_conditional_edges(
|
||
"verify_result",
|
||
verify_cond,
|
||
{
|
||
"success": END,
|
||
"failed": "handle_error",
|
||
"max_attempts": END,
|
||
},
|
||
)
|
||
|
||
# From handle_error back to execute_task
|
||
graph.add_edge("handle_error", "execute_task")
|
||
|
||
# Compile the graph into a runnable chain
|
||
agent = graph.compile()
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 7. CLI entry point
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def main() -> None:
|
||
print("Self‑correcting LangGraph agent demo")
|
||
task = input("Enter a task: ")
|
||
if not task:
|
||
print("No task provided. Exiting.")
|
||
sys.exit(0)
|
||
|
||
# Initial state
|
||
state: AgentState = {
|
||
"task": task,
|
||
"result": None,
|
||
"attempts": 1,
|
||
"status": "pending",
|
||
"error": None,
|
||
"max_attempts": 5,
|
||
}
|
||
|
||
# Run the chain
|
||
final_state = agent.invoke(state)
|
||
|
||
print("\n--- Result ---")
|
||
print(f"Task: {final_state['task']}")
|
||
print(f"Attempts: {final_state['attempts']}")
|
||
print(f"Status: {final_state['status']}")
|
||
if final_state['result']:
|
||
print(f"Result: {final_state['result']}")
|
||
if final_state['error']:
|
||
print(f"Last error: {final_state['error']}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|
||
" |