diff --git a/app/api.py b/app/api.py index 15865ea..0791cf8 100644 --- a/app/api.py +++ b/app/api.py @@ -8,12 +8,17 @@ from flask import Flask, request, jsonify from flask_cors import CORS from workflows import run_agent, graph_history_manager from llm_client import MODELS # Добавляем импорт списка моделей +from title_generator import TitleGenerator api = Flask(__name__) CORS( api ) # Разрешаем CORS для всех доменов (в production нужно настроить более строго) +# Инициализируем сервис генерации заголовков +title_generator = TitleGenerator(graph_history_manager) +title_generator.start() + @api.route('/api/chat', methods=['POST']) def chat(): @@ -84,4 +89,10 @@ def get_messages_from_root_to_node(graph_id, node_id): @api.route('/api/models', methods=['GET']) def get_available_models(): """API endpoint для получения списка доступных моделей.""" - return jsonify(list(MODELS.keys())) \ No newline at end of file + return jsonify(list(MODELS.keys())) + +@api.teardown_appcontext +def cleanup(error): + """Очистка ресурсов при завершении приложения.""" + if hasattr(api, '_title_generator'): + title_generator.stop() \ No newline at end of file diff --git a/app/graph_history_manager.py b/app/graph_history_manager.py index 416d0ba..1ab084f 100644 --- a/app/graph_history_manager.py +++ b/app/graph_history_manager.py @@ -3,13 +3,15 @@ from typing import Dict, Any, List, Optional import json import sqlite3 -import uuid # Для генерации UUID +import uuid # Для генерации UUID + # ----------------------------------------------- Exceptions ------------------------------------------------------------ class GraphUpdateConflictError(Exception): """Исключение возникает при конфликте обновления графа.""" pass + # ----------------------------------------------- GraphHistoryManager ------------------------------------------------------------ class GraphHistoryManager: """ @@ -28,7 +30,8 @@ class GraphHistoryManager: Получает соединение с базой данных. Устанавливаем таймаут для ожидания блокировки базы данных при конкурентной записи. """ - return sqlite3.connect(self.db_path, timeout=5.0) # Увеличил таймаут до 5 секунд + return sqlite3.connect(self.db_path, + timeout=5.0) # Увеличил таймаут до 5 секунд def _create_tables(self): """Создает таблицы для хранения графов, если они не существуют.""" @@ -38,7 +41,9 @@ class GraphHistoryManager: CREATE TABLE IF NOT EXISTS graphs ( id TEXT PRIMARY KEY, current_node_id TEXT DEFAULT NULL, - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP -- Добавляем метку времени для сортировки + title TEXT DEFAULT NULL, + title_generated BOOLEAN DEFAULT FALSE, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP ) """) cursor.execute(""" @@ -46,7 +51,9 @@ class GraphHistoryManager: graph_id TEXT NOT NULL, node_id TEXT NOT NULL, node_type TEXT NOT NULL, - node_data TEXT NOT NULL, -- JSON-строка для 'data' из узла + node_data TEXT NOT NULL, + title TEXT DEFAULT NULL, + title_generated BOOLEAN DEFAULT FALSE, PRIMARY KEY (graph_id, node_id), FOREIGN KEY (graph_id) REFERENCES graphs (id) ON DELETE CASCADE ) @@ -63,6 +70,17 @@ class GraphHistoryManager: FOREIGN KEY (target_node_id) REFERENCES graph_nodes_data (node_id) ON DELETE CASCADE ) """) + # Очередь для обработки заголовков + cursor.execute(""" + CREATE TABLE IF NOT EXISTS title_generation_queue ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + item_type TEXT NOT NULL, -- 'graph' or 'node' + graph_id TEXT NOT NULL, + node_id TEXT DEFAULT NULL, -- только для узлов + priority INTEGER DEFAULT 0, -- для приоритета обработки + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + """) conn.commit() # _get_max_graph_id - удален, так как используем UUID для graph_id @@ -71,54 +89,60 @@ class GraphHistoryManager: def _get_graph_nodes(self, conn, graph_id: str) -> List[Dict[str, Any]]: """Извлекает все узлы для заданного графа.""" cursor = conn.cursor() - cursor.execute("SELECT node_id, node_type, node_data FROM graph_nodes_data WHERE graph_id = ?", (graph_id,)) + cursor.execute( + "SELECT node_id, node_type, node_data, title, title_generated FROM graph_nodes_data WHERE graph_id = ?", + (graph_id, )) nodes = [] - for node_id, node_type, node_data_json in cursor.fetchall(): + for node_id, node_type, node_data_json, title, title_generated in cursor.fetchall(): + node_data = json.loads(node_data_json) + # Добавляем заголовок в данные узла + if title and title_generated: + node_data["title"] = title + node_data["title_generated"] = bool(title_generated) + nodes.append({ "id": node_id, "type": node_type, - "data": json.loads(node_data_json) + "data": node_data }) return nodes def _get_graph_edges(self, conn, graph_id: str) -> List[Dict[str, Any]]: """Извлекает все ребра для заданного графа.""" cursor = conn.cursor() - cursor.execute("SELECT edge_id, source_node_id, target_node_id FROM graph_edges_data WHERE graph_id = ?", (graph_id,)) + cursor.execute( + "SELECT edge_id, source_node_id, target_node_id FROM graph_edges_data WHERE graph_id = ?", + (graph_id, )) edges = [] for edge_id, source, target in cursor.fetchall(): - edges.append({ - "id": edge_id, - "source": source, - "target": target - }) + edges.append({"id": edge_id, "source": source, "target": target}) return edges - def _update_graph_current_node_id(self, conn, graph_id: str, new_current_node_id: str): + def _update_graph_current_node_id(self, conn, graph_id: str, + new_current_node_id: str): """Обновляет current_node_id для графа.""" cursor = conn.cursor() - cursor.execute("UPDATE graphs SET current_node_id = ? WHERE id = ?", (new_current_node_id, graph_id)) + cursor.execute("UPDATE graphs SET current_node_id = ? WHERE id = ?", + (new_current_node_id, graph_id)) def _add_graph_node(self, conn, graph_id: str, node: Dict[str, Any]): """Добавляет новый узел в граф. Использует INSERT OR IGNORE для избежания конфликтов.""" cursor = conn.cursor() cursor.execute( "INSERT OR IGNORE INTO graph_nodes_data (graph_id, node_id, node_type, node_data) VALUES (?, ?, ?, ?)", - (graph_id, node["id"], node["type"], json.dumps(node["data"])) - ) + (graph_id, node["id"], node["type"], json.dumps(node["data"]))) def _add_graph_edge(self, conn, graph_id: str, edge: Dict[str, Any]): """Добавляет новое ребро в граф. Использует INSERT OR IGNORE для избежания конфликтов.""" cursor = conn.cursor() cursor.execute( "INSERT OR IGNORE INTO graph_edges_data (graph_id, edge_id, source_node_id, target_node_id) VALUES (?, ?, ?, ?)", - (graph_id, edge["id"], edge["source"], edge["target"]) - ) + (graph_id, edge["id"], edge["source"], edge["target"])) # ----------------------------------------------- Public Methods ------------------------------------------------------------ - def save_graph_changes(self, graph_id: str, - new_nodes: List[Dict[str, Any]], - new_edges: List[Dict[str, Any]], + def save_graph_changes(self, graph_id: str, new_nodes: List[Dict[str, + Any]], + new_edges: List[Dict[str, Any]], current_node_id: str) -> str: """ Сохраняет *изменения* в граф в базе данных. @@ -128,50 +152,78 @@ class GraphHistoryManager: Если graph_id пуст, генерируется новый UUID для графа. """ if not graph_id: - graph_id = str(uuid.uuid4()) # Генерируем новый UUID для ID графа - - with self._get_connection() as conn: # Одна транзакция для всех изменений + graph_id = str(uuid.uuid4()) # Генерируем новый UUID для ID графа + + with self._get_connection( + ) as conn: # Одна транзакция для всех изменений try: # Убедимся, что запись о графе существует в главной таблице `graphs` # INSERT OR IGNORE создаст новую запись, если её нет. # Если граф уже существует, это ничего не изменит. - conn.execute("INSERT OR IGNORE INTO graphs (id) VALUES (?)", (graph_id,)) - + conn.execute("INSERT OR IGNORE INTO graphs (id) VALUES (?)", + (graph_id, )) + + # Проверяем, новый ли это граф + cursor = conn.cursor() + cursor.execute( + "SELECT COUNT(*) FROM graph_nodes_data WHERE graph_id = ?", + (graph_id, )) + existing_nodes_count = cursor.fetchone()[0] + is_new_graph = existing_nodes_count == 0 + # Добавляем новые узлы for node in new_nodes: self._add_graph_node(conn, graph_id, node) + # Добавляем в очередь заголовков с высоким приоритетом + cursor.execute( + "INSERT INTO title_generation_queue (item_type, graph_id, node_id, priority) VALUES (?, ?, ?, ?)", + ("node", graph_id, node["id"], 10)) # Добавляем новые ребра for edge in new_edges: self._add_graph_edge(conn, graph_id, edge) # Обновляем current_node_id - это атомарное изменение в рамках транзакции - self._update_graph_current_node_id(conn, graph_id, current_node_id) + self._update_graph_current_node_id(conn, graph_id, + current_node_id) - conn.commit() # Фиксируем все изменения + # Если новый граф, добавляем его в очередь заголовков + if is_new_graph: + cursor.execute( + "INSERT INTO title_generation_queue (item_type, graph_id, priority) VALUES (?, ?, ?)", + ("graph", graph_id, 10)) + + conn.commit() # Фиксируем все изменения print(f"Изменения графа {graph_id} сохранены.") return graph_id except sqlite3.OperationalError as e: - conn.rollback() # Откатываем транзакцию при ошибке (например, DB Locked) - print(f"Ошибка блокировки SQLite при сохранении изменений графа {graph_id}: {e}") + conn.rollback( + ) # Откатываем транзакцию при ошибке (например, DB Locked) + print( + f"Ошибка блокировки SQLite при сохранении изменений графа {graph_id}: {e}" + ) raise except Exception as e: - conn.rollback() # Откатываем транзакцию при других ошибках + conn.rollback() # Откатываем транзакцию при других ошибках print(f"Ошибка при сохранении изменений графа {graph_id}: {e}") raise - def get_graph(self, graph_id: str, target_node_id: Optional[str] = None) -> Optional[Dict[str, Any]]: + def get_graph( + self, + graph_id: str, + target_node_id: Optional[str] = None) -> Optional[Dict[str, Any]]: """ Получает данные графа по ID из базы данных. """ with self._get_connection() as conn: cursor = conn.cursor() - cursor.execute("SELECT current_node_id FROM graphs WHERE id = ?", (graph_id,)) + cursor.execute("SELECT current_node_id FROM graphs WHERE id = ?", + (graph_id, )) result = cursor.fetchone() if result: db_current_node_id = result[0] - + # Получаем все узлы и ребра graph_nodes = self._get_graph_nodes(conn, graph_id) graph_edges = self._get_graph_edges(conn, graph_id) @@ -179,15 +231,16 @@ class GraphHistoryManager: resolved_current_node_id = target_node_id if target_node_id else db_current_node_id messages = self.get_messages_from_root_to_node( - {"graph_nodes": graph_nodes, "graph_edges": graph_edges}, - resolved_current_node_id - ) + { + "graph_nodes": graph_nodes, + "graph_edges": graph_edges + }, resolved_current_node_id) return { "id": graph_id, "messages": messages, - "graph_nodes": graph_nodes, - "graph_edges": graph_edges, + "graph_nodes": graph_nodes, + "graph_edges": graph_edges, "current_node_id": resolved_current_node_id } else: @@ -200,31 +253,45 @@ class GraphHistoryManager: with self._get_connection() as conn: cursor = conn.cursor() # Сортируем по timestamp по убыванию, чтобы самые новые были сверху - cursor.execute("SELECT id, current_node_id FROM graphs ORDER BY timestamp DESC") + cursor.execute( + "SELECT id, current_node_id, title, title_generated FROM graphs ORDER BY timestamp DESC" + ) graphs_data = cursor.fetchall() summaries = [] - for gid, current_node_id in graphs_data: - graph_nodes = self._get_graph_nodes(conn, gid) - graph_edges = self._get_graph_edges(conn, gid) - - # Получаем все сообщения до 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" and full_messages: - first_message_content = full_messages[0].get("content", "No content") - + for gid, current_node_id, title, title_generated in graphs_data: + if title and title_generated: + display_title = title + else: + # Fallback к первому сообщению + graph_nodes = self._get_graph_nodes(conn, gid) + graph_edges = self._get_graph_edges(conn, gid) + + # Получаем все сообщения до 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" and full_messages: + first_message_content = full_messages[0].get( + "content", "No content") + + # Обрезаем до 50 символов для отображения + display_title = first_message_content[:50] + "..." if len( + first_message_content) > 50 else first_message_content + summaries.append({ "id": gid, - "first_message": first_message_content + "title": display_title, + "title_generated": bool(title_generated) }) return summaries @@ -257,18 +324,22 @@ class GraphHistoryManager: messages_for_path = [] for node_id in path_to_target: node = node_map.get(node_id) - if node and 'message' in node.get('data', {}): # Проверяем наличие поля 'message' + if node and 'message' in node.get( + 'data', {}): # Проверяем наличие поля 'message' original_message = node['data']['message'] messages_for_path.append({ - "role": original_message.get("role", "unknown"), - "type": node.get("type", "unknown"), # Используем 'type' узла - "content": original_message.get("content", ""), - "node_id": original_message.get("node_id", node_id) + "role": + original_message.get("role", "unknown"), + "type": + node.get("type", "unknown"), # Используем 'type' узла + "content": + original_message.get("content", ""), + "node_id": + original_message.get("node_id", node_id) }) return messages_for_path - def delete_graph(self, graph_id: str) -> bool: """ Удаляет граф из базы данных. @@ -277,10 +348,107 @@ class GraphHistoryManager: with self._get_connection() as conn: cursor = conn.cursor() try: - cursor.execute("DELETE FROM graphs WHERE id = ?", (graph_id,)) + cursor.execute("DELETE FROM graphs WHERE id = ?", (graph_id, )) conn.commit() return cursor.rowcount > 0 except Exception as e: conn.rollback() print(f"Ошибка при удалении графа: {e}") - return False \ No newline at end of file + return False + + # ----------------------------------------------- Title Generation Queue Methods ------------------------------------------------------------ + + def add_to_title_queue(self, + item_type: str, + graph_id: str, + node_id: str = None, + priority: int = 0): + """Добавляет элемент в очередь генерации заголовков.""" + with self._get_connection() as conn: + cursor = conn.cursor() + cursor.execute( + "INSERT INTO title_generation_queue (item_type, graph_id, node_id, priority) VALUES (?, ?, ?, ?)", + (item_type, graph_id, node_id, priority)) + conn.commit() + + def get_next_from_title_queue(self) -> Optional[Dict[str, Any]]: + """Получает следующий элемент из очереди для обработки.""" + with self._get_connection() as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT id, item_type, graph_id, node_id + FROM title_generation_queue + ORDER BY priority DESC, id ASC + LIMIT 1 + """) + result = cursor.fetchone() + if result: + queue_id, item_type, graph_id, node_id = result + # Удаляем из очереди + cursor.execute( + "DELETE FROM title_generation_queue WHERE id = ?", + (queue_id, )) + conn.commit() + return { + "item_type": item_type, + "graph_id": graph_id, + "node_id": node_id + } + return None + + def populate_initial_title_queue(self): + """Заполняет очередь элементами без сгенерированных заголовков при старте сервера.""" + with self._get_connection() as conn: + cursor = conn.cursor() + + # Очищаем существующую очередь + cursor.execute("DELETE FROM title_generation_queue") + + # Добавляем графы без заголовков + cursor.execute( + "SELECT id FROM graphs WHERE title_generated = FALSE OR title_generated IS NULL" + ) + for (graph_id, ) in cursor.fetchall(): + cursor.execute( + "INSERT INTO title_generation_queue (item_type, graph_id, priority) VALUES (?, ?, ?)", + ("graph", graph_id, 1)) + + # Добавляем узлы без заголовков + cursor.execute( + "SELECT graph_id, node_id FROM graph_nodes_data WHERE title_generated = FALSE OR title_generated IS NULL" + ) + for graph_id, node_id in cursor.fetchall(): + cursor.execute( + "INSERT INTO title_generation_queue (item_type, graph_id, node_id, priority) VALUES (?, ?, ?, ?)", + ("node", graph_id, node_id, 1)) + + conn.commit() + + # ----------------------------------------------- Title Update Methods ------------------------------------------------------------ + + def update_graph_title(self, graph_id: str, title: str): + """Обновляет заголовок графа и устанавливает флаг генерации.""" + with self._get_connection() as conn: + cursor = conn.cursor() + cursor.execute( + "UPDATE graphs SET title = ?, title_generated = TRUE WHERE id = ?", + (title, graph_id)) + conn.commit() + + def update_node_title(self, graph_id: str, node_id: str, title: str): + """Обновляет заголовок узла и устанавливает флаг генерации.""" + with self._get_connection() as conn: + cursor = conn.cursor() + # Сначала проверим, существует ли узел + cursor.execute( + "SELECT COUNT(*) FROM graph_nodes_data WHERE graph_id = ? AND node_id = ?", + (graph_id, node_id)) + count = cursor.fetchone()[0] + print(f"Найдено узлов для обновления: {count}") + + cursor.execute( + "UPDATE graph_nodes_data SET title = ?, title_generated = TRUE WHERE graph_id = ? AND node_id = ?", + (title, graph_id, node_id)) + updated_rows = cursor.rowcount + print(f"Обновлено строк: {updated_rows}") + conn.commit() diff --git a/app/llm_client.py b/app/llm_client.py index f223deb..55e53dc 100644 --- a/app/llm_client.py +++ b/app/llm_client.py @@ -17,7 +17,7 @@ MODELS: Dict[str, Dict[str, Any]] = { "name": "gemini-2.0-flash", "provider": "openai", # Изменено на "openai" "model_name": "gemini-2.0-flash", # Добавлено имя модели для LangChain - "apiBase": "https://render-service-gsu7.onrender.com/g/v1beta", + "apiBase": "https://render-service-gsu7.onrender.com/g/v1beta", # "generativelanguage.googleapis.com", # "https://render-service-gsu7.onrender.com/g/v1beta", "apiKey": "AIzaSyDpueKFWVqknVKlQn6TdasLmJ2lvAUiBik", "stream": True, "capabilities": ["vision"], @@ -26,7 +26,7 @@ MODELS: Dict[str, Dict[str, Any]] = { "name": "gemini-2.5-flash", "provider": "openai", # Изменено на "openai" "model_name": "gemini-2.5-flash", # Добавлено имя модели для LangChain - "apiBase": "https://render-service-gsu7.onrender.com/g/v1beta", + "apiBase": "https://render-service-gsu7.onrender.com/g/v1beta", # "generativelanguage.googleapis.com", # "https://render-service-gsu7.onrender.com/g/v1beta", "apiKey": "AIzaSyDpueKFWVqknVKlQn6TdasLmJ2lvAUiBik", "stream": True, "capabilities": ["vision"], @@ -43,7 +43,7 @@ MODELS: Dict[str, Dict[str, Any]] = { "mistral-small-latest": { "name": "mistral-small-latest", "provider": "mistralai", - "baseUrl": "https://render-service-gsu7.onrender.com/m", + "baseUrl": "https://render-service-gsu7.onrender.com/m", # "api.mistral.ai", # "https://render-service-gsu7.onrender.com/m", "apiKey": "Q0m29fvxBY0Cfdj4sjHaKqccy1NjonLW", "stream": True, "capabilities": ["vision"], diff --git a/app/nodes.py b/app/nodes.py index a744514..b6f6666 100644 --- a/app/nodes.py +++ b/app/nodes.py @@ -143,6 +143,8 @@ def execute_command_node(state: AgentState) -> AgentState: DEFAULT_LLM_NAME = "gemini-2.0-flash" #"gemini-2.5-flash" main_llm = get_llm(DEFAULT_LLM_NAME) +DEFAULT_SUMMARIZATION_LLM_NAME = "gemini-2.0-flash" + def call_llm_node(state: AgentState) -> AgentState: """ diff --git a/app/title_generator.py b/app/title_generator.py new file mode 100644 index 0000000..3558b86 --- /dev/null +++ b/app/title_generator.py @@ -0,0 +1,151 @@ +""" +Асинхронный сервис для генерации заголовков графов и узлов. +Работает в фоновом режиме, обрабатывая очередь элементов без заголовков. +""" + +import threading +import time +from typing import Optional +from graph_history_manager import GraphHistoryManager +from nodes import DEFAULT_SUMMARIZATION_LLM_NAME +from llm_client import get_llm +from langchain_core.messages import SystemMessage, HumanMessage + +class TitleGenerator: + """Сервис для асинхронной генерации заголовков.""" + + def __init__(self, history_manager: GraphHistoryManager): + self.history_manager = history_manager + self.llm = get_llm(DEFAULT_SUMMARIZATION_LLM_NAME) + self.running = False + self.thread: Optional[threading.Thread] = None + + # Системные промпты для генерации заголовков + self.graph_title_prompt = """Создай краткий заголовок (максимум 80 символов) для диалога на основе первого сообщения пользователя. +Заголовок должен отражать основную тему или вопрос. Отвечай только заголовком, без дополнительных объяснений. Ты должен сделать саммери, а не ответить на вопросы, если они есть в сообщении.""" + + self.node_title_prompt = """Создай краткий заголовок (максимум 40 символов) для этого сообщения/действия. +Заголовок должен кратко описывать суть сообщения или действия. Отвечай только заголовком, без дополнительных объяснений. Ты должен сделать саммери, а не ответить на вопросы, если они есть в сообщении.""" + + def start(self): + """Запускает фоновый процесс генерации заголовков.""" + if self.running: + return + + self.running = True + + # Заполняем очередь при старте + self.history_manager.populate_initial_title_queue() + + # Запускаем фоновый поток + self.thread = threading.Thread(target=self._process_queue, daemon=True) + self.thread.start() + print("Сервис генерации заголовков запущен") + + def stop(self): + """Останавливает фоновый процесс.""" + self.running = False + if self.thread: + self.thread.join() + print("Сервис генерации заголовков остановлен") + + def _process_queue(self): + """Основной цикл обработки очереди заголовков.""" + while self.running: + try: + item = self.history_manager.get_next_from_title_queue() + + if item: + if item["item_type"] == "graph": + self._generate_graph_title(item["graph_id"]) + elif item["item_type"] == "node": + self._generate_node_title(item["graph_id"], item["node_id"]) + else: + # Если очередь пуста, ждем немного + time.sleep(5) + + except Exception as e: + print(f"Ошибка при обработке очереди заголовков: {e}") + time.sleep(10) + + def _generate_graph_title(self, graph_id: str): + """Генерирует заголовок для графа.""" + try: + graph_data = self.history_manager.get_graph(graph_id) + if not graph_data: + return + + messages = graph_data.get("messages", []) + if not messages: + return + + # Находим первое пользовательское сообщение + first_user_message = None + for msg in messages: + if msg.get("role") == "user": + first_user_message = msg.get("content", "") + break + + if not first_user_message: + return + + # Генерируем заголовок + llm_messages = [ + SystemMessage(content=self.graph_title_prompt), + HumanMessage(content=first_user_message) + ] + + title = self.llm.invoke(llm_messages) + title = title.strip()[:80] # Ограничиваем длину + + # Сохраняем заголовок + self.history_manager.update_graph_title(graph_id, title) + print(f"Сгенерирован заголовок графа {graph_id}: {title}") + + except Exception as e: + print(f"Ошибка генерации заголовка графа {graph_id}: {e}") + + def _generate_node_title(self, graph_id: str, node_id: str): + """Генерирует заголовок для узла.""" + try: + graph_data = self.history_manager.get_graph(graph_id) + if not graph_data: + return + + # Находим узел по ID + target_node = None + for node in graph_data.get("graph_nodes", []): + if node["id"] == node_id: + target_node = node + break + + if not target_node: + return + + # Получаем содержимое узла + node_data = target_node.get("data", {}) + message = node_data.get("message", {}) + content = message.get("content", "") + + if not content: + # Если нет content, используем label или type + content = node_data.get("label", target_node.get("type", "")) + + if not content: + return + + # Генерируем заголовок + llm_messages = [ + SystemMessage(content=self.node_title_prompt), + HumanMessage(content=content) + ] + + title = self.llm.invoke(llm_messages) + title = title.strip()[:40] # Ограничиваем длину + + # Сохраняем заголовок + self.history_manager.update_node_title(graph_id, node_id, title) + print(f"Сгенерирован заголовок узла {node_id}: {title}") + + except Exception as e: + print(f"Ошибка генерации заголовка узла {node_id}: {e}") \ No newline at end of file diff --git a/graph_history.db b/graph_history.db index e820d3e..51cd0c5 100644 Binary files a/graph_history.db and b/graph_history.db differ diff --git a/run-llm-backend.bat b/run-llm-backend.bat new file mode 100644 index 0000000..e342cb7 --- /dev/null +++ b/run-llm-backend.bat @@ -0,0 +1,5 @@ +chcp 65001 +cd /d "D:\Docs\Гриша\Хобби\Настольные игры\ttrpg-obsidian-vault\llm-agent-backend" +call "env\Scripts\activate.bat" +python run.py +pause \ No newline at end of file