Add appending of external files, fiz appending of internal files - now via metadata (attachments)

This commit is contained in:
dimitrievgs 2025-11-05 01:38:13 +03:00
parent b0209d6b8e
commit 899b0f3d5e
4 changed files with 82 additions and 56 deletions

View File

@ -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}")

View File

@ -304,9 +304,9 @@ class GraphHistoryManager:
def get_messages_from_root_to_node(
self,
graph_id: str, # НОВЫЙ АРГУМЕНТ
graph_id: str,
graph_data: Dict[str, Any],
target_node_id: str) -> List[Dict[str, str]]:
target_node_id: str) -> List[Dict[str, Any]]: # ИЗМЕНЕНО: Dict[str, str] -> Dict[str, Any]
"""
Получает список сообщений от корневого узла до указанного узла.
Сообщения извлекаются из данных узлов, а не из отдельного поля.
@ -314,9 +314,7 @@ class GraphHistoryManager:
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]:
@ -324,33 +322,33 @@ class GraphHistoryManager:
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),
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
@ -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, {

View File

@ -96,41 +96,57 @@ def run_agent_streaming(graph_id: str,
assistant_node_id: str,
system_prompt: Optional[str] = None,
model: Optional[str] = None):
"""
Генератор для стримингового ответа LLM.
Возвращает чанки контента по мере их получения.
"""
"""Генератор для стримингового ответа 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

8
src/types/electron.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
// Добавьте этот файл, если его нет
interface Window {
electron?: {
shell: {
openPath: (path: string) => Promise<string>;
};
};
}