diff --git a/app/graph_history_manager.py b/app/graph_history_manager.py index d0ff3da..4b2c728 100644 --- a/app/graph_history_manager.py +++ b/app/graph_history_manager.py @@ -17,6 +17,7 @@ class GraphHistoryManager: def __init__(self, db_path="graph_history.db"): self.db_path = db_path + self._create_table(self._get_connection()) # Создаем таблицу при инициализации def _get_connection(self): """Получает соединение с базой данных.""" @@ -28,7 +29,6 @@ class GraphHistoryManager: cursor.execute(""" CREATE TABLE IF NOT EXISTS graphs ( id TEXT PRIMARY KEY, - messages TEXT, graph_nodes TEXT, graph_edges TEXT, current_node_id TEXT @@ -45,8 +45,12 @@ class GraphHistoryManager: result = cursor.fetchone()[0] return result if result is not None else 0 + # ---------------------------------------------------- public methods ---------------------------------------------------------------- def save_graph(self, graph_data: Any) -> str: - """Сохраняет или обновляет данные графа в базе данных и возвращает ID.""" + """ + Сохраняет или обновляет данные графа в базе данных и возвращает ID. + Не сохраняет полную историю сообщений, только структуру графа. + """ conn = self._get_connection() cursor = conn.cursor() try: @@ -58,7 +62,6 @@ class GraphHistoryManager: graph_data["id"] = graph_id # Преобразуем структуры данных в JSON-строки для хранения в SQLite - messages_json = json.dumps(graph_data.get("messages", [])) graph_nodes_json = json.dumps(graph_data.get("graph_nodes", [])) graph_edges_json = json.dumps(graph_data.get("graph_edges", [])) current_node_id = graph_data.get("current_node_id", "") @@ -72,20 +75,19 @@ class GraphHistoryManager: cursor.execute( """ UPDATE graphs SET - messages = ?, graph_nodes = ?, graph_edges = ?, current_node_id = ? WHERE id = ? - """, (messages_json, graph_nodes_json, graph_edges_json, + """, (graph_nodes_json, graph_edges_json, current_node_id, graph_id)) else: # Вставляем новую запись cursor.execute( """ - INSERT INTO graphs (id, messages, graph_nodes, graph_edges, current_node_id) - VALUES (?, ?, ?, ?, ?) - """, (graph_id, messages_json, graph_nodes_json, + INSERT INTO graphs (id, graph_nodes, graph_edges, current_node_id) + VALUES (?, ?, ?, ?) + """, (graph_id, graph_nodes_json, graph_edges_json, current_node_id)) conn.commit() @@ -94,29 +96,51 @@ class GraphHistoryManager: finally: conn.close() - def get_graph(self, graph_id: str) -> Optional[Any]: - """Получает данные графа по ID из базы данных.""" + def get_graph(self, graph_id: str, target_node_id: Optional[str] = None) -> Optional[Dict[str, Any]]: + """ + Получает данные графа по ID из базы данных. + Динамически вычисляет 'messages' до 'target_node_id'. + Если target_node_id не указан, используется current_node_id из БД. + """ conn = self._get_connection() cursor = conn.cursor() try: cursor.execute( - "SELECT messages, graph_nodes, graph_edges, current_node_id FROM graphs WHERE id = ?", + "SELECT graph_nodes, graph_edges, current_node_id FROM graphs WHERE id = ?", (graph_id, )) result = cursor.fetchone() if result: - messages_json, graph_nodes_json, graph_edges_json, current_node_id = result + graph_nodes_json, graph_edges_json, db_current_node_id = result + # Преобразуем JSON-строки обратно в структуры данных Python - messages = json.loads(messages_json) graph_nodes = json.loads(graph_nodes_json) graph_edges = json.loads(graph_edges_json) + resolved_current_node_id = target_node_id if target_node_id else db_current_node_id + + # Если нет узлов, значит граф пуст или некорректен + if not graph_nodes: + return { + "id": graph_id, + "messages": [], + "graph_nodes": [], + "graph_edges": [], + "current_node_id": resolved_current_node_id + } + + # Динамически собираем сообщения + messages = self.get_messages_from_root_to_node( + {"graph_nodes": graph_nodes, "graph_edges": graph_edges}, + resolved_current_node_id + ) + return { "id": graph_id, - "messages": messages, + "messages": messages, # Здесь будут вычисленные сообщения "graph_nodes": graph_nodes, "graph_edges": graph_edges, - "current_node_id": current_node_id + "current_node_id": resolved_current_node_id } else: return None @@ -124,54 +148,80 @@ class GraphHistoryManager: conn.close() def get_all_graphs_summary(self) -> List[Dict[str, str]]: - """Возвращает краткий список всех сохраненных графов.""" + """ + Возвращает краткий список всех сохраненных графов. + Первое сообщение извлекается путем построения пути к current_node_id + и взятия первого сообщения. + """ conn = self._get_connection() cursor = conn.cursor() try: - cursor.execute("SELECT id, messages FROM graphs") + cursor.execute("SELECT id, graph_nodes, graph_edges, current_node_id FROM graphs") graphs_data = cursor.fetchall() - return [{ - "id": - gid, - "first_message": - json.loads(messages)[0].get("content", "No content") - if messages else "No content" - } for gid, messages in graphs_data] + summaries = [] + for gid, nodes_json, edges_json, current_node_id in graphs_data: + graph_nodes = json.loads(nodes_json) + graph_edges = json.loads(edges_json) + + # Получаем все сообщения до current_node_id + full_messages = self.get_messages_from_root_to_node( + {"graph_nodes": graph_nodes, "graph_edges": graph_edges}, + current_node_id + ) + + first_message_content = "No content" + if full_messages: + # Ищем первое сообщение, которое является пользовательским + for msg in full_messages: + if msg.get("role") == "user": + first_message_content = msg.get("content", "No content") + break + if first_message_content == "No content": # Если не нашли пользовательского, берем первое ассистента + first_message_content = full_messages[0].get("content", "No content") + + summaries.append({ + "id": gid, + "first_message": first_message_content + }) + return summaries finally: conn.close() def get_messages_from_root_to_node( self, graph_data: Dict[str, Any], target_node_id: str) -> List[Dict[str, str]]: - """Получает список сообщений от корневого узла до указанного узла.""" - nodes = graph_data.get("graph_nodes", []) + """ + Получает список сообщений от корневого узла до указанного узла. + Сообщения извлекаются из данных узлов, а не из отдельного поля. + """ + all_nodes = graph_data.get("graph_nodes", []) edges = graph_data.get("graph_edges", []) - messages = graph_data.get("messages", []) + # Создаем словарь для быстрого поиска узлов по ID + node_map = {node['id']: node for node in all_nodes} # Создаем словарь для быстрого поиска родительских узлов parent_map = {edge['target']: edge['source'] for edge in edges} # Функция для рекурсивного подъема по дереву до корневого узла def get_path_to_root(node_id: str) -> List[str]: path = [node_id] - while node_id in parent_map: - node_id = parent_map[node_id] - path.append(node_id) + current = node_id + while current in parent_map: + current = parent_map[current] + path.append(current) return path[::-1] # Инвертируем, чтобы получить путь от корня # Получаем путь от корня до целевого узла - path_to_root = get_path_to_root(target_node_id) + path_to_target = get_path_to_root(target_node_id) - # Собираем сообщения, соответствующие узлам в пути + + # Собираем сообщения, соответствующие узлам в пути. + # Сообщения теперь хранятся в 'data' каждого узла при создании. messages_for_path = [] - node_ids_in_path = set(path_to_root) - - for message in messages: - if "node_id" in message and message["node_id"] in node_ids_in_path: - messages_for_path.append({ - "role": message["role"], - "content": message["content"] - }) + for node_id in path_to_target: + node = node_map.get(node_id) + if node and 'message' in node.get('data', {}): # Проверяем наличие поля 'message' + messages_for_path.append(node['data']['message']) return messages_for_path @@ -182,7 +232,7 @@ class GraphHistoryManager: try: cursor.execute("DELETE FROM graphs WHERE id = ?", (graph_id, )) conn.commit() - return True + return cursor.rowcount > 0 # Возвращает True, если была удалена хотя бы одна строка except Exception as e: print(f"Ошибка при удалении графа: {e}") return False diff --git a/app/llm_client.py b/app/llm_client.py index 30cdc52..c3230df 100644 --- a/app/llm_client.py +++ b/app/llm_client.py @@ -5,7 +5,7 @@ import requests from typing import Dict, Any, List -from langchain_core.messages import HumanMessage, SystemMessage +from langchain_core.messages import HumanMessage, SystemMessage, AIMessage # --- Конфигурация LLM --- # Пустой словарь для локальных моделей (Ollama) @@ -101,6 +101,11 @@ class CustomLLM: "role": "system", "content": msg.content }) + elif isinstance(msg, AIMessage): # Добавляем обработку AIMessage + openai_messages.append({ + "role": "assistant", + "content": msg.content + }) else: # Предполагаем, что это другие типы сообщений Langchain или словари openai_messages.append({ "role": @@ -155,6 +160,11 @@ class CustomLLM: "role": "system", "content": msg.content }) + elif isinstance(msg, AIMessage): # Добавляем обработку AIMessage + mistral_messages.append({ + "role": "assistant", + "content": msg.content + }) else: # Предполагаем, что это другие типы сообщений Langchain или словари mistral_messages.append({ "role": diff --git a/app/models.py b/app/models.py index 71cb887..f3e2809 100644 --- a/app/models.py +++ b/app/models.py @@ -14,11 +14,6 @@ class AgentState(BaseModel): command: Optional[str] = Field(None, description="Распознанная слеш-команда.") command_args: List[str] = Field([], description="Аргументы команды.") - chat_history: List[Dict[str, str]] = Field( - [], - description= - "Полная история сообщений в текущей ветке диалога. Формат: [{'role': 'user/assistant', 'content': 'message'}]" - ) llm_response: Optional[str] = Field(None, description="Ответ от LLM.") image_urls: List[str] = Field( [], description="URL сгенерированных изображений.") @@ -37,3 +32,7 @@ class AgentState(BaseModel): "start", description="ID текущего узла графа для фронтенда.") parent_node_id: Optional[str] = Field(None, description="ID родительского узла.") + # Временное хранилище для chat_history, которое не сохраняется в БД + # Оно используется для передачи контекста между узлами в пределах одного выполнения + temporary_chat_history: List[Dict[str, Any]] = Field( + [], description="Временная история чата, не сохраняемая в БД.") diff --git a/app/nodes.py b/app/nodes.py index 5948292..3f694e4 100644 --- a/app/nodes.py +++ b/app/nodes.py @@ -4,7 +4,7 @@ разбор команд, вызов LLM или выполнение сервисов. """ -from langchain_core.messages import HumanMessage, SystemMessage +from langchain_core.messages import HumanMessage, SystemMessage, AIMessage from llm_client import get_llm from services import ImageGenerationService, SubtitleService, ImageAnalysisService, SummarizationService @@ -66,21 +66,28 @@ class CommandManager: command_manager = CommandManager() +# Здесь мы меняем состояние графа, добавляя сообщение от пользователя def parse_command_node(state: AgentState) -> AgentState: - """Парсит вход пользователя на предмет слеш-команды.""" + """ + Парсит вход пользователя на предмет слеш-команды и инициализирует узел пользователя в графе. + """ print("Выполняется узел: parse_command_node") user_input = state.input.strip() - # Добавляем узел пользователя в граф для фронтенда + # Создаем ID для узла пользователя user_node_id = f"user_{len(state.graph_nodes) + 1}" + + # Добавляем узел пользователя в граф для фронтенда с полным текстом сообщения state.graph_nodes.append({ "id": user_node_id, "type": "user", "data": { - "label": - user_input[:30] + "..." if len(user_input) > 30 else user_input + "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: state.graph_edges.append({ "id": f"e{state.parent_node_id}-{user_node_id}", @@ -90,10 +97,12 @@ def parse_command_node(state: AgentState) -> AgentState: state.parent_node_id = user_node_id state.current_node_id = user_node_id - state.chat_history.append({ + # Обновляем временную историю чата, которая используется для передачи между узлами + # и для вызова LLM в текущем раунде. + state.temporary_chat_history.append({ "role": "user", "content": user_input, - "node_id": user_node_id # Сохраняем ID узла + "node_id": user_node_id }) if user_input.startswith('/'): @@ -116,7 +125,10 @@ def parse_command_node(state: AgentState) -> AgentState: def execute_command_node(state: AgentState) -> AgentState: - """Выполняет соответствующий workflow для команды.""" + """ + Выполняет соответствующий workflow для команды. + Этот узел служит маршрутизатором для выбора следующего узла в зависимости от команды. + """ print( f"Выполняется узел: execute_command_node для команды: {state.command}") # Этот узел будет выступать в роли маршрутизатора @@ -131,27 +143,32 @@ main_llm = get_llm(DEFAULT_LLM_NAME) def call_llm_node(state: AgentState) -> AgentState: - """Вызывает LLM для обработки обычного диалога или генерации ответа.""" + """ + Вызывает LLM для обработки обычного диалога или генерации ответа. + Использует `temporary_chat_history` для передачи контекста LLM. + """ print("Выполняется узел: call_llm_node (для обычного чата)") try: - # Для обычного чата LLM использует всю историю + # TODO: добавить в переписку ещё SystemMessage с системным промптом, типа: messages_for_llm.append(SystemMessage(content=msg["content"])) + # Для обычного чата LLM использует текущую временную историю messages_for_llm = [] - for msg in state.chat_history: + 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(SystemMessage(content=msg["content"])) + messages_for_llm.append(AIMessage(content=msg["content"])) response = main_llm.invoke(messages_for_llm) - # Добавляем узел LLM в граф для фронтенда llm_node_id = f"llm_{len(state.graph_nodes) + 1}" + + # Добавляем узел LLM в граф для фронтенда с полным текстом ответа state.graph_nodes.append({ "id": llm_node_id, "type": "llm", "data": { - "label": - response[:30] + "..." if len(response) > 30 else response + "label": response[:30] + "..." if len(response) > 30 else response, + "message": {"role": "assistant", "content": response, "node_id": llm_node_id} # Сохраняем сообщение здесь } }) state.graph_edges.append({ @@ -162,10 +179,11 @@ def call_llm_node(state: AgentState) -> AgentState: state.parent_node_id = llm_node_id state.current_node_id = llm_node_id - state.chat_history.append({ + # Обновляем временную историю чата с ответом LLM + state.temporary_chat_history.append({ "role": "assistant", "content": response, - "node_id": llm_node_id # Сохраняем ID узла + "node_id": llm_node_id }) state.llm_response = response @@ -182,12 +200,16 @@ image_gen_service = ImageGenerationService( 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: @@ -196,20 +218,21 @@ def generate_images_node(state: AgentState) -> AgentState: # Формируем текст ответа с использованием Markdown для отображения изображений image_markdown = "\n".join([ - f'image{i}' + f'image{i}' for i, url in enumerate(urls) ]) response_text = f"Сгенерировано {len(urls)} изображений:\n{image_markdown}" state.llm_response = response_text - # Добавляем узел генерации изображений в граф img_gen_node_id = f"img_gen_{len(state.graph_nodes) + 1}" + # Добавляем узел генерации изображений в граф state.graph_nodes.append({ "id": img_gen_node_id, "type": "tool", "data": { "label": "Генерация изображений", - "details": prompt + "details": prompt, + "message": {"role": "assistant", "content": response_text, "node_id": img_gen_node_id} } }) state.graph_edges.append({ @@ -220,8 +243,8 @@ def generate_images_node(state: AgentState) -> AgentState: state.parent_node_id = img_gen_node_id state.current_node_id = img_gen_node_id - state.llm_response = "Сгенерированы изображения." - state.chat_history.append({ + # Обновляем временную историю чата + state.temporary_chat_history.append({ "role": "assistant", "content": response_text, "node_id": img_gen_node_id # Привязываем к ID узла @@ -231,6 +254,8 @@ def generate_images_node(state: AgentState) -> AgentState: 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 @@ -239,11 +264,15 @@ image_analysis_service = ImageAnalysisService( def analyze_image_node(state: AgentState) -> AgentState: - """Анализирует изображение по URL или данным.""" + """ + Анализирует изображение по 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] @@ -253,20 +282,18 @@ def analyze_image_node(state: AgentState) -> AgentState: analysis_result = image_analysis_service.analyze_image( image_url, prompt) state.analysis_result = analysis_result - state.llm_response = f"Результат анализа изображения '{image_url}': {analysis_result}" - state.chat_history.append({ - "role": "assistant", - "content": state.llm_response - }) + response_text = f"Результат анализа изображения '{image_url}': {analysis_result}" + state.llm_response = response_text - # Добавляем узел анализа изображения в граф img_analysis_node_id = f"img_analysis_{len(state.graph_nodes) + 1}" + # Добавляем узел анализа изображения в граф state.graph_nodes.append({ "id": img_analysis_node_id, "type": "tool", "data": { "label": "Анализ изображения", - "details": f"URL: {image_url}, Промпт: {prompt}" + "details": f"URL: {image_url}, Промпт: {prompt}", + "message": {"role": "assistant", "content": response_text, "node_id": img_analysis_node_id} } }) state.graph_edges.append({ @@ -277,10 +304,19 @@ def analyze_image_node(state: AgentState) -> AgentState: 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 @@ -288,25 +324,25 @@ subtitle_service = SubtitleService() def get_meet_subtitles_node(state: AgentState) -> AgentState: - """Получает субтитры из Google Meet.""" + """ + Получает субтитры из Google Meet через команду `/subtitles_meet`. + """ print("Выполняется узел: get_meet_subtitles_node") try: subtitles = subtitle_service.get_google_meet_subtitles() state.subtitles = subtitles - state.llm_response = f"Текущие субтитры из Google Meet: {subtitles}" - state.chat_history.append({ - "role": "assistant", - "content": state.llm_response - }) + response_text = f"Текущие субтитры из Google Meet: {subtitles}" + state.llm_response = response_text - # Добавляем узел субтитров Meet в граф meet_subtitles_node_id = f"meet_subtitles_{len(state.graph_nodes) + 1}" + # Добавляем узел субтитров Meet в граф state.graph_nodes.append({ "id": meet_subtitles_node_id, "type": "tool", "data": { "label": "Субтитры Google Meet", - "details": subtitles[:30] + "..." if subtitles else "" + "details": subtitles[:30] + "..." if subtitles else "", + "message": {"role": "assistant", "content": response_text, "node_id": meet_subtitles_node_id} } }) state.graph_edges.append({ @@ -317,33 +353,42 @@ def get_meet_subtitles_node(state: AgentState) -> AgentState: 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.""" + """ + Получает субтитры из MS Teams через команду `/subtitles_teams`. + """ print("Выполняется узел: get_teams_subtitles_node") try: subtitles = subtitle_service.get_ms_teams_subtitles() state.subtitles = subtitles - state.llm_response = f"Текущие субтитры из MS Teams: {subtitles}" - state.chat_history.append({ - "role": "assistant", - "content": state.llm_response - }) + response_text = f"Текущие субтитры из MS Teams: {subtitles}" + state.llm_response = response_text - # Добавляем узел субтитров Teams в граф teams_subtitles_node_id = f"teams_subtitles_{len(state.graph_nodes) + 1}" + # Добавляем узел субтитров Teams в граф state.graph_nodes.append({ "id": teams_subtitles_node_id, "type": "tool", "data": { "label": "Субтитры MS Teams", - "details": subtitles[:30] + "..." if subtitles else "" + "details": subtitles[:30] + "..." if subtitles else "", + "message": {"role": "assistant", "content": response_text, "node_id": teams_subtitles_node_id} } }) state.graph_edges.append({ @@ -354,10 +399,19 @@ def get_teams_subtitles_node(state: AgentState) -> AgentState: 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 @@ -366,28 +420,27 @@ summarization_service = SummarizationService( def summarize_history_node(state: AgentState) -> AgentState: - """Суммирует историю диалога.""" + """ + Суммирует историю диалога, используя `temporary_chat_history` в качестве контекста. + """ print("Выполняется узел: summarize_history_node") try: - # Для суммаризации используем полную историю, которую ведет AgentState - summarized_text = summarization_service.summarize_history( - state.chat_history) + # Для суммаризации используем временную историю, подготовленную в предыдущих узлах + summarized_text = summarization_service.summarize_history_of_dicts( + state.temporary_chat_history) # Изменен метод для использования List[Dict] state.summarized_history_text = summarized_text - state.llm_response = f"История диалога суммирована:\n{summarized_text}" - state.chat_history.append({ - "role": "assistant", - "content": state.llm_response - }) + response_text = f"История диалога суммирована:\n{summarized_text}" + state.llm_response = response_text - # Добавляем узел суммаризации в граф summary_node_id = f"summary_{len(state.graph_nodes) + 1}" + # Добавляем узел суммаризации в граф state.graph_nodes.append({ "id": summary_node_id, "type": "tool", "data": { "label": "Суммаризация истории", - "details": - summarized_text[:30] + "..." if summarized_text else "" + "details": summarized_text[:30] + "..." if summarized_text else "", + "message": {"role": "assistant", "content": response_text, "node_id": summary_node_id} } }) state.graph_edges.append({ @@ -398,30 +451,38 @@ def summarize_history_node(state: AgentState) -> AgentState: 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 - state.chat_history.append({ - "role": "assistant", - "content": state.llm_response - }) - # Добавляем узел ошибки в граф error_node_id = f"error_{len(state.graph_nodes) + 1}" + # Добавляем узел ошибки в граф state.graph_nodes.append({ "id": error_node_id, "type": "error", "data": { "label": "Ошибка", - "details": state.error + "details": state.error, + "message": {"role": "assistant", "content": state.llm_response, "node_id": error_node_id} } }) state.graph_edges.append({ @@ -432,30 +493,36 @@ def handle_error_node(state: AgentState) -> AgentState: 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 - state.chat_history.append({ - "role": "assistant", - "content": state.llm_response - }) - # Добавляем узел справки в граф help_node_id = f"help_{len(state.graph_nodes) + 1}" + # Добавляем узел справки в граф state.graph_nodes.append({ "id": help_node_id, "type": "info", "data": { "label": "Справка", - "details": "Список команд" + "details": "Список команд", + "message": {"role": "assistant", "content": state.llm_response, "node_id": help_node_id} } }) state.graph_edges.append({ @@ -466,4 +533,11 @@ def help_node(state: AgentState) -> AgentState: 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 diff --git a/app/workflows.py b/app/workflows.py index ff214ca..46e5339 100644 --- a/app/workflows.py +++ b/app/workflows.py @@ -97,41 +97,57 @@ def run_agent(user_input: str, Может продолжить существующий граф по graph_id. Возвращает обновленное состояние и ID графа. """ + + # ---------------------------------------------------- Initial State Setup ---------------------------------------------------------------- initial_state = AgentState(input=user_input, parent_node_id=parent_node_id) - # Если есть существующий ID графа, загружаем его историю + # Если есть существующий ID графа, загружаем его историю и структуру + loaded_graph_data = None if existing_graph_id: - existing_graph_data = graph_history_manager.get_graph( - existing_graph_id) - if existing_graph_data: + # Получаем данные графа, включая сгенерированные сообщения до parent_node_id (либо current_node_id из БД) + # Если parent_node_id передан, используем его для построения истории. + loaded_graph_data = graph_history_manager.get_graph( + existing_graph_id, target_node_id=parent_node_id) + + if loaded_graph_data: print(f"Продолжаю существующий граф {existing_graph_id}") - initial_state.chat_history = existing_graph_data.get( - "messages", []) - initial_state.graph_nodes = existing_graph_data.get( - "graph_nodes", []) - initial_state.graph_edges = existing_graph_data.get( - "graph_edges", []) - initial_state.parent_node_id = parent_node_id or existing_graph_data.get( - "current_node_id") + # Загружаем узлы и ребра для продолжения графа + initial_state.graph_nodes = loaded_graph_data.get("graph_nodes", []) + initial_state.graph_edges = loaded_graph_data.get("graph_edges", []) + + # Устанавливаем parent_node_id для нового входного узла + # Если parent_node_id был передан в запросе, используем его, + # иначе берем current_node_id из загруженного графа. + initial_state.parent_node_id = parent_node_id or loaded_graph_data.get("current_node_id") + + # История чата для текущего раунда работы LLM формируется из загруженных сообщений. + initial_state.temporary_chat_history = loaded_graph_data.get("messages", []) + + # ---------------------------------------------------- run_agent ---------------------------------------------------------------- # Запускаем граф result = app.invoke(initial_state) final_state = AgentState(**result) - # Сохраняем обновленное состояние графа + # ---------------------------------------------------- Save Graph ---------------------------------------------------------------- + # Сохраняем обновленное состояние графа. + # Обратите внимание: chat_history больше не сохраняется напрямую, + # она является частью данных узлов в graph_nodes. graph_data_to_save = { - "id": existing_graph_id, - "messages": final_state.chat_history, + "id": existing_graph_id, # Используем существующий ID или он будет сгенерирован + # менеджером истории при первом сохранении "graph_nodes": final_state.graph_nodes, "graph_edges": final_state.graph_edges, "current_node_id": final_state.current_node_id } new_graph_id = graph_history_manager.save_graph(graph_data_to_save) - # Формируем ответ для фронтенда + # ---------------------------------------------------- prepare response ---------------------------------------------------------------- + # Для `messages` в `response_data` используем `final_state.temporary_chat_history`, + # так как она отражает только сообщения текущей ветки, добавленные в ходе этого выполнения. response_data = { "graph_id": new_graph_id, - "messages": final_state.chat_history, + "messages": final_state.temporary_chat_history, "llm_response": final_state.llm_response, "image_urls": final_state.image_urls, "analysis_result": final_state.analysis_result, diff --git a/graph_history.db b/graph_history.db index bfc11bc..e2c7b1c 100644 Binary files a/graph_history.db and b/graph_history.db differ