add nodes.py
This commit is contained in:
@@ -1,187 +1,120 @@
|
||||
import os
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langchain_core.messages import HumanMessage, SystemMessage
|
||||
from langchain_tavily import TavilySearch
|
||||
from state import BriefState
|
||||
from langchain_core.messages import HumanMessage
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
from state import ReflectState
|
||||
|
||||
# Инициализация LLM
|
||||
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.3)
|
||||
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.7)
|
||||
|
||||
# Инициализация Tavily search tool
|
||||
tavily_search = TavilySearch(
|
||||
max_results=3,
|
||||
topic="general",
|
||||
include_answer=False,
|
||||
include_raw_content=False
|
||||
)
|
||||
# Pydantic модель для валидации ответа
|
||||
class ValidAnswer(BaseModel):
|
||||
"""Модель для валидации качества ответа"""
|
||||
content: str = Field(description="Ответ на вопрос")
|
||||
is_complete: bool = Field(description="Полнота ответа (должно быть True)")
|
||||
has_examples: bool = Field(description="Наличие примеров (должно быть True)")
|
||||
is_concise: bool = Field(description="Отсутствие воды (должно быть True)")
|
||||
|
||||
# Промпты для узлов
|
||||
OUTLINE_PROMPT = """Ты — исследовательский ассистент. Составь план из 4-5 пунктов для исследования темы.
|
||||
def validate_quality(self) -> tuple[bool, str]:
|
||||
"""Проверяет качество ответа по критериям"""
|
||||
issues = []
|
||||
|
||||
Тема: {topic}
|
||||
if not self.is_complete:
|
||||
issues.append("неполный ответ")
|
||||
if not self.has_examples:
|
||||
issues.append("нет конкретных примеров")
|
||||
if not self.is_concise:
|
||||
issues.append("содержит воду/общие фразы")
|
||||
|
||||
Требования к плану:
|
||||
- Каждый пункт должен быть конкретным и исследовательским
|
||||
- Пункты должны покрывать разные аспекты темы
|
||||
- Используй четкие формулировки (например: "История развития", "Ключевые технологии", "Практические примеры")
|
||||
if not self.content or len(self.content.split()) < 50:
|
||||
issues.append("слишком короткий ответ (<50 слов)")
|
||||
|
||||
Ответ дай в виде списка, каждый пункт на новой строке, начиная с "- ".
|
||||
if len(self.content.split()) > 300:
|
||||
issues.append("слишком длинный ответ (>300 слов)")
|
||||
|
||||
Пример:
|
||||
- Текущее состояние и тренды
|
||||
- Основные вызовы и решения
|
||||
- Рекомендации для начинающих"""
|
||||
if issues:
|
||||
return False, f"Проблемы: {', '.join(issues)}"
|
||||
return True, "OK"
|
||||
|
||||
RESEARCH_PROMPT = """Ты — исследователь. На основе результатов поиска составь краткую заметку (5-8 предложений) по следующему пункту плана.
|
||||
# Промпт для генерации с инструкцией по качеству
|
||||
ANSWER_PROMPT = """Ты — эксперт, который дает качественные ответы.
|
||||
Требования к ответу:
|
||||
1. Полнота: ответ покрывает все аспекты вопроса
|
||||
2. Конкретика: есть примеры, детали, точные формулировки
|
||||
3. Краткость: 5-10 предложений, без воды
|
||||
|
||||
Пункт плана: {outline_item}
|
||||
Вопрос: {question}
|
||||
|
||||
Результаты поиска:
|
||||
{search_results}
|
||||
После ответа добавь метаданные в формате JSON:
|
||||
{{
|
||||
"is_complete": true/false,
|
||||
"has_examples": true/false,
|
||||
"is_concise": true/false
|
||||
}}
|
||||
|
||||
Заметка должна:
|
||||
- Быть информативной и конкретной
|
||||
- Опираться на факты из поиска
|
||||
- Содержать 5-8 предложений
|
||||
- Заканчиваться выводом или ключевым инсайтом"""
|
||||
Сам ответ должен быть в markdown формате.
|
||||
"""
|
||||
|
||||
SYNTHESIS_PROMPT = """Ты — аналитик. Объедини собранные заметки в связный исследовательский бриф (полстраницы - страница).
|
||||
|
||||
Тема: {topic}
|
||||
|
||||
Заметки по пунктам плана:
|
||||
{notes}
|
||||
|
||||
Требования к брифу:
|
||||
1. Начни с краткого введения (2-3 предложения)
|
||||
2. Каждый пункт плана оформи как отдельный раздел с заголовком
|
||||
3. Сохрани ключевые факты и инсайты из заметок
|
||||
4. Добавь заключение с общими выводами (3-4 предложения)
|
||||
5. Используй связующие фразы между разделами
|
||||
|
||||
Формат:
|
||||
# Исследовательский бриф: {topic}
|
||||
|
||||
## Введение
|
||||
[текст]
|
||||
|
||||
## [Заголовок пункта 1]
|
||||
[текст]
|
||||
|
||||
## [Заголовок пункта 2]
|
||||
[текст]
|
||||
|
||||
...
|
||||
|
||||
## Заключение
|
||||
[текст]"""
|
||||
|
||||
def outline_node(state: BriefState) -> BriefState:
|
||||
"""Узел 1: Составление плана исследования"""
|
||||
print("\n📋 [1/4] Составляю план исследования...")
|
||||
|
||||
prompt = OUTLINE_PROMPT.format(topic=state["topic"])
|
||||
response = llm.invoke([HumanMessage(content=prompt)])
|
||||
|
||||
# Парсим список из ответа
|
||||
lines = response.content.strip().split('\n')
|
||||
outline = []
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line.startswith('- '):
|
||||
outline.append(line[2:])
|
||||
elif line.startswith('• '):
|
||||
outline.append(line[2:])
|
||||
elif line and not line.startswith('```'):
|
||||
# Если нет маркера, убираем номер
|
||||
clean = line.lstrip('0123456789. ').strip()
|
||||
if clean:
|
||||
outline.append(clean)
|
||||
|
||||
# Ограничиваем 5 пунктами
|
||||
outline = outline[:5]
|
||||
|
||||
print(f" ✅ План составлен: {len(outline)} пунктов")
|
||||
for i, item in enumerate(outline, 1):
|
||||
print(f" {i}. {item}")
|
||||
|
||||
new_state = state.copy()
|
||||
new_state["outline"] = outline
|
||||
new_state["step_index"] = 0
|
||||
new_state["notes"] = []
|
||||
return new_state
|
||||
|
||||
def research_step_node(state: BriefState) -> BriefState:
|
||||
"""Узел 2: Исследование одного пункта плана через web search"""
|
||||
current_idx = state["step_index"]
|
||||
current_item = state["outline"][current_idx]
|
||||
|
||||
print(f"\n🔍 [2/4] Исследую пункт {current_idx + 1}/{len(state['outline'])}: {current_item}")
|
||||
|
||||
# Формируем поисковый запрос
|
||||
search_query = f"{state['topic']} {current_item}"
|
||||
print(f" Запрос в Tavily: {search_query}")
|
||||
def generate_answer(state: ReflectState) -> ReflectState:
|
||||
"""
|
||||
Узел генерации ответа с try/except и валидацией
|
||||
При ошибке заполняет поле error
|
||||
"""
|
||||
print(f"\n📝 [Попытка {state['round']}/{state['max_rounds']}] Генерация ответа...")
|
||||
|
||||
try:
|
||||
# Выполняем поиск
|
||||
search_result = tavily_search.invoke({"query": search_query})
|
||||
|
||||
# Извлекаем текст результатов
|
||||
if isinstance(search_result, list):
|
||||
results_text = "\n\n".join([
|
||||
f"Источник {i+1}: {item.get('content', '')[:500]}"
|
||||
for i, item in enumerate(search_result[:3])
|
||||
if item.get('content')
|
||||
])
|
||||
else:
|
||||
results_text = str(search_result)[:1500]
|
||||
|
||||
# Генерируем заметку на основе поиска
|
||||
prompt = RESEARCH_PROMPT.format(
|
||||
outline_item=current_item,
|
||||
search_results=results_text
|
||||
)
|
||||
# 1. Генерация ответа
|
||||
prompt = ANSWER_PROMPT.format(question=state["question"])
|
||||
response = llm.invoke([HumanMessage(content=prompt)])
|
||||
note = response.content.strip()
|
||||
|
||||
print(f" ✅ Заметка создана ({len(note)} символов)")
|
||||
# 2. Парсинг JSON-метаданных из ответа
|
||||
import json
|
||||
import re
|
||||
|
||||
# Ищем JSON в конце ответа
|
||||
json_match = re.search(r'\{[^{}]*"is_complete"[^{}]*\}', response.content)
|
||||
if not json_match:
|
||||
raise ValueError("Не найдены метаданные JSON в ответе")
|
||||
|
||||
metadata = json.loads(json_match.group())
|
||||
|
||||
# 3. Создаем объект ValidAnswer
|
||||
answer_obj = ValidAnswer(
|
||||
content=response.content,
|
||||
is_complete=metadata.get("is_complete", False),
|
||||
has_examples=metadata.get("has_examples", False),
|
||||
is_concise=metadata.get("is_concise", False)
|
||||
)
|
||||
|
||||
# 4. Валидация качества
|
||||
is_valid, error_message = answer_obj.validate_quality()
|
||||
|
||||
if not is_valid:
|
||||
raise ValueError(error_message)
|
||||
|
||||
# 5. Успех - очищаем ошибку
|
||||
print(f" ✅ Ответ прошел валидацию!")
|
||||
return {
|
||||
**state,
|
||||
"draft": answer_obj.content,
|
||||
"error": None
|
||||
}
|
||||
|
||||
except (ValidationError, ValueError, json.JSONDecodeError) as e:
|
||||
# Перехватываем все ошибки валидации
|
||||
error_msg = str(e)
|
||||
print(f" ❌ Ошибка валидации: {error_msg}")
|
||||
|
||||
return {
|
||||
**state,
|
||||
"draft": "", # Очищаем невалидный ответ
|
||||
"error": error_msg
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Ошибка поиска: {e}")
|
||||
# Fallback: заметка без поиска
|
||||
prompt = f"Напиши краткую заметку (5-8 предложений) о: {current_item} в контексте {state['topic']}"
|
||||
response = llm.invoke([HumanMessage(content=prompt)])
|
||||
note = response.content.strip()
|
||||
print(f" ✅ Заметка создана (без поиска)")
|
||||
|
||||
# Обновляем состояние
|
||||
new_state = state.copy()
|
||||
new_state["notes"] = state["notes"] + [note]
|
||||
new_state["step_index"] = state["step_index"] + 1
|
||||
|
||||
return new_state
|
||||
|
||||
def synthesize_node(state: BriefState) -> BriefState:
|
||||
"""Узел 3: Синтез финального брифа из заметок"""
|
||||
print("\n📝 [3/4] Синтезирую финальный исследовательский бриф...")
|
||||
|
||||
# Форматируем заметки с заголовками пунктов
|
||||
notes_with_headers = []
|
||||
for i, (outline_item, note) in enumerate(zip(state["outline"], state["notes"])):
|
||||
notes_with_headers.append(f"## {outline_item}\n{note}")
|
||||
|
||||
notes_text = "\n\n".join(notes_with_headers)
|
||||
|
||||
prompt = SYNTHESIS_PROMPT.format(
|
||||
topic=state["topic"],
|
||||
notes=notes_text
|
||||
)
|
||||
|
||||
response = llm.invoke([HumanMessage(content=prompt)])
|
||||
final_brief = response.content.strip()
|
||||
|
||||
print(f" ✅ Бриф создан ({len(final_brief)} символов)")
|
||||
|
||||
new_state = state.copy()
|
||||
new_state["final_brief"] = final_brief
|
||||
return new_state
|
||||
# Другие ошибки (сеть, API и т.д.)
|
||||
print(f" ❌ Критическая ошибка: {str(e)}")
|
||||
return {
|
||||
**state,
|
||||
"draft": "",
|
||||
"error": f"Критическая ошибка: {str(e)}"
|
||||
}
|
||||
Reference in New Issue
Block a user