Files
2026-06-04 16:43:40 +00:00

185 lines
8.1 KiB
Python

import os
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_tavily import TavilySearch
from state import BriefState
# Инициализация LLM и поискового инструмента
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.5)
tavily = TavilySearch(
api_key=os.getenv("TAVILY_API_KEY"),
max_results=3, # Количество результатов поиска
include_answer=True,
include_raw_content=False
)
# Промпты
OUTLINE_PROMPT = """Ты — исследовательский ассистент. Составь план исследования по теме: {topic}
Требования:
- 4-5 пунктов
- Каждый пункт должен быть конкретным аспектом для исследования
- Пункты должны логически раскрывать тему
- Формат вывода: просто список пунктов (каждый с новой строки, начинается с цифры)
Пример:
1. Основные понятия и определения
2. Ключевые технологии и инструменты
3. Практические примеры использования
4. Потенциальные риски и их mitigation
5. Лучшие практики и рекомендации
Тема: {topic}
План:"""
RESEARCH_PROMPT = """Ты — исследователь, собирающий информацию по конкретному пункту плана.
Тема исследования: {topic}
Текущий пункт плана: {outline_item}
На основе результатов поиска (ниже) составь краткую заметку (5-8 предложений), которая:
1. Отвечает на пункт плана
2. Содержит конкретные факты, данные, примеры
3. Использует информацию из результатов поиска
4. Пишется связным текстом (не маркированным списком)
Результаты поиска:
{search_results}
Заметка по пункту "{outline_item}":"""
SYNTHESIS_PROMPT = """Ты — аналитик, который создает итоговый исследовательский бриф.
Тема: {topic}
План исследования:
{outline}
Собранные заметки по пунктам:
{notes}
Задание: создай связный исследовательский бриф (объемом ½ - 1 страницу), который:
1. Имеет заголовок "Исследовательский бриф: {topic}"
2. Структурирован по пунктам плана (каждый пункт как подзаголовок)
3. Содержит введение (1 абзац) и заключение (1 абзац)
4. Объединяет заметки в единый поток, убирает повторы
5. Добавляет связующие предложения между пунктами
6. Завершается списком ключевых выводов (3-4 пункта)
Форматирование: используй markdown для заголовков и списков.
Бриф:"""
def outline_node(state: BriefState) -> BriefState:
"""Узел: генерация плана исследования"""
print(f"\n📋 Генерация плана исследования по теме: {state['topic']}")
prompt = OUTLINE_PROMPT.format(topic=state["topic"])
response = llm.invoke([HumanMessage(content=prompt)])
# Парсим план в список
outline_lines = response.content.strip().split('\n')
outline = []
for line in outline_lines:
# Убираем номера пунктов и лишние символы
cleaned = line.strip()
if cleaned and cleaned[0].isdigit():
# Убираем "1. " или "1)" и т.д.
import re
cleaned = re.sub(r'^\d+[\.\)]\s*', '', cleaned)
outline.append(cleaned)
print(f"✅ Создан план из {len(outline)} пунктов:")
for i, item in enumerate(outline, 1):
print(f" {i}. {item}")
return {
**state,
"outline": outline,
"step_index": 0,
"notes": []
}
def research_step_node(state: BriefState) -> BriefState:
"""Узел: исследование одного пункта плана с поиском в интернете"""
current_step = state["step_index"]
outline_item = state["outline"][current_step]
print(f"\n🔍 [Шаг {current_step + 1}/{len(state['outline'])}] Исследование: {outline_item}")
# Формируем поисковый запрос
search_query = f"{state['topic']} {outline_item}"
print(f" Поисковый запрос: {search_query}")
try:
# Выполняем поиск через Tavily
search_result = tavily.invoke({"query": search_query})
# Форматируем результаты поиска
if isinstance(search_result, list):
formatted_results = []
for idx, result in enumerate(search_result, 1):
if hasattr(result, 'content') and result.content:
formatted_results.append(f"{idx}. {result.content[:500]}")
elif isinstance(result, dict) and 'content' in result:
formatted_results.append(f"{idx}. {result['content'][:500]}")
search_text = "\n\n".join(formatted_results) if formatted_results else "Результаты не найдены."
elif isinstance(search_result, str):
search_text = search_result
else:
search_text = str(search_result)
# Генерируем заметку на основе поиска
research_prompt = RESEARCH_PROMPT.format(
topic=state["topic"],
outline_item=outline_item,
search_results=search_text[:1500] # Ограничиваем длину
)
response = llm.invoke([HumanMessage(content=research_prompt)])
note = response.content.strip()
print(f" ✅ Заметка создана ({len(note)} символов)")
except Exception as e:
print(f" ⚠️ Ошибка поиска: {e}")
# Fallback: генерируем заметку без поиска
fallback_prompt = f"Напиши краткую заметку (5-8 предложений) по пункту '{outline_item}' для темы '{state['topic']}' (без внешнего поиска)."
response = llm.invoke([HumanMessage(content=fallback_prompt)])
note = response.content.strip()
print(f" ⚠️ Создана fallback-заметка без поиска")
# Добавляем заметку в список
updated_notes = state["notes"] + [note]
return {
**state,
"notes": updated_notes,
"step_index": state["step_index"] + 1
}
def synthesize_node(state: BriefState) -> BriefState:
"""Узел: синтез итогового брифа из собранных заметок"""
print(f"\n📝 Синтез итогового исследовательского брифа...")
# Форматируем план и заметки для промпта
outline_text = "\n".join([f"{i+1}. {item}" for i, item in enumerate(state["outline"])])
notes_text = "\n\n---\n\n".join([
f"**Пункт {i+1}: {state['outline'][i]}**\n{note}"
for i, note in enumerate(state["notes"])
])
prompt = SYNTHESIS_PROMPT.format(
topic=state["topic"],
outline=outline_text,
notes=notes_text
)
response = llm.invoke([HumanMessage(content=prompt)])
final_brief = response.content.strip()
print(f"✅ Бриф создан ({len(final_brief)} символов)")
return {
**state,
"final_brief": final_brief
}