From 4f5c7becbdc295cb23bddce02d91ee0e5883cdde Mon Sep 17 00:00:00 2001 From: kuzakhmetovartur Date: Wed, 24 Jun 2026 15:17:01 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20solution=20for=20'=D0=9F=D1=80=D0=B0?= =?UTF-8?q?=D0=BA=D1=82=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=BE=D0=B5=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5=20=E2=84=963:=20=D0=9F?= =?UTF-8?q?=D0=B0=D0=BC=D1=8F=D1=82=D1=8C=20=D0=B8=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D1=82=D0=B2=D0=B5=D1=80=D0=B6=D0=B4=D0=B5=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=B4=D0=B5=D0=B9=D1=81=D1=82=D0=B2=D0=B8=D0=B9'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 ++ README.md | 33 ++++++++++ requirements.txt | 4 ++ src/main.py | 160 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 202 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 requirements.txt create mode 100644 src/main.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b16538b --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +.env +dist/ +build/ +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..8d09955 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# Практическое задание №3: Память и подтверждение действий + +Главная +Мои задания +Практическое задание №3: Память и подтверждение действий +5Д +EN +Практическое задание №3: Память и подтверждение действий +Зачёт +Версия 1 +Дедлайн сдачи: 31.08.2026 + +В работе + +Редактирование ответа + +Заполните ответ и отправьте работу на проверку преподавателю. + +Тип ответа +Текст +Ссылка +Файлы +Текст ответа +Прикреплённые файлы +Загрузить файл +Отправить на проверку +Отменить + +Задание + +Цель + +Доработать агента из предыдущих заданий: добавить память разговора и механизм подтверждения каждо \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8610c26 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +rich +langgraph +langchain +openai \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..bf517b4 --- /dev/null +++ b/src/main.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +""" +A simple LangGraph agent with memory and user confirmation before tool calls. +""" + +import os +import sys +from typing import Any, Dict, Optional + +from langgraph import create_agent +from langgraph.checkpoint.memory import MemorySaver +from langgraph.tools import Tool +from langchain.chat_models import ChatOpenAI +from rich.console import Console + +# --------------------------------------------------------------------------- # +# Configuration +# --------------------------------------------------------------------------- # + +# Ensure the OpenAI API key is set +OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") +if not OPENAI_API_KEY: + print("Error: OPENAI_API_KEY environment variable not set.") + sys.exit(1) + +# Rich console for pretty output +console = Console() + +# LLM model +llm = ChatOpenAI(temperature=0, openai_api_key=OPENAI_API_KEY) + +# Memory saver for conversation persistence +memory = MemorySaver() + +# --------------------------------------------------------------------------- # +# Tool definition +# --------------------------------------------------------------------------- # + +def echo_tool(message: str) -> str: + """ + A simple echo tool that returns the message back to the user. + """ + return f"Echo: {message}" + +# Wrap the function as a LangGraph Tool +echo = Tool.from_function( + fn=echo_tool, + name="echo", + description="Echoes back the provided message." +) + +# --------------------------------------------------------------------------- # +# Agent creation +# --------------------------------------------------------------------------- # + +system_prompt = """ +You are a helpful assistant. When you need to use a tool, you will call it. +""" + +agent = create_agent( + model=llm, + tools=[echo], + system_prompt=system_prompt, + checkpointer=memory, # Enable memory + interrupt_before=["tools"], # Pause before any tool call +) + +# --------------------------------------------------------------------------- # +# Conversation loop +# --------------------------------------------------------------------------- # + +def ask_and_run(user_input: Optional[str], config: Dict[str, Any]) -> None: + """ + Handles streaming from the agent, pauses before tool calls, and asks the user + for confirmation before executing the tool. + """ + # If user_input is provided, send it as a new message + if user_input is not None: + # The agent expects a dict with a "messages" key + input_payload = {"messages": [{"role": "user", "content": user_input}]} + else: + # None means resume from the paused state + input_payload = {} + + # Stream the agent's response + for chunk_type, chunk_data in agent.stream( + input_payload, + config=config, + stream_mode=["messages", "updates"], + ): + # Handle message chunks + if chunk_type == "messages": + # chunk_data is a list of messages; print the last one + if isinstance(chunk_data, list) and chunk_data: + last_msg = chunk_data[-1] + if last_msg.get("role") == "assistant": + console.print(f"[bold cyan]Assistant:[/bold cyan] {last_msg.get('content', '')}") + elif last_msg.get("role") == "tool": + console.print(f"[bold magenta]Tool Output:[/bold magenta] {last_msg.get('content', '')}") + else: + console.print(last_msg.get("content", "")) + else: + console.print(chunk_data) + + # Detect interrupt before tool call + if "__interrupt__" in chunk_data and agent.get_state(config).next == ("tools",): + # Retrieve the pending tool call + state = agent.get_state(config) + try: + last_message = state.values["messages"][-1] + tool_call = last_message.tool_calls[0] + tool_name = tool_call["name"] + tool_args = tool_call["args"] + console.print(f"[yellow]Agent wants to call tool:[/yellow] {tool_name}({tool_args})") + except Exception as e: + console.print(f"[red]Error retrieving tool call: {e}[/red]") + break + + # Ask user for confirmation + console.print("[bold]Allow tool execution? (Y/n):[/bold] ", end="") + answer = input().strip().lower() + if answer in ("", "y", "yes"): + console.print("[green]Executing tool...[/green]") + # Recursively resume the agent + ask_and_run(None, config) + else: + console.print("[red]Tool execution cancelled by user.[/red]") + break + + # Handle updates (optional) + if chunk_type == "updates": + # For this simple example, we ignore updates + pass + + +def main() -> None: + """ + Main conversation loop. + """ + # Use a fixed thread ID for this session + config = {"configurable": {"thread_id": "thread-1"}} + + console.print("[bold green]Welcome to the LangGraph Agent![/bold green]") + console.print("Type your messages below. Press Ctrl+C to exit.\n") + + while True: + try: + user_input = input("[bold]You:[/bold] ") + if not user_input: + continue + ask_and_run(user_input, config) + except KeyboardInterrupt: + console.print("\n[bold red]Exiting...[/bold red]") + break + except Exception as e: + console.print(f"[red]Unexpected error: {e}[/red]") + + +if __name__ == "__main__": + main() \ No newline at end of file