work on separation / streaming

This commit is contained in:
dimitrievgs 2025-10-11 23:45:33 +03:00
parent ee84617a1c
commit 45d892127d
6 changed files with 114 additions and 58 deletions

View File

@ -1,3 +1,5 @@
{ {
"terminal.integrated.defaultProfile.windows": "Git Bash" "terminal.integrated.defaultProfile.windows": "Git Bash",
"python-envs.defaultEnvManager": "ms-python.python:venv",
"python-envs.pythonProjects": []
} }

View File

@ -7,6 +7,7 @@
import json import json
from flask import Flask, request, jsonify from flask import Flask, request, jsonify
from flask_cors import CORS from flask_cors import CORS
from flask_socketio import SocketIO, emit
from workflows import run_agent, graph_history_manager, run_agent_streaming from workflows import run_agent, graph_history_manager, run_agent_streaming
from llm_client import MODELS # Добавляем импорт списка моделей from llm_client import MODELS # Добавляем импорт списка моделей
@ -16,6 +17,26 @@ CORS(
) # Разрешаем CORS для всех доменов (в production нужно настроить более строго) ) # Разрешаем CORS для всех доменов (в production нужно настроить более строго)
# ----------------------------------------------- WebSocket Initialization ---------------------------------------------------------------
socketio = SocketIO(api, cors_allowed_origins="*") # В production настроить CORS строже
graph_history_manager.set_socketio(socketio)
# ----------------------------------------------- WebSocket Events ---------------------------------------------------------------
@socketio.on('connect')
def handle_connect():
"""Обработчик подключения клиента к WebSocket."""
print(f"Client connected: {request.sid}")
@socketio.on('disconnect')
def handle_disconnect():
"""Обработчик отключения клиента от WebSocket."""
print(f"Client disconnected: {request.sid}")
# ----------------------------------------------- API Endpoints ---------------------------------------------------------------
@api.route('/api/chat', methods=['POST']) @api.route('/api/chat', methods=['POST'])
def chat(): def chat():
"""API endpoint для обработки сообщений и возврата ответа.""" """API endpoint для обработки сообщений и возврата ответа."""
@ -184,12 +205,15 @@ def regenerate_message():
try: try:
# Определяем родительский узел # Определяем родительский узел
parent_node_id = graph_history_manager.get_parent_node_id(graph_id, node_id) parent_node_id = graph_history_manager.get_parent_node_id(
graph_id, node_id)
if not parent_node_id: if not parent_node_id:
return jsonify({"error": "Не удалось найти родительский узел."}, 404) return jsonify({"error": "Не удалось найти родительский узел."},
404)
# Создаём новый узел-заглушку для регенерации # Создаём новый узел-заглушку для регенерации
new_assistant_node_id = graph_history_manager.create_assistant_placeholder_node(graph_id, parent_node_id) new_assistant_node_id = graph_history_manager.create_assistant_placeholder_node(
graph_id, parent_node_id)
return jsonify({ return jsonify({
"new_node_id": new_assistant_node_id, "new_node_id": new_assistant_node_id,

View File

@ -29,6 +29,12 @@ class GraphHistoryManager:
self.title_generator = TitleGenerator(self) self.title_generator = TitleGenerator(self)
self.title_generator.start() self.title_generator.start()
self.socketio = None
def set_socketio(self, socketio):
socketio = socketio
self.title_generator.socketio = socketio
def __del__(self): def __del__(self):
""" """
Деструктор класса. Останавливает генератор заголовков при уничтожении объекта. Деструктор класса. Останавливает генератор заголовков при уничтожении объекта.

View File

@ -22,6 +22,8 @@ class TitleGenerator:
self.running = False self.running = False
self.thread: Optional[threading.Thread] = None self.thread: Optional[threading.Thread] = None
self.socketio = None
# Системные промпты для генерации заголовков # Системные промпты для генерации заголовков
self.graph_title_prompt = """Создай краткий заголовок (максимум 80 символов) для диалога на основе первого сообщения пользователя. self.graph_title_prompt = """Создай краткий заголовок (максимум 80 символов) для диалога на основе первого сообщения пользователя.
Заголовок должен отражать основную тему или вопрос. Заголовок должен быть простым текстом, без какого-либо форматирования или использования Заголовок должен отражать основную тему или вопрос. Заголовок должен быть простым текстом, без какого-либо форматирования или использования
@ -185,6 +187,14 @@ class TitleGenerator:
self.history_manager.update_graph_title(graph_id, title) self.history_manager.update_graph_title(graph_id, title)
print(f"Сгенерирован заголовок графа {graph_id}: {title}") print(f"Сгенерирован заголовок графа {graph_id}: {title}")
# ----------------------------------------------- WebSocket Notification ---------------------------------------------------------------
# Отправляем событие через WebSocket
if self.socketio:
self.socketio.emit('graph_title_updated', {
'graph_id': graph_id,
'title': title
})
except Exception as e: except Exception as e:
print(f"Ошибка генерации заголовка графа {graph_id}: {e}") print(f"Ошибка генерации заголовка графа {graph_id}: {e}")
@ -230,22 +240,18 @@ class TitleGenerator:
self.history_manager.update_node_title(graph_id, node_id, title) self.history_manager.update_node_title(graph_id, node_id, title)
print(f"Сгенерирован заголовок узла {node_id}: {title}") print(f"Сгенерирован заголовок узла {node_id}: {title}")
# ----------------------------------------------- WebSocket Notification ---------------------------------------------------------------
# Отправляем событие через WebSocket
if self.socketio:
self.socketio.emit('node_title_updated', {
'graph_id': graph_id,
'node_id': node_id,
'title': title
})
except Exception as e: except Exception as e:
print(f"Ошибка генерации заголовка узла {node_id}: {e}") print(f"Ошибка генерации заголовка узла {node_id}: {e}")
'''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 add_node_to_queue(self, cursor, graph_id, node_id): def add_node_to_queue(self, cursor, graph_id, node_id):
# Добавляем в очередь заголовков с высоким приоритетом # Добавляем в очередь заголовков с высоким приоритетом
cursor.execute( cursor.execute(

View File

@ -9,7 +9,7 @@ from langgraph.graph import StateGraph, END
from graph_history_manager import GraphHistoryManager from graph_history_manager import GraphHistoryManager
from models import AgentState from models import AgentState
from nodes import parse_command_node, execute_command_node, call_llm_node, generate_images_node, analyze_image_node, get_meet_subtitles_node, get_teams_subtitles_node, summarize_history_node, handle_error_node, help_node from nodes import parse_command_node, execute_command_node, call_llm_node, generate_images_node, analyze_image_node, get_meet_subtitles_node, get_teams_subtitles_node, summarize_history_node, handle_error_node, help_node
import uuid # Добавлено для генерации UUID import uuid # Добавлено для генерации UUID
from llm_client import get_llm from llm_client import get_llm
# --- LangGraph: Построение графа --- # --- LangGraph: Построение графа ---
@ -103,12 +103,10 @@ def run_agent(user_input: str,
""" """
# ---------------------------------------------------- Initial State Setup ---------------------------------------------------------------- # ---------------------------------------------------- Initial State Setup ----------------------------------------------------------------
initial_state = AgentState( initial_state = AgentState(input=user_input,
input=user_input, parent_node_id=parent_node_id,
parent_node_id=parent_node_id, system_prompt=system_prompt,
system_prompt=system_prompt, selected_model=model or "gemini-2.5-flash")
selected_model=model or "gemini-2.5-flash"
)
# Сохраняем исходные ID узлов и ребер для определения новых после выполнения графа # Сохраняем исходные ID узлов и ребер для определения новых после выполнения графа
original_node_ids: Set[str] = set() original_node_ids: Set[str] = set()
@ -125,26 +123,35 @@ def run_agent(user_input: str,
if loaded_graph_data: if loaded_graph_data:
print(f"Продолжаю существующий граф {existing_graph_id}") print(f"Продолжаю существующий граф {existing_graph_id}")
# Загружаем узлы и ребра для продолжения графа # Загружаем узлы и ребра для продолжения графа
initial_state.graph_nodes = loaded_graph_data.get("graph_nodes", []) initial_state.graph_nodes = loaded_graph_data.get(
initial_state.graph_edges = loaded_graph_data.get("graph_edges", []) "graph_nodes", [])
initial_state.graph_edges = loaded_graph_data.get(
"graph_edges", [])
# Сохраняем ID загруженных узлов и ребер # Сохраняем ID загруженных узлов и ребер
original_node_ids = {node['id'] for node in initial_state.graph_nodes} original_node_ids = {
original_edge_ids = {edge['id'] for edge in initial_state.graph_edges} node['id']
for node in initial_state.graph_nodes
}
original_edge_ids = {
edge['id']
for edge in initial_state.graph_edges
}
# Устанавливаем parent_node_id для нового входного узла # Устанавливаем parent_node_id для нового входного узла
# Если parent_node_id был передан в запросе, используем его, # Если parent_node_id был передан в запросе, используем его,
# иначе берем current_node_id из загруженного графа. # иначе берем current_node_id из загруженного графа.
initial_state.parent_node_id = parent_node_id or loaded_graph_data.get("current_node_id") initial_state.parent_node_id = parent_node_id or loaded_graph_data.get(
"current_node_id")
# История чата для текущего раунда работы LLM формируется из загруженных сообщений. # История чата для текущего раунда работы LLM формируется из загруженных сообщений.
initial_state.temporary_chat_history = loaded_graph_data.get("messages", []) initial_state.temporary_chat_history = loaded_graph_data.get(
"messages", [])
# Определяем graph_id, который будет использоваться для сохранения. # Определяем graph_id, который будет использоваться для сохранения.
# Если существующий ID не передан, GraphHistoryManager сгенерирует новый. # Если существующий ID не передан, GraphHistoryManager сгенерирует новый.
graph_id_to_save = existing_graph_id graph_id_to_save = existing_graph_id
# ---------------------------------------------------- run_agent ---------------------------------------------------------------- # ---------------------------------------------------- run_agent ----------------------------------------------------------------
# Запускаем граф # Запускаем граф
result = app.invoke(initial_state) result = app.invoke(initial_state)
@ -152,18 +159,21 @@ def run_agent(user_input: str,
# ---------------------------------------------------- Save Graph ---------------------------------------------------------------- # ---------------------------------------------------- Save Graph ----------------------------------------------------------------
# Определяем новые узлы и ребра для сохранения # Определяем новые узлы и ребра для сохранения
newly_added_nodes = [node for node in final_state.graph_nodes if node['id'] not in original_node_ids] newly_added_nodes = [
newly_added_edges = [edge for edge in final_state.graph_edges if edge['id'] not in original_edge_ids] node for node in final_state.graph_nodes
if node['id'] not in original_node_ids
]
newly_added_edges = [
edge for edge in final_state.graph_edges
if edge['id'] not in original_edge_ids
]
# Сохраняем обновленное состояние графа. # Сохраняем обновленное состояние графа.
# Обратите внимание: chat_history больше не сохраняется напрямую, # Обратите внимание: chat_history больше не сохраняется напрямую,
# она является частью данных узлов в graph_nodes. # она является частью данных узлов в graph_nodes.
final_graph_id = graph_history_manager.save_graph_changes( final_graph_id = graph_history_manager.save_graph_changes(
graph_id_to_save, graph_id_to_save, newly_added_nodes, newly_added_edges,
newly_added_nodes, final_state.current_node_id)
newly_added_edges,
final_state.current_node_id
)
# ---------------------------------------------------- prepare response ---------------------------------------------------------------- # ---------------------------------------------------- prepare response ----------------------------------------------------------------
# Для `messages` в `response_data` используем `final_state.temporary_chat_history`, # Для `messages` в `response_data` используем `final_state.temporary_chat_history`,
@ -185,14 +195,20 @@ def run_agent(user_input: str,
} }
return response_data return response_data
def run_agent_streaming(graph_id: str, user_node_id: str, assistant_node_id: str, system_prompt: Optional[str] = None, model: Optional[str] = None):
def run_agent_streaming(graph_id: str,
user_node_id: str,
assistant_node_id: str,
system_prompt: Optional[str] = None,
model: Optional[str] = None):
""" """
Генератор для стримингового ответа LLM. Генератор для стримингового ответа LLM.
Возвращает чанки контента по мере их получения. Возвращает чанки контента по мере их получения.
""" """
try: try:
# Загружаем граф и историю до user_node_id # Загружаем граф и историю до user_node_id
loaded_graph_data = graph_history_manager.get_graph(graph_id, target_node_id=user_node_id) loaded_graph_data = graph_history_manager.get_graph(
graph_id, target_node_id=user_node_id)
if not loaded_graph_data: if not loaded_graph_data:
yield {"type": "error", "error": "Граф не найден."} yield {"type": "error", "error": "Граф не найден."}
return return
@ -216,7 +232,8 @@ def run_agent_streaming(graph_id: str, user_node_id: str, assistant_node_id: str
llm = get_llm(model or "gemini-2.5-flash") llm = get_llm(model or "gemini-2.5-flash")
for chunk in llm.stream2(messages_for_llm): for chunk in llm.stream2(messages_for_llm):
content = chunk.content if hasattr(chunk, 'content') else str(chunk) content = chunk.content if hasattr(chunk,
'content') else str(chunk)
yield {"type": "chunk", "content": content} yield {"type": "chunk", "content": content}
except Exception as e: except Exception as e:

View File

@ -6,5 +6,6 @@ langchain_google_genai
flask flask
flask_cors flask_cors
flask_socketio
mistralai mistralai