llm-agent-backend/app/workflows.py

164 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Определяет логику рабочих процессов агента,
используя LangGraph для управления состояниями
и переходами между узлами обработки.
"""
from typing import Dict, Any, Optional
from langgraph.graph import StateGraph, END
from graph_history_manager import GraphHistoryManager
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
# --- LangGraph: Построение графа ---
graph_history_manager = GraphHistoryManager()
workflow = StateGraph(AgentState)
# Добавляем узлы
workflow.add_node("parse_command", parse_command_node)
workflow.add_node("execute_command",
execute_command_node) # Это по сути роутер
workflow.add_node("call_llm", call_llm_node)
workflow.add_node("generate_images", generate_images_node)
workflow.add_node("analyze_image", analyze_image_node)
workflow.add_node("get_meet_subtitles", get_meet_subtitles_node)
workflow.add_node("get_teams_subtitles", get_teams_subtitles_node)
workflow.add_node("summarize_history", summarize_history_node)
workflow.add_node("handle_error", handle_error_node)
workflow.add_node("help", help_node)
# Устанавливаем точку входа
workflow.set_entry_point("parse_command")
# Определяем маршрутизацию после parse_command
def route_command(state: AgentState) -> str:
"""Маршрутизирует выполнение на основе распознанной команды."""
if state.error:
return "handle_error" # Если была ошибка парсинга команды
elif state.command == "imagine":
return "generate_images"
elif state.command == "analyze":
return "analyze_image"
elif state.command == "subtitles_meet":
return "get_meet_subtitles"
elif state.command == "subtitles_teams":
return "get_teams_subtitles"
elif state.command == "summarize":
return "summarize_history"
elif state.command == "help":
return "help"
elif state.command == "chat":
return "call_llm"
elif state.command == "error": # Команда была, но не известная
return "handle_error"
else:
# Fallback на LLM, если команда не распознана или пуста (хотя parse_command должен был бы ее поймать)
return "call_llm"
workflow.add_conditional_edges(
"parse_command", # Откуда
route_command, # Функция, определяющая куда идти
{
"generate_images": "generate_images",
"analyze_image": "analyze_image",
"get_meet_subtitles": "get_meet_subtitles",
"get_teams_subtitles": "get_teams_subtitles",
"summarize_history": "summarize_history",
"help": "help",
"call_llm": "call_llm",
"handle_error":
"handle_error", # Для ошибок, найденных в parse_command
},
)
# Все узлы операций (кроме ошибок) ведут к END после завершения
workflow.add_edge("call_llm", END)
workflow.add_edge("generate_images", END)
workflow.add_edge("analyze_image", END)
workflow.add_edge("get_meet_subtitles", END)
workflow.add_edge("get_teams_subtitles", END)
workflow.add_edge("summarize_history", END)
workflow.add_edge("help", END)
workflow.add_edge("handle_error",
END) # Ошибка - это тоже конечный пункт для текущего запроса
# Компилируем граф
app = workflow.compile()
def run_agent(user_input: str,
existing_graph_id: Optional[str] = None,
parent_node_id: Optional[str] = None) -> Dict[str, Any]:
"""
Запускает агента с заданным пользовательским вводом.
Может продолжить существующий граф по graph_id.
Возвращает обновленное состояние и ID графа.
"""
# ---------------------------------------------------- Initial State Setup ----------------------------------------------------------------
initial_state = AgentState(input=user_input, parent_node_id=parent_node_id)
# Если есть существующий ID графа, загружаем его историю и структуру
loaded_graph_data = None
if existing_graph_id:
# Получаем данные графа, включая сгенерированные сообщения до parent_node_id (либо current_node_id из БД)
# Если parent_node_id передан, используем его для построения истории.
loaded_graph_data = graph_history_manager.get_graph(
existing_graph_id, target_node_id=parent_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)
final_state = AgentState(**result)
# ---------------------------------------------------- Save Graph ----------------------------------------------------------------
# Сохраняем обновленное состояние графа.
# Обратите внимание: chat_history больше не сохраняется напрямую,
# она является частью данных узлов в graph_nodes.
graph_data_to_save = {
"id": existing_graph_id, # Используем существующий ID или он будет сгенерирован
# менеджером истории при первом сохранении
"graph_nodes": final_state.graph_nodes,
"graph_edges": final_state.graph_edges,
"current_node_id": final_state.current_node_id
}
new_graph_id = graph_history_manager.save_graph(graph_data_to_save)
# ---------------------------------------------------- prepare response ----------------------------------------------------------------
# Для `messages` в `response_data` используем `final_state.temporary_chat_history`,
# так как она отражает только сообщения текущей ветки, добавленные в ходе этого выполнения.
response_data = {
"graph_id": new_graph_id,
"messages": final_state.temporary_chat_history,
"llm_response": final_state.llm_response,
"image_urls": final_state.image_urls,
"analysis_result": final_state.analysis_result,
"subtitles": final_state.subtitles,
"summarized_history_text": final_state.summarized_history_text,
"error": final_state.error,
"graph_visualization_data": {
"nodes": final_state.graph_nodes,
"edges": final_state.graph_edges,
"current_node_id": final_state.current_node_id
}
}
return response_data