198 lines
6.3 KiB
Python
198 lines
6.3 KiB
Python
"""
|
||
Self‑correcting LangGraph agent.
|
||
|
||
The agent receives a natural‑language task, executes it via an unreliable tool,
|
||
then asks an LLM to judge whether the result is correct. If the judge says
|
||
"failed" the agent retries until success or a maximum number of attempts.
|
||
|
||
The implementation uses LangGraph 1.x and LangChain 1.x.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import random
|
||
import sys
|
||
from typing import TypedDict
|
||
|
||
# LangChain imports
|
||
from langchain_openai import ChatOpenAI
|
||
from langchain_core.messages import HumanMessage, AIMessage
|
||
|
||
# LangGraph imports
|
||
from langgraph.graph import StateGraph, END
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 1. State definition
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class AgentState(TypedDict):
|
||
task: str
|
||
result: str
|
||
attempts: int
|
||
status: str # pending | success | failed | max_attempts
|
||
error: str | None
|
||
max_attempts: int
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 2. Unreliable tool
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def unreliable_tool(task: str) -> str:
|
||
"""Simulate a tool that fails 30 % of the time.
|
||
|
||
The tool simply returns the string ``f"Result of {task}"`` but raises a
|
||
``ValueError`` with 30 % probability.
|
||
"""
|
||
if random.random() < 0.3:
|
||
raise ValueError("Simulated tool failure")
|
||
return f"Result of {task}"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 3. LLM judge
|
||
# ---------------------------------------------------------------------------
|
||
|
||
# Create a lightweight LLM instance. The user must set the OPENAI_API_KEY
|
||
# environment variable or provide a key in the code.
|
||
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
|
||
|
||
# The judge prompt asks the model to answer only "success" or "failed".
|
||
JUDGE_PROMPT = (
|
||
"You are a strict judge. Given the following result of a task, answer only "
|
||
"one word: success or failed.\n\nResult: {result}\nAnswer:" # no extra formatting
|
||
)
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 4. Node functions
|
||
# ---------------------------------------------------------------------------
|
||
|
||
async def execute_task(state: AgentState) -> AgentState:
|
||
"""Execute the task using the unreliable tool.
|
||
|
||
On success, store the result. On failure, capture the exception.
|
||
"""
|
||
try:
|
||
result = unreliable_tool(state["task"])
|
||
state["result"] = result
|
||
state["error"] = None
|
||
except Exception as exc:
|
||
state["result"] = ""
|
||
state["error"] = str(exc)
|
||
return state
|
||
|
||
async def verify_result(state: AgentState) -> AgentState:
|
||
"""Ask the LLM to judge the result.
|
||
|
||
The LLM must return either "success" or "failed".
|
||
"""
|
||
if state["error"]:
|
||
# If the tool raised an exception, we consider it a failure.
|
||
state["status"] = "failed"
|
||
return state
|
||
|
||
# Build the prompt with the result.
|
||
prompt = JUDGE_PROMPT.format(result=state["result"])
|
||
messages = [HumanMessage(content=prompt)]
|
||
ai_msg: AIMessage = await llm.ainvoke(messages)
|
||
verdict = ai_msg.content.strip().lower()
|
||
if verdict == "success":
|
||
state["status"] = "success"
|
||
else:
|
||
state["status"] = "failed"
|
||
return state
|
||
|
||
async def handle_error(state: AgentState) -> AgentState:
|
||
"""Increment attempts and prepare for retry.
|
||
|
||
If the maximum number of attempts is reached, set status to
|
||
"max_attempts".
|
||
"""
|
||
state["attempts"] += 1
|
||
if state["attempts"] >= state["max_attempts"]:
|
||
state["status"] = "max_attempts"
|
||
else:
|
||
# Reset result and error for the next attempt.
|
||
state["result"] = ""
|
||
state["error"] = None
|
||
return state
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 5. Graph construction
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def build_graph(max_attempts: int = 5) -> StateGraph[AgentState]:
|
||
graph = StateGraph(AgentState)
|
||
graph.add_node("execute_task", execute_task)
|
||
graph.add_node("verify_result", verify_result)
|
||
graph.add_node("handle_error", handle_error)
|
||
|
||
# Define the flow: execute → verify → (success → END | failed → handle_error → execute)
|
||
graph.add_edge("execute_task", "verify_result")
|
||
graph.add_edge("handle_error", "execute_task")
|
||
|
||
# Conditional router based on status after verification.
|
||
def router(state: AgentState) -> str:
|
||
return state["status"]
|
||
|
||
graph.add_conditional_edges(
|
||
"verify_result",
|
||
router,
|
||
{
|
||
"success": END,
|
||
"failed": "handle_error",
|
||
"max_attempts": END,
|
||
},
|
||
)
|
||
|
||
graph.set_entry_point("execute_task")
|
||
return graph
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 6. CLI driver
|
||
# ---------------------------------------------------------------------------
|
||
|
||
async def main():
|
||
if len(sys.argv) > 1:
|
||
task = " ".join(sys.argv[1:])
|
||
else:
|
||
task = input("Введите задачу: ")
|
||
|
||
max_attempts = 5
|
||
graph = build_graph(max_attempts)
|
||
app = graph.compile()
|
||
|
||
# Initial state
|
||
state: AgentState = {
|
||
"task": task,
|
||
"result": "",
|
||
"attempts": 0,
|
||
"status": "pending",
|
||
"error": None,
|
||
"max_attempts": max_attempts,
|
||
}
|
||
|
||
# Run the graph until it ends.
|
||
async for partial_state in app.stream(state):
|
||
# Print progress when attempts change.
|
||
if partial_state["attempts"] != state["attempts"]:
|
||
print(f"Попытка {partial_state['attempts']}:", end=" ")
|
||
if partial_state["error"]:
|
||
print(f"Error → {partial_state['error']}")
|
||
else:
|
||
print(f"результат {partial_state['result']}")
|
||
state = partial_state
|
||
|
||
# Final status
|
||
print("\nИтог:")
|
||
if state["status"] == "success":
|
||
print(f"Успех за {state['attempts']} попыток. Результат: {state['result']}")
|
||
elif state["status"] == "max_attempts":
|
||
print(f"Не удалось за {state['attempts']} попыток. Последняя ошибка: {state['error']}")
|
||
else:
|
||
print(f"Не удалось. Последняя ошибка: {state['error']}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
import asyncio
|
||
|
||
asyncio.run(main())
|