Add сaching of files, content of files no longer stored in db, add $ and !$ shoftcuts
This commit is contained in:
parent
899b0f3d5e
commit
00f3038d1b
20
app/api.py
20
app/api.py
|
|
@ -202,14 +202,15 @@ def send_user_message():
|
||||||
message = data.get("message")
|
message = data.get("message")
|
||||||
graph_id = data.get("graph_id")
|
graph_id = data.get("graph_id")
|
||||||
parent_node_id = data.get("parent_node_id")
|
parent_node_id = data.get("parent_node_id")
|
||||||
attachments = data.get("attachments", []) # НОВОЕ
|
attachments = data.get("attachments", [])
|
||||||
|
cache_folder = data.get("cache_folder")
|
||||||
|
|
||||||
if not message:
|
if not message:
|
||||||
return jsonify({"error": "Сообщение не может быть пустым."}, 400)
|
return jsonify({"error": "Сообщение не может быть пустым."}, 400)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = graph_history_manager.create_user_node(
|
result = graph_history_manager.create_user_node(
|
||||||
message, graph_id, parent_node_id, attachments # ИЗМЕНЕНО
|
message, graph_id, parent_node_id, attachments, cache_folder
|
||||||
)
|
)
|
||||||
graph_history_manager.title_generator.add_node_to_queue_direct(
|
graph_history_manager.title_generator.add_node_to_queue_direct(
|
||||||
result.get("graph_id"), result.get("node_id"))
|
result.get("graph_id"), result.get("node_id"))
|
||||||
|
|
@ -227,6 +228,7 @@ def chat_stream():
|
||||||
user_node_id = data.get("user_node_id")
|
user_node_id = data.get("user_node_id")
|
||||||
system_prompt = data.get("system_prompt")
|
system_prompt = data.get("system_prompt")
|
||||||
model = data.get("model")
|
model = data.get("model")
|
||||||
|
cache_folder = data.get("cache_folder")
|
||||||
# Необязательный параметр для существующего узла ассистента при регенерации
|
# Необязательный параметр для существующего узла ассистента при регенерации
|
||||||
existing_assistant_node_id = data.get("existing_assistant_node_id")
|
existing_assistant_node_id = data.get("existing_assistant_node_id")
|
||||||
|
|
||||||
|
|
@ -254,7 +256,8 @@ def chat_stream():
|
||||||
try:
|
try:
|
||||||
for chunk_data in run_agent_streaming(graph_id, user_node_id,
|
for chunk_data in run_agent_streaming(graph_id, user_node_id,
|
||||||
assistant_node_id,
|
assistant_node_id,
|
||||||
system_prompt, model):
|
system_prompt, model,
|
||||||
|
cache_folder):
|
||||||
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"
|
||||||
|
|
@ -324,6 +327,7 @@ def regenerate_message():
|
||||||
"node_id") # Это ID узла, который нужно регенерировать (llm-ответ)
|
"node_id") # Это ID узла, который нужно регенерировать (llm-ответ)
|
||||||
model = data.get("model")
|
model = data.get("model")
|
||||||
system_prompt = data.get("system_prompt")
|
system_prompt = data.get("system_prompt")
|
||||||
|
cache_folder = data.get("cache_folder")
|
||||||
|
|
||||||
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)
|
||||||
|
|
@ -353,14 +357,12 @@ def regenerate_message():
|
||||||
graph_id, parent_node_id)
|
graph_id, parent_node_id)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"existing_assistant_node_id":
|
"existing_assistant_node_id": assistant_node_id_to_use,
|
||||||
assistant_node_id_to_use, # Возвращаем ID узла, который будет регенерирован/использован
|
"user_node_id": parent_node_id,
|
||||||
"user_node_id":
|
|
||||||
parent_node_id, # Это родительский узел (пользовательский)
|
|
||||||
"graph_id": graph_id,
|
"graph_id": graph_id,
|
||||||
"model": model,
|
"model": model,
|
||||||
"system_prompt":
|
"system_prompt": system_prompt,
|
||||||
system_prompt # НОВОЕ: Возвращаем системный промпт, чтобы ChatView мог его передать в streamLLMResponse
|
"cache_folder": cache_folder
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Ошибка при регенерации сообщения: {e}")
|
print(f"Ошибка при регенерации сообщения: {e}")
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,9 @@ class GraphHistoryManager:
|
||||||
"""
|
"""
|
||||||
Деструктор класса. Останавливает генератор заголовков при уничтожении объекта.
|
Деструктор класса. Останавливает генератор заголовков при уничтожении объекта.
|
||||||
"""
|
"""
|
||||||
print("🚩 Завершение работы GraphHistoryManager. Останавливаем генератор заголовков...")
|
print(
|
||||||
|
"🚩 Завершение работы GraphHistoryManager. Останавливаем генератор заголовков..."
|
||||||
|
)
|
||||||
self.stop_title_generator()
|
self.stop_title_generator()
|
||||||
|
|
||||||
def stop_title_generator(self):
|
def stop_title_generator(self):
|
||||||
|
|
@ -107,6 +109,23 @@ class GraphHistoryManager:
|
||||||
FOREIGN KEY (target_node_id) REFERENCES graph_nodes_data (node_id) ON DELETE CASCADE
|
FOREIGN KEY (target_node_id) REFERENCES graph_nodes_data (node_id) ON DELETE CASCADE
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS graph_attachments (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
graph_id TEXT NOT NULL,
|
||||||
|
node_id TEXT NOT NULL,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
source_path TEXT NOT NULL,
|
||||||
|
cached_path TEXT NOT NULL,
|
||||||
|
cache_folder TEXT NOT NULL,
|
||||||
|
uuid TEXT NOT NULL,
|
||||||
|
permanent BOOLEAN NOT NULL,
|
||||||
|
mime_type TEXT,
|
||||||
|
FOREIGN KEY (graph_id, node_id) REFERENCES graph_nodes_data (graph_id, node_id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
""")
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
def _ensure_graph_exists(self, graph_id: str) -> bool:
|
def _ensure_graph_exists(self, graph_id: str) -> bool:
|
||||||
|
|
@ -117,7 +136,7 @@ class GraphHistoryManager:
|
||||||
"""
|
"""
|
||||||
with self._get_connection() as conn:
|
with self._get_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("SELECT 1 FROM graphs WHERE id = ?", (graph_id,))
|
cursor.execute("SELECT 1 FROM graphs WHERE id = ?", (graph_id, ))
|
||||||
return cursor.fetchone() is not None
|
return cursor.fetchone() is not None
|
||||||
|
|
||||||
# ----------------------------------------------- Graph Node/Edge Access Helpers ------------------------------------------------------------
|
# ----------------------------------------------- Graph Node/Edge Access Helpers ------------------------------------------------------------
|
||||||
|
|
@ -128,18 +147,15 @@ class GraphHistoryManager:
|
||||||
"SELECT node_id, node_type, node_data, title, title_generated FROM graph_nodes_data WHERE graph_id = ?",
|
"SELECT node_id, node_type, node_data, title, title_generated FROM graph_nodes_data WHERE graph_id = ?",
|
||||||
(graph_id, ))
|
(graph_id, ))
|
||||||
nodes = []
|
nodes = []
|
||||||
for node_id, node_type, node_data_json, title, title_generated in cursor.fetchall():
|
for node_id, node_type, node_data_json, title, title_generated in cursor.fetchall(
|
||||||
|
):
|
||||||
node_data = json.loads(node_data_json)
|
node_data = json.loads(node_data_json)
|
||||||
# Добавляем заголовок в данные узла
|
# Добавляем заголовок в данные узла
|
||||||
if title and title_generated:
|
if title and title_generated:
|
||||||
node_data["title"] = title
|
node_data["title"] = title
|
||||||
node_data["title_generated"] = bool(title_generated)
|
node_data["title_generated"] = bool(title_generated)
|
||||||
|
|
||||||
nodes.append({
|
nodes.append({"id": node_id, "type": node_type, "data": node_data})
|
||||||
"id": node_id,
|
|
||||||
"type": node_type,
|
|
||||||
"data": node_data
|
|
||||||
})
|
|
||||||
return nodes
|
return nodes
|
||||||
|
|
||||||
def _get_graph_edges(self, conn, graph_id: str) -> List[Dict[str, Any]]:
|
def _get_graph_edges(self, conn, graph_id: str) -> List[Dict[str, Any]]:
|
||||||
|
|
@ -187,7 +203,8 @@ class GraphHistoryManager:
|
||||||
try:
|
try:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"INSERT INTO graphs (id, title, title_generated, custom_system_prompt) VALUES (?, ?, FALSE, NULL)",
|
"INSERT INTO graphs (id, title, title_generated, custom_system_prompt) VALUES (?, ?, FALSE, NULL)",
|
||||||
(new_graph_id, "Новый граф") # Устанавливаем заголовок по умолчанию
|
(new_graph_id, "Новый граф"
|
||||||
|
) # Устанавливаем заголовок по умолчанию
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return new_graph_id
|
return new_graph_id
|
||||||
|
|
@ -209,8 +226,9 @@ class GraphHistoryManager:
|
||||||
|
|
||||||
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, custom_system_prompt FROM graphs WHERE id = ?", # Извлекаем title и title_generated
|
cursor.execute(
|
||||||
(graph_id, ))
|
"SELECT current_node_id, title, title_generated, custom_system_prompt FROM graphs WHERE id = ?", # Извлекаем title и title_generated
|
||||||
|
(graph_id, ))
|
||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
|
|
@ -223,8 +241,7 @@ class GraphHistoryManager:
|
||||||
resolved_current_node_id = target_node_id if target_node_id else db_current_node_id
|
resolved_current_node_id = target_node_id if target_node_id else db_current_node_id
|
||||||
|
|
||||||
messages = self.get_messages_from_root_to_node(
|
messages = self.get_messages_from_root_to_node(
|
||||||
graph_id,
|
graph_id, {
|
||||||
{
|
|
||||||
"graph_nodes": graph_nodes,
|
"graph_nodes": graph_nodes,
|
||||||
"graph_edges": graph_edges
|
"graph_edges": graph_edges
|
||||||
}, resolved_current_node_id)
|
}, resolved_current_node_id)
|
||||||
|
|
@ -274,8 +291,7 @@ class GraphHistoryManager:
|
||||||
|
|
||||||
# Получаем все сообщения до current_node_id
|
# Получаем все сообщения до current_node_id
|
||||||
full_messages = self.get_messages_from_root_to_node(
|
full_messages = self.get_messages_from_root_to_node(
|
||||||
gid,
|
gid, {
|
||||||
{
|
|
||||||
"graph_nodes": graph_nodes,
|
"graph_nodes": graph_nodes,
|
||||||
"graph_edges": graph_edges
|
"graph_edges": graph_edges
|
||||||
}, current_node_id)
|
}, current_node_id)
|
||||||
|
|
@ -306,10 +322,9 @@ class GraphHistoryManager:
|
||||||
self,
|
self,
|
||||||
graph_id: str,
|
graph_id: str,
|
||||||
graph_data: Dict[str, Any],
|
graph_data: Dict[str, Any],
|
||||||
target_node_id: str) -> List[Dict[str, Any]]: # ИЗМЕНЕНО: Dict[str, str] -> Dict[str, Any]
|
target_node_id: str) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Получает список сообщений от корневого узла до указанного узла.
|
Получает список сообщений от корневого узла до указанного узла.
|
||||||
Сообщения извлекаются из данных узлов, а не из отдельного поля.
|
|
||||||
"""
|
"""
|
||||||
all_nodes = graph_data.get("graph_nodes", [])
|
all_nodes = graph_data.get("graph_nodes", [])
|
||||||
edges = graph_data.get("graph_edges", [])
|
edges = graph_data.get("graph_edges", [])
|
||||||
|
|
@ -331,24 +346,28 @@ class GraphHistoryManager:
|
||||||
path_to_target = get_path_to_root(target_node_id)
|
path_to_target = get_path_to_root(target_node_id)
|
||||||
|
|
||||||
messages_for_path = []
|
messages_for_path = []
|
||||||
for node_id in path_to_target:
|
|
||||||
node = node_map.get(node_id)
|
|
||||||
if node and 'message' in node.get('data', {}):
|
|
||||||
original_message = node['data']['message']
|
|
||||||
|
|
||||||
message_dict = {
|
with self._get_connection() as conn: # TODO: Это, кажется, не нужно
|
||||||
"role": original_message.get("role", "unknown"),
|
for node_id in path_to_target:
|
||||||
"type": node.get("type", "unknown"),
|
node = node_map.get(node_id)
|
||||||
"content": original_message.get("content", ""),
|
if node and 'message' in node.get('data', {}):
|
||||||
"node_id": original_message.get("node_id", node_id),
|
original_message = node['data']['message']
|
||||||
"graph_id": graph_id
|
|
||||||
}
|
|
||||||
|
|
||||||
# НОВОЕ: Добавляем attachments, если они есть
|
message_dict = {
|
||||||
if "attachments" in original_message:
|
"role": original_message.get("role", "unknown"),
|
||||||
message_dict["attachments"] = original_message["attachments"]
|
"type": node.get("type", "unknown"),
|
||||||
|
"content": original_message.get("content", ""),
|
||||||
|
"node_id": original_message.get("node_id", node_id),
|
||||||
|
"graph_id": graph_id
|
||||||
|
}
|
||||||
|
|
||||||
messages_for_path.append(message_dict)
|
# ИЗМЕНЕНО: Загружаем attachments из БД
|
||||||
|
if original_message.get("role") == "user":
|
||||||
|
attachments = self._get_attachments(conn, graph_id, node_id)
|
||||||
|
if attachments:
|
||||||
|
message_dict["attachments"] = attachments
|
||||||
|
|
||||||
|
messages_for_path.append(message_dict)
|
||||||
|
|
||||||
return messages_for_path
|
return messages_for_path
|
||||||
|
|
||||||
|
|
@ -368,7 +387,7 @@ class GraphHistoryManager:
|
||||||
print(f"Ошибка при удалении графа: {e}")
|
print(f"Ошибка при удалении графа: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def delete_all_graphs(self) -> bool: # ДОБАВЛЕНО
|
def delete_all_graphs(self) -> bool: # ДОБАВЛЕНО
|
||||||
"""Удаляет все графы из базы данных."""
|
"""Удаляет все графы из базы данных."""
|
||||||
with self._get_connection() as conn:
|
with self._get_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
@ -381,7 +400,7 @@ class GraphHistoryManager:
|
||||||
print(f"Ошибка при удалении всех графов: {e}")
|
print(f"Ошибка при удалении всех графов: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def rename_graph(self, graph_id: str, new_title: str) -> bool: # ДОБАВЛЕНО
|
def rename_graph(self, graph_id: str, new_title: str) -> bool: # ДОБАВЛЕНО
|
||||||
"""Переименовывает граф, устанавливая пользовательское название."""
|
"""Переименовывает граф, устанавливая пользовательское название."""
|
||||||
with self._get_connection() as conn:
|
with self._get_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
@ -405,18 +424,23 @@ class GraphHistoryManager:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
try:
|
try:
|
||||||
# 1. Получаем информацию о текущем узле графа
|
# 1. Получаем информацию о текущем узле графа
|
||||||
cursor.execute("SELECT current_node_id FROM graphs WHERE id = ?", (graph_id,))
|
cursor.execute(
|
||||||
current_graph_node_id = cursor.fetchone()[0] if cursor.rowcount > 0 else None
|
"SELECT current_node_id FROM graphs WHERE id = ?",
|
||||||
|
(graph_id, ))
|
||||||
|
current_graph_node_id = cursor.fetchone(
|
||||||
|
)[0] if cursor.rowcount > 0 else None
|
||||||
|
|
||||||
# 2. Находим родителей и детей удаляемого узла
|
# 2. Находим родителей и детей удаляемого узла
|
||||||
# Родители
|
# Родители
|
||||||
cursor.execute("SELECT source_node_id FROM graph_edges_data WHERE graph_id = ? AND target_node_id = ?",
|
cursor.execute(
|
||||||
(graph_id, node_id))
|
"SELECT source_node_id FROM graph_edges_data WHERE graph_id = ? AND target_node_id = ?",
|
||||||
|
(graph_id, node_id))
|
||||||
parents = [row[0] for row in cursor.fetchall()]
|
parents = [row[0] for row in cursor.fetchall()]
|
||||||
|
|
||||||
# Дети
|
# Дети
|
||||||
cursor.execute("SELECT target_node_id FROM graph_edges_data WHERE graph_id = ? AND source_node_id = ?",
|
cursor.execute(
|
||||||
(graph_id, node_id))
|
"SELECT target_node_id FROM graph_edges_data WHERE graph_id = ? AND source_node_id = ?",
|
||||||
|
(graph_id, node_id))
|
||||||
children = [row[0] for row in cursor.fetchall()]
|
children = [row[0] for row in cursor.fetchall()]
|
||||||
|
|
||||||
# 3. Переподключаем родителей к детям
|
# 3. Переподключаем родителей к детям
|
||||||
|
|
@ -429,12 +453,14 @@ class GraphHistoryManager:
|
||||||
(graph_id, new_edge_id, parent_id, child_id))
|
(graph_id, new_edge_id, parent_id, child_id))
|
||||||
|
|
||||||
# 4. Удаляем все ребра, связанные с удаляемым узлом (входящие и исходящие)
|
# 4. Удаляем все ребра, связанные с удаляемым узлом (входящие и исходящие)
|
||||||
cursor.execute("DELETE FROM graph_edges_data WHERE graph_id = ? AND (source_node_id = ? OR target_node_id = ?)",
|
cursor.execute(
|
||||||
(graph_id, node_id, node_id))
|
"DELETE FROM graph_edges_data WHERE graph_id = ? AND (source_node_id = ? OR target_node_id = ?)",
|
||||||
|
(graph_id, node_id, node_id))
|
||||||
|
|
||||||
# 5. Удаляем сам узел
|
# 5. Удаляем сам узел
|
||||||
cursor.execute("DELETE FROM graph_nodes_data WHERE graph_id = ? AND node_id = ?",
|
cursor.execute(
|
||||||
(graph_id, node_id))
|
"DELETE FROM graph_nodes_data WHERE graph_id = ? AND node_id = ?",
|
||||||
|
(graph_id, node_id))
|
||||||
node_deleted = cursor.rowcount > 0
|
node_deleted = cursor.rowcount > 0
|
||||||
|
|
||||||
# TODO: Здесь не полная логика, т.к. если родителей нет, то current_node_id=None останется
|
# TODO: Здесь не полная логика, т.к. если родителей нет, то current_node_id=None останется
|
||||||
|
|
@ -449,16 +475,21 @@ class GraphHistoryManager:
|
||||||
new_current_node_id = children[0]
|
new_current_node_id = children[0]
|
||||||
# Если нет ни родителей, ни детей, current_node_id останется None.
|
# Если нет ни родителей, ни детей, current_node_id останется None.
|
||||||
|
|
||||||
cursor.execute("UPDATE graphs SET current_node_id = ? WHERE id = ?",
|
cursor.execute(
|
||||||
(new_current_node_id, graph_id))
|
"UPDATE graphs SET current_node_id = ? WHERE id = ?",
|
||||||
print(f"current_node_id для графа {graph_id} обновлен на {new_current_node_id}")
|
(new_current_node_id, graph_id))
|
||||||
|
print(
|
||||||
|
f"current_node_id для графа {graph_id} обновлен на {new_current_node_id}"
|
||||||
|
)
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return node_deleted
|
return node_deleted
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
conn.rollback()
|
conn.rollback()
|
||||||
print(f"Ошибка при удалении узла {node_id} из графа {graph_id}: {e}")
|
print(
|
||||||
raise # Пробрасываем исключение, чтобы API мог вернуть 500
|
f"Ошибка при удалении узла {node_id} из графа {graph_id}: {e}"
|
||||||
|
)
|
||||||
|
raise # Пробрасываем исключение, чтобы API мог вернуть 500
|
||||||
|
|
||||||
# ----------------------------------------------- Title Update Methods ------------------------------------------------------------
|
# ----------------------------------------------- Title Update Methods ------------------------------------------------------------
|
||||||
|
|
||||||
|
|
@ -489,9 +520,14 @@ class GraphHistoryManager:
|
||||||
print(f"Обновлено строк: {updated_rows}")
|
print(f"Обновлено строк: {updated_rows}")
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
def create_user_node(self, message: str, graph_id: Optional[str] = None,
|
|
||||||
parent_node_id: Optional[str] = None,
|
def create_user_node(
|
||||||
attachments: List[Dict[str, Any]] = None) -> Dict[str, Any]: # ИЗМЕНЕНО
|
self,
|
||||||
|
message: str,
|
||||||
|
graph_id: Optional[str] = None,
|
||||||
|
parent_node_id: Optional[str] = None,
|
||||||
|
attachments: List[Dict[str, Any]] = None,
|
||||||
|
cache_folder: Optional[str] = None) -> Dict[str, Any]:
|
||||||
"""Создаёт узел пользователя с вложениями."""
|
"""Создаёт узел пользователя с вложениями."""
|
||||||
if not graph_id:
|
if not graph_id:
|
||||||
graph_id = str(uuid.uuid4())
|
graph_id = str(uuid.uuid4())
|
||||||
|
|
@ -503,12 +539,13 @@ class GraphHistoryManager:
|
||||||
with self._get_connection() as conn:
|
with self._get_connection() as conn:
|
||||||
try:
|
try:
|
||||||
node_data = {
|
node_data = {
|
||||||
"label": message[:30] + "..." if len(message) > 30 else message,
|
"label":
|
||||||
|
message[:30] + "..." if len(message) > 30 else message,
|
||||||
"message": {
|
"message": {
|
||||||
"role": "user",
|
"role": "user",
|
||||||
"content": message,
|
"content": message,
|
||||||
"node_id": user_node_id,
|
"node_id": user_node_id
|
||||||
"attachments": attachments or [] # НОВОЕ
|
# УДАЛЕНО: "attachments": attachments or []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -518,13 +555,19 @@ class GraphHistoryManager:
|
||||||
"data": node_data
|
"data": node_data
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# ДОБАВЛЕНО: Сохраняем attachments отдельно
|
||||||
|
if attachments:
|
||||||
|
self._save_attachments(conn, graph_id, user_node_id,
|
||||||
|
attachments, cache_folder)
|
||||||
|
|
||||||
if parent_node_id:
|
if parent_node_id:
|
||||||
edge_id = str(uuid.uuid4())
|
edge_id = str(uuid.uuid4())
|
||||||
self._add_graph_edge(conn, graph_id, {
|
self._add_graph_edge(
|
||||||
"id": edge_id,
|
conn, graph_id, {
|
||||||
"source": parent_node_id,
|
"id": edge_id,
|
||||||
"target": user_node_id
|
"source": parent_node_id,
|
||||||
})
|
"target": user_node_id
|
||||||
|
})
|
||||||
|
|
||||||
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()
|
||||||
|
|
@ -542,7 +585,8 @@ class GraphHistoryManager:
|
||||||
print(f"Ошибка при создании узла пользователя: {e}")
|
print(f"Ошибка при создании узла пользователя: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def create_assistant_placeholder_node(self, graph_id: str, parent_node_id: str) -> str:
|
def create_assistant_placeholder_node(self, graph_id: str,
|
||||||
|
parent_node_id: str) -> str:
|
||||||
"""
|
"""
|
||||||
Создаёт узел-заглушку для ответа ассистента.
|
Создаёт узел-заглушку для ответа ассистента.
|
||||||
"""
|
"""
|
||||||
|
|
@ -552,7 +596,11 @@ class GraphHistoryManager:
|
||||||
try:
|
try:
|
||||||
node_data = {
|
node_data = {
|
||||||
"label": "Получение ответа...",
|
"label": "Получение ответа...",
|
||||||
"message": {"role": "assistant", "content": "", "node_id": assistant_node_id},
|
"message": {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "",
|
||||||
|
"node_id": assistant_node_id
|
||||||
|
},
|
||||||
"is_placeholder": True
|
"is_placeholder": True
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -563,13 +611,15 @@ class GraphHistoryManager:
|
||||||
})
|
})
|
||||||
|
|
||||||
edge_id = str(uuid.uuid4())
|
edge_id = str(uuid.uuid4())
|
||||||
self._add_graph_edge(conn, graph_id, {
|
self._add_graph_edge(
|
||||||
"id": edge_id,
|
conn, graph_id, {
|
||||||
"source": parent_node_id,
|
"id": edge_id,
|
||||||
"target": assistant_node_id
|
"source": parent_node_id,
|
||||||
})
|
"target": assistant_node_id
|
||||||
|
})
|
||||||
|
|
||||||
self._update_graph_current_node_id(conn, graph_id, assistant_node_id)
|
self._update_graph_current_node_id(conn, graph_id,
|
||||||
|
assistant_node_id)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
return assistant_node_id
|
return assistant_node_id
|
||||||
|
|
@ -578,8 +628,9 @@ class GraphHistoryManager:
|
||||||
print(f"Ошибка при создании узла-заглушки: {e}")
|
print(f"Ошибка при создании узла-заглушки: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def mark_node_as_placeholder_and_clear_content(self, graph_id: str,
|
||||||
def mark_node_as_placeholder_and_clear_content(self, graph_id: str, node_id: str, parent_node_id: str):
|
node_id: str,
|
||||||
|
parent_node_id: str):
|
||||||
"""
|
"""
|
||||||
Очищает содержимое существующего узла LLM, помечает его как заглушку,
|
Очищает содержимое существующего узла LLM, помечает его как заглушку,
|
||||||
и обновляет current_node_id графа.
|
и обновляет current_node_id графа.
|
||||||
|
|
@ -591,8 +642,9 @@ class GraphHistoryManager:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
try:
|
try:
|
||||||
# 1. Получаем текущие данные узла
|
# 1. Получаем текущие данные узла
|
||||||
cursor.execute("SELECT node_data FROM graph_nodes_data WHERE graph_id = ? AND node_id = ?",
|
cursor.execute(
|
||||||
(graph_id, node_id))
|
"SELECT node_data FROM graph_nodes_data WHERE graph_id = ? AND node_id = ?",
|
||||||
|
(graph_id, node_id))
|
||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
|
|
@ -606,54 +658,65 @@ class GraphHistoryManager:
|
||||||
node_data["title_generated"] = False
|
node_data["title_generated"] = False
|
||||||
|
|
||||||
# Обновляем узел
|
# Обновляем узел
|
||||||
cursor.execute("UPDATE graph_nodes_data SET node_type = ?, node_data = ?, title = NULL, title_generated = FALSE WHERE graph_id = ? AND node_id = ?",
|
cursor.execute(
|
||||||
("llm", json.dumps(node_data), graph_id, node_id))
|
"UPDATE graph_nodes_data SET node_type = ?, node_data = ?, title = NULL, title_generated = FALSE WHERE graph_id = ? AND node_id = ?",
|
||||||
|
("llm", json.dumps(node_data), graph_id, node_id))
|
||||||
|
|
||||||
# 2. Обновляем current_node_id графа на этот узел
|
# 2. Обновляем current_node_id графа на этот узел
|
||||||
self._update_graph_current_node_id(conn, graph_id, node_id)
|
self._update_graph_current_node_id(conn, graph_id, node_id)
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Узел с ID {node_id} не найден в графе {graph_id} для обновления.")
|
raise ValueError(
|
||||||
|
f"Узел с ID {node_id} не найден в графе {graph_id} для обновления."
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
conn.rollback()
|
conn.rollback()
|
||||||
print(f"Ошибка при очистке и маркировке узла {node_id} как заглушки: {e}")
|
print(
|
||||||
|
f"Ошибка при очистке и маркировке узла {node_id} как заглушки: {e}"
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def update_assistant_node_content(self, graph_id: str, node_id: str, content: str):
|
def update_assistant_node_content(self, graph_id: str, node_id: str,
|
||||||
|
content: str):
|
||||||
"""
|
"""
|
||||||
Обновляет содержимое узла ассистента после завершения стриминга.
|
Обновляет содержимое узла ассистента после завершения стриминга.
|
||||||
"""
|
"""
|
||||||
with self._get_connection() as conn:
|
with self._get_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
try:
|
try:
|
||||||
cursor.execute("SELECT node_data FROM graph_nodes_data WHERE graph_id = ? AND node_id = ?",
|
cursor.execute(
|
||||||
(graph_id, node_id))
|
"SELECT node_data FROM graph_nodes_data WHERE graph_id = ? AND node_id = ?",
|
||||||
|
(graph_id, node_id))
|
||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
node_data = json.loads(result[0])
|
node_data = json.loads(result[0])
|
||||||
node_data["message"]["content"] = content
|
node_data["message"]["content"] = content
|
||||||
node_data["label"] = content[:30] + "..." if len(content) > 30 else content
|
node_data["label"] = content[:30] + "..." if len(
|
||||||
|
content) > 30 else content
|
||||||
node_data["is_placeholder"] = False
|
node_data["is_placeholder"] = False
|
||||||
|
|
||||||
cursor.execute("UPDATE graph_nodes_data SET node_data = ? WHERE graph_id = ? AND node_id = ?",
|
cursor.execute(
|
||||||
(json.dumps(node_data), graph_id, node_id))
|
"UPDATE graph_nodes_data SET node_data = ? WHERE graph_id = ? AND node_id = ?",
|
||||||
|
(json.dumps(node_data), graph_id, node_id))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
conn.rollback()
|
conn.rollback()
|
||||||
print(f"Ошибка при обновлении узла: {e}")
|
print(f"Ошибка при обновлении узла: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def mark_node_as_error(self, graph_id: str, node_id: str, error_message: str):
|
def mark_node_as_error(self, graph_id: str, node_id: str,
|
||||||
|
error_message: str):
|
||||||
"""
|
"""
|
||||||
Помечает узел как ошибочный.
|
Помечает узел как ошибочный.
|
||||||
"""
|
"""
|
||||||
with self._get_connection() as conn:
|
with self._get_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
try:
|
try:
|
||||||
cursor.execute("SELECT node_data FROM graph_nodes_data WHERE graph_id = ? AND node_id = ?",
|
cursor.execute(
|
||||||
(graph_id, node_id))
|
"SELECT node_data FROM graph_nodes_data WHERE graph_id = ? AND node_id = ?",
|
||||||
|
(graph_id, node_id))
|
||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
|
|
@ -662,8 +725,9 @@ class GraphHistoryManager:
|
||||||
node_data["label"] = "Ошибка"
|
node_data["label"] = "Ошибка"
|
||||||
node_data["is_placeholder"] = False
|
node_data["is_placeholder"] = False
|
||||||
|
|
||||||
cursor.execute("UPDATE graph_nodes_data SET node_type = ?, node_data = ? WHERE graph_id = ? AND node_id = ?",
|
cursor.execute(
|
||||||
("error", json.dumps(node_data), graph_id, node_id))
|
"UPDATE graph_nodes_data SET node_type = ?, node_data = ? WHERE graph_id = ? AND node_id = ?",
|
||||||
|
("error", json.dumps(node_data), graph_id, node_id))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
conn.rollback()
|
conn.rollback()
|
||||||
|
|
@ -676,8 +740,9 @@ class GraphHistoryManager:
|
||||||
"""
|
"""
|
||||||
with self._get_connection() as conn:
|
with self._get_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("SELECT source_node_id FROM graph_edges_data WHERE graph_id = ? AND target_node_id = ?",
|
cursor.execute(
|
||||||
(graph_id, node_id))
|
"SELECT source_node_id FROM graph_edges_data WHERE graph_id = ? AND target_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
|
||||||
|
|
||||||
|
|
@ -690,8 +755,9 @@ class GraphHistoryManager:
|
||||||
"""
|
"""
|
||||||
with self._get_connection() as conn:
|
with self._get_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("SELECT node_type FROM graph_nodes_data WHERE graph_id = ? AND node_id = ?",
|
cursor.execute(
|
||||||
(graph_id, node_id))
|
"SELECT node_type FROM graph_nodes_data WHERE graph_id = ? AND 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
|
||||||
|
|
||||||
|
|
@ -703,11 +769,13 @@ class GraphHistoryManager:
|
||||||
"""
|
"""
|
||||||
with self._get_connection() as conn:
|
with self._get_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("SELECT title_generated FROM graphs WHERE id = ?", (graph_id,))
|
cursor.execute("SELECT title_generated FROM graphs WHERE id = ?",
|
||||||
|
(graph_id, ))
|
||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
return bool(result[0]) if result else False
|
return bool(result[0]) if result else False
|
||||||
|
|
||||||
def update_graph_settings(self, graph_id: str, custom_system_prompt: Optional[str]) -> bool:
|
def update_graph_settings(self, graph_id: str,
|
||||||
|
custom_system_prompt: Optional[str]) -> bool:
|
||||||
"""
|
"""
|
||||||
Обновляет настройки графа, включая пользовательский системный промпт.
|
Обновляет настройки графа, включая пользовательский системный промпт.
|
||||||
Если граф не существует, он будет создан.
|
Если граф не существует, он будет создан.
|
||||||
|
|
@ -717,7 +785,9 @@ class GraphHistoryManager:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not self._ensure_graph_exists(graph_id):
|
if not self._ensure_graph_exists(graph_id):
|
||||||
print(f"Граф с ID {graph_id} не найден. Невозможно обновить настройки.")
|
print(
|
||||||
|
f"Граф с ID {graph_id} не найден. Невозможно обновить настройки."
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
with self._get_connection() as conn:
|
with self._get_connection() as conn:
|
||||||
|
|
@ -725,11 +795,55 @@ class GraphHistoryManager:
|
||||||
try:
|
try:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"UPDATE graphs SET custom_system_prompt = ? WHERE id = ?",
|
"UPDATE graphs SET custom_system_prompt = ? WHERE id = ?",
|
||||||
(custom_system_prompt, graph_id)
|
(custom_system_prompt, graph_id))
|
||||||
)
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return cursor.rowcount > 0
|
return cursor.rowcount > 0
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
conn.rollback()
|
conn.rollback()
|
||||||
print(f"Ошибка при обновлении системного промпта для графа {graph_id}: {e}")
|
print(
|
||||||
|
f"Ошибка при обновлении системного промпта для графа {graph_id}: {e}"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _save_attachments(self, conn, graph_id: str, node_id: str,
|
||||||
|
attachments: List[Dict[str, Any]],
|
||||||
|
cache_folder: Optional[str] = None):
|
||||||
|
"""Сохраняет attachments в базу данных."""
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
for att in attachments:
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO graph_attachments
|
||||||
|
(graph_id, node_id, type, name, source_path, cached_path, cache_folder, uuid, permanent, mime_type)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(graph_id, node_id, att.get('type'), att.get('name'),
|
||||||
|
att.get('sourcePath'), att.get('cachedPath'), cache_folder or '',
|
||||||
|
att.get('uuid'), att.get('permanent', False), att.get('mimeType')))
|
||||||
|
|
||||||
|
def _get_attachments(self, conn, graph_id: str,
|
||||||
|
node_id: str) -> List[Dict[str, Any]]:
|
||||||
|
"""Получает attachments для узла."""
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT type, name, source_path, cached_path, cache_folder, uuid, permanent, mime_type
|
||||||
|
FROM graph_attachments
|
||||||
|
WHERE graph_id = ? AND node_id = ?
|
||||||
|
""", (graph_id, node_id))
|
||||||
|
|
||||||
|
attachments = []
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
attachments.append({
|
||||||
|
'type': row[0],
|
||||||
|
'name': row[1],
|
||||||
|
'sourcePath': row[2],
|
||||||
|
'cachedPath': row[3],
|
||||||
|
'cacheFolder': row[4],
|
||||||
|
'uuid': row[5],
|
||||||
|
'permanent': bool(row[6]),
|
||||||
|
'mimeType': row[7]
|
||||||
|
})
|
||||||
|
|
||||||
|
return attachments
|
||||||
|
|
|
||||||
138
app/workflows.py
138
app/workflows.py
|
|
@ -11,6 +11,10 @@ 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
|
||||||
|
|
||||||
|
import os
|
||||||
|
import base64
|
||||||
|
from pathlib import Path
|
||||||
# --- LangGraph: Построение графа ---
|
# --- LangGraph: Построение графа ---
|
||||||
|
|
||||||
graph_history_manager = GraphHistoryManager()
|
graph_history_manager = GraphHistoryManager()
|
||||||
|
|
@ -92,10 +96,11 @@ app = workflow.compile()
|
||||||
|
|
||||||
|
|
||||||
def run_agent_streaming(graph_id: str,
|
def run_agent_streaming(graph_id: str,
|
||||||
user_node_id: str,
|
user_node_id: str,
|
||||||
assistant_node_id: str,
|
assistant_node_id: str,
|
||||||
system_prompt: Optional[str] = None,
|
system_prompt: Optional[str] = None,
|
||||||
model: Optional[str] = None):
|
model: Optional[str] = None,
|
||||||
|
cache_folder: Optional[str] = None):
|
||||||
"""Генератор для стримингового ответа LLM с поддержкой вложений."""
|
"""Генератор для стримингового ответа LLM с поддержкой вложений."""
|
||||||
try:
|
try:
|
||||||
loaded_graph_data = graph_history_manager.get_graph(
|
loaded_graph_data = graph_history_manager.get_graph(
|
||||||
|
|
@ -113,22 +118,45 @@ def run_agent_streaming(graph_id: str,
|
||||||
if system_prompt:
|
if system_prompt:
|
||||||
messages_for_llm.append(SystemMessage(content=system_prompt))
|
messages_for_llm.append(SystemMessage(content=system_prompt))
|
||||||
|
|
||||||
# ИЗМЕНЕНО: Обрабатываем вложения
|
# Обрабатываем вложения
|
||||||
for i, msg in enumerate(messages):
|
for i, msg in enumerate(messages):
|
||||||
is_last = (i == len(messages) - 1)
|
is_last = (i == len(messages) - 1)
|
||||||
|
|
||||||
if msg["role"] == "user":
|
if msg["role"] == "user":
|
||||||
content = msg["content"]
|
text_content = msg["content"]
|
||||||
attachments = msg.get("attachments", [])
|
attachments = msg.get("attachments", [])
|
||||||
|
|
||||||
# Раскрываем постоянные вложения и вложения последнего сообщения
|
# Проверяем, есть ли мультимодальный контент
|
||||||
|
has_multimodal = False
|
||||||
|
content_parts = [{"type": "text", "text": text_content}]
|
||||||
|
|
||||||
|
# Обрабатываем вложения
|
||||||
if attachments:
|
if attachments:
|
||||||
for att in attachments:
|
for att in attachments:
|
||||||
|
# Раскрываем только permanent или вложения последнего сообщения
|
||||||
if att.get("permanent") or is_last:
|
if att.get("permanent") or is_last:
|
||||||
formatted_content = format_attachment_for_llm(att)
|
attachment_content = prepare_attachment_for_llm(
|
||||||
content += "\n" + formatted_content
|
att, cache_folder)
|
||||||
|
|
||||||
messages_for_llm.append(HumanMessage(content=content))
|
# Если это изображение, то это мультимодальный контент
|
||||||
|
if attachment_content.get("type") == "image_url":
|
||||||
|
has_multimodal = True
|
||||||
|
|
||||||
|
content_parts.append(attachment_content)
|
||||||
|
|
||||||
|
# Формируем финальный контент
|
||||||
|
if has_multimodal:
|
||||||
|
# Мультимодальное сообщение
|
||||||
|
messages_for_llm.append(
|
||||||
|
HumanMessage(content=content_parts))
|
||||||
|
else:
|
||||||
|
# Текстовое сообщение (объединяем все текстовые части)
|
||||||
|
full_text = text_content
|
||||||
|
for part in content_parts[
|
||||||
|
1:]: # Пропускаем первую часть (основной текст)
|
||||||
|
if part.get("type") == "text":
|
||||||
|
full_text += part.get("text", "")
|
||||||
|
messages_for_llm.append(HumanMessage(content=full_text))
|
||||||
|
|
||||||
elif msg["role"] == "assistant":
|
elif msg["role"] == "assistant":
|
||||||
messages_for_llm.append(AIMessage(content=msg["content"]))
|
messages_for_llm.append(AIMessage(content=msg["content"]))
|
||||||
|
|
@ -136,14 +164,102 @@ def run_agent_streaming(graph_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:
|
||||||
print(f"Ошибка при стриминге: {e}")
|
print(f"Ошибка при стриминге: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
yield {"type": "error", "error": str(e)}
|
yield {"type": "error", "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_attachment_for_llm(attachment: Dict[str, Any],
|
||||||
|
cache_folder: str) -> Dict[str, Any]:
|
||||||
|
"""Подготавливает вложение для отправки в LLM."""
|
||||||
|
mime_type = attachment.get("mimeType", "application/octet-stream")
|
||||||
|
cached_path = attachment.get("cachedPath")
|
||||||
|
att_cache_folder = attachment.get("cacheFolder", cache_folder) # ИЗМЕНЕНО
|
||||||
|
source_path = attachment.get("sourcePath")
|
||||||
|
att_type = attachment.get("type")
|
||||||
|
name = attachment.get("name")
|
||||||
|
|
||||||
|
# Получаем полный путь к кешированному файлу
|
||||||
|
full_cached_path = os.path.join(att_cache_folder, cached_path)
|
||||||
|
|
||||||
|
# Проверяем существование кеша
|
||||||
|
if not os.path.exists(full_cached_path):
|
||||||
|
print(f"❌ Кеш не найден: {full_cached_path}")
|
||||||
|
return {"type": "text", "text": f"\n[Файл {name} недоступен]\n"}
|
||||||
|
|
||||||
|
# Проверяем тип файла
|
||||||
|
if mime_type.startswith('image/'):
|
||||||
|
# Для изображений отправляем как base64
|
||||||
|
with open(full_cached_path, 'rb') as f:
|
||||||
|
image_data = base64.b64encode(f.read()).decode('utf-8')
|
||||||
|
|
||||||
|
return {
|
||||||
|
"type": "image_url",
|
||||||
|
"image_url": {
|
||||||
|
"url": f"data:{mime_type};base64,{image_data}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elif mime_type.startswith('text/') or mime_type in [
|
||||||
|
'application/json', 'application/xml'
|
||||||
|
]:
|
||||||
|
# Для текстовых файлов вставляем содержимое
|
||||||
|
try:
|
||||||
|
with open(full_cached_path, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
# Если не получилось прочитать как текст
|
||||||
|
return {
|
||||||
|
"type": "text",
|
||||||
|
"text": f"\n[Файл {name} не может быть прочитан как текст]\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
att_type_ru = "промпта" if att_type == "prompt" else "файла"
|
||||||
|
header = f"\n\n--- Содержимое {att_type_ru}: {name} ---\n"
|
||||||
|
footer = f"\n--- Конец {att_type_ru}: {name} ---\n\n"
|
||||||
|
|
||||||
|
return {"type": "text", "text": header + content.strip() + footer}
|
||||||
|
else:
|
||||||
|
# Для других типов файлов возвращаем информацию
|
||||||
|
return {
|
||||||
|
"type":
|
||||||
|
"text",
|
||||||
|
"text":
|
||||||
|
f"\n[Файл {name} ({mime_type}) не может быть отображен в чате]\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def restore_cache_from_source(source_path: str, att_type: str,
|
||||||
|
cache_folder: str, cached_path: str) -> str:
|
||||||
|
"""Восстанавливает кеш из исходного файла."""
|
||||||
|
full_cached_path = os.path.join(cache_folder, cached_path)
|
||||||
|
|
||||||
|
# Создаем папку кеша, если не существует
|
||||||
|
os.makedirs(cache_folder, exist_ok=True)
|
||||||
|
|
||||||
|
if att_type == 'external':
|
||||||
|
# Для внешних файлов source_path - абсолютный
|
||||||
|
if not os.path.exists(source_path):
|
||||||
|
raise FileNotFoundError(f"Исходный файл не найден: {source_path}")
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
shutil.copy2(source_path, full_cached_path)
|
||||||
|
else:
|
||||||
|
# Для внутренних файлов и промптов нужно получить полный путь
|
||||||
|
# Это требует доступа к vault, что на бэкенде недоступно
|
||||||
|
# Поэтому это нужно делать на фронтенде
|
||||||
|
raise NotImplementedError(
|
||||||
|
"Восстановление кеша для внутренних файлов должно происходить на фронтенде"
|
||||||
|
)
|
||||||
|
|
||||||
|
return full_cached_path
|
||||||
|
|
||||||
|
|
||||||
def format_attachment_for_llm(attachment: Dict[str, Any]) -> str:
|
def format_attachment_for_llm(attachment: Dict[str, Any]) -> str:
|
||||||
"""Форматирует вложение для вставки в сообщение LLM."""
|
"""Форматирует вложение для вставки в сообщение LLM."""
|
||||||
att_type = "промпта" if attachment["type"] == "prompt" else "файла"
|
att_type = "промпта" if attachment["type"] == "prompt" else "файла"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user