From 2d4a5d73c78dfdd74f27e6e77edf78aca2c4ee56 Mon Sep 17 00:00:00 2001 From: MrKan Date: Sun, 19 Apr 2026 21:02:56 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B8=D0=BD=D1=81=D1=82=D1=80=D1=83=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=20=D0=B4=D0=BB=D1=8F=20=D0=BE=D1=82=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20=D1=84=D0=B0=D0=B9=D0=BB=D0=B0?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5?= =?UTF-8?q?=D0=BB=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/agent/base.py | 2 ++ src/agent/service.py | 35 +++++++++++++++++++++++------------ src/agent/tools.py | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 12 deletions(-) create mode 100644 src/agent/tools.py diff --git a/src/agent/base.py b/src/agent/base.py index fe08072..d85775f 100644 --- a/src/agent/base.py +++ b/src/agent/base.py @@ -5,6 +5,7 @@ from langchain_openai import ChatOpenAI from langgraph.checkpoint.memory import MemorySaver from src.agent.backends import IsolatedShellBackend +from src.agent.tools import send_file def create_agent(): @@ -25,6 +26,7 @@ def create_agent(): return create_deep_agent( model=model, + tools=[send_file], system_prompt="You are a helpful assistant.", checkpointer=MemorySaver(), backend=backend, diff --git a/src/agent/service.py b/src/agent/service.py index 170bb53..90c4663 100644 --- a/src/agent/service.py +++ b/src/agent/service.py @@ -3,8 +3,12 @@ from abc import abstractmethod from src.agent.base import create_agent from lambda_agent_api.server import ( - AgentEventUnion, MsgEventTextChunk, MsgEventToolCallChunk, - MsgEventToolResult, MsgEventEnd + AgentEventUnion, + MsgEventTextChunk, + MsgEventToolCallChunk, + MsgEventToolResult, + MsgEventSendFile, + MsgEventEnd, ) @@ -12,6 +16,7 @@ class ChatBusyError(Exception): """ Чат занят в другом блоке ``with`` """ + pass @@ -24,15 +29,15 @@ class AgentChat(AsyncContextManager[Self]): Перед вызовом любых методов (``astream`` и т. д.) необходимо войти в блок ``with``. Объект получается из AgentService.chat(). """ + chat_id: int @abstractmethod - def astream(self, text: str) -> AsyncIterator[AgentEventUnion]: - ... + def astream(self, text: str) -> AsyncIterator[AgentEventUnion]: ... class AgentService: - _instance = None # синглтон + _instance = None # синглтон def __new__(cls): if cls._instance is None: @@ -45,7 +50,7 @@ class AgentService: Своеобразная реализация Mutex'а. Служит прослойкой до методов AgentService, но подставляет в них 'захваченный' chat_id. """ - __locks: set[int] = set() # чаты, которые уже "взяты" + __locks: set[int] = set() # чаты, которые уже "взяты" def __init__(self, service: AgentService, chat_id: int) -> None: self.__chat_id = chat_id @@ -78,21 +83,23 @@ class AgentService: """ 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}} # Используем astream_events для перехвата детальных событий (инструменты, чанки и т.д.) async for event in self._agent.astream_events( {"messages": [{"role": "user", "content": text}]}, config=config, - version="v2" # Обязательно v2 для современных версий LangChain + version="v2", # Обязательно v2 для современных версий LangChain ): kind = event["event"] # 1. Агент генерирует токены (текст или аргументы для инструмента) if kind == "on_chat_model_stream": chunk = event["data"]["chunk"] - + # Если генерируется обычный текст if chunk.content: yield MsgEventTextChunk(text=chunk.content) @@ -102,15 +109,19 @@ class AgentService: for tool_chunk in chunk.tool_call_chunks: yield MsgEventToolCallChunk( tool_name=tool_chunk.get("name"), - args_chunk=tool_chunk.get("args") + args_chunk=tool_chunk.get("args"), ) # 2. Инструмент завершил работу и вернул результат elif kind == "on_tool_end": yield MsgEventToolResult( - tool_name=event["name"], - result=event["data"].get("output") + tool_name=event["name"], 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. В конце генерации отправляем событие завершения yield MsgEventEnd(tokens_used=0) # потом заменить на метадату diff --git a/src/agent/tools.py b/src/agent/tools.py new file mode 100644 index 0000000..b87aa9e --- /dev/null +++ b/src/agent/tools.py @@ -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}' отправлен пользователю"