From b3fa39d1ce6a1c7c4168a11931be8322880180f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D1=80=D0=B8=D1=8F=20=D0=91=D0=B5=D1=80=D0=B4?= =?UTF-8?q?=D0=BD=D0=B8=D0=BA=D0=BE=D0=B2=D0=B0?= Date: Wed, 27 May 2026 14:22:49 +0000 Subject: [PATCH] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=BA=D1=82=D0=B8=D1=87?= =?UTF-8?q?=D0=B5=D1=81=D0=BA=D0=BE=D0=B5=20=D0=B7=D0=B0=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=E2=84=963:=20=D0=9F=D0=B0=D0=BC=D1=8F=D1=82?= =?UTF-8?q?=D1=8C=20=D0=B8=20=D0=BF=D0=BE=D0=B4=D1=82=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=B6=D0=B4=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B4=D0=B5=D0=B9=D1=81?= =?UTF-8?q?=D1=82=D0=B2=D0=B8=D0=B9:=20solution.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solution.py | 154 ++++++++++-------- 1 file changed, 90 insertions(+), 64 deletions(-) diff --git a/solutions/69a474cdc46fd26feae69896_Практическое_задание__3__Память_и_подтве/solution.py b/solutions/69a474cdc46fd26feae69896_Практическое_задание__3__Память_и_подтве/solution.py index f058e08..d2d7018 100644 --- a/solutions/69a474cdc46fd26feae69896_Практическое_задание__3__Память_и_подтве/solution.py +++ b/solutions/69a474cdc46fd26feae69896_Практическое_задание__3__Память_и_подтве/solution.py @@ -1,102 +1,128 @@ # solution.py -import sys +import os from typing import Any, Dict, Iterable, Tuple -# 1️⃣ Rich для красивого вывода +from langchain_openai import ChatOpenAI +from langgraph.checkpoint.memory import MemorySaver +from langgraph.graph import StateGraph +from langgraph.prebuilt.tool_executor import ToolExecutor from rich.console import Console console = Console() -# 2️⃣ LangGraph и необходимые компоненты -from langgraph.checkpoint.memory import MemorySaver -from langgraph.graph import StateGraph -from langgraph.prebuilt import create_chat_agent -from langchain_openai import ChatOpenAI -from langchain_core.messages import HumanMessage, AIMessage, ToolMessage - -# 3️⃣ Определяем простой инструмент (пример) -def echo_tool(args: Dict[str, Any]) -> str: - """Простейший инструмент, который возвращает переданный текст.""" - return f"Эхо: {args.get('text', '')}" - -# 4️⃣ Создаём LLM -llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0) - -# 5️⃣ Настраиваем память и агент с паузой перед инструментом -memory = MemorySaver() - -agent = create_chat_agent( - llm=llm, - tools=[echo_tool], - system_prompt="Ты — полезный ассистент, который сначала спрашивает подтверждение перед вызовом инструмента.", - checkpointer=memory, - interrupt_before=["tools"], # пауза перед каждым инструментом +# ---------- 1. Настройка модели и инструментов ---------- +llm = ChatOpenAI( + model="gpt-4o-mini", + temperature=0, ) -# 6️⃣ Функция для обработки потокового ответа и подтверждения +def echo_tool(args: Dict[str, Any]) -> str: + """Простейший инструмент для демонстрации.""" + return f"Эхо: {args.get('message', '')}" + +tools = [echo_tool] +tool_executor = ToolExecutor(tools) + + +# ---------- 2. Создание графа агента ---------- +def create_agent() -> StateGraph: + """ + Возвращает готовый граф агента с памятью и возможностью + прерывания перед вызовом инструмента. + """ + # Состояние: хранит сообщения + def add_message(state: Dict[str, Any], message: Dict[str, str]) -> Dict[str, Any]: + state["messages"].append(message) + return state + + graph = StateGraph(add_message) + + # Входной узел – пользовательское сообщение + graph.set_entry_point("user") + + # Узлы для обработки сообщений и вызова инструментов + graph.add_node("assistant", lambda state: {"messages": [state["messages"][-1]]}) + graph.add_node("tools", tool_executor) + + # Переходы + graph.set_conditional_edges( + "assistant", + lambda _: "tools" if any(m.get("tool_calls") for m in _["messages"]) else "final", + ) + graph.add_edge("tools", "assistant") + graph.add_edge("assistant", "final") + + # Конфигурация прерывания + graph.set_interrupt_before(["tools"]) + + return graph + + +agent = create_agent() +memory = MemorySaver() +agent.compile(checkpointer=memory) + + +# ---------- 3. Функция взаимодействия ---------- def ask_and_run(user_input: Dict[str, Any], config: Dict[str, Any]) -> None: """ - Отправляем запрос агенту, обрабатываем потоковые чанки, - при необходимости запрашиваем у пользователя подтверждение перед вызовом инструмента. + Запускает потоковый ответ агента с возможностью подтверждения + вызова инструмента пользователем. """ - # Запускаем поток - for chunk in agent.stream(user_input, config=config, stream_mode=["messages", "updates"]): + # Инициализируем сообщение пользователя в состоянии + if user_input is not None: + agent.send(user_input, config=config) + + for chunk_type, chunk_data in agent.stream( + {}, config=config, stream_mode=["messages", "updates"] + ): state = agent.get_state(config) - chunk_type, chunk_data = chunk - # 6.1️⃣ Печатаем токены LLM в режиме потока + # 3.1 Вывод токенов LLM if chunk_type == "messages": - for msg in chunk_data: - if isinstance(msg, AIMessage): - console.print(f"[bold cyan]AI:[/bold cyan] {msg.content}", end="") - elif isinstance(msg, ToolMessage): - # Инструмент уже вызван – выводим результат - console.print(f"\n[green]Tool result:[/green] {msg.content}") + token = chunk_data["content"] or "" + console.print(token, end="", style="cyan") + continue - # 6.2️⃣ Обрабатываем обновления (например, вызовы инструментов) + # 3.2 Обработка обновлений (вызовы инструментов) if chunk_type == "updates": for update in chunk_data: - if isinstance(update, dict) and "tool_calls" in update: - console.print("\n[magenta]Инструмент готов к выполнению:[/magenta]") - tool_call = update["tool_calls"][0] - name = tool_call["name"] - args = tool_call.get("args", {}) - console.print(f"[yellow]{name}({args})[/yellow]") + console.print(f"\n[bold magenta]Обновление:{update}") + continue - # 6.3️⃣ Проверяем паузу перед инструментом + # 3.3 Пауза перед вызовом инструмента if "__interrupt__" in chunk_data and state.next == ("tools",): - # Получаем последний вызов инструмента - last_msg = state.values["messages"][-1] - tool_call = last_msg.tool_calls[0] + tool_call = state.values["messages"][-1]["tool_calls"][0] name = tool_call["name"] args = tool_call.get("args", {}) - console.print("\n\n[bold red]Пауза:[/bold red] Агент хочет вызвать инструмент.") - console.print(f"[yellow]{name}({args})[/yellow]") - answer = input("[green]Разрешить? (Y/n): [/green]").strip().lower() - if answer in ("", "y", "yes"): - # Продолжаем с того же состояния + console.print(f"\n[bold yellow]Пауза: [{name}]({args})") + answer = input("Разрешить? (Y/n): ").strip().lower() + if answer in ("y", ""): + # Возобновляем с того места, где остановились ask_and_run(None, config) else: - console.print("[red]Отменено. Конец диалога.[/red]") - sys.exit(0) + console.print("[red]Отменено пользователем.") + break -# 7️⃣ Основной цикл чата + +# ---------- 4. Основной цикл ---------- def main() -> None: thread_id = "thread-1" config = {"configurable": {"thread_id": thread_id}} - console.print("[bold blue]Запуск агента. Введите 'exit' для завершения.[/bold blue]") - while True: - user_input = input("\n[cyan]Вы:[/cyan] ") - if user_input.lower() == "exit": - console.print("[bold green]До свидания![/bold green]") - break + console.print("[bold green]Запуск агента. Введите 'exit' для выхода.\n") + while True: + user_input = input("\nВы: ") + if user_input.lower() == "exit": + break ask_and_run( {"messages": [{"role": "human", "content": user_input}]}, config, ) + console.print("[bold green]До свидания!") + if __name__ == "__main__": main() \ No newline at end of file