Fix bug with sending all the messages from all nodes instead of current branch

This commit is contained in:
dimitrievgs 2025-09-15 00:36:48 +03:00
parent b634337dda
commit c1d990fd93
6 changed files with 286 additions and 137 deletions

View File

@ -17,6 +17,7 @@ class GraphHistoryManager:
def __init__(self, db_path="graph_history.db"): def __init__(self, db_path="graph_history.db"):
self.db_path = db_path self.db_path = db_path
self._create_table(self._get_connection()) # Создаем таблицу при инициализации
def _get_connection(self): def _get_connection(self):
"""Получает соединение с базой данных.""" """Получает соединение с базой данных."""
@ -28,7 +29,6 @@ class GraphHistoryManager:
cursor.execute(""" cursor.execute("""
CREATE TABLE IF NOT EXISTS graphs ( CREATE TABLE IF NOT EXISTS graphs (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
messages TEXT,
graph_nodes TEXT, graph_nodes TEXT,
graph_edges TEXT, graph_edges TEXT,
current_node_id TEXT current_node_id TEXT
@ -45,8 +45,12 @@ class GraphHistoryManager:
result = cursor.fetchone()[0] result = cursor.fetchone()[0]
return result if result is not None else 0 return result if result is not None else 0
# ---------------------------------------------------- public methods ----------------------------------------------------------------
def save_graph(self, graph_data: Any) -> str: def save_graph(self, graph_data: Any) -> str:
"""Сохраняет или обновляет данные графа в базе данных и возвращает ID.""" """
Сохраняет или обновляет данные графа в базе данных и возвращает ID.
Не сохраняет полную историю сообщений, только структуру графа.
"""
conn = self._get_connection() conn = self._get_connection()
cursor = conn.cursor() cursor = conn.cursor()
try: try:
@ -58,7 +62,6 @@ class GraphHistoryManager:
graph_data["id"] = graph_id graph_data["id"] = graph_id
# Преобразуем структуры данных в JSON-строки для хранения в SQLite # Преобразуем структуры данных в JSON-строки для хранения в SQLite
messages_json = json.dumps(graph_data.get("messages", []))
graph_nodes_json = json.dumps(graph_data.get("graph_nodes", [])) graph_nodes_json = json.dumps(graph_data.get("graph_nodes", []))
graph_edges_json = json.dumps(graph_data.get("graph_edges", [])) graph_edges_json = json.dumps(graph_data.get("graph_edges", []))
current_node_id = graph_data.get("current_node_id", "") current_node_id = graph_data.get("current_node_id", "")
@ -72,20 +75,19 @@ class GraphHistoryManager:
cursor.execute( cursor.execute(
""" """
UPDATE graphs SET UPDATE graphs SET
messages = ?,
graph_nodes = ?, graph_nodes = ?,
graph_edges = ?, graph_edges = ?,
current_node_id = ? current_node_id = ?
WHERE id = ? WHERE id = ?
""", (messages_json, graph_nodes_json, graph_edges_json, """, (graph_nodes_json, graph_edges_json,
current_node_id, graph_id)) current_node_id, graph_id))
else: else:
# Вставляем новую запись # Вставляем новую запись
cursor.execute( cursor.execute(
""" """
INSERT INTO graphs (id, messages, graph_nodes, graph_edges, current_node_id) INSERT INTO graphs (id, graph_nodes, graph_edges, current_node_id)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?)
""", (graph_id, messages_json, graph_nodes_json, """, (graph_id, graph_nodes_json,
graph_edges_json, current_node_id)) graph_edges_json, current_node_id))
conn.commit() conn.commit()
@ -94,29 +96,51 @@ class GraphHistoryManager:
finally: finally:
conn.close() conn.close()
def get_graph(self, graph_id: str) -> Optional[Any]: def get_graph(self, graph_id: str, target_node_id: Optional[str] = None) -> Optional[Dict[str, Any]]:
"""Получает данные графа по ID из базы данных.""" """
Получает данные графа по ID из базы данных.
Динамически вычисляет 'messages' до 'target_node_id'.
Если target_node_id не указан, используется current_node_id из БД.
"""
conn = self._get_connection() conn = self._get_connection()
cursor = conn.cursor() cursor = conn.cursor()
try: try:
cursor.execute( cursor.execute(
"SELECT messages, graph_nodes, graph_edges, current_node_id FROM graphs WHERE id = ?", "SELECT graph_nodes, graph_edges, current_node_id FROM graphs WHERE id = ?",
(graph_id, )) (graph_id, ))
result = cursor.fetchone() result = cursor.fetchone()
if result: if result:
messages_json, graph_nodes_json, graph_edges_json, current_node_id = result graph_nodes_json, graph_edges_json, db_current_node_id = result
# Преобразуем JSON-строки обратно в структуры данных Python # Преобразуем JSON-строки обратно в структуры данных Python
messages = json.loads(messages_json)
graph_nodes = json.loads(graph_nodes_json) graph_nodes = json.loads(graph_nodes_json)
graph_edges = json.loads(graph_edges_json) graph_edges = json.loads(graph_edges_json)
resolved_current_node_id = target_node_id if target_node_id else db_current_node_id
# Если нет узлов, значит граф пуст или некорректен
if not graph_nodes:
return {
"id": graph_id,
"messages": [],
"graph_nodes": [],
"graph_edges": [],
"current_node_id": resolved_current_node_id
}
# Динамически собираем сообщения
messages = self.get_messages_from_root_to_node(
{"graph_nodes": graph_nodes, "graph_edges": graph_edges},
resolved_current_node_id
)
return { return {
"id": graph_id, "id": graph_id,
"messages": messages, "messages": messages, # Здесь будут вычисленные сообщения
"graph_nodes": graph_nodes, "graph_nodes": graph_nodes,
"graph_edges": graph_edges, "graph_edges": graph_edges,
"current_node_id": current_node_id "current_node_id": resolved_current_node_id
} }
else: else:
return None return None
@ -124,54 +148,80 @@ class GraphHistoryManager:
conn.close() conn.close()
def get_all_graphs_summary(self) -> List[Dict[str, str]]: def get_all_graphs_summary(self) -> List[Dict[str, str]]:
"""Возвращает краткий список всех сохраненных графов.""" """
Возвращает краткий список всех сохраненных графов.
Первое сообщение извлекается путем построения пути к current_node_id
и взятия первого сообщения.
"""
conn = self._get_connection() conn = self._get_connection()
cursor = conn.cursor() cursor = conn.cursor()
try: try:
cursor.execute("SELECT id, messages FROM graphs") cursor.execute("SELECT id, graph_nodes, graph_edges, current_node_id FROM graphs")
graphs_data = cursor.fetchall() graphs_data = cursor.fetchall()
return [{ summaries = []
"id": for gid, nodes_json, edges_json, current_node_id in graphs_data:
gid, graph_nodes = json.loads(nodes_json)
"first_message": graph_edges = json.loads(edges_json)
json.loads(messages)[0].get("content", "No content")
if messages else "No content" # Получаем все сообщения до current_node_id
} for gid, messages in graphs_data] full_messages = self.get_messages_from_root_to_node(
{"graph_nodes": graph_nodes, "graph_edges": graph_edges},
current_node_id
)
first_message_content = "No content"
if full_messages:
# Ищем первое сообщение, которое является пользовательским
for msg in full_messages:
if msg.get("role") == "user":
first_message_content = msg.get("content", "No content")
break
if first_message_content == "No content": # Если не нашли пользовательского, берем первое ассистента
first_message_content = full_messages[0].get("content", "No content")
summaries.append({
"id": gid,
"first_message": first_message_content
})
return summaries
finally: finally:
conn.close() conn.close()
def get_messages_from_root_to_node( def get_messages_from_root_to_node(
self, graph_data: Dict[str, Any], self, graph_data: Dict[str, Any],
target_node_id: str) -> List[Dict[str, str]]: target_node_id: str) -> List[Dict[str, str]]:
"""Получает список сообщений от корневого узла до указанного узла.""" """
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", [])
messages = graph_data.get("messages", [])
# Создаем словарь для быстрого поиска узлов по ID
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]
while node_id in parent_map: current = node_id
node_id = parent_map[node_id] while current in parent_map:
path.append(node_id) current = parent_map[current]
path.append(current)
return path[::-1] # Инвертируем, чтобы получить путь от корня return path[::-1] # Инвертируем, чтобы получить путь от корня
# Получаем путь от корня до целевого узла # Получаем путь от корня до целевого узла
path_to_root = get_path_to_root(target_node_id) path_to_target = get_path_to_root(target_node_id)
# Собираем сообщения, соответствующие узлам в пути
# Собираем сообщения, соответствующие узлам в пути.
# Сообщения теперь хранятся в 'data' каждого узла при создании.
messages_for_path = [] messages_for_path = []
node_ids_in_path = set(path_to_root) for node_id in path_to_target:
node = node_map.get(node_id)
for message in messages: if node and 'message' in node.get('data', {}): # Проверяем наличие поля 'message'
if "node_id" in message and message["node_id"] in node_ids_in_path: messages_for_path.append(node['data']['message'])
messages_for_path.append({
"role": message["role"],
"content": message["content"]
})
return messages_for_path return messages_for_path
@ -182,7 +232,7 @@ class GraphHistoryManager:
try: try:
cursor.execute("DELETE FROM graphs WHERE id = ?", (graph_id, )) cursor.execute("DELETE FROM graphs WHERE id = ?", (graph_id, ))
conn.commit() conn.commit()
return True return cursor.rowcount > 0 # Возвращает True, если была удалена хотя бы одна строка
except Exception as e: except Exception as e:
print(f"Ошибка при удалении графа: {e}") print(f"Ошибка при удалении графа: {e}")
return False return False

View File

@ -5,7 +5,7 @@
import requests import requests
from typing import Dict, Any, List from typing import Dict, Any, List
from langchain_core.messages import HumanMessage, SystemMessage from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
# --- Конфигурация LLM --- # --- Конфигурация LLM ---
# Пустой словарь для локальных моделей (Ollama) # Пустой словарь для локальных моделей (Ollama)
@ -101,6 +101,11 @@ class CustomLLM:
"role": "system", "role": "system",
"content": msg.content "content": msg.content
}) })
elif isinstance(msg, AIMessage): # Добавляем обработку AIMessage
openai_messages.append({
"role": "assistant",
"content": msg.content
})
else: # Предполагаем, что это другие типы сообщений Langchain или словари else: # Предполагаем, что это другие типы сообщений Langchain или словари
openai_messages.append({ openai_messages.append({
"role": "role":
@ -155,6 +160,11 @@ class CustomLLM:
"role": "system", "role": "system",
"content": msg.content "content": msg.content
}) })
elif isinstance(msg, AIMessage): # Добавляем обработку AIMessage
mistral_messages.append({
"role": "assistant",
"content": msg.content
})
else: # Предполагаем, что это другие типы сообщений Langchain или словари else: # Предполагаем, что это другие типы сообщений Langchain или словари
mistral_messages.append({ mistral_messages.append({
"role": "role":

View File

@ -14,11 +14,6 @@ class AgentState(BaseModel):
command: Optional[str] = Field(None, command: Optional[str] = Field(None,
description="Распознанная слеш-команда.") description="Распознанная слеш-команда.")
command_args: List[str] = Field([], description="Аргументы команды.") command_args: List[str] = Field([], description="Аргументы команды.")
chat_history: List[Dict[str, str]] = Field(
[],
description=
"Полная история сообщений в текущей ветке диалога. Формат: [{'role': 'user/assistant', 'content': 'message'}]"
)
llm_response: Optional[str] = Field(None, description="Ответ от LLM.") llm_response: Optional[str] = Field(None, description="Ответ от LLM.")
image_urls: List[str] = Field( image_urls: List[str] = Field(
[], description="URL сгенерированных изображений.") [], description="URL сгенерированных изображений.")
@ -37,3 +32,7 @@ class AgentState(BaseModel):
"start", description="ID текущего узла графа для фронтенда.") "start", description="ID текущего узла графа для фронтенда.")
parent_node_id: Optional[str] = Field(None, parent_node_id: Optional[str] = Field(None,
description="ID родительского узла.") description="ID родительского узла.")
# Временное хранилище для chat_history, которое не сохраняется в БД
# Оно используется для передачи контекста между узлами в пределах одного выполнения
temporary_chat_history: List[Dict[str, Any]] = Field(
[], description="Временная история чата, не сохраняемая в БД.")

View File

@ -4,7 +4,7 @@
разбор команд, вызов LLM или выполнение сервисов. разбор команд, вызов LLM или выполнение сервисов.
""" """
from langchain_core.messages import HumanMessage, SystemMessage from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
from llm_client import get_llm from llm_client import get_llm
from services import ImageGenerationService, SubtitleService, ImageAnalysisService, SummarizationService from services import ImageGenerationService, SubtitleService, ImageAnalysisService, SummarizationService
@ -66,21 +66,28 @@ class CommandManager:
command_manager = CommandManager() command_manager = CommandManager()
# Здесь мы меняем состояние графа, добавляя сообщение от пользователя
def parse_command_node(state: AgentState) -> AgentState: def parse_command_node(state: AgentState) -> AgentState:
"""Парсит вход пользователя на предмет слеш-команды.""" """
Парсит вход пользователя на предмет слеш-команды и инициализирует узел пользователя в графе.
"""
print("Выполняется узел: parse_command_node") print("Выполняется узел: parse_command_node")
user_input = state.input.strip() user_input = state.input.strip()
# Добавляем узел пользователя в граф для фронтенда # Создаем ID для узла пользователя
user_node_id = f"user_{len(state.graph_nodes) + 1}" user_node_id = f"user_{len(state.graph_nodes) + 1}"
# Добавляем узел пользователя в граф для фронтенда с полным текстом сообщения
state.graph_nodes.append({ state.graph_nodes.append({
"id": user_node_id, "id": user_node_id,
"type": "user", "type": "user",
"data": { "data": {
"label": "label": user_input[:30] + "..." if len(user_input) > 30 else user_input,
user_input[:30] + "..." if len(user_input) > 30 else user_input "message": {"role": "user", "content": user_input, "node_id": user_node_id} # Сохраняем сообщение здесь
} }
}) })
# Добавляем ребро к новому узлу, если есть родительский узел
if state.parent_node_id: if state.parent_node_id:
state.graph_edges.append({ state.graph_edges.append({
"id": f"e{state.parent_node_id}-{user_node_id}", "id": f"e{state.parent_node_id}-{user_node_id}",
@ -90,10 +97,12 @@ def parse_command_node(state: AgentState) -> AgentState:
state.parent_node_id = user_node_id state.parent_node_id = user_node_id
state.current_node_id = user_node_id state.current_node_id = user_node_id
state.chat_history.append({ # Обновляем временную историю чата, которая используется для передачи между узлами
# и для вызова LLM в текущем раунде.
state.temporary_chat_history.append({
"role": "user", "role": "user",
"content": user_input, "content": user_input,
"node_id": user_node_id # Сохраняем ID узла "node_id": user_node_id
}) })
if user_input.startswith('/'): if user_input.startswith('/'):
@ -116,7 +125,10 @@ def parse_command_node(state: AgentState) -> AgentState:
def execute_command_node(state: AgentState) -> AgentState: def execute_command_node(state: AgentState) -> AgentState:
"""Выполняет соответствующий workflow для команды.""" """
Выполняет соответствующий workflow для команды.
Этот узел служит маршрутизатором для выбора следующего узла в зависимости от команды.
"""
print( print(
f"Выполняется узел: execute_command_node для команды: {state.command}") f"Выполняется узел: execute_command_node для команды: {state.command}")
# Этот узел будет выступать в роли маршрутизатора # Этот узел будет выступать в роли маршрутизатора
@ -131,27 +143,32 @@ main_llm = get_llm(DEFAULT_LLM_NAME)
def call_llm_node(state: AgentState) -> AgentState: def call_llm_node(state: AgentState) -> AgentState:
"""Вызывает LLM для обработки обычного диалога или генерации ответа.""" """
Вызывает LLM для обработки обычного диалога или генерации ответа.
Использует `temporary_chat_history` для передачи контекста LLM.
"""
print("Выполняется узел: call_llm_node (для обычного чата)") print("Выполняется узел: call_llm_node (для обычного чата)")
try: try:
# Для обычного чата LLM использует всю историю # TODO: добавить в переписку ещё SystemMessage с системным промптом, типа: messages_for_llm.append(SystemMessage(content=msg["content"]))
# Для обычного чата LLM использует текущую временную историю
messages_for_llm = [] messages_for_llm = []
for msg in state.chat_history: for msg in state.temporary_chat_history: # Используем temporary_chat_history
if msg["role"] == "user": if msg["role"] == "user":
messages_for_llm.append(HumanMessage(content=msg["content"])) messages_for_llm.append(HumanMessage(content=msg["content"]))
elif msg["role"] == "assistant": elif msg["role"] == "assistant":
messages_for_llm.append(SystemMessage(content=msg["content"])) messages_for_llm.append(AIMessage(content=msg["content"]))
response = main_llm.invoke(messages_for_llm) response = main_llm.invoke(messages_for_llm)
# Добавляем узел LLM в граф для фронтенда
llm_node_id = f"llm_{len(state.graph_nodes) + 1}" llm_node_id = f"llm_{len(state.graph_nodes) + 1}"
# Добавляем узел LLM в граф для фронтенда с полным текстом ответа
state.graph_nodes.append({ state.graph_nodes.append({
"id": llm_node_id, "id": llm_node_id,
"type": "llm", "type": "llm",
"data": { "data": {
"label": "label": response[:30] + "..." if len(response) > 30 else response,
response[:30] + "..." if len(response) > 30 else response "message": {"role": "assistant", "content": response, "node_id": llm_node_id} # Сохраняем сообщение здесь
} }
}) })
state.graph_edges.append({ state.graph_edges.append({
@ -162,10 +179,11 @@ def call_llm_node(state: AgentState) -> AgentState:
state.parent_node_id = llm_node_id state.parent_node_id = llm_node_id
state.current_node_id = llm_node_id state.current_node_id = llm_node_id
state.chat_history.append({ # Обновляем временную историю чата с ответом LLM
state.temporary_chat_history.append({
"role": "assistant", "role": "assistant",
"content": response, "content": response,
"node_id": llm_node_id # Сохраняем ID узла "node_id": llm_node_id
}) })
state.llm_response = response state.llm_response = response
@ -182,12 +200,16 @@ image_gen_service = ImageGenerationService(
def generate_images_node(state: AgentState) -> AgentState: def generate_images_node(state: AgentState) -> AgentState:
"""Генерирует изображения по промпту.""" """
Генерирует изображения по промпту, переданному в команде `/imagine`.
"""
print("Выполняется узел: generate_images_node") print("Выполняется узел: generate_images_node")
prompt = " ".join(state.command_args) prompt = " ".join(state.command_args)
if not prompt: if not prompt:
state.error = "Для команды /imagine требуется промпт. Пример: /imagine кошка на луне" state.error = "Для команды /imagine требуется промпт. Пример: /imagine кошка на луне"
state.llm_response = state.error state.llm_response = state.error
# Добавляем ошибку во временную историю для согласованности
state.temporary_chat_history.append({"role": "assistant", "content": state.error})
return state return state
try: try:
@ -196,20 +218,21 @@ def generate_images_node(state: AgentState) -> AgentState:
# Формируем текст ответа с использованием Markdown для отображения изображений # Формируем текст ответа с использованием Markdown для отображения изображений
image_markdown = "\n".join([ image_markdown = "\n".join([
f'<a href="{url}"><img src="{url}" alt="image{i}" style="max-width: 300px; max-height: 300px;"></a>' f'<a href="{url}" target="_blank"><img src="{url}" alt="image{i}" style="max-width: 300px; max-height: 300px;"></a>'
for i, url in enumerate(urls) for i, url in enumerate(urls)
]) ])
response_text = f"Сгенерировано {len(urls)} изображений:\n{image_markdown}" response_text = f"Сгенерировано {len(urls)} изображений:\n{image_markdown}"
state.llm_response = response_text state.llm_response = response_text
# Добавляем узел генерации изображений в граф
img_gen_node_id = f"img_gen_{len(state.graph_nodes) + 1}" img_gen_node_id = f"img_gen_{len(state.graph_nodes) + 1}"
# Добавляем узел генерации изображений в граф
state.graph_nodes.append({ state.graph_nodes.append({
"id": img_gen_node_id, "id": img_gen_node_id,
"type": "tool", "type": "tool",
"data": { "data": {
"label": "Генерация изображений", "label": "Генерация изображений",
"details": prompt "details": prompt,
"message": {"role": "assistant", "content": response_text, "node_id": img_gen_node_id}
} }
}) })
state.graph_edges.append({ state.graph_edges.append({
@ -220,8 +243,8 @@ def generate_images_node(state: AgentState) -> AgentState:
state.parent_node_id = img_gen_node_id state.parent_node_id = img_gen_node_id
state.current_node_id = img_gen_node_id state.current_node_id = img_gen_node_id
state.llm_response = "Сгенерированы изображения." # Обновляем временную историю чата
state.chat_history.append({ state.temporary_chat_history.append({
"role": "assistant", "role": "assistant",
"content": response_text, "content": response_text,
"node_id": img_gen_node_id # Привязываем к ID узла "node_id": img_gen_node_id # Привязываем к ID узла
@ -231,6 +254,8 @@ def generate_images_node(state: AgentState) -> AgentState:
state.error = f"Ошибка генерации изображений: {e}" state.error = f"Ошибка генерации изображений: {e}"
state.llm_response = f"Произошла ошибка при генерации изображений: {e}" state.llm_response = f"Произошла ошибка при генерации изображений: {e}"
print(state.error) print(state.error)
# Добавляем ошибку во временную историю
state.temporary_chat_history.append({"role": "assistant", "content": state.llm_response})
return state return state
@ -239,11 +264,15 @@ image_analysis_service = ImageAnalysisService(
def analyze_image_node(state: AgentState) -> AgentState: def analyze_image_node(state: AgentState) -> AgentState:
"""Анализирует изображение по URL или данным.""" """
Анализирует изображение по URL и промпту, переданным в команде `/analyze`.
"""
print("Выполняется узел: analyze_image_node") print("Выполняется узел: analyze_image_node")
if len(state.command_args) < 2: if len(state.command_args) < 2:
state.error = "Для команды /analyze требуется URL изображения и промпт. Пример: /analyze [url] 'что на изображении?'" state.error = "Для команды /analyze требуется URL изображения и промпт. Пример: /analyze [url] 'что на изображении?'"
state.llm_response = state.error state.llm_response = state.error
# Добавляем ошибку во временную историю для согласованности
state.temporary_chat_history.append({"role": "assistant", "content": state.error})
return state return state
image_url = state.command_args[0] image_url = state.command_args[0]
@ -253,20 +282,18 @@ def analyze_image_node(state: AgentState) -> AgentState:
analysis_result = image_analysis_service.analyze_image( analysis_result = image_analysis_service.analyze_image(
image_url, prompt) image_url, prompt)
state.analysis_result = analysis_result state.analysis_result = analysis_result
state.llm_response = f"Результат анализа изображения '{image_url}': {analysis_result}" response_text = f"Результат анализа изображения '{image_url}': {analysis_result}"
state.chat_history.append({ state.llm_response = response_text
"role": "assistant",
"content": state.llm_response
})
# Добавляем узел анализа изображения в граф
img_analysis_node_id = f"img_analysis_{len(state.graph_nodes) + 1}" img_analysis_node_id = f"img_analysis_{len(state.graph_nodes) + 1}"
# Добавляем узел анализа изображения в граф
state.graph_nodes.append({ state.graph_nodes.append({
"id": img_analysis_node_id, "id": img_analysis_node_id,
"type": "tool", "type": "tool",
"data": { "data": {
"label": "Анализ изображения", "label": "Анализ изображения",
"details": f"URL: {image_url}, Промпт: {prompt}" "details": f"URL: {image_url}, Промпт: {prompt}",
"message": {"role": "assistant", "content": response_text, "node_id": img_analysis_node_id}
} }
}) })
state.graph_edges.append({ state.graph_edges.append({
@ -277,10 +304,19 @@ def analyze_image_node(state: AgentState) -> AgentState:
state.parent_node_id = img_analysis_node_id state.parent_node_id = img_analysis_node_id
state.current_node_id = img_analysis_node_id state.current_node_id = img_analysis_node_id
# Обновляем временную историю чата
state.temporary_chat_history.append({
"role": "assistant",
"content": response_text,
"node_id": img_analysis_node_id
})
except Exception as e: except Exception as e:
state.error = f"Ошибка анализа изображения: {e}" state.error = f"Ошибка анализа изображения: {e}"
state.llm_response = f"Произошла ошибка при анализе изображения: {e}" state.llm_response = f"Произошла ошибка при анализе изображения: {e}"
print(state.error) print(state.error)
# Добавляем ошибку во временную историю
state.temporary_chat_history.append({"role": "assistant", "content": state.llm_response})
return state return state
@ -288,25 +324,25 @@ subtitle_service = SubtitleService()
def get_meet_subtitles_node(state: AgentState) -> AgentState: def get_meet_subtitles_node(state: AgentState) -> AgentState:
"""Получает субтитры из Google Meet.""" """
Получает субтитры из Google Meet через команду `/subtitles_meet`.
"""
print("Выполняется узел: get_meet_subtitles_node") print("Выполняется узел: get_meet_subtitles_node")
try: try:
subtitles = subtitle_service.get_google_meet_subtitles() subtitles = subtitle_service.get_google_meet_subtitles()
state.subtitles = subtitles state.subtitles = subtitles
state.llm_response = f"Текущие субтитры из Google Meet: {subtitles}" response_text = f"Текущие субтитры из Google Meet: {subtitles}"
state.chat_history.append({ state.llm_response = response_text
"role": "assistant",
"content": state.llm_response
})
# Добавляем узел субтитров Meet в граф
meet_subtitles_node_id = f"meet_subtitles_{len(state.graph_nodes) + 1}" meet_subtitles_node_id = f"meet_subtitles_{len(state.graph_nodes) + 1}"
# Добавляем узел субтитров Meet в граф
state.graph_nodes.append({ state.graph_nodes.append({
"id": meet_subtitles_node_id, "id": meet_subtitles_node_id,
"type": "tool", "type": "tool",
"data": { "data": {
"label": "Субтитры Google Meet", "label": "Субтитры Google Meet",
"details": subtitles[:30] + "..." if subtitles else "" "details": subtitles[:30] + "..." if subtitles else "",
"message": {"role": "assistant", "content": response_text, "node_id": meet_subtitles_node_id}
} }
}) })
state.graph_edges.append({ state.graph_edges.append({
@ -317,33 +353,42 @@ def get_meet_subtitles_node(state: AgentState) -> AgentState:
state.parent_node_id = meet_subtitles_node_id state.parent_node_id = meet_subtitles_node_id
state.current_node_id = meet_subtitles_node_id state.current_node_id = meet_subtitles_node_id
# Обновляем временную историю чата
state.temporary_chat_history.append({
"role": "assistant",
"content": response_text,
"node_id": meet_subtitles_node_id
})
except Exception as e: except Exception as e:
state.error = f"Ошибка получения субтитров Google Meet: {e}" state.error = f"Ошибка получения субтитров Google Meet: {e}"
state.llm_response = f"Произошла ошибка при получении субтитров Google Meet: {e}" state.llm_response = f"Произошла ошибка при получении субтитров Google Meet: {e}"
print(state.error) print(state.error)
# Добавляем ошибку во временную историю
state.temporary_chat_history.append({"role": "assistant", "content": state.llm_response})
return state return state
def get_teams_subtitles_node(state: AgentState) -> AgentState: def get_teams_subtitles_node(state: AgentState) -> AgentState:
"""Получает субтитры из MS Teams.""" """
Получает субтитры из MS Teams через команду `/subtitles_teams`.
"""
print("Выполняется узел: get_teams_subtitles_node") print("Выполняется узел: get_teams_subtitles_node")
try: try:
subtitles = subtitle_service.get_ms_teams_subtitles() subtitles = subtitle_service.get_ms_teams_subtitles()
state.subtitles = subtitles state.subtitles = subtitles
state.llm_response = f"Текущие субтитры из MS Teams: {subtitles}" response_text = f"Текущие субтитры из MS Teams: {subtitles}"
state.chat_history.append({ state.llm_response = response_text
"role": "assistant",
"content": state.llm_response
})
# Добавляем узел субтитров Teams в граф
teams_subtitles_node_id = f"teams_subtitles_{len(state.graph_nodes) + 1}" teams_subtitles_node_id = f"teams_subtitles_{len(state.graph_nodes) + 1}"
# Добавляем узел субтитров Teams в граф
state.graph_nodes.append({ state.graph_nodes.append({
"id": teams_subtitles_node_id, "id": teams_subtitles_node_id,
"type": "tool", "type": "tool",
"data": { "data": {
"label": "Субтитры MS Teams", "label": "Субтитры MS Teams",
"details": subtitles[:30] + "..." if subtitles else "" "details": subtitles[:30] + "..." if subtitles else "",
"message": {"role": "assistant", "content": response_text, "node_id": teams_subtitles_node_id}
} }
}) })
state.graph_edges.append({ state.graph_edges.append({
@ -354,10 +399,19 @@ def get_teams_subtitles_node(state: AgentState) -> AgentState:
state.parent_node_id = teams_subtitles_node_id state.parent_node_id = teams_subtitles_node_id
state.current_node_id = teams_subtitles_node_id state.current_node_id = teams_subtitles_node_id
# Обновляем временную историю чата
state.temporary_chat_history.append({
"role": "assistant",
"content": response_text,
"node_id": teams_subtitles_node_id
})
except Exception as e: except Exception as e:
state.error = f"Ошибка получения субтитров MS Teams: {e}" state.error = f"Ошибка получения субтитров MS Teams: {e}"
state.llm_response = f"Произошла ошибка при получении субтитров MS Teams: {e}" state.llm_response = f"Произошла ошибка при получении субтитров MS Teams: {e}"
print(state.error) print(state.error)
# Добавляем ошибку во временную историю
state.temporary_chat_history.append({"role": "assistant", "content": state.llm_response})
return state return state
@ -366,28 +420,27 @@ summarization_service = SummarizationService(
def summarize_history_node(state: AgentState) -> AgentState: def summarize_history_node(state: AgentState) -> AgentState:
"""Суммирует историю диалога.""" """
Суммирует историю диалога, используя `temporary_chat_history` в качестве контекста.
"""
print("Выполняется узел: summarize_history_node") print("Выполняется узел: summarize_history_node")
try: try:
# Для суммаризации используем полную историю, которую ведет AgentState # Для суммаризации используем временную историю, подготовленную в предыдущих узлах
summarized_text = summarization_service.summarize_history( summarized_text = summarization_service.summarize_history_of_dicts(
state.chat_history) state.temporary_chat_history) # Изменен метод для использования List[Dict]
state.summarized_history_text = summarized_text state.summarized_history_text = summarized_text
state.llm_response = f"История диалога суммирована:\n{summarized_text}" response_text = f"История диалога суммирована:\n{summarized_text}"
state.chat_history.append({ state.llm_response = response_text
"role": "assistant",
"content": state.llm_response
})
# Добавляем узел суммаризации в граф
summary_node_id = f"summary_{len(state.graph_nodes) + 1}" summary_node_id = f"summary_{len(state.graph_nodes) + 1}"
# Добавляем узел суммаризации в граф
state.graph_nodes.append({ state.graph_nodes.append({
"id": summary_node_id, "id": summary_node_id,
"type": "tool", "type": "tool",
"data": { "data": {
"label": "Суммаризация истории", "label": "Суммаризация истории",
"details": "details": summarized_text[:30] + "..." if summarized_text else "",
summarized_text[:30] + "..." if summarized_text else "" "message": {"role": "assistant", "content": response_text, "node_id": summary_node_id}
} }
}) })
state.graph_edges.append({ state.graph_edges.append({
@ -398,30 +451,38 @@ def summarize_history_node(state: AgentState) -> AgentState:
state.parent_node_id = summary_node_id state.parent_node_id = summary_node_id
state.current_node_id = summary_node_id state.current_node_id = summary_node_id
# Обновляем временную историю чата
state.temporary_chat_history.append({
"role": "assistant",
"content": response_text,
"node_id": summary_node_id
})
except Exception as e: except Exception as e:
state.error = f"Ошибка суммаризации истории: {e}" state.error = f"Ошибка суммаризации истории: {e}"
state.llm_response = f"Произошла ошибка при суммаризации истории: {e}" state.llm_response = f"Произошла ошибка при суммаризации истории: {e}"
print(state.error) print(state.error)
# Добавляем ошибку во временную историю
state.temporary_chat_history.append({"role": "assistant", "content": state.llm_response})
return state return state
def handle_error_node(state: AgentState) -> AgentState: def handle_error_node(state: AgentState) -> AgentState:
"""Обрабатывает ошибки команд.""" """
Обрабатывает ошибки команд, формируя ответ для пользователя и добавляя его в граф.
"""
print(f"Выполняется узел: handle_error_node. Ошибка: {state.error}") print(f"Выполняется узел: handle_error_node. Ошибка: {state.error}")
state.llm_response = state.error state.llm_response = state.error
state.chat_history.append({
"role": "assistant",
"content": state.llm_response
})
# Добавляем узел ошибки в граф
error_node_id = f"error_{len(state.graph_nodes) + 1}" error_node_id = f"error_{len(state.graph_nodes) + 1}"
# Добавляем узел ошибки в граф
state.graph_nodes.append({ state.graph_nodes.append({
"id": error_node_id, "id": error_node_id,
"type": "error", "type": "error",
"data": { "data": {
"label": "Ошибка", "label": "Ошибка",
"details": state.error "details": state.error,
"message": {"role": "assistant", "content": state.llm_response, "node_id": error_node_id}
} }
}) })
state.graph_edges.append({ state.graph_edges.append({
@ -432,30 +493,36 @@ def handle_error_node(state: AgentState) -> AgentState:
state.parent_node_id = error_node_id state.parent_node_id = error_node_id
state.current_node_id = error_node_id state.current_node_id = error_node_id
# Обновляем временную историю чата
state.temporary_chat_history.append({
"role": "assistant",
"content": state.llm_response,
"node_id": error_node_id
})
return state return state
def help_node(state: AgentState) -> AgentState: def help_node(state: AgentState) -> AgentState:
"""Предоставляет информацию о доступных командах.""" """
Предоставляет информацию о доступных командах, формируя ответ для пользователя и добавляя его в граф.
"""
print("Выполняется узел: help_node") print("Выполняется узел: help_node")
commands_info = command_manager.get_commands() commands_info = command_manager.get_commands()
help_text = "Доступные команды:\n" help_text = "Доступные команды:\n"
for cmd, info in commands_info.items(): for cmd, info in commands_info.items():
help_text += f"/{cmd}: {info['description']}\n" help_text += f"/{cmd}: {info['description']}\n"
state.llm_response = help_text state.llm_response = help_text
state.chat_history.append({
"role": "assistant",
"content": state.llm_response
})
# Добавляем узел справки в граф
help_node_id = f"help_{len(state.graph_nodes) + 1}" help_node_id = f"help_{len(state.graph_nodes) + 1}"
# Добавляем узел справки в граф
state.graph_nodes.append({ state.graph_nodes.append({
"id": help_node_id, "id": help_node_id,
"type": "info", "type": "info",
"data": { "data": {
"label": "Справка", "label": "Справка",
"details": "Список команд" "details": "Список команд",
"message": {"role": "assistant", "content": state.llm_response, "node_id": help_node_id}
} }
}) })
state.graph_edges.append({ state.graph_edges.append({
@ -466,4 +533,11 @@ def help_node(state: AgentState) -> AgentState:
state.parent_node_id = help_node_id state.parent_node_id = help_node_id
state.current_node_id = help_node_id state.current_node_id = help_node_id
# Обновляем временную историю чата
state.temporary_chat_history.append({
"role": "assistant",
"content": state.llm_response,
"node_id": help_node_id
})
return state return state

View File

@ -97,41 +97,57 @@ def run_agent(user_input: str,
Может продолжить существующий граф по graph_id. Может продолжить существующий граф по graph_id.
Возвращает обновленное состояние и ID графа. Возвращает обновленное состояние и ID графа.
""" """
# ---------------------------------------------------- Initial State Setup ----------------------------------------------------------------
initial_state = AgentState(input=user_input, parent_node_id=parent_node_id) initial_state = AgentState(input=user_input, parent_node_id=parent_node_id)
# Если есть существующий ID графа, загружаем его историю # Если есть существующий ID графа, загружаем его историю и структуру
loaded_graph_data = None
if existing_graph_id: if existing_graph_id:
existing_graph_data = graph_history_manager.get_graph( # Получаем данные графа, включая сгенерированные сообщения до parent_node_id (либо current_node_id из БД)
existing_graph_id) # Если parent_node_id передан, используем его для построения истории.
if existing_graph_data: loaded_graph_data = graph_history_manager.get_graph(
print(f"Продолжаю существующий граф {existing_graph_id}") existing_graph_id, target_node_id=parent_node_id)
initial_state.chat_history = existing_graph_data.get(
"messages", [])
initial_state.graph_nodes = existing_graph_data.get(
"graph_nodes", [])
initial_state.graph_edges = existing_graph_data.get(
"graph_edges", [])
initial_state.parent_node_id = parent_node_id or existing_graph_data.get(
"current_node_id")
if loaded_graph_data:
print(f"Продолжаю существующий граф {existing_graph_id}")
# Загружаем узлы и ребра для продолжения графа
initial_state.graph_nodes = loaded_graph_data.get("graph_nodes", [])
initial_state.graph_edges = loaded_graph_data.get("graph_edges", [])
# Устанавливаем parent_node_id для нового входного узла
# Если parent_node_id был передан в запросе, используем его,
# иначе берем current_node_id из загруженного графа.
initial_state.parent_node_id = parent_node_id or loaded_graph_data.get("current_node_id")
# История чата для текущего раунда работы LLM формируется из загруженных сообщений.
initial_state.temporary_chat_history = loaded_graph_data.get("messages", [])
# ---------------------------------------------------- run_agent ----------------------------------------------------------------
# Запускаем граф # Запускаем граф
result = app.invoke(initial_state) result = app.invoke(initial_state)
final_state = AgentState(**result) final_state = AgentState(**result)
# Сохраняем обновленное состояние графа # ---------------------------------------------------- Save Graph ----------------------------------------------------------------
# Сохраняем обновленное состояние графа.
# Обратите внимание: chat_history больше не сохраняется напрямую,
# она является частью данных узлов в graph_nodes.
graph_data_to_save = { graph_data_to_save = {
"id": existing_graph_id, "id": existing_graph_id, # Используем существующий ID или он будет сгенерирован
"messages": final_state.chat_history, # менеджером истории при первом сохранении
"graph_nodes": final_state.graph_nodes, "graph_nodes": final_state.graph_nodes,
"graph_edges": final_state.graph_edges, "graph_edges": final_state.graph_edges,
"current_node_id": final_state.current_node_id "current_node_id": final_state.current_node_id
} }
new_graph_id = graph_history_manager.save_graph(graph_data_to_save) new_graph_id = graph_history_manager.save_graph(graph_data_to_save)
# Формируем ответ для фронтенда # ---------------------------------------------------- prepare response ----------------------------------------------------------------
# Для `messages` в `response_data` используем `final_state.temporary_chat_history`,
# так как она отражает только сообщения текущей ветки, добавленные в ходе этого выполнения.
response_data = { response_data = {
"graph_id": new_graph_id, "graph_id": new_graph_id,
"messages": final_state.chat_history, "messages": final_state.temporary_chat_history,
"llm_response": final_state.llm_response, "llm_response": final_state.llm_response,
"image_urls": final_state.image_urls, "image_urls": final_state.image_urls,
"analysis_result": final_state.analysis_result, "analysis_result": final_state.analysis_result,

Binary file not shown.