""" Определяет логику рабочих процессов агента, используя LangGraph для управления состояниями и переходами между узлами обработки. """ from typing import Dict, Any, Optional, Set from langgraph.graph import StateGraph, END from graph_history_manager import GraphHistoryManager from models import AgentState from nodes import parse_command_node, execute_command_node, call_llm_node, generate_images_node, analyze_image_node, get_meet_subtitles_node, get_teams_subtitles_node, summarize_history_node, handle_error_node, help_node import uuid # Добавлено для генерации UUID from llm_client import get_llm # --- LangGraph: Построение графа --- graph_history_manager = GraphHistoryManager() workflow = StateGraph(AgentState) # Добавляем узлы workflow.add_node("parse_command", parse_command_node) workflow.add_node("execute_command", execute_command_node) # Это по сути роутер workflow.add_node("call_llm", call_llm_node) workflow.add_node("generate_images", generate_images_node) workflow.add_node("analyze_image", analyze_image_node) workflow.add_node("get_meet_subtitles", get_meet_subtitles_node) workflow.add_node("get_teams_subtitles", get_teams_subtitles_node) workflow.add_node("summarize_history", summarize_history_node) workflow.add_node("handle_error", handle_error_node) workflow.add_node("help", help_node) # Устанавливаем точку входа workflow.set_entry_point("parse_command") # Определяем маршрутизацию после parse_command def route_command(state: AgentState) -> str: """Маршрутизирует выполнение на основе распознанной команды.""" if state.error: return "handle_error" # Если была ошибка парсинга команды elif state.command == "imagine": return "generate_images" elif state.command == "analyze": return "analyze_image" elif state.command == "subtitles_meet": return "get_meet_subtitles" elif state.command == "subtitles_teams": return "get_teams_subtitles" elif state.command == "summarize": return "summarize_history" elif state.command == "help": return "help" elif state.command == "chat": return "call_llm" elif state.command == "error": # Команда была, но не известная return "handle_error" else: # Fallback на LLM, если команда не распознана или пуста (хотя parse_command должен был бы ее поймать) return "call_llm" workflow.add_conditional_edges( "parse_command", # Откуда route_command, # Функция, определяющая куда идти { "generate_images": "generate_images", "analyze_image": "analyze_image", "get_meet_subtitles": "get_meet_subtitles", "get_teams_subtitles": "get_teams_subtitles", "summarize_history": "summarize_history", "help": "help", "call_llm": "call_llm", "handle_error": "handle_error", # Для ошибок, найденных в parse_command }, ) # Все узлы операций (кроме ошибок) ведут к END после завершения workflow.add_edge("call_llm", END) workflow.add_edge("generate_images", END) workflow.add_edge("analyze_image", END) workflow.add_edge("get_meet_subtitles", END) workflow.add_edge("get_teams_subtitles", END) workflow.add_edge("summarize_history", END) workflow.add_edge("help", END) workflow.add_edge("handle_error", END) # Ошибка - это тоже конечный пункт для текущего запроса # Компилируем граф app = workflow.compile() def run_agent_streaming(graph_id: str, user_node_id: str, assistant_node_id: str, system_prompt: Optional[str] = None, model: Optional[str] = None): """Генератор для стримингового ответа LLM с поддержкой вложений.""" try: loaded_graph_data = graph_history_manager.get_graph( graph_id, target_node_id=user_node_id) if not loaded_graph_data: yield {"type": "error", "error": "Граф не найден."} return messages = loaded_graph_data.get("messages", []) from langchain_core.messages import HumanMessage, SystemMessage, AIMessage messages_for_llm = [] if system_prompt: messages_for_llm.append(SystemMessage(content=system_prompt)) # ИЗМЕНЕНО: Обрабатываем вложения for i, msg in enumerate(messages): is_last = (i == len(messages) - 1) if msg["role"] == "user": content = msg["content"] attachments = msg.get("attachments", []) # Раскрываем постоянные вложения и вложения последнего сообщения if attachments: for att in attachments: if att.get("permanent") or is_last: formatted_content = format_attachment_for_llm(att) content += "\n" + formatted_content messages_for_llm.append(HumanMessage(content=content)) elif msg["role"] == "assistant": messages_for_llm.append(AIMessage(content=msg["content"])) # Стриминг ответа llm = get_llm(model or "gemini-2.5-flash") for chunk in llm.stream2(messages_for_llm): content = chunk.content if hasattr(chunk, 'content') else str(chunk) yield {"type": "chunk", "content": content} except Exception as e: print(f"Ошибка при стриминге: {e}") yield {"type": "error", "error": str(e)} def format_attachment_for_llm(attachment: Dict[str, Any]) -> str: """Форматирует вложение для вставки в сообщение LLM.""" att_type = "промпта" if attachment["type"] == "prompt" else "файла" header = f"\n\n--- Содержимое {att_type}: {attachment['name']} ---\n" footer = f"\n--- Конец {att_type}: {attachment['name']} ---\n\n" return header + attachment.get("content", "").strip() + footer