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 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():
|
||||
|
|
@ -85,3 +90,9 @@ def get_messages_from_root_to_node(graph_id, node_id):
|
|||
def get_available_models():
|
||||
"""API endpoint для получения списка доступных моделей."""
|
||||
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 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,53 +89,59 @@ 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]],
|
||||
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:
|
||||
"""
|
||||
|
|
@ -130,43 +154,71 @@ class GraphHistoryManager:
|
|||
if not graph_id:
|
||||
graph_id = str(uuid.uuid4()) # Генерируем новый UUID для ID графа
|
||||
|
||||
with self._get_connection() as conn: # Одна транзакция для всех изменений
|
||||
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)
|
||||
|
||||
# Если новый граф, добавляем его в очередь заголовков
|
||||
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() # Откатываем транзакцию при других ошибках
|
||||
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:
|
||||
|
|
@ -179,9 +231,10 @@ 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,
|
||||
|
|
@ -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:
|
||||
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
|
||||
)
|
||||
{
|
||||
"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")
|
||||
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")
|
||||
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:
|
||||
"""
|
||||
Удаляет граф из базы данных.
|
||||
|
|
@ -284,3 +355,100 @@ class GraphHistoryManager:
|
|||
conn.rollback()
|
||||
print(f"Ошибка при удалении графа: {e}")
|
||||
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",
|
||||
"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"],
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
"""
|
||||
|
|
|
|||
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