llm-agent-backend/app/api.py

253 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Этот файл содержит 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/<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/<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/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:
return jsonify({"error": "В графе нет активного узла, до которого можно было бы получить сообщения."}, 404)
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")
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 # Передаем выбранную модель
})
except Exception as e:
print(f"Ошибка при регенерации сообщения: {e}")
return jsonify({"error": str(e)}, 500)