Update agent.py

This commit is contained in:
2026-06-02 15:59:37 +00:00
parent 68f081481d
commit 6e2f940e51
+117 -115
View File
@@ -1,20 +1,11 @@
"""Selfcorrecting LangGraph agent. """
Selfcorrecting LangGraph agent.
The agent receives a *task* string. It executes the task via an The agent receives a naturallanguage task, executes it via an unreliable tool,
`unreliable_tool` that sometimes raises a ValueError. After execution then asks an LLM to judge whether the result is correct. If the judge says
it asks the LLM (OpenAI or Ollama) to judge whether the *result* is "failed" the agent retries until success or a maximum number of attempts.
correct. If the judge says ``failed`` the agent retries until
``max_attempts`` is reached.
The implementation uses LangGraph's lowlevel API: a StateGraph The implementation uses LangGraph 1.x and LangChain 1.x.
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 from __future__ import annotations
@@ -23,7 +14,11 @@ import random
import sys import sys
from typing import TypedDict from typing import TypedDict
# LangChain imports
from langchain_openai import ChatOpenAI from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage
# LangGraph imports
from langgraph.graph import StateGraph, END from langgraph.graph import StateGraph, END
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -32,164 +27,171 @@ from langgraph.graph import StateGraph, END
class AgentState(TypedDict): class AgentState(TypedDict):
task: str task: str
result: str | None result: str
attempts: int attempts: int
status: str # pending | success | failed | max_attempts status: str # pending | success | failed | max_attempts
error: str | None error: str | None
max_attempts: int max_attempts: int
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 2. Unreliable tool 30% chance of raising ValueError # 2. Unreliable tool
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def unreliable_tool(task: str) -> str: def unreliable_tool(task: str) -> str:
"""Simulate a tool that sometimes fails. """Simulate a tool that fails 30% of the time.
The function simply returns ``task`` reversed (as a dummy result) but The tool simply returns the string ``f"Result of {task}"`` but raises a
raises a ValueError 30% of the time. ``ValueError`` with 30% probability.
""" """
if random.random() < 0.3: if random.random() < 0.3:
raise ValueError("Simulated tool failure") raise ValueError("Simulated tool failure")
return task[::-1] # dummy "computation" return f"Result of {task}"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 3. LLM judge asks for "success" or "failed" # 3. LLM judge
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
llm = ChatOpenAI(temperature=0, model="gpt-4o-mini") # or use Ollama # 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: async def verify_result(state: AgentState) -> AgentState:
"""Ask the LLM whether the result is correct. """Ask the LLM to judge the result.
The prompt forces the model to answer only "success" or "failed". The LLM must return either "success" or "failed".
""" """
if state["result"] is None: if state["error"]:
# Should not happen guard # If the tool raised an exception, we consider it a failure.
state["status"] = "failed" state["status"] = "failed"
return state return state
prompt = ( # Build the prompt with the result.
f"Task: {state['task']}\n" prompt = JUDGE_PROMPT.format(result=state["result"])
f"Result: {state['result']}\n" messages = [HumanMessage(content=prompt)]
"Is this result correct? Respond with only 'success' or 'failed'." ai_msg: AIMessage = await llm.ainvoke(messages)
) verdict = ai_msg.content.strip().lower()
response = llm.invoke(prompt)
verdict = response.content.strip().lower()
if verdict == "success": if verdict == "success":
state["status"] = "success" state["status"] = "success"
else: else:
state["status"] = "failed" state["status"] = "failed"
return state 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: async def handle_error(state: AgentState) -> AgentState:
"""Increment attempt counter and decide whether to retry.""" """Increment attempts and prepare for retry.
If the maximum number of attempts is reached, set status to
"max_attempts".
"""
state["attempts"] += 1 state["attempts"] += 1
if state["attempts"] >= state["max_attempts"]: if state["attempts"] >= state["max_attempts"]:
state["status"] = "max_attempts" state["status"] = "max_attempts"
else: else:
# Reset result and error for next try # Reset result and error for the next attempt.
state["result"] = None state["result"] = ""
state["error"] = None state["error"] = None
return state return state
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 6. Build the graph # 5. Graph construction
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
graph = StateGraph(AgentState) 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)
# Add nodes # Define the flow: execute → verify → (success → END | failed → handle_error → execute)
graph.add_node("execute_task", execute_task) graph.add_edge("execute_task", "verify_result")
graph.add_node("verify_result", verify_result) graph.add_edge("handle_error", "execute_task")
graph.add_node("handle_error", handle_error)
# Define edges # Conditional router based on status after verification.
# Start → execute_task def router(state: AgentState) -> str:
graph.set_entry_point("execute_task") return state["status"]
# After execution, go to verification graph.add_conditional_edges(
graph.add_edge("execute_task", "verify_result") "verify_result",
router,
{
"success": END,
"failed": "handle_error",
"max_attempts": END,
},
)
# Verification outcomes graph.set_entry_point("execute_task")
# success → END return graph
# 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 # 6. CLI driver
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def main() -> None: async def main():
print("Selfcorrecting LangGraph agent demo") if len(sys.argv) > 1:
task = input("Enter a task: ") task = " ".join(sys.argv[1:])
if not task: else:
print("No task provided. Exiting.") task = input("Введите задачу: ")
sys.exit(0)
max_attempts = 5
graph = build_graph(max_attempts)
app = graph.compile()
# Initial state # Initial state
state: AgentState = { state: AgentState = {
"task": task, "task": task,
"result": None, "result": "",
"attempts": 1, "attempts": 0,
"status": "pending", "status": "pending",
"error": None, "error": None,
"max_attempts": 5, "max_attempts": max_attempts,
} }
# Run the chain # Run the graph until it ends.
final_state = agent.invoke(state) 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
print("\n--- Result ---") # Final status
print(f"Task: {final_state['task']}") print("\nИтог:")
print(f"Attempts: {final_state['attempts']}") if state["status"] == "success":
print(f"Status: {final_state['status']}") print(f"Успех за {state['attempts']} попыток. Результат: {state['result']}")
if final_state['result']: elif state["status"] == "max_attempts":
print(f"Result: {final_state['result']}") print(f"Не удалось за {state['attempts']} попыток. Последняя ошибка: {state['error']}")
if final_state['error']: else:
print(f"Last error: {final_state['error']}") print(f"Не удалось. Последняя ошибка: {state['error']}")
if __name__ == "__main__": if __name__ == "__main__":
main() import asyncio
"
asyncio.run(main())