llm-agent-backend/app/nodes.py
2025-09-17 02:23:19 +03:00

558 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Этот файл содержит узлы для графа состояний агента.
Каждый узел выполняет определенную функцию, такую как
разбор команд, вызов LLM или выполнение сервисов.
"""
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
from llm_client import get_llm
from services import ImageGenerationService, SubtitleService, ImageAnalysisService, SummarizationService
from models import AgentState
from typing import Dict, Any, Optional
import uuid # Добавлено для генерации UUID
class CommandManager:
"""
Менеджер для хранения и получения слеш-команд.
В реальном приложении команды будут храниться в БД.
"""
def __init__(self):
self._commands = {
"help": {
"description": "Показывает список доступных команд.",
"workflow": "help_workflow"
},
"imagine": {
"description":
"Генерирует изображения по промпту. Использование: /imagine [prompt]",
"workflow": "image_generation_workflow"
},
"analyze": {
"description":
"Анализирует изображение по URL и промпту. Использование: /analyze [url] [prompt]",
"workflow": "image_analysis_workflow"
},
"subtitles_meet": {
"description": "Получает текущие субтитры из Google Meet.",
"workflow": "get_meet_subtitles_workflow"
},
"subtitles_teams": {
"description": "Получает текущие субтитры из MS Teams.",
"workflow": "get_teams_subtitles_workflow"
},
"summarize": {
"description": "Суммирует текущую историю диалога.",
"workflow": "summarize_history_workflow"
},
"chat": {
"description": "Ведет обычный диалог с LLM.",
"workflow": "chat_workflow"
},
}
def get_commands(self) -> Dict[str, Any]:
"""Возвращает все доступные команды."""
return self._commands
def get_command_workflow(self, command_name: str) -> Optional[str]:
"""Возвращает имя workflow для заданной команды."""
return self._commands.get(command_name, {}).get("workflow")
# Инициализация сервисов (пример, убедитесь, что они доступны в этом файле или импортированы)
command_manager = CommandManager()
# Здесь мы меняем состояние графа, добавляя сообщение от пользователя
def parse_command_node(state: AgentState) -> AgentState:
"""
Парсит вход пользователя на предмет слеш-команды и инициализирует узел пользователя в графе.
"""
print("Выполняется узел: parse_command_node")
user_input = state.input.strip()
# Создаем ID для узла пользователя с помощью UUID
user_node_id = str(uuid.uuid4())
# Добавляем узел пользователя в граф для фронтенда с полным текстом сообщения
state.graph_nodes.append({
"id": user_node_id,
"type": "user",
"data": {
"label": user_input[:30] + "..." if len(user_input) > 30 else user_input,
"message": {"role": "user", "content": user_input, "node_id": user_node_id} # Сохраняем сообщение здесь
}
})
# Добавляем ребро к новому узлу, если есть родительский узел
if state.parent_node_id:
edge_id = str(uuid.uuid4())
state.graph_edges.append({
"id": edge_id,
"source": state.parent_node_id,
"target": user_node_id
})
state.parent_node_id = user_node_id
state.current_node_id = user_node_id
# Обновляем временную историю чата, которая используется для передачи между узлами
# и для вызова LLM в текущем раунде.
state.temporary_chat_history.append({
"role": "user",
"content": user_input,
"node_id": user_node_id
})
if user_input.startswith('/'):
parts = user_input[1:].split(' ', 1)
command_name = parts[0].lower()
command_args = parts[1].split(' ') if len(parts) > 1 else []
if command_name in command_manager.get_commands():
state.command = command_name
state.command_args = command_args
print(
f"Команда распознана: /{state.command} с аргументами: {state.command_args}"
)
else:
state.error = f"Неизвестная команда: /{command_name}. Введите /help для списка команд."
print(state.error)
state.command = "error" # Специальный флаг для обработки ошибок команд
else:
state.command = "chat" # Обычный чат
return state
def execute_command_node(state: AgentState) -> AgentState:
"""
Выполняет соответствующий workflow для команды.
Этот узел служит маршрутизатором для выбора следующего узла в зависимости от команды.
"""
print(
f"Выполняется узел: execute_command_node для команды: {state.command}")
# Этот узел будет выступать в роли маршрутизатора
# Фактическая логика команды будет в отдельных узлах, вызываемых по условию
return state # Просто передаем состояние дальше, чтобы маршрутизатор смог выбрать следующий узел
# Предполагаем, что используем Gemini для основного LLM и суммаризации/анализа
# В реальной системе можно выбрать динамически
DEFAULT_LLM_NAME = "gemini-2.0-flash" #"gemini-2.5-flash"
main_llm = get_llm(DEFAULT_LLM_NAME)
def call_llm_node(state: AgentState) -> AgentState:
"""
Вызывает LLM для обработки обычного диалога или генерации ответа.
Использует `temporary_chat_history` для передачи контекста LLM.
"""
print("Выполняется узел: call_llm_node (для обычного чата)")
try:
messages_for_llm = []
if state.system_prompt:
messages_for_llm.append(SystemMessage(content=state.system_prompt))
# Для обычного чата LLM использует текущую временную историю
for msg in state.temporary_chat_history: # Используем temporary_chat_history
if msg["role"] == "user":
messages_for_llm.append(HumanMessage(content=msg["content"]))
elif msg["role"] == "assistant":
messages_for_llm.append(AIMessage(content=msg["content"]))
model_name = state.selected_model
llm = get_llm(model_name)
response = llm.invoke(messages_for_llm)
llm_node_id = str(uuid.uuid4())
# Добавляем узел LLM в граф для фронтенда с полным текстом ответа
state.graph_nodes.append({
"id": llm_node_id,
"type": "llm",
"data": {
"label": response[:30] + "..." if len(response) > 30 else response,
"message": {"role": "assistant", "content": response, "node_id": llm_node_id} # Сохраняем сообщение здесь
}
})
edge_id = str(uuid.uuid4())
state.graph_edges.append({
"id": edge_id,
"source": state.parent_node_id,
"target": llm_node_id
})
state.parent_node_id = llm_node_id
state.current_node_id = llm_node_id
# Обновляем временную историю чата с ответом LLM
state.temporary_chat_history.append({
"role": "assistant",
"content": response,
"node_id": llm_node_id
})
state.llm_response = response
except Exception as e:
state.error = f"Ошибка при вызове LLM: {e}"
state.llm_response = f"Произошла ошибка: {e}"
print(state.error)
return state
image_gen_service = ImageGenerationService(
base_url="https://render-service-gsu7.onrender.com/g2"
) # TODO: Замените на реальный URL
def generate_images_node(state: AgentState) -> AgentState:
"""
Генерирует изображения по промпту, переданному в команде `/imagine`.
"""
print("Выполняется узел: generate_images_node")
prompt = " ".join(state.command_args)
if not prompt:
state.error = "Для команды /imagine требуется промпт. Пример: /imagine кошка на луне"
state.llm_response = state.error
# Добавляем ошибку во временную историю для согласованности
state.temporary_chat_history.append({"role": "assistant", "content": state.error})
return state
try:
urls = image_gen_service.generate_images(prompt)
state.image_urls = urls
# Формируем текст ответа с использованием Markdown для отображения изображений
image_markdown = "\n".join([
f'<a href="{url}" target="_blank"><img src="{url}" alt="image{i}" style="max-width: 300px; max-height: 300px;"></a>'
for i, url in enumerate(urls)
])
response_text = f"Сгенерировано {len(urls)} изображений:\n{image_markdown}"
state.llm_response = response_text
img_gen_node_id = str(uuid.uuid4())
# Добавляем узел генерации изображений в граф
state.graph_nodes.append({
"id": img_gen_node_id,
"type": "tool",
"data": {
"label": "Генерация изображений",
"details": prompt,
"message": {"role": "assistant", "content": response_text, "node_id": img_gen_node_id}
}
})
edge_id = str(uuid.uuid4())
state.graph_edges.append({
"id": edge_id,
"source": state.parent_node_id,
"target": img_gen_node_id
})
state.parent_node_id = img_gen_node_id
state.current_node_id = img_gen_node_id
# Обновляем временную историю чата
state.temporary_chat_history.append({
"role": "assistant",
"content": response_text,
"node_id": img_gen_node_id # Привязываем к ID узла
})
except Exception as e:
state.error = f"Ошибка генерации изображений: {e}"
state.llm_response = f"Произошла ошибка при генерации изображений: {e}"
print(state.error)
# Добавляем ошибку во временную историю
state.temporary_chat_history.append({"role": "assistant", "content": state.llm_response})
return state
image_analysis_service = ImageAnalysisService(
llm=get_llm("gemini-2.5-flash")) # Gemini для анализа изображений
def analyze_image_node(state: AgentState) -> AgentState:
"""
Анализирует изображение по URL и промпту, переданным в команде `/analyze`.
"""
print("Выполняется узел: analyze_image_node")
if len(state.command_args) < 2:
state.error = "Для команды /analyze требуется URL изображения и промпт. Пример: /analyze [url] 'что на изображении?'"
state.llm_response = state.error
# Добавляем ошибку во временную историю для согласованности
state.temporary_chat_history.append({"role": "assistant", "content": state.error})
return state
image_url = state.command_args[0]
prompt = " ".join(state.command_args[1:])
try:
analysis_result = image_analysis_service.analyze_image(
image_url, prompt)
state.analysis_result = analysis_result
response_text = f"Результат анализа изображения '{image_url}': {analysis_result}"
state.llm_response = response_text
img_analysis_node_id = str(uuid.uuid4())
# Добавляем узел анализа изображения в граф
state.graph_nodes.append({
"id": img_analysis_node_id,
"type": "tool",
"data": {
"label": "Анализ изображения",
"details": f"URL: {image_url}, Промпт: {prompt}",
"message": {"role": "assistant", "content": response_text, "node_id": img_analysis_node_id}
}
})
edge_id = str(uuid.uuid4())
state.graph_edges.append({
"id": edge_id,
"source": state.parent_node_id,
"target": img_analysis_node_id
})
state.parent_node_id = img_analysis_node_id
state.current_node_id = img_analysis_node_id
# Обновляем временную историю чата
state.temporary_chat_history.append({
"role": "assistant",
"content": response_text,
"node_id": img_analysis_node_id
})
except Exception as e:
state.error = f"Ошибка анализа изображения: {e}"
state.llm_response = f"Произошла ошибка при анализе изображения: {e}"
print(state.error)
# Добавляем ошибку во временную историю
state.temporary_chat_history.append({"role": "assistant", "content": state.llm_response})
return state
subtitle_service = SubtitleService()
def get_meet_subtitles_node(state: AgentState) -> AgentState:
"""
Получает субтитры из Google Meet через команду `/subtitles_meet`.
"""
print("Выполняется узел: get_meet_subtitles_node")
try:
subtitles = subtitle_service.get_google_meet_subtitles()
state.subtitles = subtitles
response_text = f"Текущие субтитры из Google Meet: {subtitles}"
state.llm_response = response_text
meet_subtitles_node_id = str(uuid.uuid4())
# Добавляем узел субтитров Meet в граф
state.graph_nodes.append({
"id": meet_subtitles_node_id,
"type": "tool",
"data": {
"label": "Субтитры Google Meet",
"details": subtitles[:30] + "..." if subtitles else "",
"message": {"role": "assistant", "content": response_text, "node_id": meet_subtitles_node_id}
}
})
edge_id = str(uuid.uuid4())
state.graph_edges.append({
"id": edge_id,
"source": state.parent_node_id,
"target": meet_subtitles_node_id
})
state.parent_node_id = meet_subtitles_node_id
state.current_node_id = meet_subtitles_node_id
# Обновляем временную историю чата
state.temporary_chat_history.append({
"role": "assistant",
"content": response_text,
"node_id": meet_subtitles_node_id
})
except Exception as e:
state.error = f"Ошибка получения субтитров Google Meet: {e}"
state.llm_response = f"Произошла ошибка при получении субтитров Google Meet: {e}"
print(state.error)
# Добавляем ошибку во временную историю
state.temporary_chat_history.append({"role": "assistant", "content": state.llm_response})
return state
def get_teams_subtitles_node(state: AgentState) -> AgentState:
"""
Получает субтитры из MS Teams через команду `/subtitles_teams`.
"""
print("Выполняется узел: get_teams_subtitles_node")
try:
subtitles = subtitle_service.get_ms_teams_subtitles()
state.subtitles = subtitles
response_text = f"Текущие субтитры из MS Teams: {subtitles}"
state.llm_response = response_text
teams_subtitles_node_id = str(uuid.uuid4())
# Добавляем узел субтитров Teams в граф
state.graph_nodes.append({
"id": teams_subtitles_node_id,
"type": "tool",
"data": {
"label": "Субтитры MS Teams",
"details": subtitles[:30] + "..." if subtitles else "",
"message": {"role": "assistant", "content": response_text, "node_id": teams_subtitles_node_id}
}
})
edge_id = str(uuid.uuid4())
state.graph_edges.append({
"id": edge_id,
"source": state.parent_node_id,
"target": teams_subtitles_node_id
})
state.parent_node_id = teams_subtitles_node_id
state.current_node_id = teams_subtitles_node_id
# Обновляем временную историю чата
state.temporary_chat_history.append({
"role": "assistant",
"content": response_text,
"node_id": teams_subtitles_node_id
})
except Exception as e:
state.error = f"Ошибка получения субтитров MS Teams: {e}"
state.llm_response = f"Произошла ошибка при получении субтитров MS Teams: {e}"
print(state.error)
# Добавляем ошибку во временную историю
state.temporary_chat_history.append({"role": "assistant", "content": state.llm_response})
return state
summarization_service = SummarizationService(
llm=get_llm("gemini-2.5-flash")) # Gemini для суммаризации
def summarize_history_node(state: AgentState) -> AgentState:
"""
Суммирует историю диалога, используя `temporary_chat_history` в качестве контекста.
"""
print("Выполняется узел: summarize_history_node")
try:
# Для суммаризации используем временную историю, подготовленную в предыдущих узлах
summarized_text = summarization_service.summarize_history_of_dicts(
state.temporary_chat_history) # Изменен метод для использования List[Dict]
state.summarized_history_text = summarized_text
response_text = f"История диалога суммирована:\n{summarized_text}"
state.llm_response = response_text
summary_node_id = str(uuid.uuid4())
# Добавляем узел суммаризации в граф
state.graph_nodes.append({
"id": summary_node_id,
"type": "tool",
"data": {
"label": "Суммаризация истории",
"details": summarized_text[:30] + "..." if summarized_text else "",
"message": {"role": "assistant", "content": response_text, "node_id": summary_node_id}
}
})
edge_id = str(uuid.uuid4())
state.graph_edges.append({
"id": edge_id,
"source": state.parent_node_id,
"target": summary_node_id
})
state.parent_node_id = summary_node_id
state.current_node_id = summary_node_id
# Обновляем временную историю чата
state.temporary_chat_history.append({
"role": "assistant",
"content": response_text,
"node_id": summary_node_id
})
except Exception as e:
state.error = f"Ошибка суммаризации истории: {e}"
state.llm_response = f"Произошла ошибка при суммаризации истории: {e}"
print(state.error)
# Добавляем ошибку во временную историю
state.temporary_chat_history.append({"role": "assistant", "content": state.llm_response})
return state
def handle_error_node(state: AgentState) -> AgentState:
"""
Обрабатывает ошибки команд, формируя ответ для пользователя и добавляя его в граф.
"""
print(f"Выполняется узел: handle_error_node. Ошибка: {state.error}")
state.llm_response = state.error
error_node_id = str(uuid.uuid4())
# Добавляем узел ошибки в граф
state.graph_nodes.append({
"id": error_node_id,
"type": "error",
"data": {
"label": "Ошибка",
"details": state.error,
"message": {"role": "assistant", "content": state.llm_response, "node_id": error_node_id}
}
})
edge_id = str(uuid.uuid4())
state.graph_edges.append({
"id": edge_id,
"source": state.parent_node_id,
"target": error_node_id
})
state.parent_node_id = error_node_id
state.current_node_id = error_node_id
# Обновляем временную историю чата
state.temporary_chat_history.append({
"role": "assistant",
"content": state.llm_response,
"node_id": error_node_id
})
return state
def help_node(state: AgentState) -> AgentState:
"""
Предоставляет информацию о доступных командах, формируя ответ для пользователя и добавляя его в граф.
"""
print("Выполняется узел: help_node")
commands_info = command_manager.get_commands()
help_text = "Доступные команды:\n"
for cmd, info in commands_info.items():
help_text += f"/{cmd}: {info['description']}\n"
state.llm_response = help_text
help_node_id = str(uuid.uuid4())
# Добавляем узел справки в граф
state.graph_nodes.append({
"id": help_node_id,
"type": "info",
"data": {
"label": "Справка",
"details": "Список команд",
"message": {"role": "assistant", "content": state.llm_response, "node_id": help_node_id}
}
})
edge_id = str(uuid.uuid4())
state.graph_edges.append({
"id": edge_id,
"source": state.parent_node_id,
"target": help_node_id
})
state.parent_node_id = help_node_id
state.current_node_id = help_node_id
# Обновляем временную историю чата
state.temporary_chat_history.append({
"role": "assistant",
"content": state.llm_response,
"node_id": help_node_id
})
return state