558 lines
24 KiB
Python
558 lines
24 KiB
Python
"""
|
||
Этот файл содержит узлы для графа состояний агента.
|
||
Каждый узел выполняет определенную функцию, такую как
|
||
разбор команд, вызов 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 |