Add title generator

This commit is contained in:
dimitrievgs 2025-09-24 02:27:03 +03:00
parent b59c53ad07
commit 5a1fb4d482
7 changed files with 410 additions and 73 deletions

View File

@ -8,12 +8,17 @@ from flask import Flask, request, jsonify
from flask_cors import CORS from flask_cors import CORS
from workflows import run_agent, graph_history_manager from workflows import run_agent, graph_history_manager
from llm_client import MODELS # Добавляем импорт списка моделей from llm_client import MODELS # Добавляем импорт списка моделей
from title_generator import TitleGenerator
api = Flask(__name__) api = Flask(__name__)
CORS( CORS(
api api
) # Разрешаем CORS для всех доменов (в production нужно настроить более строго) ) # Разрешаем CORS для всех доменов (в production нужно настроить более строго)
# Инициализируем сервис генерации заголовков
title_generator = TitleGenerator(graph_history_manager)
title_generator.start()
@api.route('/api/chat', methods=['POST']) @api.route('/api/chat', methods=['POST'])
def chat(): def chat():
@ -85,3 +90,9 @@ def get_messages_from_root_to_node(graph_id, node_id):
def get_available_models(): def get_available_models():
"""API endpoint для получения списка доступных моделей.""" """API endpoint для получения списка доступных моделей."""
return jsonify(list(MODELS.keys())) return jsonify(list(MODELS.keys()))
@api.teardown_appcontext
def cleanup(error):
"""Очистка ресурсов при завершении приложения."""
if hasattr(api, '_title_generator'):
title_generator.stop()

View File

@ -5,11 +5,13 @@ import json
import sqlite3 import sqlite3
import uuid # Для генерации UUID import uuid # Для генерации UUID
# ----------------------------------------------- Exceptions ------------------------------------------------------------ # ----------------------------------------------- Exceptions ------------------------------------------------------------
class GraphUpdateConflictError(Exception): class GraphUpdateConflictError(Exception):
"""Исключение возникает при конфликте обновления графа.""" """Исключение возникает при конфликте обновления графа."""
pass pass
# ----------------------------------------------- GraphHistoryManager ------------------------------------------------------------ # ----------------------------------------------- GraphHistoryManager ------------------------------------------------------------
class 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): def _create_tables(self):
"""Создает таблицы для хранения графов, если они не существуют.""" """Создает таблицы для хранения графов, если они не существуют."""
@ -38,7 +41,9 @@ class GraphHistoryManager:
CREATE TABLE IF NOT EXISTS graphs ( CREATE TABLE IF NOT EXISTS graphs (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
current_node_id TEXT DEFAULT NULL, 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(""" cursor.execute("""
@ -46,7 +51,9 @@ class GraphHistoryManager:
graph_id TEXT NOT NULL, graph_id TEXT NOT NULL,
node_id TEXT NOT NULL, node_id TEXT NOT NULL,
node_type 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), PRIMARY KEY (graph_id, node_id),
FOREIGN KEY (graph_id) REFERENCES graphs (id) ON DELETE CASCADE 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 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() conn.commit()
# _get_max_graph_id - удален, так как используем UUID для graph_id # _get_max_graph_id - удален, так как используем UUID для graph_id
@ -71,53 +89,59 @@ class GraphHistoryManager:
def _get_graph_nodes(self, conn, graph_id: str) -> List[Dict[str, Any]]: def _get_graph_nodes(self, conn, graph_id: str) -> List[Dict[str, Any]]:
"""Извлекает все узлы для заданного графа.""" """Извлекает все узлы для заданного графа."""
cursor = conn.cursor() 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 = [] 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({ nodes.append({
"id": node_id, "id": node_id,
"type": node_type, "type": node_type,
"data": json.loads(node_data_json) "data": node_data
}) })
return nodes return nodes
def _get_graph_edges(self, conn, graph_id: str) -> List[Dict[str, Any]]: def _get_graph_edges(self, conn, graph_id: str) -> List[Dict[str, Any]]:
"""Извлекает все ребра для заданного графа.""" """Извлекает все ребра для заданного графа."""
cursor = conn.cursor() 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 = [] edges = []
for edge_id, source, target in cursor.fetchall(): for edge_id, source, target in cursor.fetchall():
edges.append({ edges.append({"id": edge_id, "source": source, "target": target})
"id": edge_id,
"source": source,
"target": target
})
return edges 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 для графа.""" """Обновляет current_node_id для графа."""
cursor = conn.cursor() 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]): def _add_graph_node(self, conn, graph_id: str, node: Dict[str, Any]):
"""Добавляет новый узел в граф. Использует INSERT OR IGNORE для избежания конфликтов.""" """Добавляет новый узел в граф. Использует INSERT OR IGNORE для избежания конфликтов."""
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute( cursor.execute(
"INSERT OR IGNORE INTO graph_nodes_data (graph_id, node_id, node_type, node_data) VALUES (?, ?, ?, ?)", "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]): def _add_graph_edge(self, conn, graph_id: str, edge: Dict[str, Any]):
"""Добавляет новое ребро в граф. Использует INSERT OR IGNORE для избежания конфликтов.""" """Добавляет новое ребро в граф. Использует INSERT OR IGNORE для избежания конфликтов."""
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute( cursor.execute(
"INSERT OR IGNORE INTO graph_edges_data (graph_id, edge_id, source_node_id, target_node_id) VALUES (?, ?, ?, ?)", "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 ------------------------------------------------------------ # ----------------------------------------------- Public Methods ------------------------------------------------------------
def save_graph_changes(self, graph_id: str, def save_graph_changes(self, graph_id: str, new_nodes: List[Dict[str,
new_nodes: List[Dict[str, Any]], Any]],
new_edges: List[Dict[str, Any]], new_edges: List[Dict[str, Any]],
current_node_id: str) -> str: current_node_id: str) -> str:
""" """
@ -130,43 +154,71 @@ class GraphHistoryManager:
if not graph_id: if not graph_id:
graph_id = str(uuid.uuid4()) # Генерируем новый UUID для ID графа graph_id = str(uuid.uuid4()) # Генерируем новый UUID для ID графа
with self._get_connection() as conn: # Одна транзакция для всех изменений with self._get_connection(
) as conn: # Одна транзакция для всех изменений
try: try:
# Убедимся, что запись о графе существует в главной таблице `graphs` # Убедимся, что запись о графе существует в главной таблице `graphs`
# INSERT OR IGNORE создаст новую запись, если её нет. # 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: for node in new_nodes:
self._add_graph_node(conn, graph_id, node) 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: for edge in new_edges:
self._add_graph_edge(conn, graph_id, edge) self._add_graph_edge(conn, graph_id, edge)
# Обновляем current_node_id - это атомарное изменение в рамках транзакции # Обновляем 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)
# Если новый граф, добавляем его в очередь заголовков
if is_new_graph:
cursor.execute(
"INSERT INTO title_generation_queue (item_type, graph_id, priority) VALUES (?, ?, ?)",
("graph", graph_id, 10))
conn.commit() # Фиксируем все изменения conn.commit() # Фиксируем все изменения
print(f"Изменения графа {graph_id} сохранены.") print(f"Изменения графа {graph_id} сохранены.")
return graph_id return graph_id
except sqlite3.OperationalError as e: except sqlite3.OperationalError as e:
conn.rollback() # Откатываем транзакцию при ошибке (например, DB Locked) conn.rollback(
print(f"Ошибка блокировки SQLite при сохранении изменений графа {graph_id}: {e}") ) # Откатываем транзакцию при ошибке (например, DB Locked)
print(
f"Ошибка блокировки SQLite при сохранении изменений графа {graph_id}: {e}"
)
raise raise
except Exception as e: except Exception as e:
conn.rollback() # Откатываем транзакцию при других ошибках conn.rollback() # Откатываем транзакцию при других ошибках
print(f"Ошибка при сохранении изменений графа {graph_id}: {e}") print(f"Ошибка при сохранении изменений графа {graph_id}: {e}")
raise 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 из базы данных. Получает данные графа по ID из базы данных.
""" """
with self._get_connection() as conn: with self._get_connection() as conn:
cursor = conn.cursor() 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() result = cursor.fetchone()
if result: if result:
@ -179,9 +231,10 @@ class GraphHistoryManager:
resolved_current_node_id = target_node_id if target_node_id else db_current_node_id resolved_current_node_id = target_node_id if target_node_id else db_current_node_id
messages = self.get_messages_from_root_to_node( 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 { return {
"id": graph_id, "id": graph_id,
@ -200,31 +253,45 @@ class GraphHistoryManager:
with self._get_connection() as conn: with self._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
# Сортируем по timestamp по убыванию, чтобы самые новые были сверху # Сортируем по 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() graphs_data = cursor.fetchall()
summaries = [] summaries = []
for gid, current_node_id in graphs_data: 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_nodes = self._get_graph_nodes(conn, gid)
graph_edges = self._get_graph_edges(conn, gid) graph_edges = self._get_graph_edges(conn, gid)
# Получаем все сообщения до current_node_id # Получаем все сообщения до current_node_id
full_messages = self.get_messages_from_root_to_node( full_messages = self.get_messages_from_root_to_node(
{"graph_nodes": graph_nodes, "graph_edges": graph_edges}, {
current_node_id "graph_nodes": graph_nodes,
) "graph_edges": graph_edges
}, current_node_id)
first_message_content = "No content" first_message_content = "No content"
if full_messages: if full_messages:
for msg in full_messages: for msg in full_messages:
if msg.get("role") == "user": if msg.get("role") == "user":
first_message_content = msg.get("content", "No content") first_message_content = msg.get(
"content", "No content")
break break
if first_message_content == "No content" and full_messages: if first_message_content == "No content" and full_messages:
first_message_content = full_messages[0].get("content", "No content") 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({ summaries.append({
"id": gid, "id": gid,
"first_message": first_message_content "title": display_title,
"title_generated": bool(title_generated)
}) })
return summaries return summaries
@ -257,18 +324,22 @@ class GraphHistoryManager:
messages_for_path = [] messages_for_path = []
for node_id in path_to_target: for node_id in path_to_target:
node = node_map.get(node_id) 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'] original_message = node['data']['message']
messages_for_path.append({ messages_for_path.append({
"role": original_message.get("role", "unknown"), "role":
"type": node.get("type", "unknown"), # Используем 'type' узла original_message.get("role", "unknown"),
"content": original_message.get("content", ""), "type":
"node_id": original_message.get("node_id", node_id) node.get("type", "unknown"), # Используем 'type' узла
"content":
original_message.get("content", ""),
"node_id":
original_message.get("node_id", node_id)
}) })
return messages_for_path return messages_for_path
def delete_graph(self, graph_id: str) -> bool: def delete_graph(self, graph_id: str) -> bool:
""" """
Удаляет граф из базы данных. Удаляет граф из базы данных.
@ -284,3 +355,100 @@ class GraphHistoryManager:
conn.rollback() conn.rollback()
print(f"Ошибка при удалении графа: {e}") print(f"Ошибка при удалении графа: {e}")
return False 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()

View File

@ -17,7 +17,7 @@ MODELS: Dict[str, Dict[str, Any]] = {
"name": "gemini-2.0-flash", "name": "gemini-2.0-flash",
"provider": "openai", # Изменено на "openai" "provider": "openai", # Изменено на "openai"
"model_name": "gemini-2.0-flash", # Добавлено имя модели для LangChain "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", "apiKey": "AIzaSyDpueKFWVqknVKlQn6TdasLmJ2lvAUiBik",
"stream": True, "stream": True,
"capabilities": ["vision"], "capabilities": ["vision"],
@ -26,7 +26,7 @@ MODELS: Dict[str, Dict[str, Any]] = {
"name": "gemini-2.5-flash", "name": "gemini-2.5-flash",
"provider": "openai", # Изменено на "openai" "provider": "openai", # Изменено на "openai"
"model_name": "gemini-2.5-flash", # Добавлено имя модели для LangChain "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", "apiKey": "AIzaSyDpueKFWVqknVKlQn6TdasLmJ2lvAUiBik",
"stream": True, "stream": True,
"capabilities": ["vision"], "capabilities": ["vision"],
@ -43,7 +43,7 @@ MODELS: Dict[str, Dict[str, Any]] = {
"mistral-small-latest": { "mistral-small-latest": {
"name": "mistral-small-latest", "name": "mistral-small-latest",
"provider": "mistralai", "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", "apiKey": "Q0m29fvxBY0Cfdj4sjHaKqccy1NjonLW",
"stream": True, "stream": True,
"capabilities": ["vision"], "capabilities": ["vision"],

View File

@ -143,6 +143,8 @@ def execute_command_node(state: AgentState) -> AgentState:
DEFAULT_LLM_NAME = "gemini-2.0-flash" #"gemini-2.5-flash" DEFAULT_LLM_NAME = "gemini-2.0-flash" #"gemini-2.5-flash"
main_llm = get_llm(DEFAULT_LLM_NAME) main_llm = get_llm(DEFAULT_LLM_NAME)
DEFAULT_SUMMARIZATION_LLM_NAME = "gemini-2.0-flash"
def call_llm_node(state: AgentState) -> AgentState: def call_llm_node(state: AgentState) -> AgentState:
""" """

151
app/title_generator.py Normal file
View File

@ -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}")

Binary file not shown.

5
run-llm-backend.bat Normal file
View File

@ -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