diff --git a/nodes.py b/nodes.py index e6dbca0..22c5cbb 100644 --- a/nodes.py +++ b/nodes.py @@ -1,120 +1,139 @@ from langchain_openai import ChatOpenAI -from langchain_core.messages import HumanMessage -from pydantic import BaseModel, Field, ValidationError +from langchain_core.messages import HumanMessage, SystemMessage from state import ReflectState # Инициализация LLM llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.7) -# 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)") - - def validate_quality(self) -> tuple[bool, str]: - """Проверяет качество ответа по критериям""" - issues = [] - - 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" +# Промпты для узлов +DRAFT_PROMPT = """Ты — эксперт, который дает краткие, но содержательные ответы. +Ответь на вопрос пользователя в 5-10 предложений. +Будь конкретным, избегай общих фраз и "воды". -# Промпт для генерации с инструкцией по качеству -ANSWER_PROMPT = """Ты — эксперт, который дает качественные ответы. -Требования к ответу: +Вопрос: {question}""" + +REFLECT_PROMPT = """Ты — строгий критик, оценивающий качество ответа. +Критерии оценки: 1. Полнота: ответ покрывает все аспекты вопроса 2. Конкретика: есть примеры, детали, точные формулировки -3. Краткость: 5-10 предложений, без воды +3. Отсутствие воды: нет общих фраз, повторений, лишних слов +Ответ для оценки: {draft} + +Твой ответ должен быть в формате: +VERDICT: [ok/needs_revision] +CRITIQUE: [2-3 конкретных пункта замечаний или похвала] + +Примеры: +VERDICT: ok +CRITIQUE: Ответ полный, есть примеры, нет воды. + +VERDICT: needs_revision +CRITIQUE: 1. Нет конкретных примеров. 2. Слишком много общих фраз. 3. Не раскрыта разница в контексте MCP.""" + +REWRITE_PROMPT = """Улучши ответ на вопрос, учитывая замечания критика. Вопрос: {question} +Предыдущая версия: {draft} +Замечания критика: {critique} -После ответа добавь метаданные в формате JSON: -{{ - "is_complete": true/false, - "has_examples": true/false, - "is_concise": true/false -}} +Дай исправленную версию (5-10 предложений), которая: +- Учитывает все замечания +- Становится более полной и конкретной +- Убирает воду +- Сохраняет краткость (5-10 предложений)""" -Сам ответ должен быть в markdown формате. -""" - -def generate_answer(state: ReflectState) -> ReflectState: +def draft_answer(state: ReflectState) -> ReflectState: """ - Узел генерации ответа с try/except и валидацией + Узел: первичная генерация ответа с try/except При ошибке заполняет поле error """ - print(f"\n📝 [Попытка {state['round']}/{state['max_rounds']}] Генерация ответа...") + print(f"\n📝 [Раунд {state['round']}] Генерация черновика...") try: - # 1. Генерация ответа - prompt = ANSWER_PROMPT.format(question=state["question"]) + prompt = DRAFT_PROMPT.format(question=state["question"]) response = llm.invoke([HumanMessage(content=prompt)]) - # 2. Парсинг JSON-метаданных из ответа - import json - import re + # Проверка качества ответа (базовая валидация) + if len(response.content.split()) < 50: + raise ValueError("Слишком короткий ответ (<50 слов)") - # Ищем JSON в конце ответа - json_match = re.search(r'\{[^{}]*"is_complete"[^{}]*\}', response.content) - if not json_match: - raise ValueError("Не найдены метаданные JSON в ответе") + if len(response.content.split()) > 300: + raise ValueError("Слишком длинный ответ (>300 слов)") - metadata = json.loads(json_match.group()) + print(f" ✅ Черновик успешно создан") + new_state = state.copy() + new_state["draft"] = response.content + new_state["error"] = None # Очищаем ошибку + return new_state - # 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: - # Другие ошибки (сеть, API и т.д.) - print(f" ❌ Критическая ошибка: {str(e)}") - return { - **state, - "draft": "", - "error": f"Критическая ошибка: {str(e)}" - } \ No newline at end of file + print(f" ❌ Ошибка генерации: {str(e)}") + new_state = state.copy() + new_state["draft"] = "" + new_state["error"] = str(e) + return new_state + +def reflect(state: ReflectState) -> ReflectState: + """Узел: критическая оценка ответа LLM-критиком""" + print(f"🔍 [Раунд {state['round']}] Рефлексия критика...") + + try: + prompt = REFLECT_PROMPT.format(draft=state["draft"]) + response = llm.invoke([HumanMessage(content=prompt)]) + + # Парсим ответ критика + lines = response.content.strip().split('\n') + verdict = "needs_revision" + critique = response.content + + for line in lines: + if line.startswith("VERDICT:"): + verdict = line.replace("VERDICT:", "").strip().lower() + elif line.startswith("CRITIQUE:"): + critique = line.replace("CRITIQUE:", "").strip() + + if verdict not in ["ok", "needs_revision"]: + verdict = "needs_revision" + + new_state = state.copy() + new_state["verdict"] = verdict + new_state["critique"] = critique + + print(f" Вердикт: {verdict.upper()}") + if verdict == "needs_revision": + print(f" Замечания: {critique[:100]}...") + + return new_state + + except Exception as e: + # Если критик сломался, считаем что нужна доработка + print(f" ⚠️ Ошибка критика: {str(e)}") + new_state = state.copy() + new_state["verdict"] = "needs_revision" + new_state["critique"] = f"Критик не смог оценить ответ: {str(e)}" + return new_state + +def rewrite(state: ReflectState) -> ReflectState: + """Узел: переписывание ответа с учетом замечаний""" + print(f"✏️ [Раунд {state['round']}] Переписывание с учетом критики...") + + try: + prompt = REWRITE_PROMPT.format( + question=state["question"], + draft=state["draft"], + critique=state["critique"] + ) + response = llm.invoke([HumanMessage(content=prompt)]) + + new_state = state.copy() + new_state["draft"] = response.content + new_state["round"] = state["round"] + 1 + new_state["error"] = None + return new_state + + except Exception as e: + print(f" ❌ Ошибка переписывания: {str(e)}") + new_state = state.copy() + new_state["error"] = str(e) + new_state["round"] = state["round"] + 1 + return new_state \ No newline at end of file