Add appending of external files, fiz appending of internal files - now via metadata (attachments)
This commit is contained in:
parent
b0209d6b8e
commit
899b0f3d5e
|
|
@ -197,22 +197,22 @@ def get_available_models():
|
||||||
|
|
||||||
@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 для создания узла пользователя с вложениями."""
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
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", []) # НОВОЕ
|
||||||
|
|
||||||
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)
|
message, graph_id, parent_node_id, attachments # ИЗМЕНЕНО
|
||||||
|
)
|
||||||
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"))
|
||||||
|
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Ошибка при создании узла пользователя: {e}")
|
print(f"Ошибка при создании узла пользователя: {e}")
|
||||||
|
|
|
||||||
|
|
@ -303,55 +303,53 @@ class GraphHistoryManager:
|
||||||
return summaries
|
return summaries
|
||||||
|
|
||||||
def get_messages_from_root_to_node(
|
def get_messages_from_root_to_node(
|
||||||
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, str]]:
|
target_node_id: str) -> List[Dict[str, Any]]: # ИЗМЕНЕНО: Dict[str, str] -> 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", [])
|
||||||
|
|
||||||
# Создаем словарь для быстрого поиска узлов по ID
|
|
||||||
node_map = {node['id']: node for node in all_nodes}
|
node_map = {node['id']: node for node in all_nodes}
|
||||||
# Создаем словарь для быстрого поиска родительских узлов
|
|
||||||
parent_map = {edge['target']: edge['source'] for edge in edges}
|
parent_map = {edge['target']: edge['source'] for edge in edges}
|
||||||
|
|
||||||
def get_path_to_root(node_id: str) -> List[str]:
|
def get_path_to_root(node_id: str) -> List[str]:
|
||||||
path = [node_id]
|
path = [node_id]
|
||||||
current = node_id
|
current = node_id
|
||||||
while current in parent_map:
|
while current in parent_map:
|
||||||
current = parent_map[current]
|
current = parent_map[current]
|
||||||
# Проверяем, чтобы не зациклиться (хотя в древовидном графе это не должно быть проблемой)
|
|
||||||
if current in path:
|
if current in path:
|
||||||
print(f"Обнаружен цикл при построении пути к узлу {node_id}. Путь: {path}")
|
print(f"Обнаружен цикл при построении пути к узлу {node_id}. Путь: {path}")
|
||||||
break
|
break
|
||||||
path.append(current)
|
path.append(current)
|
||||||
return path[::-1] # Разворачиваем путь от корня до целевого узла
|
return path[::-1]
|
||||||
|
|
||||||
path_to_target = get_path_to_root(target_node_id)
|
path_to_target = get_path_to_root(target_node_id)
|
||||||
# Собираем сообщения, соответствующие узлам в пути.
|
|
||||||
# Сообщения теперь хранятся в 'data' каждого узла при создании.
|
|
||||||
messages_for_path = []
|
messages_for_path = []
|
||||||
for node_id in path_to_target:
|
for node_id in path_to_target:
|
||||||
node = node_map.get(node_id)
|
node = node_map.get(node_id)
|
||||||
if node and 'message' in node.get(
|
if node and 'message' in node.get('data', {}):
|
||||||
'data', {}): # Проверяем наличие поля 'message'
|
|
||||||
original_message = node['data']['message']
|
original_message = node['data']['message']
|
||||||
messages_for_path.append({
|
|
||||||
"role":
|
message_dict = {
|
||||||
original_message.get("role", "unknown"),
|
"role": original_message.get("role", "unknown"),
|
||||||
"type":
|
"type": node.get("type", "unknown"),
|
||||||
node.get("type", "unknown"),
|
"content": original_message.get("content", ""),
|
||||||
"content":
|
"node_id": original_message.get("node_id", node_id),
|
||||||
original_message.get("content", ""),
|
"graph_id": graph_id
|
||||||
"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
|
return messages_for_path
|
||||||
|
|
||||||
def delete_graph(self, graph_id: str) -> bool:
|
def delete_graph(self, graph_id: str) -> bool:
|
||||||
|
|
@ -491,13 +489,12 @@ 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) -> Dict[str, Any]:
|
def create_user_node(self, message: str, graph_id: Optional[str] = None,
|
||||||
"""
|
parent_node_id: Optional[str] = None,
|
||||||
Создаёт узел пользователя и возвращает graph_id и node_id.
|
attachments: List[Dict[str, Any]] = None) -> Dict[str, Any]: # ИЗМЕНЕНО
|
||||||
"""
|
"""Создаёт узел пользователя с вложениями."""
|
||||||
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):
|
if not self._ensure_graph_exists(graph_id):
|
||||||
raise Exception(f"Граф с ID {graph_id} не найден.")
|
raise Exception(f"Граф с ID {graph_id} не найден.")
|
||||||
|
|
||||||
|
|
@ -507,7 +504,12 @@ class GraphHistoryManager:
|
||||||
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": {"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, {
|
self._add_graph_node(conn, graph_id, {
|
||||||
|
|
|
||||||
|
|
@ -92,45 +92,61 @@ 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):
|
||||||
"""
|
"""Генератор для стримингового ответа LLM с поддержкой вложений."""
|
||||||
Генератор для стримингового ответа LLM.
|
|
||||||
Возвращает чанки контента по мере их получения.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
# Загружаем граф и историю до user_node_id
|
|
||||||
loaded_graph_data = graph_history_manager.get_graph(
|
loaded_graph_data = graph_history_manager.get_graph(
|
||||||
graph_id, target_node_id=user_node_id)
|
graph_id, target_node_id=user_node_id)
|
||||||
|
|
||||||
if not loaded_graph_data:
|
if not loaded_graph_data:
|
||||||
yield {"type": "error", "error": "Граф не найден."}
|
yield {"type": "error", "error": "Граф не найден."}
|
||||||
return
|
return
|
||||||
|
|
||||||
messages = loaded_graph_data.get("messages", [])
|
messages = loaded_graph_data.get("messages", [])
|
||||||
|
|
||||||
# Формируем сообщения для LLM
|
|
||||||
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
|
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
|
||||||
|
|
||||||
messages_for_llm = []
|
messages_for_llm = []
|
||||||
if system_prompt:
|
if system_prompt:
|
||||||
messages_for_llm.append(SystemMessage(content=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":
|
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":
|
elif msg["role"] == "assistant":
|
||||||
messages_for_llm.append(AIMessage(content=msg["content"]))
|
messages_for_llm.append(AIMessage(content=msg["content"]))
|
||||||
|
|
||||||
# Получаем LLM и стримим ответ
|
# Стриминг ответа
|
||||||
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 = chunk.content if hasattr(chunk, 'content') else str(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}")
|
||||||
yield {"type": "error", "error": str(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
8
src/types/electron.d.ts
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
// Добавьте этот файл, если его нет
|
||||||
|
interface Window {
|
||||||
|
electron?: {
|
||||||
|
shell: {
|
||||||
|
openPath: (path: string) => Promise<string>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user