213 lines
7.2 KiB
Python
213 lines
7.2 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
|
||
"""
|
||
main.py – запуск LangGraph‑агента с самопроверкой и retry.
|
||
|
||
Требования:
|
||
- Python 3.10+
|
||
- pip install langgraph langchain-openai
|
||
"""
|
||
|
||
import random
|
||
from typing import TypedDict, Dict, Any
|
||
|
||
from langchain_openai import ChatOpenAI
|
||
from langgraph.graph import StateGraph, END
|
||
from langgraph.prebuilt import InMemorySaver
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# 1. Состояние графа
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
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(input_text: str) -> str:
|
||
"""
|
||
Случайно бросает ValueError с вероятностью ~30%.
|
||
Иначе возвращает результат выполнения простого арифметического выражения.
|
||
"""
|
||
if random.random() < 0.3:
|
||
raise ValueError("Инструмент случайно упал")
|
||
# Простейший парсер: ищем числа и оператор +, -, *, /
|
||
try:
|
||
left, op, right = input_text.split()
|
||
left_val = float(left)
|
||
right_val = float(right)
|
||
if op == '+':
|
||
return str(left_val + right_val)
|
||
elif op == '-':
|
||
return str(left_val - right_val)
|
||
elif op == '*':
|
||
return str(left_val * right_val)
|
||
elif op == '/':
|
||
return str(left_val / right_val)
|
||
except Exception:
|
||
pass
|
||
# Если не удалось распарсить – просто возвращаем строку
|
||
return f"результат: {input_text}"
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# 3. Узлы графа
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
def execute_task(state: AgentState) -> Dict[str, Any]:
|
||
"""
|
||
Выполняет задачу через unreliable_tool.
|
||
При ошибке сохраняется сообщение об ошибке и статус «failed».
|
||
"""
|
||
try:
|
||
result = unreliable_tool(state["task"])
|
||
return {
|
||
"result": result,
|
||
"error": None,
|
||
"status": "pending", # переход к verify_result
|
||
}
|
||
except Exception as exc:
|
||
return {
|
||
"result": "",
|
||
"error": str(exc),
|
||
"status": "failed",
|
||
}
|
||
|
||
|
||
def verify_result(state: AgentState) -> Dict[str, Any]:
|
||
"""
|
||
LLM‑оценка результата. Модель должна ответить «success» или «failed».
|
||
Если результат пустой – сразу считается «failed».
|
||
"""
|
||
if not state["result"]:
|
||
return {"status": "failed"}
|
||
|
||
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
|
||
prompt = (
|
||
f"Задача: {state['task']}\n"
|
||
f"Результат: {state['result']}\n\n"
|
||
"Оцените результат. Ответьте только одним словом: success или failed."
|
||
)
|
||
response = llm.invoke(prompt).content.strip().lower()
|
||
if response.startswith("success"):
|
||
return {"status": "success"}
|
||
else:
|
||
return {"status": "failed"}
|
||
|
||
|
||
def handle_error(state: AgentState) -> Dict[str, Any]:
|
||
"""
|
||
Увеличивает счётчик попыток и готовит повторный запуск.
|
||
Если достигнут лимит – статус «max_attempts».
|
||
"""
|
||
attempts = state["attempts"] + 1
|
||
if attempts >= state["max_attempts"]:
|
||
return {"attempts": attempts, "status": "max_attempts"}
|
||
else:
|
||
return {"attempts": attempts, "status": "pending"}
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# 4. Построение графа
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
builder = StateGraph(AgentState)
|
||
|
||
# Добавляем узлы
|
||
builder.add_node("execute_task", execute_task)
|
||
builder.add_node("verify_result", verify_result)
|
||
builder.add_node("handle_error", handle_error)
|
||
|
||
# Определяем переходы
|
||
builder.set_entry_point("execute_task")
|
||
|
||
builder.add_conditional_edges(
|
||
"execute_task",
|
||
lambda x: x["status"],
|
||
{
|
||
"pending": "verify_result",
|
||
"failed": "handle_error",
|
||
},
|
||
)
|
||
|
||
builder.add_conditional_edges(
|
||
"verify_result",
|
||
lambda x: x["status"],
|
||
{
|
||
"success": END,
|
||
"failed": "handle_error",
|
||
},
|
||
)
|
||
|
||
builder.add_conditional_edges(
|
||
"handle_error",
|
||
lambda x: x["status"],
|
||
{
|
||
"pending": "execute_task",
|
||
"max_attempts": END,
|
||
},
|
||
)
|
||
|
||
graph = builder.compile(saver=InMemorySaver())
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# 5. Запуск
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
def main() -> None:
|
||
"""
|
||
CLI‑интерфейс: пользователь вводит задачу, например «2 + 2».
|
||
Выводятся номера попыток и финальный статус.
|
||
"""
|
||
task = input("Введите задачу (пример: 2 + 2): ").strip()
|
||
if not task:
|
||
print("Задача не может быть пустой.")
|
||
return
|
||
|
||
# Инициализируем состояние
|
||
initial_state: AgentState = {
|
||
"task": task,
|
||
"result": "",
|
||
"attempts": 0,
|
||
"status": "pending",
|
||
"error": None,
|
||
"max_attempts": 5,
|
||
}
|
||
|
||
print("\nЗапуск агента...\n")
|
||
|
||
# Запускаем граф
|
||
for step in graph.stream(initial_state, stream_mode="values"):
|
||
state = step.values
|
||
attempts = state["attempts"] + 1 if state["status"] != "pending" else state["attempts"]
|
||
print(f"Попытка {attempts}:")
|
||
if state.get("error"):
|
||
print(f" Ошибка инструмента: {state['error']}")
|
||
else:
|
||
print(f" Результат: {state['result']}")
|
||
|
||
if state["status"] == "success":
|
||
print("\nИтог: success за", attempts, "попыток")
|
||
break
|
||
elif state["status"] == "max_attempts":
|
||
print("\nИтог: max_attempts – задача не решена после 5 попыток")
|
||
break
|
||
|
||
else:
|
||
# Если цикл завершился без явного выхода (должно быть)
|
||
print("\nНеизвестный конец выполнения.")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main() |