Add graph options with custom system prompt, make more correct handling for New Chat button

This commit is contained in:
dimitrievgs 2025-10-19 16:05:25 +03:00
parent d6d7aabb0e
commit 84137bcf57
2 changed files with 224 additions and 53 deletions

View File

@ -16,25 +16,28 @@ CORS(
api api
) # Разрешаем CORS для всех доменов (в production нужно настроить более строго) ) # Разрешаем CORS для всех доменов (в production нужно настроить более строго)
# ----------------------------------------------- WebSocket Initialization --------------------------------------------------------------- # ----------------------------------------------- WebSocket Initialization ---------------------------------------------------------------
socketio = SocketIO(api, cors_allowed_origins="*") # В production настроить CORS строже socketio = SocketIO(
api, cors_allowed_origins="*") # В production настроить CORS строже
graph_history_manager.set_socketio(socketio) graph_history_manager.set_socketio(socketio)
# ----------------------------------------------- WebSocket Events --------------------------------------------------------------- # ----------------------------------------------- WebSocket Events ---------------------------------------------------------------
@socketio.on('connect') @socketio.on('connect')
def handle_connect(): def handle_connect():
"""Обработчик подключения клиента к WebSocket.""" """Обработчик подключения клиента к WebSocket."""
print(f"Client connected: {request.sid}") print(f"Client connected: {request.sid}")
@socketio.on('disconnect') @socketio.on('disconnect')
def handle_disconnect(): def handle_disconnect():
"""Обработчик отключения клиента от WebSocket.""" """Обработчик отключения клиента от WebSocket."""
print(f"Client disconnected: {request.sid}") print(f"Client disconnected: {request.sid}")
@socketio.on('client_proactive_ping') @socketio.on('client_proactive_ping')
def handle_client_proactive_ping(data=None): def handle_client_proactive_ping(data=None):
"""Обработчик проактивного пинга от клиента.""" """Обработчик проактивного пинга от клиента."""
@ -42,8 +45,10 @@ def handle_client_proactive_ping(data=None):
# Возвращаем словарь, который будет преобразован в JSON и отправлен как ответ на callback # Возвращаем словарь, который будет преобразован в JSON и отправлен как ответ на callback
return {'status': 'pong'} return {'status': 'pong'}
# ----------------------------------------------- API Endpoints --------------------------------------------------------------- # ----------------------------------------------- API Endpoints ---------------------------------------------------------------
@api.route('/api/graphs', methods=['GET']) @api.route('/api/graphs', methods=['GET'])
def get_graphs(): def get_graphs():
"""API endpoint для получения списка графов.""" """API endpoint для получения списка графов."""
@ -52,6 +57,18 @@ def get_graphs():
return jsonify(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']) @api.route('/api/graphs/<graph_id>', methods=['GET'])
def get_graph_by_id(graph_id): def get_graph_by_id(graph_id):
"""API endpoint для получения данных конкретного графа по ID.""" """API endpoint для получения данных конкретного графа по ID."""
@ -87,7 +104,8 @@ def delete_all_graphs():
def rename_graph(graph_id): def rename_graph(graph_id):
"""API endpoint для переименования графа.""" """API endpoint для переименования графа."""
if not graph_id: if not graph_id:
return jsonify({"error": "Не указан ID графа для переименования."}, 400) return jsonify({"error": "Не указан ID графа для переименования."},
400)
data = request.get_json() data = request.get_json()
new_title = data.get("title") new_title = data.get("title")
@ -98,23 +116,58 @@ def rename_graph(graph_id):
if graph_history_manager.rename_graph(graph_id, new_title.strip()): if graph_history_manager.rename_graph(graph_id, new_title.strip()):
return jsonify({"message": f"Граф {graph_id} успешно переименован."}) return jsonify({"message": f"Граф {graph_id} успешно переименован."})
else: else:
return jsonify({"error": f"Не удалось переименовать граф {graph_id}."}, 500) return jsonify({"error": f"Не удалось переименовать граф {graph_id}."},
500)
@api.route('/api/graphs/<graph_id>/nodes/<node_id>', methods=['DELETE']) @api.route('/api/graphs/<graph_id>/nodes/<node_id>', methods=['DELETE'])
def delete_graph_node(graph_id, node_id): def delete_graph_node(graph_id, node_id):
"""API endpoint для удаления узла графа.""" """API endpoint для удаления узла графа."""
if not graph_id or not node_id: if not graph_id or not node_id:
return jsonify({"error": "Не указаны ID графа или узла для удаления."}, 400) return jsonify({"error": "Не указаны ID графа или узла для удаления."},
400)
try: try:
if graph_history_manager.delete_node(graph_id, node_id): if graph_history_manager.delete_node(graph_id, node_id):
return jsonify({"message": f"Узел {node_id} успешно удален из графа {graph_id}."}) return jsonify({
"message":
f"Узел {node_id} успешно удален из графа {graph_id}."
})
else: else:
return jsonify({"error": f"Не удалось удалить узел {node_id} из графа {graph_id}."}, 500) return jsonify(
{
"error":
f"Не удалось удалить узел {node_id} из графа {graph_id}."
}, 500)
except Exception as e: except Exception as e:
print(f"Ошибка при удалении узла {node_id} из графа {graph_id}: {e}") print(f"Ошибка при удалении узла {node_id} из графа {graph_id}: {e}")
return jsonify({"error": f"Ошибка сервера при удалении узла: {str(e)}"}, 500) 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']) @api.route('/api/messages/<graph_id>/<node_id>', methods=['GET'])
@ -128,17 +181,20 @@ def get_messages_from_root_to_node(graph_id, node_id):
if node_id == 'last': if node_id == 'last':
target_node_id = graph_data.get('current_node_id') target_node_id = graph_data.get('current_node_id')
if not target_node_id: if not target_node_id:
return jsonify({"error": "В графе нет активного узла, до которого можно было бы получить сообщения."}, 404) # Если запрошен 'last', но активного узла нет, возвращаем пустой список
return jsonify([])
messages = graph_history_manager.get_messages_from_root_to_node( messages = graph_history_manager.get_messages_from_root_to_node(
graph_id, graph_data, target_node_id) graph_id, graph_data, target_node_id)
return jsonify(messages) return jsonify(messages)
@api.route('/api/models', methods=['GET']) @api.route('/api/models', methods=['GET'])
def get_available_models(): def get_available_models():
"""API endpoint для получения списка доступных моделей.""" """API endpoint для получения списка доступных моделей."""
return jsonify(list(MODELS.keys())) return jsonify(list(MODELS.keys()))
@api.route('/api/chat/send', methods=['POST']) @api.route('/api/chat/send', methods=['POST'])
def send_user_message(): def send_user_message():
"""API endpoint для создания узла пользователя и получения graph_id.""" """API endpoint для создания узла пользователя и получения graph_id."""
@ -151,9 +207,11 @@ def send_user_message():
return jsonify({"error": "Сообщение не может быть пустым."}, 400) return jsonify({"error": "Сообщение не может быть пустым."}, 400)
try: try:
result = graph_history_manager.create_user_node(message, graph_id, parent_node_id) 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")) graph_history_manager.title_generator.add_node_to_queue_direct(
result.get("graph_id"), result.get("node_id"))
return jsonify(result) return jsonify(result)
except Exception as e: except Exception as e:
@ -180,42 +238,49 @@ def chat_stream():
try: try:
if existing_assistant_node_id: if existing_assistant_node_id:
assistant_node_id = existing_assistant_node_id assistant_node_id = existing_assistant_node_id
# НОВОЕ: Помечаем существующий узел как заглушку и очищаем его содержимое
graph_history_manager.mark_node_as_placeholder_and_clear_content( graph_history_manager.mark_node_as_placeholder_and_clear_content(
graph_id, assistant_node_id, user_node_id 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" yield f"data: {json.dumps({'type': 'node_created', 'node_id': assistant_node_id, 'reused': True})}\n\n"
else: else:
# Создаём узел-заглушку для ответа LLM # Создаём узел-заглушку для ответа LLM
assistant_node_id = graph_history_manager.create_assistant_placeholder_node(graph_id, user_node_id) assistant_node_id = graph_history_manager.create_assistant_placeholder_node(
graph_id, user_node_id)
# Отправляем ID нового узла клиенту # Отправляем ID нового узла клиенту
yield f"data: {json.dumps({'type': 'node_created', 'node_id': assistant_node_id, 'reused': False})}\n\n" yield f"data: {json.dumps({'type': 'node_created', 'node_id': assistant_node_id, 'reused': False})}\n\n"
# Запускаем стриминг ответа от LLM # Запускаем стриминг ответа от LLM
accumulated_content = "" accumulated_content = ""
for chunk_data in run_agent_streaming(graph_id, user_node_id, assistant_node_id, system_prompt, model): for chunk_data in run_agent_streaming(graph_id, user_node_id,
assistant_node_id,
system_prompt, model):
if chunk_data.get("type") == "chunk": if chunk_data.get("type") == "chunk":
accumulated_content += chunk_data.get("content", "") accumulated_content += chunk_data.get("content", "")
yield f"data: {json.dumps(chunk_data)}\n\n" yield f"data: {json.dumps(chunk_data)}\n\n"
elif chunk_data.get("type") == "error": elif chunk_data.get("type") == "error":
# Если произошла ошибка во время стриминга, помечаем узел как ошибочный # Если произошла ошибка во время стриминга, помечаем узел как ошибочный
if assistant_node_id: if assistant_node_id:
graph_history_manager.mark_node_as_error(graph_id, assistant_node_id, chunk_data.get("error", "Неизвестная ошибка при стриминге")) graph_history_manager.mark_node_as_error(
graph_id, assistant_node_id,
chunk_data.get("error",
"Неизвестная ошибка при стриминге"))
if graph_history_manager.socketio: if graph_history_manager.socketio:
graph_history_manager.socketio.emit('node_type_changed', { graph_history_manager.socketio.emit(
'graph_id': graph_id, 'node_type_changed', {
'node_id': assistant_node_id, 'graph_id': graph_id,
'node_type': 'error' 'node_id': assistant_node_id,
}) 'node_type': 'error'
})
yield f"data: {json.dumps(chunk_data)}\n\n" yield f"data: {json.dumps(chunk_data)}\n\n"
return # Прекращаем генерацию при ошибке return # Прекращаем генерацию при ошибке
# После завершения стриминга обновляем узел полным контентом # После завершения стриминга обновляем узел полным контентом
if assistant_node_id: if assistant_node_id:
graph_history_manager.update_assistant_node_content(graph_id, assistant_node_id, accumulated_content) 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) 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" yield f"data: {json.dumps({'type': 'done', 'node_id': assistant_node_id})}\n\n"
@ -223,17 +288,24 @@ def chat_stream():
print(f"Ошибка при стриминге ответа: {e}") print(f"Ошибка при стриминге ответа: {e}")
error_message = str(e) error_message = str(e)
if assistant_node_id: if assistant_node_id:
graph_history_manager.mark_node_as_error(graph_id, assistant_node_id, f"Ошибка при стриминге: {error_message}") graph_history_manager.mark_node_as_error(
graph_id, assistant_node_id,
f"Ошибка при стриминге: {error_message}")
if graph_history_manager.socketio: if graph_history_manager.socketio:
graph_history_manager.socketio.emit('node_type_changed', { graph_history_manager.socketio.emit(
'graph_id': graph_id, 'node_type_changed', {
'node_id': assistant_node_id, 'graph_id': graph_id,
'node_type': 'error' 'node_id': assistant_node_id,
}) 'node_type': 'error'
error_data = {'type': 'error', 'error': f"Ошибка при стриминге: {error_message}"} })
error_data = {
'type': 'error',
'error': f"Ошибка при стриминге: {error_message}"
}
yield f"data: {json.dumps(error_data)}\n\n" yield f"data: {json.dumps(error_data)}\n\n"
return Response(stream_with_context(generate()), content_type='text/event-stream') return Response(stream_with_context(generate()),
content_type='text/event-stream')
@api.route('/api/chat/regenerate', methods=['POST']) @api.route('/api/chat/regenerate', methods=['POST'])
@ -244,17 +316,24 @@ def regenerate_message():
""" """
data = request.get_json() data = request.get_json()
graph_id = data.get("graph_id") graph_id = data.get("graph_id")
node_id = data.get("node_id") # Это ID узла, который нужно регенерировать (llm-ответ) node_id = data.get(
"node_id") # Это ID узла, который нужно регенерировать (llm-ответ)
model = data.get("model") model = data.get("model")
system_prompt = data.get("system_prompt")
if not graph_id or not node_id: if not graph_id or not node_id:
return jsonify({"error": "Не указаны graph_id или node_id."}, 400) return jsonify({"error": "Не указаны graph_id или node_id."}, 400)
try: try:
# Определяем родительский узел для LLM-ответа. Это должен быть user-узел. # Определяем родительский узел для LLM-ответа. Это должен быть user-узел.
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)
# НОВОЕ: Проверяем тип узла # НОВОЕ: Проверяем тип узла
node_type = graph_history_manager.get_node_type(graph_id, node_id) node_type = graph_history_manager.get_node_type(graph_id, node_id)
@ -263,16 +342,21 @@ def regenerate_message():
if node_type == 'error': if node_type == 'error':
# Если узел был ошибочным, переиспользуем его # Если узел был ошибочным, переиспользуем его
# Очищаем его содержимое и помечаем как заглушку сразу же # Очищаем его содержимое и помечаем как заглушку сразу же
assistant_node_id_to_use = node_id # Используем тот же ID assistant_node_id_to_use = node_id # Используем тот же ID
else: else:
# Если узел не был ошибочным, создаем новый узел LLM-ответа # Если узел не был ошибочным, создаем новый узел LLM-ответа
assistant_node_id_to_use = graph_history_manager.create_assistant_placeholder_node(graph_id, parent_node_id) assistant_node_id_to_use = graph_history_manager.create_assistant_placeholder_node(
graph_id, parent_node_id)
return jsonify({ return jsonify({
"existing_assistant_node_id": assistant_node_id_to_use, # Возвращаем ID узла, который будет регенерирован/использован "existing_assistant_node_id":
"user_node_id": parent_node_id, # Это родительский узел (пользовательский) assistant_node_id_to_use, # Возвращаем ID узла, который будет регенерирован/использован
"user_node_id":
parent_node_id, # Это родительский узел (пользовательский)
"graph_id": graph_id, "graph_id": graph_id,
"model": model # Передаем выбранную модель "model": model,
"system_prompt":
system_prompt # НОВОЕ: Возвращаем системный промпт, чтобы ChatView мог его передать в streamLLMResponse
}) })
except Exception as e: except Exception as e:
print(f"Ошибка при регенерации сообщения: {e}") print(f"Ошибка при регенерации сообщения: {e}")

View File

@ -70,9 +70,19 @@ class GraphHistoryManager:
current_node_id TEXT DEFAULT NULL, current_node_id TEXT DEFAULT NULL,
title TEXT DEFAULT NULL, title TEXT DEFAULT NULL,
title_generated BOOLEAN DEFAULT FALSE, title_generated BOOLEAN DEFAULT FALSE,
custom_system_prompt TEXT DEFAULT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
) )
""") """)
# временно так
cursor.execute("PRAGMA table_info(graphs)")
columns = [col[1] for col in cursor.fetchall()]
if 'custom_system_prompt' not in columns:
cursor.execute(
"ALTER TABLE graphs ADD COLUMN custom_system_prompt TEXT DEFAULT NULL"
)
cursor.execute(""" cursor.execute("""
CREATE TABLE IF NOT EXISTS graph_nodes_data ( CREATE TABLE IF NOT EXISTS graph_nodes_data (
graph_id TEXT NOT NULL, graph_id TEXT NOT NULL,
@ -99,7 +109,16 @@ class GraphHistoryManager:
""") """)
conn.commit() conn.commit()
# _get_max_graph_id - удален, так как используем UUID для graph_id def _ensure_graph_exists(self, graph_id: str) -> bool:
"""
Проверяет существование графа с заданным ID.
@param graph_id: ID графа.
@returns: True, если граф существует, False в противном случае.
"""
with self._get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT 1 FROM graphs WHERE id = ?", (graph_id,))
return cursor.fetchone() is not None
# ----------------------------------------------- Graph Node/Edge Access Helpers ------------------------------------------------------------ # ----------------------------------------------- Graph Node/Edge Access Helpers ------------------------------------------------------------
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]]:
@ -157,6 +176,26 @@ class GraphHistoryManager:
# ----------------------------------------------- Public Methods ------------------------------------------------------------ # ----------------------------------------------- Public Methods ------------------------------------------------------------
def create_new_graph(self) -> str:
"""
Создает новый пустой граф в базе данных и возвращает его ID.
@returns: ID нового графа.
"""
new_graph_id = str(uuid.uuid4())
with self._get_connection() as conn:
cursor = conn.cursor()
try:
cursor.execute(
"INSERT INTO graphs (id, title, title_generated, custom_system_prompt) VALUES (?, ?, FALSE, NULL)",
(new_graph_id, "Новый граф") # Устанавливаем заголовок по умолчанию
)
conn.commit()
return new_graph_id
except Exception as e:
conn.rollback()
print(f"Ошибка при создании нового графа: {e}")
raise
def get_graph( def get_graph(
self, self,
graph_id: str, graph_id: str,
@ -164,14 +203,18 @@ class GraphHistoryManager:
""" """
Получает данные графа по ID из базы данных. Получает данные графа по ID из базы данных.
""" """
if not self._ensure_graph_exists(graph_id):
print(f"Граф с ID {graph_id} не найден.")
return None
with self._get_connection() as conn: with self._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("SELECT current_node_id, title, title_generated FROM graphs WHERE id = ?", # Извлекаем title и title_generated cursor.execute("SELECT current_node_id, title, title_generated, custom_system_prompt FROM graphs WHERE id = ?", # Извлекаем title и title_generated
(graph_id, )) (graph_id, ))
result = cursor.fetchone() result = cursor.fetchone()
if result: if result:
db_current_node_id, graph_title, graph_title_generated = result # Распаковываем полученные значения db_current_node_id, graph_title, graph_title_generated, custom_system_prompt = result
# Получаем все узлы и ребра # Получаем все узлы и ребра
graph_nodes = self._get_graph_nodes(conn, graph_id) graph_nodes = self._get_graph_nodes(conn, graph_id)
@ -202,8 +245,9 @@ class GraphHistoryManager:
"graph_nodes": graph_nodes, "graph_nodes": graph_nodes,
"graph_edges": graph_edges, "graph_edges": graph_edges,
"current_node_id": resolved_current_node_id, "current_node_id": resolved_current_node_id,
"title": graph_title if graph_title_generated else None, # Включаем сгенерированный заголовок "title": graph_title if graph_title_generated else None,
"first_message": first_message_content # Включаем первое сообщение "first_message": first_message_content,
"custom_system_prompt": custom_system_prompt
} }
else: else:
return None return None
@ -454,12 +498,13 @@ class GraphHistoryManager:
if not graph_id: if not graph_id:
graph_id = str(uuid.uuid4()) graph_id = str(uuid.uuid4())
if not self._ensure_graph_exists(graph_id):
raise Exception(f"Граф с ID {graph_id} не найден.")
user_node_id = str(uuid.uuid4()) user_node_id = str(uuid.uuid4())
with self._get_connection() as conn: with self._get_connection() as conn:
try: try:
conn.execute("INSERT OR IGNORE INTO graphs (id) VALUES (?)", (graph_id,))
node_data = { node_data = {
"label": message[:30] + "..." if len(message) > 30 else message, "label": message[:30] + "..." if len(message) > 30 else message,
"message": {"role": "user", "content": message, "node_id": user_node_id} "message": {"role": "user", "content": message, "node_id": user_node_id}
@ -482,6 +527,9 @@ class GraphHistoryManager:
self._update_graph_current_node_id(conn, graph_id, user_node_id) self._update_graph_current_node_id(conn, graph_id, user_node_id)
conn.commit() conn.commit()
if not self.get_graph_title_generation_status(graph_id):
self.title_generator.add_graph_to_queue_direct(graph_id)
return { return {
"graph_id": graph_id, "graph_id": graph_id,
"node_id": user_node_id, "node_id": user_node_id,
@ -644,3 +692,42 @@ class GraphHistoryManager:
(graph_id, node_id)) (graph_id, node_id))
result = cursor.fetchone() result = cursor.fetchone()
return result[0] if result else None return result[0] if result else None
def get_graph_title_generation_status(self, graph_id: str) -> bool:
"""
Проверяет, был ли заголовок графа уже сгенерирован (или установлен вручную).
@param graph_id: ID графа.
@returns: True, если заголовок был сгенерирован/установлен, False в противном случае.
"""
with self._get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT title_generated FROM graphs WHERE id = ?", (graph_id,))
result = cursor.fetchone()
return bool(result[0]) if result else False
def update_graph_settings(self, graph_id: str, custom_system_prompt: Optional[str]) -> bool:
"""
Обновляет настройки графа, включая пользовательский системный промпт.
Если граф не существует, он будет создан.
@param graph_id: ID графа.
@param custom_system_prompt: Новый пользовательский системный промпт (или None для сброса).
@returns: True в случае успеха, False в противном случае.
"""
if not self._ensure_graph_exists(graph_id):
print(f"Граф с ID {graph_id} не найден. Невозможно обновить настройки.")
return False
with self._get_connection() as conn:
cursor = conn.cursor()
try:
cursor.execute(
"UPDATE graphs SET custom_system_prompt = ? WHERE id = ?",
(custom_system_prompt, graph_id)
)
conn.commit()
return cursor.rowcount > 0
except Exception as e:
conn.rollback()
print(f"Ошибка при обновлении системного промпта для графа {graph_id}: {e}")
return False