From 899b0f3d5e2aef02c2ee95347f8c14b26fec4f36 Mon Sep 17 00:00:00 2001 From: dimitrievgs Date: Wed, 5 Nov 2025 01:38:13 +0300 Subject: [PATCH] Add appending of external files, fiz appending of internal files - now via metadata (attachments) --- app/api.py | 8 ++--- app/graph_history_manager.py | 68 +++++++++++++++++++----------------- app/workflows.py | 54 ++++++++++++++++++---------- src/types/electron.d.ts | 8 +++++ 4 files changed, 82 insertions(+), 56 deletions(-) create mode 100644 src/types/electron.d.ts diff --git a/app/api.py b/app/api.py index 29d11c7..dd0b608 100644 --- a/app/api.py +++ b/app/api.py @@ -197,22 +197,22 @@ def get_available_models(): @api.route('/api/chat/send', methods=['POST']) def send_user_message(): - """API endpoint для создания узла пользователя и получения graph_id.""" + """API endpoint для создания узла пользователя с вложениями.""" data = request.get_json() message = data.get("message") graph_id = data.get("graph_id") parent_node_id = data.get("parent_node_id") + attachments = data.get("attachments", []) # НОВОЕ if not message: return jsonify({"error": "Сообщение не может быть пустым."}, 400) try: result = graph_history_manager.create_user_node( - message, graph_id, parent_node_id) - + message, graph_id, parent_node_id, attachments # ИЗМЕНЕНО + ) 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}") diff --git a/app/graph_history_manager.py b/app/graph_history_manager.py index 01aacc6..a21f41b 100644 --- a/app/graph_history_manager.py +++ b/app/graph_history_manager.py @@ -303,55 +303,53 @@ class GraphHistoryManager: return summaries def get_messages_from_root_to_node( - self, - graph_id: str, # НОВЫЙ АРГУМЕНТ - graph_data: Dict[str, Any], - target_node_id: str) -> List[Dict[str, str]]: + self, + graph_id: str, + graph_data: Dict[str, Any], + target_node_id: str) -> List[Dict[str, Any]]: # ИЗМЕНЕНО: Dict[str, str] -> Dict[str, Any] """ Получает список сообщений от корневого узла до указанного узла. Сообщения извлекаются из данных узлов, а не из отдельного поля. """ all_nodes = graph_data.get("graph_nodes", []) edges = graph_data.get("graph_edges", []) - - # Создаем словарь для быстрого поиска узлов по ID + node_map = {node['id']: node for node in all_nodes} - # Создаем словарь для быстрого поиска родительских узлов parent_map = {edge['target']: edge['source'] for edge in edges} - + def get_path_to_root(node_id: str) -> List[str]: path = [node_id] current = node_id while current in parent_map: current = parent_map[current] - # Проверяем, чтобы не зациклиться (хотя в древовидном графе это не должно быть проблемой) if current in path: print(f"Обнаружен цикл при построении пути к узлу {node_id}. Путь: {path}") break path.append(current) - return path[::-1] # Разворачиваем путь от корня до целевого узла - + return path[::-1] + path_to_target = get_path_to_root(target_node_id) - # Собираем сообщения, соответствующие узлам в пути. - # Сообщения теперь хранятся в 'data' каждого узла при создании. + messages_for_path = [] for node_id in path_to_target: node = node_map.get(node_id) - if node and 'message' in node.get( - 'data', {}): # Проверяем наличие поля 'message' + if node and 'message' in node.get('data', {}): original_message = node['data']['message'] - messages_for_path.append({ - "role": - original_message.get("role", "unknown"), - "type": - node.get("type", "unknown"), - "content": - original_message.get("content", ""), - "node_id": - original_message.get("node_id", node_id), - "graph_id": graph_id - }) - + + message_dict = { + "role": original_message.get("role", "unknown"), + "type": node.get("type", "unknown"), + "content": original_message.get("content", ""), + "node_id": original_message.get("node_id", node_id), + "graph_id": graph_id + } + + # НОВОЕ: Добавляем attachments, если они есть + if "attachments" in original_message: + message_dict["attachments"] = original_message["attachments"] + + messages_for_path.append(message_dict) + return messages_for_path def delete_graph(self, graph_id: str) -> bool: @@ -491,13 +489,12 @@ class GraphHistoryManager: print(f"Обновлено строк: {updated_rows}") conn.commit() - def create_user_node(self, message: str, graph_id: Optional[str] = None, parent_node_id: Optional[str] = None) -> Dict[str, Any]: - """ - Создаёт узел пользователя и возвращает graph_id и node_id. - """ + def create_user_node(self, message: str, graph_id: Optional[str] = None, + parent_node_id: Optional[str] = None, + attachments: List[Dict[str, Any]] = None) -> Dict[str, Any]: # ИЗМЕНЕНО + """Создаёт узел пользователя с вложениями.""" if not graph_id: graph_id = str(uuid.uuid4()) - if not self._ensure_graph_exists(graph_id): raise Exception(f"Граф с ID {graph_id} не найден.") @@ -507,7 +504,12 @@ class GraphHistoryManager: try: node_data = { "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, + "attachments": attachments or [] # НОВОЕ + } } self._add_graph_node(conn, graph_id, { diff --git a/app/workflows.py b/app/workflows.py index ed4485f..c7481c7 100644 --- a/app/workflows.py +++ b/app/workflows.py @@ -92,45 +92,61 @@ app = workflow.compile() 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. - Возвращает чанки контента по мере их получения. - """ + user_node_id: str, + assistant_node_id: str, + system_prompt: Optional[str] = None, + model: Optional[str] = None): + """Генератор для стримингового ответа LLM с поддержкой вложений.""" try: - # Загружаем граф и историю до user_node_id loaded_graph_data = graph_history_manager.get_graph( graph_id, target_node_id=user_node_id) + if not loaded_graph_data: yield {"type": "error", "error": "Граф не найден."} return messages = loaded_graph_data.get("messages", []) - - # Формируем сообщения для LLM + from langchain_core.messages import HumanMessage, SystemMessage, AIMessage - + messages_for_llm = [] if system_prompt: messages_for_llm.append(SystemMessage(content=system_prompt)) - for msg in messages: + # ИЗМЕНЕНО: Обрабатываем вложения + for i, msg in enumerate(messages): + is_last = (i == len(messages) - 1) + if msg["role"] == "user": - messages_for_llm.append(HumanMessage(content=msg["content"])) + content = msg["content"] + attachments = msg.get("attachments", []) + + # Раскрываем постоянные вложения и вложения последнего сообщения + if attachments: + for att in attachments: + if att.get("permanent") or is_last: + formatted_content = format_attachment_for_llm(att) + content += "\n" + formatted_content + + messages_for_llm.append(HumanMessage(content=content)) + elif msg["role"] == "assistant": messages_for_llm.append(AIMessage(content=msg["content"])) - # Получаем LLM и стримим ответ + # Стриминг ответа llm = get_llm(model or "gemini-2.5-flash") - 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} - + except Exception as e: print(f"Ошибка при стриминге: {e}") yield {"type": "error", "error": str(e)} + + +def format_attachment_for_llm(attachment: Dict[str, Any]) -> str: + """Форматирует вложение для вставки в сообщение LLM.""" + att_type = "промпта" if attachment["type"] == "prompt" else "файла" + header = f"\n\n--- Содержимое {att_type}: {attachment['name']} ---\n" + footer = f"\n--- Конец {att_type}: {attachment['name']} ---\n\n" + return header + attachment.get("content", "").strip() + footer \ No newline at end of file diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts new file mode 100644 index 0000000..356d257 --- /dev/null +++ b/src/types/electron.d.ts @@ -0,0 +1,8 @@ +// Добавьте этот файл, если его нет +interface Window { + electron?: { + shell: { + openPath: (path: string) => Promise; + }; + }; +} \ No newline at end of file