364 lines
17 KiB
Python
364 lines
17 KiB
Python
"""
|
||
Этот файл содержит API endpoints, созданные с использованием Flask.
|
||
Он обеспечивает взаимодействие с агентом через HTTP запросы,
|
||
включая обработку сообщений, управление графами и получение истории.
|
||
"""
|
||
|
||
import json
|
||
from flask import Flask, request, jsonify, Response, stream_with_context
|
||
from flask_cors import CORS
|
||
from flask_socketio import SocketIO
|
||
from workflows import graph_history_manager, run_agent_streaming
|
||
from llm_client import MODELS # Добавляем импорт списка моделей
|
||
|
||
api = Flask(__name__)
|
||
CORS(
|
||
api
|
||
) # Разрешаем 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}")
|
||
|
||
|
||
@socketio.on('client_proactive_ping')
|
||
def handle_client_proactive_ping(data=None):
|
||
"""Обработчик проактивного пинга от клиента."""
|
||
print(f"Received proactive ping from client: {request.sid}. Sending pong.")
|
||
# Возвращаем словарь, который будет преобразован в JSON и отправлен как ответ на callback
|
||
return {'status': 'pong'}
|
||
|
||
|
||
# ----------------------------------------------- API Endpoints ---------------------------------------------------------------
|
||
|
||
|
||
@api.route('/api/graphs', methods=['GET'])
|
||
def get_graphs():
|
||
"""API endpoint для получения списка графов."""
|
||
graphs = graph_history_manager.get_all_graphs_summary()
|
||
print("get_graphs response:", graphs) # Добавлено логирование
|
||
return jsonify(graphs)
|
||
|
||
|
||
@api.route('/api/graphs/new', methods=['POST'])
|
||
def create_new_graph_endpoint():
|
||
"""API endpoint для создания нового пустого графа."""
|
||
try:
|
||
new_graph_id = graph_history_manager.create_new_graph()
|
||
return jsonify({"graph_id": new_graph_id}), 201
|
||
except Exception as e:
|
||
print(f"Ошибка при создании нового графа через API: {e}")
|
||
return jsonify({"error": f"Не удалось создать новый граф: {str(e)}"},
|
||
500)
|
||
|
||
|
||
@api.route('/api/graphs/<graph_id>', methods=['GET'])
|
||
def get_graph_by_id(graph_id):
|
||
"""API endpoint для получения данных конкретного графа по ID."""
|
||
graph_data = graph_history_manager.get_graph(graph_id)
|
||
if graph_data:
|
||
return jsonify(graph_data)
|
||
else:
|
||
return jsonify({"error": f"Граф с ID {graph_id} не найден."}, 404)
|
||
|
||
|
||
@api.route('/api/graphs/<graph_id>', methods=['DELETE'])
|
||
def delete_graph(graph_id):
|
||
"""API endpoint для удаления графа."""
|
||
if not graph_id:
|
||
return jsonify({"error": "Не указан ID графа для удаления."}, 400)
|
||
|
||
if graph_history_manager.delete_graph(graph_id):
|
||
return jsonify({"message": f"Граф {graph_id} успешно удален."})
|
||
else:
|
||
return jsonify({"error": f"Не удалось удалить граф {graph_id}."}, 500)
|
||
|
||
|
||
@api.route('/api/graphs', methods=['DELETE'])
|
||
def delete_all_graphs():
|
||
"""API endpoint для удаления всех графов."""
|
||
if graph_history_manager.delete_all_graphs():
|
||
return jsonify({"message": "Все графы успешно удалены."})
|
||
else:
|
||
return jsonify({"error": "Не удалось удалить все графы."}, 500)
|
||
|
||
|
||
@api.route('/api/graphs/<graph_id>/rename', methods=['PATCH'])
|
||
def rename_graph(graph_id):
|
||
"""API endpoint для переименования графа."""
|
||
if not graph_id:
|
||
return jsonify({"error": "Не указан ID графа для переименования."},
|
||
400)
|
||
|
||
data = request.get_json()
|
||
new_title = data.get("title")
|
||
|
||
if not new_title or not new_title.strip():
|
||
return jsonify({"error": "Название не может быть пустым."}, 400)
|
||
|
||
if graph_history_manager.rename_graph(graph_id, new_title.strip()):
|
||
return jsonify({"message": f"Граф {graph_id} успешно переименован."})
|
||
else:
|
||
return jsonify({"error": f"Не удалось переименовать граф {graph_id}."},
|
||
500)
|
||
|
||
|
||
@api.route('/api/graphs/<graph_id>/nodes/<node_id>', methods=['DELETE'])
|
||
def delete_graph_node(graph_id, node_id):
|
||
"""API endpoint для удаления узла графа."""
|
||
if not graph_id or not node_id:
|
||
return jsonify({"error": "Не указаны ID графа или узла для удаления."},
|
||
400)
|
||
|
||
try:
|
||
if graph_history_manager.delete_node(graph_id, node_id):
|
||
return jsonify({
|
||
"message":
|
||
f"Узел {node_id} успешно удален из графа {graph_id}."
|
||
})
|
||
else:
|
||
return jsonify(
|
||
{
|
||
"error":
|
||
f"Не удалось удалить узел {node_id} из графа {graph_id}."
|
||
}, 500)
|
||
except Exception as e:
|
||
print(f"Ошибка при удалении узла {node_id} из графа {graph_id}: {e}")
|
||
return jsonify(
|
||
{"error": f"Ошибка сервера при удалении узла: {str(e)}"}, 500)
|
||
|
||
|
||
@api.route('/api/graphs/<graph_id>/settings', methods=['PATCH'])
|
||
def update_graph_settings_endpoint(graph_id):
|
||
"""API endpoint для обновления настроек графа (например, пользовательского системного промпта)."""
|
||
data = request.get_json()
|
||
custom_system_prompt = data.get(
|
||
"custom_system_prompt") # Может быть None для сброса
|
||
|
||
if not graph_id:
|
||
return jsonify(
|
||
{"error": "Не указан ID графа для обновления настроек."}, 400)
|
||
|
||
try:
|
||
if graph_history_manager.update_graph_settings(graph_id,
|
||
custom_system_prompt):
|
||
return jsonify(
|
||
{"message": f"Настройки графа {graph_id} успешно обновлены."})
|
||
else:
|
||
return jsonify(
|
||
{"error": f"Не удалось обновить настройки графа {graph_id}."},
|
||
500)
|
||
except Exception as e:
|
||
print(f"Ошибка при обновлении настроек графа {graph_id}: {e}")
|
||
return jsonify({"error": f"Ошибка сервера: {str(e)}"}, 500)
|
||
|
||
|
||
@api.route('/api/messages/<graph_id>/<node_id>', methods=['GET'])
|
||
def get_messages_from_root_to_node(graph_id, node_id):
|
||
"""API endpoint для получения сообщений от корня до выбранной ноды."""
|
||
graph_data = graph_history_manager.get_graph(graph_id)
|
||
if not graph_data:
|
||
return jsonify({"error": "Граф не найден."}, 404)
|
||
|
||
target_node_id = node_id
|
||
if node_id == 'last':
|
||
target_node_id = graph_data.get('current_node_id')
|
||
if not target_node_id:
|
||
# Если запрошен 'last', но активного узла нет, возвращаем пустой список
|
||
return jsonify([])
|
||
|
||
messages = graph_history_manager.get_messages_from_root_to_node(
|
||
graph_id, graph_data, target_node_id)
|
||
return jsonify(messages)
|
||
|
||
|
||
@api.route('/api/models', methods=['GET'])
|
||
def get_available_models():
|
||
"""API endpoint для получения списка доступных моделей."""
|
||
return jsonify(list(MODELS.keys()))
|
||
|
||
|
||
@api.route('/api/chat/send', methods=['POST'])
|
||
def send_user_message():
|
||
"""API endpoint для создания узла пользователя и получения graph_id."""
|
||
data = request.get_json()
|
||
message = data.get("message")
|
||
graph_id = data.get("graph_id")
|
||
parent_node_id = data.get("parent_node_id")
|
||
|
||
if not message:
|
||
return jsonify({"error": "Сообщение не может быть пустым."}, 400)
|
||
|
||
try:
|
||
result = graph_history_manager.create_user_node(
|
||
message, graph_id, parent_node_id)
|
||
|
||
graph_history_manager.title_generator.add_node_to_queue_direct(
|
||
result.get("graph_id"), result.get("node_id"))
|
||
|
||
return jsonify(result)
|
||
except Exception as e:
|
||
print(f"Ошибка при создании узла пользователя: {e}")
|
||
return jsonify({"error": str(e)}, 500)
|
||
|
||
|
||
@api.route('/api/chat/stream', methods=['POST'])
|
||
def chat_stream():
|
||
"""API endpoint для стриминга ответа LLM."""
|
||
data = request.get_json()
|
||
graph_id = data.get("graph_id")
|
||
user_node_id = data.get("user_node_id")
|
||
system_prompt = data.get("system_prompt")
|
||
model = data.get("model")
|
||
# Необязательный параметр для существующего узла ассистента при регенерации
|
||
existing_assistant_node_id = data.get("existing_assistant_node_id")
|
||
|
||
if not graph_id or not user_node_id:
|
||
return jsonify({"error": "Не указаны graph_id или user_node_id."}, 400)
|
||
|
||
def generate():
|
||
assistant_node_id = None
|
||
try:
|
||
if existing_assistant_node_id:
|
||
assistant_node_id = existing_assistant_node_id
|
||
graph_history_manager.mark_node_as_placeholder_and_clear_content(
|
||
graph_id, assistant_node_id, user_node_id)
|
||
yield f"data: {json.dumps({'type': 'node_created', 'node_id': assistant_node_id, 'reused': True})}\n\n"
|
||
else:
|
||
# Создаём узел-заглушку для ответа LLM
|
||
assistant_node_id = graph_history_manager.create_assistant_placeholder_node(
|
||
graph_id, user_node_id)
|
||
# Отправляем ID нового узла клиенту
|
||
yield f"data: {json.dumps({'type': 'node_created', 'node_id': assistant_node_id, 'reused': False})}\n\n"
|
||
|
||
# Запускаем стриминг ответа от LLM
|
||
accumulated_content = ""
|
||
for chunk_data in run_agent_streaming(graph_id, user_node_id,
|
||
assistant_node_id,
|
||
system_prompt, model):
|
||
if chunk_data.get("type") == "chunk":
|
||
accumulated_content += chunk_data.get("content", "")
|
||
yield f"data: {json.dumps(chunk_data)}\n\n"
|
||
elif chunk_data.get("type") == "error":
|
||
# Если произошла ошибка во время стриминга, помечаем узел как ошибочный
|
||
if assistant_node_id:
|
||
graph_history_manager.mark_node_as_error(
|
||
graph_id, assistant_node_id,
|
||
chunk_data.get("error",
|
||
"Неизвестная ошибка при стриминге"))
|
||
if graph_history_manager.socketio:
|
||
graph_history_manager.socketio.emit(
|
||
'node_type_changed', {
|
||
'graph_id': graph_id,
|
||
'node_id': assistant_node_id,
|
||
'node_type': 'error'
|
||
})
|
||
yield f"data: {json.dumps(chunk_data)}\n\n"
|
||
return # Прекращаем генерацию при ошибке
|
||
|
||
# После завершения стриминга обновляем узел полным контентом
|
||
if assistant_node_id:
|
||
graph_history_manager.update_assistant_node_content(
|
||
graph_id, assistant_node_id, accumulated_content)
|
||
|
||
# Инициируем генерацию заголовка
|
||
graph_history_manager.title_generator.add_node_to_queue_direct(
|
||
graph_id, assistant_node_id)
|
||
|
||
yield f"data: {json.dumps({'type': 'done', 'node_id': assistant_node_id})}\n\n"
|
||
|
||
except Exception as e:
|
||
print(f"Ошибка при стриминге ответа: {e}")
|
||
error_message = str(e)
|
||
if assistant_node_id:
|
||
graph_history_manager.mark_node_as_error(
|
||
graph_id, assistant_node_id,
|
||
f"Ошибка при стриминге: {error_message}")
|
||
if graph_history_manager.socketio:
|
||
graph_history_manager.socketio.emit(
|
||
'node_type_changed', {
|
||
'graph_id': graph_id,
|
||
'node_id': assistant_node_id,
|
||
'node_type': 'error'
|
||
})
|
||
error_data = {
|
||
'type': 'error',
|
||
'error': f"Ошибка при стриминге: {error_message}"
|
||
}
|
||
yield f"data: {json.dumps(error_data)}\n\n"
|
||
|
||
return Response(stream_with_context(generate()),
|
||
content_type='text/event-stream')
|
||
|
||
|
||
@api.route('/api/chat/regenerate', methods=['POST'])
|
||
def regenerate_message():
|
||
"""API endpoint для регенерации сообщения.
|
||
|
||
При ошибке LLM-узла, переиспользует его. В противном случае, создает новый узел.
|
||
"""
|
||
data = request.get_json()
|
||
graph_id = data.get("graph_id")
|
||
node_id = data.get(
|
||
"node_id") # Это ID узла, который нужно регенерировать (llm-ответ)
|
||
model = data.get("model")
|
||
system_prompt = data.get("system_prompt")
|
||
|
||
if not graph_id or not node_id:
|
||
return jsonify({"error": "Не указаны graph_id или node_id."}, 400)
|
||
|
||
try:
|
||
# Определяем родительский узел для LLM-ответа. Это должен быть user-узел.
|
||
parent_node_id = graph_history_manager.get_parent_node_id(
|
||
graph_id, node_id)
|
||
if not parent_node_id:
|
||
return jsonify(
|
||
{
|
||
"error":
|
||
"Не удалось найти родительский узел для регенерации."
|
||
}, 404)
|
||
|
||
# НОВОЕ: Проверяем тип узла
|
||
node_type = graph_history_manager.get_node_type(graph_id, node_id)
|
||
|
||
assistant_node_id_to_use = None
|
||
if node_type == 'error':
|
||
# Если узел был ошибочным, переиспользуем его
|
||
# Очищаем его содержимое и помечаем как заглушку сразу же
|
||
assistant_node_id_to_use = node_id # Используем тот же ID
|
||
else:
|
||
# Если узел не был ошибочным, создаем новый узел LLM-ответа
|
||
assistant_node_id_to_use = graph_history_manager.create_assistant_placeholder_node(
|
||
graph_id, parent_node_id)
|
||
|
||
return jsonify({
|
||
"existing_assistant_node_id":
|
||
assistant_node_id_to_use, # Возвращаем ID узла, который будет регенерирован/использован
|
||
"user_node_id":
|
||
parent_node_id, # Это родительский узел (пользовательский)
|
||
"graph_id": graph_id,
|
||
"model": model,
|
||
"system_prompt":
|
||
system_prompt # НОВОЕ: Возвращаем системный промпт, чтобы ChatView мог его передать в streamLLMResponse
|
||
})
|
||
except Exception as e:
|
||
print(f"Ошибка при регенерации сообщения: {e}")
|
||
return jsonify({"error": str(e)}, 500)
|