Add agency
This commit is contained in:
parent
91daa5b2bd
commit
d071d16bd2
27
app/api.py
27
app/api.py
|
|
@ -223,7 +223,7 @@ def send_user_message():
|
|||
|
||||
@api.route('/api/chat/stream', methods=['POST'])
|
||||
def chat_stream():
|
||||
"""API endpoint для стриминга ответа LLM."""
|
||||
"""API endpoint для стриминга ответа LLM и работы ReAct-агента."""
|
||||
data = request.get_json()
|
||||
graph_id = data.get("graph_id")
|
||||
user_node_id = data.get("user_node_id")
|
||||
|
|
@ -232,12 +232,16 @@ def chat_stream():
|
|||
cache_folder = data.get("cache_folder")
|
||||
temperature = data.get("temperature", DEFAULT_TEMPERATURE)
|
||||
max_tokens = data.get("max_tokens")
|
||||
agency_mode = data.get("agency_mode", False) # НОВОЕ: Перехватываем режим тумблера "Агент"
|
||||
# Необязательный параметр для существующего узла ассистента при регенерации
|
||||
existing_assistant_node_id = data.get("existing_assistant_node_id")
|
||||
|
||||
if not graph_id or not user_node_id:
|
||||
return jsonify({"error": "Не указаны graph_id или user_node_id."}, 400)
|
||||
|
||||
# Подтягиваем глобальные настройки Obsidian (синхронизируются через /settings/sync)
|
||||
global current_obsidian_settings
|
||||
|
||||
def generate():
|
||||
assistant_node_id = None
|
||||
try:
|
||||
|
|
@ -253,23 +257,28 @@ def chat_stream():
|
|||
# Отправляем ID нового узла клиенту
|
||||
yield f"data: {json.dumps({'type': 'node_created', 'node_id': assistant_node_id, 'reused': False})}\n\n"
|
||||
|
||||
# Запускаем стриминг ответа от LLM
|
||||
# Запускаем стриминг ответа от LLM (используем обновленный агент)
|
||||
accumulated_content = ""
|
||||
|
||||
try:
|
||||
for chunk_data in run_agent_streaming(graph_id, user_node_id,
|
||||
assistant_node_id,
|
||||
system_prompt, model,
|
||||
cache_folder, temperature, max_tokens):
|
||||
cache_folder, temperature, max_tokens,
|
||||
agency_mode, current_obsidian_settings):
|
||||
|
||||
# Прокидываем любые успешные этапы: куски текста или статусы работы тулов
|
||||
if chunk_data.get("type") in ["chunk", "tool_start", "tool_end"]:
|
||||
if chunk_data.get("type") == "chunk":
|
||||
accumulated_content += chunk_data.get("content", "")
|
||||
yield f"data: {json.dumps(chunk_data)}\n\n"
|
||||
|
||||
# Обработка ошибки во время стриминга (крашим узел на фронте)
|
||||
elif chunk_data.get("type") == "error":
|
||||
if assistant_node_id:
|
||||
graph_history_manager.mark_node_as_error(
|
||||
graph_id, assistant_node_id,
|
||||
chunk_data.get("error",
|
||||
"Неизвестная ошибка при стриминге"))
|
||||
chunk_data.get("error", "Неизвестная ошибка при стриминге"))
|
||||
if graph_history_manager.socketio:
|
||||
graph_history_manager.socketio.emit(
|
||||
'node_type_changed', {
|
||||
|
|
@ -279,22 +288,24 @@ def chat_stream():
|
|||
})
|
||||
yield f"data: {json.dumps(chunk_data)}\n\n"
|
||||
return
|
||||
|
||||
except GeneratorExit:
|
||||
# НОВОЕ: Обработка прерывания генератора (клиент отключился)
|
||||
# Обработка прерывания генератора (если клиент нажал Stop на фронте или разорвал коннект)
|
||||
print(f"⚠️ Клиент отменил стрим для узла {assistant_node_id}")
|
||||
|
||||
# После завершения стриминга обновляем узел полным контентом
|
||||
# После завершения стриминга (успешного) обновляем узел полным контентом в базе SQLite
|
||||
if assistant_node_id:
|
||||
graph_history_manager.update_assistant_node_content(
|
||||
graph_id, assistant_node_id, accumulated_content)
|
||||
|
||||
# Инициируем генерацию заголовка
|
||||
# Инициируем фоновую генерацию заголовка
|
||||
graph_history_manager.title_generator.add_node_to_queue_direct(
|
||||
graph_id, assistant_node_id)
|
||||
|
||||
yield f"data: {json.dumps({'type': 'done', 'node_id': assistant_node_id})}\n\n"
|
||||
|
||||
except Exception as e:
|
||||
# Отлов непредсказуемых внешних ошибок в самом генераторе API
|
||||
print(f"Ошибка при стриминге ответа: {e}")
|
||||
error_message = str(e)
|
||||
if assistant_node_id:
|
||||
|
|
|
|||
|
|
@ -348,134 +348,6 @@ class CustomLLM:
|
|||
raise
|
||||
return ""
|
||||
|
||||
def stream2(self, messages: List[Any], temperature: float = DEFAULT_TEMPERATURE, max_tokens: Optional[int] = None):
|
||||
"""
|
||||
Генератор для стриминга ответа от LLM.
|
||||
Возвращает чанки контента по мере их получения.
|
||||
"""
|
||||
if self.provider == "openai":
|
||||
openai_messages = self._prepare_openai_messages(messages)
|
||||
|
||||
try:
|
||||
kwargs = {"temperature": temperature}
|
||||
if max_tokens:
|
||||
kwargs["max_tokens"] = int(max_tokens)
|
||||
|
||||
response = self.client.chat.completions.create(
|
||||
model=self.model_name,
|
||||
messages=openai_messages,
|
||||
stream=True,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
for chunk in response:
|
||||
chunk_message = chunk.choices[0].delta.content
|
||||
if chunk_message is not None:
|
||||
yield chunk_message
|
||||
|
||||
except GeneratorExit:
|
||||
# НОВОЕ: Обработка прерывания генератора
|
||||
print(f"⚠️ Стрим OpenAI прерван для модели {self.model_name}")
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"Ошибка при стриминге OpenAI API: {e}")
|
||||
raise
|
||||
|
||||
elif self.provider == "mistralai":
|
||||
mistralai_messages = self._prepare_mistralai_messages(messages)
|
||||
|
||||
try:
|
||||
kwargs = {"temperature": temperature}
|
||||
if max_tokens:
|
||||
kwargs["max_tokens"] = int(max_tokens)
|
||||
|
||||
response = self.client.chat.stream(model=self.model_name,
|
||||
messages=mistralai_messages,
|
||||
**kwargs)
|
||||
|
||||
for chunk in response:
|
||||
delta = chunk.data.choices[0].delta
|
||||
chunk_message = getattr(delta, "content", None)
|
||||
|
||||
if chunk_message is not None:
|
||||
yield chunk_message
|
||||
|
||||
if getattr(delta, "finish_reason", None) is not None:
|
||||
break
|
||||
|
||||
except GeneratorExit:
|
||||
# НОВОЕ: Обработка прерывания генератора
|
||||
print(f"⚠️ Стрим Mistral прерван для модели {self.model_name}")
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"Ошибка при стриминге Mistral API: {e}")
|
||||
raise
|
||||
|
||||
def _prepare_openai_messages(self,
|
||||
messages: List[Any]) -> List[Dict[str, str]]:
|
||||
"""Подготавливает сообщения в формате OpenAI."""
|
||||
openai_messages = []
|
||||
if isinstance(messages, str):
|
||||
openai_messages.append({"role": "user", "content": messages})
|
||||
elif isinstance(messages, list):
|
||||
for msg in messages:
|
||||
if isinstance(msg, HumanMessage):
|
||||
openai_messages.append({
|
||||
"role": "user",
|
||||
"content": msg.content
|
||||
})
|
||||
elif isinstance(msg, SystemMessage):
|
||||
openai_messages.append({
|
||||
"role": "system",
|
||||
"content": msg.content
|
||||
})
|
||||
elif isinstance(msg, AIMessage):
|
||||
openai_messages.append({
|
||||
"role": "assistant",
|
||||
"content": msg.content
|
||||
})
|
||||
else:
|
||||
openai_messages.append({
|
||||
"role":
|
||||
msg.type if hasattr(msg, 'type') else 'user',
|
||||
"content":
|
||||
msg.content
|
||||
})
|
||||
return openai_messages
|
||||
|
||||
def _prepare_mistralai_messages(
|
||||
self, messages: List[Any]) -> List[Dict[str, str]]:
|
||||
"""Подготавливает сообщения в формате Mistral AI."""
|
||||
mistralai_messages = []
|
||||
if isinstance(messages, str):
|
||||
mistralai_messages.append({"role": "user", "content": messages})
|
||||
elif isinstance(messages, list):
|
||||
for msg in messages:
|
||||
if isinstance(msg, HumanMessage):
|
||||
mistralai_messages.append({
|
||||
"role": "user",
|
||||
"content": msg.content
|
||||
})
|
||||
elif isinstance(msg, SystemMessage):
|
||||
mistralai_messages.append({
|
||||
"role": "system",
|
||||
"content": msg.content
|
||||
})
|
||||
elif isinstance(msg, AIMessage):
|
||||
mistralai_messages.append({
|
||||
"role": "assistant",
|
||||
"content": msg.content
|
||||
})
|
||||
else:
|
||||
mistralai_messages.append({
|
||||
"role":
|
||||
msg.type if hasattr(msg, 'type') else 'user',
|
||||
"content":
|
||||
msg.content
|
||||
})
|
||||
return mistralai_messages
|
||||
|
||||
|
||||
def get_llm(name: str) -> CustomLLM:
|
||||
"""
|
||||
Возвращает экземпляр CustomLLM для заданной модели.
|
||||
|
|
@ -484,8 +356,3 @@ def get_llm(name: str) -> CustomLLM:
|
|||
if not config:
|
||||
raise ValueError(f"Модель '{name}' не сконфигурирована.")
|
||||
return CustomLLM(config)
|
||||
|
||||
|
||||
def get_available_models():
|
||||
"""Возвращает список доступных моделей."""
|
||||
return list(MODELS.keys())
|
||||
|
|
|
|||
558
app/nodes.py
558
app/nodes.py
|
|
@ -1,558 +0,0 @@
|
|||
"""
|
||||
Этот файл содержит узлы для графа состояний агента.
|
||||
Каждый узел выполняет определенную функцию, такую как
|
||||
разбор команд, вызов LLM или выполнение сервисов.
|
||||
"""
|
||||
|
||||
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
|
||||
|
||||
from llm_client import get_llm
|
||||
from services import ImageGenerationService, SubtitleService, ImageAnalysisService, SummarizationService
|
||||
from models import AgentState
|
||||
|
||||
from typing import Dict, Any, Optional
|
||||
import uuid # Добавлено для генерации UUID
|
||||
|
||||
|
||||
class CommandManager:
|
||||
"""
|
||||
Менеджер для хранения и получения слеш-команд.
|
||||
В реальном приложении команды будут храниться в БД.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._commands = {
|
||||
"help": {
|
||||
"description": "Показывает список доступных команд.",
|
||||
"workflow": "help_workflow"
|
||||
},
|
||||
"imagine": {
|
||||
"description":
|
||||
"Генерирует изображения по промпту. Использование: /imagine [prompt]",
|
||||
"workflow": "image_generation_workflow"
|
||||
},
|
||||
"analyze": {
|
||||
"description":
|
||||
"Анализирует изображение по URL и промпту. Использование: /analyze [url] [prompt]",
|
||||
"workflow": "image_analysis_workflow"
|
||||
},
|
||||
"subtitles_meet": {
|
||||
"description": "Получает текущие субтитры из Google Meet.",
|
||||
"workflow": "get_meet_subtitles_workflow"
|
||||
},
|
||||
"subtitles_teams": {
|
||||
"description": "Получает текущие субтитры из MS Teams.",
|
||||
"workflow": "get_teams_subtitles_workflow"
|
||||
},
|
||||
"summarize": {
|
||||
"description": "Суммирует текущую историю диалога.",
|
||||
"workflow": "summarize_history_workflow"
|
||||
},
|
||||
"chat": {
|
||||
"description": "Ведет обычный диалог с LLM.",
|
||||
"workflow": "chat_workflow"
|
||||
},
|
||||
}
|
||||
|
||||
def get_commands(self) -> Dict[str, Any]:
|
||||
"""Возвращает все доступные команды."""
|
||||
return self._commands
|
||||
|
||||
def get_command_workflow(self, command_name: str) -> Optional[str]:
|
||||
"""Возвращает имя workflow для заданной команды."""
|
||||
return self._commands.get(command_name, {}).get("workflow")
|
||||
|
||||
|
||||
# Инициализация сервисов (пример, убедитесь, что они доступны в этом файле или импортированы)
|
||||
command_manager = CommandManager()
|
||||
|
||||
|
||||
# Здесь мы меняем состояние графа, добавляя сообщение от пользователя
|
||||
def parse_command_node(state: AgentState) -> AgentState:
|
||||
"""
|
||||
Парсит вход пользователя на предмет слеш-команды и инициализирует узел пользователя в графе.
|
||||
"""
|
||||
print("Выполняется узел: parse_command_node")
|
||||
user_input = state.input.strip()
|
||||
|
||||
# Создаем ID для узла пользователя с помощью UUID
|
||||
user_node_id = str(uuid.uuid4())
|
||||
|
||||
# Добавляем узел пользователя в граф для фронтенда с полным текстом сообщения
|
||||
state.graph_nodes.append({
|
||||
"id": user_node_id,
|
||||
"type": "user",
|
||||
"data": {
|
||||
"label": 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:
|
||||
edge_id = str(uuid.uuid4())
|
||||
state.graph_edges.append({
|
||||
"id": edge_id,
|
||||
"source": state.parent_node_id,
|
||||
"target": user_node_id
|
||||
})
|
||||
state.parent_node_id = user_node_id
|
||||
state.current_node_id = user_node_id
|
||||
|
||||
# Обновляем временную историю чата, которая используется для передачи между узлами
|
||||
# и для вызова LLM в текущем раунде.
|
||||
state.temporary_chat_history.append({
|
||||
"role": "user",
|
||||
"content": user_input,
|
||||
"node_id": user_node_id
|
||||
})
|
||||
|
||||
if user_input.startswith('/'):
|
||||
parts = user_input[1:].split(' ', 1)
|
||||
command_name = parts[0].lower()
|
||||
command_args = parts[1].split(' ') if len(parts) > 1 else []
|
||||
if command_name in command_manager.get_commands():
|
||||
state.command = command_name
|
||||
state.command_args = command_args
|
||||
print(
|
||||
f"Команда распознана: /{state.command} с аргументами: {state.command_args}"
|
||||
)
|
||||
else:
|
||||
state.error = f"Неизвестная команда: /{command_name}. Введите /help для списка команд."
|
||||
print(state.error)
|
||||
state.command = "error" # Специальный флаг для обработки ошибок команд
|
||||
else:
|
||||
state.command = "chat" # Обычный чат
|
||||
return state
|
||||
|
||||
|
||||
def execute_command_node(state: AgentState) -> AgentState:
|
||||
"""
|
||||
Выполняет соответствующий workflow для команды.
|
||||
Этот узел служит маршрутизатором для выбора следующего узла в зависимости от команды.
|
||||
"""
|
||||
print(
|
||||
f"Выполняется узел: execute_command_node для команды: {state.command}")
|
||||
# Этот узел будет выступать в роли маршрутизатора
|
||||
# Фактическая логика команды будет в отдельных узлах, вызываемых по условию
|
||||
return state # Просто передаем состояние дальше, чтобы маршрутизатор смог выбрать следующий узел
|
||||
|
||||
|
||||
# Предполагаем, что используем Gemini для основного LLM и суммаризации/анализа
|
||||
# В реальной системе можно выбрать динамически
|
||||
DEFAULT_LLM_NAME = "gemini-2.0-flash" #"gemini-2.5-flash"
|
||||
main_llm = get_llm(DEFAULT_LLM_NAME)
|
||||
|
||||
|
||||
def call_llm_node(state: AgentState) -> AgentState:
|
||||
"""
|
||||
Вызывает LLM для обработки обычного диалога или генерации ответа.
|
||||
Использует `temporary_chat_history` для передачи контекста LLM.
|
||||
"""
|
||||
print("Выполняется узел: call_llm_node (для обычного чата)")
|
||||
try:
|
||||
messages_for_llm = []
|
||||
|
||||
if state.system_prompt:
|
||||
messages_for_llm.append(SystemMessage(content=state.system_prompt))
|
||||
|
||||
# Для обычного чата LLM использует текущую временную историю
|
||||
for msg in state.temporary_chat_history: # Используем temporary_chat_history
|
||||
if msg["role"] == "user":
|
||||
messages_for_llm.append(HumanMessage(content=msg["content"]))
|
||||
elif msg["role"] == "assistant":
|
||||
messages_for_llm.append(AIMessage(content=msg["content"]))
|
||||
|
||||
model_name = state.selected_model
|
||||
llm = get_llm(model_name)
|
||||
response = llm.invoke(messages_for_llm)
|
||||
|
||||
llm_node_id = str(uuid.uuid4())
|
||||
|
||||
# Добавляем узел LLM в граф для фронтенда с полным текстом ответа
|
||||
state.graph_nodes.append({
|
||||
"id": llm_node_id,
|
||||
"type": "llm",
|
||||
"data": {
|
||||
"label": response[:30] + "..." if len(response) > 30 else response,
|
||||
"message": {"role": "assistant", "content": response, "node_id": llm_node_id} # Сохраняем сообщение здесь
|
||||
}
|
||||
})
|
||||
edge_id = str(uuid.uuid4())
|
||||
state.graph_edges.append({
|
||||
"id": edge_id,
|
||||
"source": state.parent_node_id,
|
||||
"target": llm_node_id
|
||||
})
|
||||
state.parent_node_id = llm_node_id
|
||||
state.current_node_id = llm_node_id
|
||||
|
||||
# Обновляем временную историю чата с ответом LLM
|
||||
state.temporary_chat_history.append({
|
||||
"role": "assistant",
|
||||
"content": response,
|
||||
"node_id": llm_node_id
|
||||
})
|
||||
state.llm_response = response
|
||||
|
||||
except Exception as e:
|
||||
state.error = f"Ошибка при вызове LLM: {e}"
|
||||
state.llm_response = f"Произошла ошибка: {e}"
|
||||
print(state.error)
|
||||
return state
|
||||
|
||||
|
||||
image_gen_service = ImageGenerationService(
|
||||
base_url="https://render-service-gsu7.onrender.com/g2"
|
||||
) # TODO: Замените на реальный URL
|
||||
|
||||
|
||||
def generate_images_node(state: AgentState) -> AgentState:
|
||||
"""
|
||||
Генерирует изображения по промпту, переданному в команде `/imagine`.
|
||||
"""
|
||||
print("Выполняется узел: generate_images_node")
|
||||
prompt = " ".join(state.command_args)
|
||||
if not prompt:
|
||||
state.error = "Для команды /imagine требуется промпт. Пример: /imagine кошка на луне"
|
||||
state.llm_response = state.error
|
||||
# Добавляем ошибку во временную историю для согласованности
|
||||
state.temporary_chat_history.append({"role": "assistant", "content": state.error})
|
||||
return state
|
||||
|
||||
try:
|
||||
urls = image_gen_service.generate_images(prompt)
|
||||
state.image_urls = urls
|
||||
|
||||
# Формируем текст ответа с использованием Markdown для отображения изображений
|
||||
image_markdown = "\n".join([
|
||||
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)
|
||||
])
|
||||
response_text = f"Сгенерировано {len(urls)} изображений:\n{image_markdown}"
|
||||
state.llm_response = response_text
|
||||
|
||||
img_gen_node_id = str(uuid.uuid4())
|
||||
# Добавляем узел генерации изображений в граф
|
||||
state.graph_nodes.append({
|
||||
"id": img_gen_node_id,
|
||||
"type": "tool",
|
||||
"data": {
|
||||
"label": "Генерация изображений",
|
||||
"details": prompt,
|
||||
"message": {"role": "assistant", "content": response_text, "node_id": img_gen_node_id}
|
||||
}
|
||||
})
|
||||
edge_id = str(uuid.uuid4())
|
||||
state.graph_edges.append({
|
||||
"id": edge_id,
|
||||
"source": state.parent_node_id,
|
||||
"target": img_gen_node_id
|
||||
})
|
||||
state.parent_node_id = img_gen_node_id
|
||||
state.current_node_id = img_gen_node_id
|
||||
|
||||
# Обновляем временную историю чата
|
||||
state.temporary_chat_history.append({
|
||||
"role": "assistant",
|
||||
"content": response_text,
|
||||
"node_id": img_gen_node_id # Привязываем к ID узла
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
state.error = f"Ошибка генерации изображений: {e}"
|
||||
state.llm_response = f"Произошла ошибка при генерации изображений: {e}"
|
||||
print(state.error)
|
||||
# Добавляем ошибку во временную историю
|
||||
state.temporary_chat_history.append({"role": "assistant", "content": state.llm_response})
|
||||
return state
|
||||
|
||||
|
||||
image_analysis_service = ImageAnalysisService(
|
||||
llm=get_llm("gemini-2.5-flash")) # Gemini для анализа изображений
|
||||
|
||||
|
||||
def analyze_image_node(state: AgentState) -> AgentState:
|
||||
"""
|
||||
Анализирует изображение по URL и промпту, переданным в команде `/analyze`.
|
||||
"""
|
||||
print("Выполняется узел: analyze_image_node")
|
||||
if len(state.command_args) < 2:
|
||||
state.error = "Для команды /analyze требуется URL изображения и промпт. Пример: /analyze [url] 'что на изображении?'"
|
||||
state.llm_response = state.error
|
||||
# Добавляем ошибку во временную историю для согласованности
|
||||
state.temporary_chat_history.append({"role": "assistant", "content": state.error})
|
||||
return state
|
||||
|
||||
image_url = state.command_args[0]
|
||||
prompt = " ".join(state.command_args[1:])
|
||||
|
||||
try:
|
||||
analysis_result = image_analysis_service.analyze_image(
|
||||
image_url, prompt)
|
||||
state.analysis_result = analysis_result
|
||||
response_text = f"Результат анализа изображения '{image_url}': {analysis_result}"
|
||||
state.llm_response = response_text
|
||||
|
||||
img_analysis_node_id = str(uuid.uuid4())
|
||||
# Добавляем узел анализа изображения в граф
|
||||
state.graph_nodes.append({
|
||||
"id": img_analysis_node_id,
|
||||
"type": "tool",
|
||||
"data": {
|
||||
"label": "Анализ изображения",
|
||||
"details": f"URL: {image_url}, Промпт: {prompt}",
|
||||
"message": {"role": "assistant", "content": response_text, "node_id": img_analysis_node_id}
|
||||
}
|
||||
})
|
||||
edge_id = str(uuid.uuid4())
|
||||
state.graph_edges.append({
|
||||
"id": edge_id,
|
||||
"source": state.parent_node_id,
|
||||
"target": img_analysis_node_id
|
||||
})
|
||||
state.parent_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:
|
||||
state.error = f"Ошибка анализа изображения: {e}"
|
||||
state.llm_response = f"Произошла ошибка при анализе изображения: {e}"
|
||||
print(state.error)
|
||||
# Добавляем ошибку во временную историю
|
||||
state.temporary_chat_history.append({"role": "assistant", "content": state.llm_response})
|
||||
return state
|
||||
|
||||
|
||||
subtitle_service = SubtitleService()
|
||||
|
||||
|
||||
def get_meet_subtitles_node(state: AgentState) -> AgentState:
|
||||
"""
|
||||
Получает субтитры из Google Meet через команду `/subtitles_meet`.
|
||||
"""
|
||||
print("Выполняется узел: get_meet_subtitles_node")
|
||||
try:
|
||||
subtitles = subtitle_service.get_google_meet_subtitles()
|
||||
state.subtitles = subtitles
|
||||
response_text = f"Текущие субтитры из Google Meet: {subtitles}"
|
||||
state.llm_response = response_text
|
||||
|
||||
meet_subtitles_node_id = str(uuid.uuid4())
|
||||
# Добавляем узел субтитров Meet в граф
|
||||
state.graph_nodes.append({
|
||||
"id": meet_subtitles_node_id,
|
||||
"type": "tool",
|
||||
"data": {
|
||||
"label": "Субтитры Google Meet",
|
||||
"details": subtitles[:30] + "..." if subtitles else "",
|
||||
"message": {"role": "assistant", "content": response_text, "node_id": meet_subtitles_node_id}
|
||||
}
|
||||
})
|
||||
edge_id = str(uuid.uuid4())
|
||||
state.graph_edges.append({
|
||||
"id": edge_id,
|
||||
"source": state.parent_node_id,
|
||||
"target": meet_subtitles_node_id
|
||||
})
|
||||
state.parent_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:
|
||||
state.error = f"Ошибка получения субтитров Google Meet: {e}"
|
||||
state.llm_response = f"Произошла ошибка при получении субтитров Google Meet: {e}"
|
||||
print(state.error)
|
||||
# Добавляем ошибку во временную историю
|
||||
state.temporary_chat_history.append({"role": "assistant", "content": state.llm_response})
|
||||
return state
|
||||
|
||||
|
||||
def get_teams_subtitles_node(state: AgentState) -> AgentState:
|
||||
"""
|
||||
Получает субтитры из MS Teams через команду `/subtitles_teams`.
|
||||
"""
|
||||
print("Выполняется узел: get_teams_subtitles_node")
|
||||
try:
|
||||
subtitles = subtitle_service.get_ms_teams_subtitles()
|
||||
state.subtitles = subtitles
|
||||
response_text = f"Текущие субтитры из MS Teams: {subtitles}"
|
||||
state.llm_response = response_text
|
||||
|
||||
teams_subtitles_node_id = str(uuid.uuid4())
|
||||
# Добавляем узел субтитров Teams в граф
|
||||
state.graph_nodes.append({
|
||||
"id": teams_subtitles_node_id,
|
||||
"type": "tool",
|
||||
"data": {
|
||||
"label": "Субтитры MS Teams",
|
||||
"details": subtitles[:30] + "..." if subtitles else "",
|
||||
"message": {"role": "assistant", "content": response_text, "node_id": teams_subtitles_node_id}
|
||||
}
|
||||
})
|
||||
edge_id = str(uuid.uuid4())
|
||||
state.graph_edges.append({
|
||||
"id": edge_id,
|
||||
"source": state.parent_node_id,
|
||||
"target": teams_subtitles_node_id
|
||||
})
|
||||
state.parent_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:
|
||||
state.error = f"Ошибка получения субтитров MS Teams: {e}"
|
||||
state.llm_response = f"Произошла ошибка при получении субтитров MS Teams: {e}"
|
||||
print(state.error)
|
||||
# Добавляем ошибку во временную историю
|
||||
state.temporary_chat_history.append({"role": "assistant", "content": state.llm_response})
|
||||
return state
|
||||
|
||||
|
||||
summarization_service = SummarizationService(
|
||||
llm=get_llm("gemini-2.5-flash")) # Gemini для суммаризации
|
||||
|
||||
|
||||
def summarize_history_node(state: AgentState) -> AgentState:
|
||||
"""
|
||||
Суммирует историю диалога, используя `temporary_chat_history` в качестве контекста.
|
||||
"""
|
||||
print("Выполняется узел: summarize_history_node")
|
||||
try:
|
||||
# Для суммаризации используем временную историю, подготовленную в предыдущих узлах
|
||||
summarized_text = summarization_service.summarize_history_of_dicts(
|
||||
state.temporary_chat_history) # Изменен метод для использования List[Dict]
|
||||
state.summarized_history_text = summarized_text
|
||||
response_text = f"История диалога суммирована:\n{summarized_text}"
|
||||
state.llm_response = response_text
|
||||
|
||||
summary_node_id = str(uuid.uuid4())
|
||||
# Добавляем узел суммаризации в граф
|
||||
state.graph_nodes.append({
|
||||
"id": summary_node_id,
|
||||
"type": "tool",
|
||||
"data": {
|
||||
"label": "Суммаризация истории",
|
||||
"details": summarized_text[:30] + "..." if summarized_text else "",
|
||||
"message": {"role": "assistant", "content": response_text, "node_id": summary_node_id}
|
||||
}
|
||||
})
|
||||
edge_id = str(uuid.uuid4())
|
||||
state.graph_edges.append({
|
||||
"id": edge_id,
|
||||
"source": state.parent_node_id,
|
||||
"target": summary_node_id
|
||||
})
|
||||
state.parent_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:
|
||||
state.error = f"Ошибка суммаризации истории: {e}"
|
||||
state.llm_response = f"Произошла ошибка при суммаризации истории: {e}"
|
||||
print(state.error)
|
||||
# Добавляем ошибку во временную историю
|
||||
state.temporary_chat_history.append({"role": "assistant", "content": state.llm_response})
|
||||
return state
|
||||
|
||||
|
||||
def handle_error_node(state: AgentState) -> AgentState:
|
||||
"""
|
||||
Обрабатывает ошибки команд, формируя ответ для пользователя и добавляя его в граф.
|
||||
"""
|
||||
print(f"Выполняется узел: handle_error_node. Ошибка: {state.error}")
|
||||
state.llm_response = state.error
|
||||
|
||||
error_node_id = str(uuid.uuid4())
|
||||
# Добавляем узел ошибки в граф
|
||||
state.graph_nodes.append({
|
||||
"id": error_node_id,
|
||||
"type": "error",
|
||||
"data": {
|
||||
"label": "Ошибка",
|
||||
"details": state.error,
|
||||
"message": {"role": "assistant", "content": state.llm_response, "node_id": error_node_id}
|
||||
}
|
||||
})
|
||||
edge_id = str(uuid.uuid4())
|
||||
state.graph_edges.append({
|
||||
"id": edge_id,
|
||||
"source": state.parent_node_id,
|
||||
"target": error_node_id
|
||||
})
|
||||
state.parent_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
|
||||
|
||||
|
||||
def help_node(state: AgentState) -> AgentState:
|
||||
"""
|
||||
Предоставляет информацию о доступных командах, формируя ответ для пользователя и добавляя его в граф.
|
||||
"""
|
||||
print("Выполняется узел: help_node")
|
||||
commands_info = command_manager.get_commands()
|
||||
help_text = "Доступные команды:\n"
|
||||
for cmd, info in commands_info.items():
|
||||
help_text += f"/{cmd}: {info['description']}\n"
|
||||
state.llm_response = help_text
|
||||
|
||||
help_node_id = str(uuid.uuid4())
|
||||
# Добавляем узел справки в граф
|
||||
state.graph_nodes.append({
|
||||
"id": help_node_id,
|
||||
"type": "info",
|
||||
"data": {
|
||||
"label": "Справка",
|
||||
"details": "Список команд",
|
||||
"message": {"role": "assistant", "content": state.llm_response, "node_id": help_node_id}
|
||||
}
|
||||
})
|
||||
edge_id = str(uuid.uuid4())
|
||||
state.graph_edges.append({
|
||||
"id": edge_id,
|
||||
"source": state.parent_node_id,
|
||||
"target": help_node_id
|
||||
})
|
||||
state.parent_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
|
||||
172
app/react_agent.py
Normal file
172
app/react_agent.py
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
import functools
|
||||
import importlib.util
|
||||
import inspect
|
||||
import subprocess
|
||||
import json
|
||||
import os
|
||||
from langchain_core.tools import StructuredTool
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
from langchain_openai import ChatOpenAI
|
||||
from llm_client import MODELS
|
||||
|
||||
import importlib.util
|
||||
import inspect
|
||||
import subprocess
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from typing import Any
|
||||
from langchain_core.tools import StructuredTool
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
def build_react_agent(model_name: str, temperature: float, max_tokens: int, agency_mode: bool, obsidian_settings: dict):
|
||||
"""Инициализация ядра на базе LangGraph."""
|
||||
m_cfg = MODELS.get(model_name)
|
||||
llm = ChatOpenAI(
|
||||
api_key=m_cfg["apiKey"],
|
||||
base_url=m_cfg["apiBase"],
|
||||
model_name=m_cfg["model_name"],
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens if max_tokens else None,
|
||||
streaming=True
|
||||
)
|
||||
tools = get_dynamic_tools(obsidian_settings) if agency_mode else []
|
||||
return create_react_agent(llm, tools=tools)
|
||||
|
||||
# Специфическая Pydantic-модель для инструментов без скрипта
|
||||
class PurePromptSchema(BaseModel):
|
||||
context: str = Field(description="Контекст, данные или текст, к которому нужно применить эту инструкцию/промпт.")
|
||||
|
||||
|
||||
def create_subprocess_tool(script_path: str, method_name: str,
|
||||
hidden_prompt: str):
|
||||
"""Вид 2: Инструмент со скриптом + пост-процессинг."""
|
||||
spec = importlib.util.spec_from_file_location("user_dynamic_tool",
|
||||
script_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
|
||||
try:
|
||||
spec.loader.exec_module(module)
|
||||
func = getattr(module, method_name)
|
||||
docstring = inspect.getdoc(func) or f"Выполняет скрипт {method_name}"
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Ошибка чтения сигнатуры {script_path}: {e}")
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(**kwargs: Any) -> str:
|
||||
runner_code = f"""
|
||||
import json
|
||||
import sys
|
||||
import importlib.util
|
||||
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location('sub_module', r'{script_path}')
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
kwargs = json.loads(sys.stdin.read())
|
||||
result = getattr(module, '{method_name}')(**kwargs)
|
||||
print(result)
|
||||
except Exception as e:
|
||||
print(f"SCRIPT RUNTIME ERROR: {{e}}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
"""
|
||||
try:
|
||||
process = subprocess.run([sys.executable, "-c", runner_code],
|
||||
input=json.dumps(kwargs),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding='utf-8',
|
||||
check=True)
|
||||
raw_output = process.stdout.strip()
|
||||
|
||||
# Прилизывание для скрипта
|
||||
if hidden_prompt:
|
||||
return f"РЕЗУЛЬТАТ ВЫПОЛНЕНИЯ СКРИПТА:\n{raw_output}\n\nИНСТРУКЦИЯ ПО ФОРМАТИРОВАНИЮ ОТВЕТА ДЛЯ АГЕНТА:\n{hidden_prompt}"
|
||||
return raw_output
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
# Даже если скрипт упал, мы отдаем ошибку + промпт, чтобы агент мог обработать её по инструкции
|
||||
error_msg = f"ОШИБКА ИСПОЛНЕНИЯ (Код {e.returncode}):\nSTDOUT: {e.stdout}\nSTDERR: {e.stderr}"
|
||||
if hidden_prompt:
|
||||
return f"{error_msg}\n\nИНСТРУКЦИЯ НА СЛУЧАЙ ОШИБКИ:\n{hidden_prompt}"
|
||||
return error_msg
|
||||
|
||||
return StructuredTool.from_function(func=wrapper,
|
||||
name=func.__name__,
|
||||
description=docstring,
|
||||
infer_schema=True)
|
||||
|
||||
def get_dynamic_tools(obsidian_settings: dict):
|
||||
"""Собирает инструменты двух видов на основе настроек Obsidian."""
|
||||
tools = []
|
||||
custom_tools = obsidian_settings.get("customTools", [])
|
||||
|
||||
for ct in custom_tools:
|
||||
name = ct.get("name")
|
||||
desc = ct.get("description")
|
||||
full_path = ct.get("scriptPath", "")
|
||||
# Поддерживаем оба ключа, если в JS/TS они не раскрылись
|
||||
hidden_prompt = ct.get("hiddenPromptExpanded") or ct.get("hiddenPrompt", "")
|
||||
|
||||
if not name:
|
||||
continue # Инструмент без имени создать нельзя
|
||||
|
||||
# Парсим путь к скрипту (учитываем Windows-пути с буквой диска)
|
||||
script_path = ""
|
||||
method_name = "main"
|
||||
|
||||
if full_path:
|
||||
parts = full_path.rsplit(":", 1) # Разделяем строку с конца 1 раз
|
||||
|
||||
if len(parts) == 2:
|
||||
# Если слева от разделения ровно 1 символ-буква (например, 'D'),
|
||||
# значит это путь без метода, а двоеточие — от диска Windows.
|
||||
if len(parts[0]) == 1 and parts[0].isalpha():
|
||||
script_path = full_path
|
||||
else:
|
||||
script_path = parts[0]
|
||||
method_name = parts[1]
|
||||
else:
|
||||
script_path = full_path
|
||||
|
||||
# ПРОВЕРКА: Есть ли физический скрипт?
|
||||
is_valid_script = script_path and os.path.exists(script_path)
|
||||
|
||||
if not is_valid_script:
|
||||
# ----------------------------------------------------
|
||||
# ВИД 1: Чистый промпт (Инструмент-инструкция / Системная подсказка)
|
||||
# ----------------------------------------------------
|
||||
def create_pure_prompt_wrapper(prompt_text: str):
|
||||
# Функция принимает context, который ИИ обязан туда положить
|
||||
def pure_prompt_wrapper(context: str) -> str:
|
||||
return (
|
||||
f"ВХОДНЫЕ ДАННЫЕ ОТ ПОЛЬЗОВАТЕЛЯ/МОДЕЛИ:\n{context}\n\n"
|
||||
f"ИНСТРУКЦИЯ ПО ОБРАБОТКЕ И ПРИЛИЗЫВАНИЮ (ОБЯЗАТЕЛЬНО К ИСПОЛНЕНИЮ):\n{prompt_text}"
|
||||
)
|
||||
return pure_prompt_wrapper
|
||||
|
||||
tool = StructuredTool.from_function(
|
||||
func=create_pure_prompt_wrapper(hidden_prompt),
|
||||
name=name,
|
||||
description=desc or "Инструмент для применения специфических правил форматирования или инструкций.",
|
||||
args_schema=PurePromptSchema # Жестко навязываем схему с аргументом context!
|
||||
)
|
||||
tools.append(tool)
|
||||
print(f"🛠 Инструмент-промпт '{tool.name}' (без скрипта) успешно инициализирован.")
|
||||
|
||||
else:
|
||||
# ----------------------------------------------------
|
||||
# ВИД 2: Скрипт + Прилизывание
|
||||
# ----------------------------------------------------
|
||||
try:
|
||||
tool = create_subprocess_tool(script_path, method_name, hidden_prompt)
|
||||
tool.name = name # Приоритет имени из UI Obsidian
|
||||
if desc:
|
||||
tool.description = desc
|
||||
tools.append(tool)
|
||||
print(f"🛠 Инструмент-скрипт '{tool.name}' успешно инициализирован.")
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка генерации инструмента-скрипта '{name}': {e}")
|
||||
|
||||
return tools
|
||||
179
app/workflows.py
179
app/workflows.py
|
|
@ -1,99 +1,17 @@
|
|||
"""
|
||||
Определяет логику рабочих процессов агента,
|
||||
используя LangGraph для управления состояниями
|
||||
и переходами между узлами обработки.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional, Set
|
||||
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
|
||||
import uuid # Добавлено для генерации UUID
|
||||
from llm_client import DEFAULT_TEMPERATURE, get_llm
|
||||
|
||||
import os
|
||||
import base64
|
||||
from pathlib import Path
|
||||
# --- LangGraph: Построение графа ---
|
||||
import json
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage, AIMessageChunk
|
||||
from react_agent import build_react_agent
|
||||
# Убедитесь, что graph_history_manager импортирован.
|
||||
# Если код в workflows.py, то graph_history_manager создается/импортируется там.
|
||||
|
||||
from graph_history_manager import GraphHistoryManager
|
||||
|
||||
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_streaming(graph_id: str,
|
||||
user_node_id: str,
|
||||
|
|
@ -101,9 +19,15 @@ def run_agent_streaming(graph_id: str,
|
|||
system_prompt: Optional[str] = None,
|
||||
model: Optional[str] = None,
|
||||
cache_folder: Optional[str] = None,
|
||||
temperature: float = DEFAULT_TEMPERATURE,
|
||||
max_tokens: Optional[int] = None):
|
||||
"""Генератор для стримингового ответа LLM с поддержкой вложений."""
|
||||
temperature: float = 0.7,
|
||||
max_tokens: Optional[int] = None,
|
||||
agency_mode: bool = False,
|
||||
obsidian_settings: dict = None):
|
||||
"""
|
||||
Основной генератор: Запускает LangGraph / ReAct агента.
|
||||
Поддерживает мультимодальность, чтение файлов из Vault и исполнение Python-тулов.
|
||||
Отдает словари, которые api.py запакует в SSE-события.
|
||||
"""
|
||||
try:
|
||||
loaded_graph_data = graph_history_manager.get_graph(
|
||||
graph_id, target_node_id=user_node_id)
|
||||
|
|
@ -114,8 +38,7 @@ def run_agent_streaming(graph_id: str,
|
|||
|
||||
messages = loaded_graph_data.get("messages", [])
|
||||
|
||||
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
|
||||
|
||||
# 2. Формируем контекст LangChain с учетом файлов и изображений (вложений)
|
||||
messages_for_llm = []
|
||||
if system_prompt:
|
||||
messages_for_llm.append(SystemMessage(content=system_prompt))
|
||||
|
|
@ -123,9 +46,10 @@ def run_agent_streaming(graph_id: str,
|
|||
# Обрабатываем вложения
|
||||
for i, msg in enumerate(messages):
|
||||
is_last = (i == len(messages) - 1)
|
||||
role = msg.get("role")
|
||||
|
||||
if msg["role"] == "user":
|
||||
text_content = msg["content"]
|
||||
if role == "user":
|
||||
text_content = msg.get("content", "")
|
||||
attachments = msg.get("attachments", [])
|
||||
|
||||
# Проверяем, есть ли мультимодальный контент
|
||||
|
|
@ -135,7 +59,7 @@ def run_agent_streaming(graph_id: str,
|
|||
# Обрабатываем вложения
|
||||
if attachments:
|
||||
for att in attachments:
|
||||
# Раскрываем только permanent или вложения последнего сообщения
|
||||
# Прикрепляем файл только если он перманентный (@@/##) или это последнее сообщение
|
||||
if att.get("permanent") or is_last:
|
||||
attachment_content = prepare_attachment_for_llm(
|
||||
att, cache_folder)
|
||||
|
|
@ -146,7 +70,7 @@ def run_agent_streaming(graph_id: str,
|
|||
|
||||
content_parts.append(attachment_content)
|
||||
|
||||
# Формируем финальный контент
|
||||
# Если есть картинки, шлем list of dicts. Иначе склеиваем в один текст.
|
||||
if has_multimodal:
|
||||
# Мультимодальное сообщение
|
||||
messages_for_llm.append(
|
||||
|
|
@ -160,15 +84,53 @@ def run_agent_streaming(graph_id: str,
|
|||
full_text += part.get("text", "")
|
||||
messages_for_llm.append(HumanMessage(content=full_text))
|
||||
|
||||
elif msg["role"] == "assistant":
|
||||
messages_for_llm.append(AIMessage(content=msg["content"]))
|
||||
elif role == "assistant":
|
||||
messages_for_llm.append(AIMessage(content=msg.get("content", "")))
|
||||
|
||||
# Стриминг ответа
|
||||
llm = get_llm(model or "gemini-2.5-flash")
|
||||
for chunk in llm.stream2(messages_for_llm, temperature=temperature, max_tokens=max_tokens):
|
||||
content = chunk.content if hasattr(chunk,
|
||||
'content') else str(chunk)
|
||||
yield {"type": "chunk", "content": content}
|
||||
# 3. Создаем агента. Если agency_mode == False, он соберет 0 тулов и отработает как обычная LLM.
|
||||
agent_executor = build_react_agent(
|
||||
model_name=model,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
agency_mode=agency_mode,
|
||||
obsidian_settings=obsidian_settings or {}
|
||||
)
|
||||
|
||||
# 4. Выполняем цикл с потоковой передачей событий
|
||||
step_counter = 0
|
||||
|
||||
# LangGraph Stream Mode 'messages': отдает чанки токенов и вызовы функций
|
||||
for chunk, metadata in agent_executor.stream({"messages": messages_for_llm}, stream_mode="messages"):
|
||||
|
||||
if isinstance(chunk, AIMessageChunk):
|
||||
# Агент запросил вызов инструмента (Формирование JSON аргументов)
|
||||
if hasattr(chunk, "tool_call_chunks") and chunk.tool_call_chunks:
|
||||
for tc in chunk.tool_call_chunks:
|
||||
if "name" in tc and tc["name"]:
|
||||
step_counter += 1
|
||||
yield {
|
||||
"type": "tool_start",
|
||||
"name": tc["name"],
|
||||
"step": step_counter,
|
||||
"node_id": assistant_node_id
|
||||
}
|
||||
|
||||
# Обычный стриминг текста финального ответа
|
||||
if chunk.content:
|
||||
yield {
|
||||
"type": "chunk",
|
||||
"content": chunk.content,
|
||||
"node_id": assistant_node_id
|
||||
}
|
||||
|
||||
# Тул отработал и принес ответ
|
||||
elif hasattr(chunk, "type") and chunk.type == "tool":
|
||||
yield {
|
||||
"type": "tool_end",
|
||||
"name": chunk.name,
|
||||
"step": step_counter,
|
||||
"node_id": assistant_node_id
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"Ошибка при стриминге: {e}")
|
||||
|
|
@ -270,3 +232,4 @@ def format_attachment_for_llm(attachment: Dict[str, Any]) -> str:
|
|||
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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user