feat: solution for 'Практическое задание №3: Память и подтверждение действий'

This commit is contained in:
2026-06-24 15:27:26 +03:00
parent 3e87be504b
commit f45e780aba
3 changed files with 146 additions and 143 deletions
+54 -25
View File
@@ -1,33 +1,62 @@
# Практическое задание №3: Память и подтверждение действий # LangGraph Agent with Memory and Tool Confirmation
Главная This project demonstrates a simple conversational agent built with **LangGraph** that:
Мои задания
Практическое задание №3: Память и подтверждение действий
EN
Практическое задание №3: Память и подтверждение действий
Зачёт
Версия 2
Дедлайн сдачи: 31.08.2026
В работе - **Remembers** the conversation history across turns using an inmemory checkpoint.
- **Pauses** before invoking any tool, asking the user for confirmation.
- Uses the **Rich** library for pretty console output.
Редактирование ответа ## Prerequisites
Заполните ответ и отправьте работу на проверку преподавателю. - Python 3.10+
- An OpenAI API key (set as the `OPENAI_API_KEY` environment variable).
Тип ответа ## Installation
Текст
Ссылка
Файлы
Ссылка (URL)
Прикреплённые файлы
Загрузить файл
Отправить на проверку
Отменить
Задание ```bash
# Clone the repository (or copy the files)
git clone https://github.com/yourusername/langgraph-agent.git
cd langgraph-agent
Цель # Create a virtual environment (optional but recommended)
python -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
Доработать агента из предыдущих заданий: добавить память разговора и механизм подтверждения каждо # Install dependencies
pip install -r requirements.txt
```
## Usage
```bash
python src/main.py
```
You will see a prompt:
```
Start chat. Type 'exit' to quit.
You:
```
Type any message. If the agent decides to use a tool (e.g., the `echo` tool), it will pause and ask:
```
Agent wants to call tool: echo({"text":"Hello"})
Разрешить? (Y/n):
```
- Type `Y` or press Enter to allow the tool to run.
- Type `n` to cancel the tool call.
The conversation history is preserved across turns, so the agent can refer back to earlier messages.
## Customizing
- **Tools**: Add more tools by defining functions decorated with `@tool` from `langchain.tools`.
- **Memory**: Replace `MemorySaver()` with a persistent checkpoint (e.g., `RedisSaver`) for longterm storage.
- **Prompt**: Modify the `system_prompt` in `create_agent` to change the agents behavior.
## License
MIT License
+5 -4
View File
@@ -1,4 +1,5 @@
rich langgraph==0.0.41
langgraph langchain==0.1.12
langchain langchain-openai==0.0.7
openai rich==13.7.1
openai==1.12.0
+87 -114
View File
@@ -1,160 +1,133 @@
#!/usr/bin/env python3 #!/usr/bin/env python
# -*- coding: utf-8 -*-
""" """
A simple LangGraph agent with memory and user confirmation before tool calls. A simple LangGraph agent with conversation memory and tool usage confirmation.
""" """
import os import os
import sys from typing import Any, Dict, Iterable, Tuple
from typing import Any, Dict, Optional
from langgraph import create_agent
from langgraph.checkpoint.memory import MemorySaver from langgraph.checkpoint.memory import MemorySaver
from langgraph.tools import Tool from langgraph.prebuilt import create_agent
from langchain.chat_models import ChatOpenAI from langgraph.graph import END
from langchain_openai import ChatOpenAI
from langchain.tools import tool
from rich.console import Console from rich.console import Console
# --------------------------------------------------------------------------- # # Initialize Rich 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() console = Console()
# LLM model # Ensure OpenAI API key is set
llm = ChatOpenAI(temperature=0, openai_api_key=OPENAI_API_KEY) if "OPENAI_API_KEY" not in os.environ:
console.print("[red]Error:[/red] OPENAI_API_KEY environment variable not set.")
console.print("Please set it before running the script.")
exit(1)
# Memory saver for conversation persistence # Define a simple echo tool
@tool
def echo(text: str) -> str:
"""
Echo the input text back to the user.
"""
return f"Echo: {text}"
# Initialize the LLM
llm = ChatOpenAI(temperature=0)
# Initialize memory saver
memory = MemorySaver() memory = MemorySaver()
# --------------------------------------------------------------------------- # # Create the agent with interrupt_before to pause before tool calls
# 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( agent = create_agent(
model=llm, model=llm,
tools=[echo], tools=[echo],
system_prompt=system_prompt, system_prompt=(
checkpointer=memory, # Enable memory "You are a helpful assistant. "
interrupt_before=["tools"], # Pause before any tool call "When you need to use a tool, you will be paused for confirmation."
),
checkpointer=memory,
interrupt_before=["tools"],
) )
# --------------------------------------------------------------------------- # def ask_and_run(user_input: str | None, config: Dict[str, Any]) -> None:
# 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 Send user input to the agent, handle tool call interruptions,
for confirmation before executing the tool. and recursively resume or cancel based on user confirmation.
""" """
# If user_input is provided, send it as a new message # Prepare the input payload
payload = {"messages": []}
if user_input is not None: if user_input is not None:
# The agent expects a dict with a "messages" key payload["messages"].append({"role": "user", "content": user_input})
input_payload = {"messages": [{"role": "user", "content": user_input}]}
else:
# None means resume from the paused state
input_payload = {}
# Stream the agent's response # Stream the agent's response
for chunk_type, chunk_data in agent.stream( for chunk in agent.stream(
input_payload, payload,
config=config, config=config,
stream_mode=["messages", "updates"], stream_mode=["messages", "updates"],
): ):
state = agent.get_state(config)
chunk_type, chunk_data = chunk
# Handle message chunks # Handle message chunks
if chunk_type == "messages": if chunk_type == "messages":
# chunk_data is a list of messages; print the last one for msg in chunk_data:
if isinstance(chunk_data, list) and chunk_data: role = msg.get("role")
last_msg = chunk_data[-1] content = msg.get("content", "")
if last_msg.get("role") == "assistant": if role == "assistant":
console.print(f"[bold cyan]Assistant:[/bold cyan] {last_msg.get('content', '')}") console.print(f"[bold green]Assistant:[/bold green] {content}")
elif last_msg.get("role") == "tool": elif role == "user":
console.print(f"[bold magenta]Tool Output:[/bold magenta] {last_msg.get('content', '')}") console.print(f"[bold blue]User:[/bold blue] {content}")
else:
console.print(last_msg.get("content", ""))
else:
console.print(chunk_data)
# Detect interrupt before tool call # Detect interruption before tool call
if "__interrupt__" in chunk_data and agent.get_state(config).next == ("tools",): if "__interrupt__" in chunk_data and state.next == ("tools",):
# Retrieve the pending tool call # Extract the pending tool call
state = agent.get_state(config) last_msg = state.values["messages"][-1]
try: tool_calls = last_msg.get("tool_calls", [])
last_message = state.values["messages"][-1] if not tool_calls:
tool_call = last_message.tool_calls[0] console.print("[red]Error:[/red] No tool call found during interruption.")
tool_name = tool_call["name"] return
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 tool_call = tool_calls[0]
console.print("[bold]Allow tool execution? (Y/n):[/bold] ", end="") tool_name = tool_call.get("name")
answer = input().strip().lower() tool_args = tool_call.get("args", {})
if answer in ("", "y", "yes"):
console.print("[green]Executing tool...[/green]") console.print(
# Recursively resume the agent f"[yellow]Agent wants to call tool:[/yellow] {tool_name}({tool_args})"
)
answer = console.input("Разрешить? (Y/n): ")
if answer.lower().strip() in ("", "y", "yes"):
# Resume the agent from the interruption point
ask_and_run(None, config) ask_and_run(None, config)
return
else: else:
console.print("[red]Tool execution cancelled by user.[/red]") console.print("[red]Отменено[/red]")
break return
# Handle updates (optional) # Handle updates (e.g., tool results) if needed
if chunk_type == "updates": if chunk_type == "updates":
# For this simple example, we ignore updates # In this simple example we don't process updates separately
pass pass
# If the agent has finished, exit the loop
if state.next == END:
return
def main() -> None: def main() -> None:
""" """
Main conversation loop. Main chat loop.
""" """
# Use a fixed thread ID for this session thread_id = "thread-1"
config = {"configurable": {"thread_id": "thread-1"}} config = {"configurable": {"thread_id": thread_id}}
console.print("[bold green]Welcome to the LangGraph Agent![/bold green]")
console.print("Type your messages below. Press Ctrl+C to exit.\n")
console.print("[bold cyan]Start chat. Type 'exit' to quit.[/bold cyan]")
while True: while True:
try: user_input = console.input("[bold blue]You:[/bold blue] ")
user_input = input("[bold]You:[/bold] ") if user_input.lower() in ("exit", "quit"):
if not user_input: console.print("[bold magenta]Goodbye![/bold magenta]")
continue
ask_and_run(user_input, config)
except KeyboardInterrupt:
console.print("\n[bold red]Exiting...[/bold red]")
break break
except Exception as e: ask_and_run(user_input, config)
console.print(f"[red]Unexpected error: {e}[/red]")
if __name__ == "__main__": if __name__ == "__main__":
main() main()