Add title generator
This commit is contained in:
parent
b59c53ad07
commit
5a1fb4d482
11
app/api.py
11
app/api.py
|
|
@ -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()
|
||||||
|
|
@ -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:
|
||||||
"""
|
"""
|
||||||
Удаляет граф из базы данных.
|
Удаляет граф из базы данных.
|
||||||
|
|
@ -277,10 +348,107 @@ class GraphHistoryManager:
|
||||||
with self._get_connection() as conn:
|
with self._get_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
try:
|
try:
|
||||||
cursor.execute("DELETE FROM graphs WHERE id = ?", (graph_id,))
|
cursor.execute("DELETE FROM graphs WHERE id = ?", (graph_id, ))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return cursor.rowcount > 0
|
return cursor.rowcount > 0
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
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()
|
||||||
|
|
|
||||||
|
|
@ -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"],
|
||||||
|
|
|
||||||
|
|
@ -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
151
app/title_generator.py
Normal 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}")
|
||||||
BIN
graph_history.db
BIN
graph_history.db
Binary file not shown.
5
run-llm-backend.bat
Normal file
5
run-llm-backend.bat
Normal 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
|
||||||
Loading…
Reference in New Issue
Block a user