инструмент для отправки файла пользователю

This commit is contained in:
Егор Кандрушин 2026-04-19 21:02:56 +03:00
parent 1c2f9495db
commit 2d4a5d73c7
3 changed files with 65 additions and 12 deletions

View file

@ -5,6 +5,7 @@ from langchain_openai import ChatOpenAI
from langgraph.checkpoint.memory import MemorySaver from langgraph.checkpoint.memory import MemorySaver
from src.agent.backends import IsolatedShellBackend from src.agent.backends import IsolatedShellBackend
from src.agent.tools import send_file
def create_agent(): def create_agent():
@ -25,6 +26,7 @@ def create_agent():
return create_deep_agent( return create_deep_agent(
model=model, model=model,
tools=[send_file],
system_prompt="You are a helpful assistant.", system_prompt="You are a helpful assistant.",
checkpointer=MemorySaver(), checkpointer=MemorySaver(),
backend=backend, backend=backend,

View file

@ -3,8 +3,12 @@ from abc import abstractmethod
from src.agent.base import create_agent from src.agent.base import create_agent
from lambda_agent_api.server import ( from lambda_agent_api.server import (
AgentEventUnion, MsgEventTextChunk, MsgEventToolCallChunk, AgentEventUnion,
MsgEventToolResult, MsgEventEnd MsgEventTextChunk,
MsgEventToolCallChunk,
MsgEventToolResult,
MsgEventSendFile,
MsgEventEnd,
) )
@ -12,6 +16,7 @@ class ChatBusyError(Exception):
""" """
Чат занят в другом блоке ``with`` Чат занят в другом блоке ``with``
""" """
pass pass
@ -24,15 +29,15 @@ class AgentChat(AsyncContextManager[Self]):
Перед вызовом любых методов (``astream`` и т. д.) необходимо войти в блок ``with``. Перед вызовом любых методов (``astream`` и т. д.) необходимо войти в блок ``with``.
Объект получается из AgentService.chat(). Объект получается из AgentService.chat().
""" """
chat_id: int chat_id: int
@abstractmethod @abstractmethod
def astream(self, text: str) -> AsyncIterator[AgentEventUnion]: def astream(self, text: str) -> AsyncIterator[AgentEventUnion]: ...
...
class AgentService: class AgentService:
_instance = None # синглтон _instance = None # синглтон
def __new__(cls): def __new__(cls):
if cls._instance is None: if cls._instance is None:
@ -45,7 +50,7 @@ class AgentService:
Своеобразная реализация Mutex'а. Служит прослойкой до методов AgentService, но подставляет в них 'захваченный' chat_id. Своеобразная реализация Mutex'а. Служит прослойкой до методов AgentService, но подставляет в них 'захваченный' chat_id.
""" """
__locks: set[int] = set() # чаты, которые уже "взяты" __locks: set[int] = set() # чаты, которые уже "взяты"
def __init__(self, service: AgentService, chat_id: int) -> None: def __init__(self, service: AgentService, chat_id: int) -> None:
self.__chat_id = chat_id self.__chat_id = chat_id
@ -78,14 +83,16 @@ class AgentService:
""" """
return self.__AgentChat(self, chat_id) return self.__AgentChat(self, chat_id)
async def __astream(self, chat_id: int, text: str) -> AsyncIterator[AgentEventUnion]: async def __astream(
self, chat_id: int, text: str
) -> AsyncIterator[AgentEventUnion]:
config = {"configurable": {"thread_id": chat_id}} config = {"configurable": {"thread_id": chat_id}}
# Используем astream_events для перехвата детальных событий (инструменты, чанки и т.д.) # Используем astream_events для перехвата детальных событий (инструменты, чанки и т.д.)
async for event in self._agent.astream_events( async for event in self._agent.astream_events(
{"messages": [{"role": "user", "content": text}]}, {"messages": [{"role": "user", "content": text}]},
config=config, config=config,
version="v2" # Обязательно v2 для современных версий LangChain version="v2", # Обязательно v2 для современных версий LangChain
): ):
kind = event["event"] kind = event["event"]
@ -102,15 +109,19 @@ class AgentService:
for tool_chunk in chunk.tool_call_chunks: for tool_chunk in chunk.tool_call_chunks:
yield MsgEventToolCallChunk( yield MsgEventToolCallChunk(
tool_name=tool_chunk.get("name"), tool_name=tool_chunk.get("name"),
args_chunk=tool_chunk.get("args") args_chunk=tool_chunk.get("args"),
) )
# 2. Инструмент завершил работу и вернул результат # 2. Инструмент завершил работу и вернул результат
elif kind == "on_tool_end": elif kind == "on_tool_end":
yield MsgEventToolResult( yield MsgEventToolResult(
tool_name=event["name"], tool_name=event["name"], result=event["data"].get("output")
result=event["data"].get("output")
) )
# 3. Кастомные события (send_file и др.)
elif kind == "on_custom_event":
if event["name"] == "send_file":
yield MsgEventSendFile(path=event["data"]["path"])
# 3. В конце генерации отправляем событие завершения # 3. В конце генерации отправляем событие завершения
yield MsgEventEnd(tokens_used=0) # потом заменить на метадату yield MsgEventEnd(tokens_used=0) # потом заменить на метадату

40
src/agent/tools.py Normal file
View file

@ -0,0 +1,40 @@
import os
from pathlib import Path
from langchain_core.callbacks import adispatch_custom_event
from langchain_core.tools import tool
@tool
async def send_file(path: str) -> str:
"""Отправить файл пользователю.
Используй этот инструмент, когда пользователь просит:
- скачать файл
- отправить файл
- прислать документ
- получить файл
- открыть/показать файл в чате
Этот инструмент НЕ используется когда:
- пользователь просто хочет прочитать содержимое файла (используй read_file)
- нужно создать или изменить файл (используй write_file/edit_file)
Args:
path: Путь к файлу относительно /workspace (например: 'report.pdf', 'docs/readme.txt', 'output/data.json')
Returns:
Подтверждение отправки или сообщение об ошибке
"""
workspace = os.environ.get("WORKSPACE_DIR", "/workspace")
full_path = Path(workspace) / path
if not full_path.exists():
return f"Ошибка: файл '{path}' не найден в /workspace"
if not full_path.is_file():
return f"Ошибка: '{path}' не является файлом"
await adispatch_custom_event(name="send_file", data={"path": path})
return f"Файл '{path}' отправлен пользователю"