{ "cells": [ { "cell_type": "code", "execution_count": 54, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✅ API ключ загружен\n" ] } ], "source": [ "import os\n", "from typing import Literal, Optional\n", "from dotenv import load_dotenv\n", "from pydantic import BaseModel, Field\n", "from langchain_openai import ChatOpenAI\n", "from langchain_core.prompts import ChatPromptTemplate\n", "from langchain_core.output_parsers import PydanticOutputParser\n", "import pandas as pd\n", "from tqdm import tqdm\n", "\n", "load_dotenv()\n", "\n", "OPENROUTER_API_KEY = os.getenv(\"OPENROUTER_API_KEY\")\n", "if not OPENROUTER_API_KEY:\n", " raise ValueError(\"Не найден OPENROUTER_API_KEY в переменных окружения\")\n", "\n", "print(\"✅ API ключ загружен\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Загрузка и фильтрация результатов классификации\n" ] }, { "cell_type": "code", "execution_count": 80, "metadata": {}, "outputs": [], "source": [ "df = pd.read_csv(\"classification_results.csv\")\n", "filtered_df = df.loc[df[\"is_unambiguous\"] & df[\"category\"].isin([\"event\", \"statement\", \"fact\"])]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Генерация вопросов с помощью LangChain\n" ] }, { "cell_type": "code", "execution_count": 81, "metadata": {}, "outputs": [], "source": [ "# Шаг 1: Модель для извлечения ответа\n", "class ExtractedAnswer(BaseModel):\n", " \"\"\"Извлечённый ответ из main_fact\"\"\"\n", " \n", " answer: str = Field(\n", " description=\"Краткий, конкретный ответ, который можно дать на вопрос о main_fact\"\n", " )\n", " answer_type: Literal[\"entity\", \"number\", \"date\", \"action\", \"description\"] = Field(\n", " description=\"Тип ответа: entity - сущность/название, number - число, date - дата, action - действие, description - описание\"\n", " )\n", " key_info: str = Field(\n", " description=\"Ключевая информация, которая ОБЯЗАТЕЛЬНО должна присутствовать в любом корректном ответе\"\n", " )\n", "\n", "\n", "# Шаг 2: Модель для генерации вопросов к ответу\n", "class QuestionPair(BaseModel):\n", " \"\"\"Пара вопросов с одинаковым ответом\"\"\"\n", " \n", " strict_question: str = Field(\n", " description=\"Формальный, точный вопрос. Конкретный и однозначный.\"\n", " )\n", " real_question: str = Field(\n", " description=\"Разговорная, человечная формулировка того же вопроса. Как бы спросил обычный человек.\"\n", " )\n", " question_type: Literal[\"what\", \"when\", \"where\", \"who\", \"how_much\", \"how_many\", \"why\", \"how\"] = Field(\n", " description=\"Тип вопроса\"\n", " )\n", "\n", "\n", "class QAResult(BaseModel):\n", " \"\"\"Финальный результат: ответ + 2 вопроса\"\"\"\n", " \n", " answer: ExtractedAnswer\n", " questions: QuestionPair\n" ] }, { "cell_type": "code", "execution_count": 62, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✅ Двухшаговый агент создан (модель: qwen/qwen3-next-80b-a3b-instruct)\n" ] } ], "source": [ "def create_llm(model: str = \"openai/gpt-4o-mini\", temperature: float = 0.0) -> ChatOpenAI:\n", " \"\"\"Создает экземпляр LLM через OpenRouter\"\"\"\n", " return ChatOpenAI(\n", " model=model,\n", " temperature=temperature,\n", " openai_api_key=OPENROUTER_API_KEY,\n", " openai_api_base=\"https://api.proxyapi.ru/openrouter/v1\",\n", " )\n", "\n", "MODEL_NAME = \"qwen/qwen3-next-80b-a3b-instruct\"\n", "llm = create_llm(model=MODEL_NAME, temperature=0.3)\n", "\n", "# === ШАГ 1: Извлечение ответа ===\n", "answer_parser = PydanticOutputParser(pydantic_object=ExtractedAnswer)\n", "\n", "answer_prompt = ChatPromptTemplate.from_messages([\n", " (\"system\", \"\"\"Ты - эксперт по извлечению ключевой информации из новостных текстов.\n", "\n", "Твоя задача - извлечь КРАТКИЙ ОТВЕТ из main_fact. Этот ответ будет использоваться как эталонный ответ на вопросы.\n", "\n", "## Правила:\n", "1. Ответ должен быть КРАТКИМ и КОНКРЕТНЫМ (1-2 предложения максимум)\n", "2. Ответ должен содержать ГЛАВНУЮ информацию из main_fact\n", "3. Определи тип ответа: сущность, число, дата, действие или описание\n", "4. Выдели key_info - минимальную информацию, без которой ответ будет неполным\n", "\n", "{format_instructions}\"\"\"),\n", " (\"human\", \"\"\"Извлеки ответ из следующего факта:\n", "\n", "## main_fact: {main_fact}\"\"\")\n", "])\n", "\n", "answer_chain = answer_prompt | llm | answer_parser\n", "\n", "# === ШАГ 2: Генерация вопросов к ответу ===\n", "question_parser = PydanticOutputParser(pydantic_object=QuestionPair)\n", "\n", "question_prompt = ChatPromptTemplate.from_messages([\n", " (\"system\", \"\"\"Ты - эксперт по созданию вопросов для систем вопрос-ответ (QA).\n", "\n", "Тебе дан ОТВЕТ. Твоя задача - сгенерировать 2 ВОПРОСА, на которые этот ответ будет ЕДИНСТВЕННО ВЕРНЫМ.\n", "\n", "## КРИТИЧЕСКИ ВАЖНО:\n", "- Оба вопроса ДОЛЖНЫ иметь ОДИНАКОВЫЙ ответ = \"{answer}\"\n", "- Вопросы отличаются ТОЛЬКО стилем формулировки, НЕ содержанием\n", "\n", "## strict_question (формальный):\n", "- Точная, академическая формулировка\n", "- Использует полные названия и термины\n", "- Пример: \"Какое решение принял Центральный банк РФ относительно ключевой ставки?\"\n", "\n", "## real_question (разговорный):\n", "- Как спросил бы обычный человек в разговоре\n", "- Может опускать детали, которые понятны из контекста\n", "- Пример: \"Что там ЦБ со ставкой сделал?\"\n", "\n", "{format_instructions}\"\"\"),\n", " (\"human\", \"\"\"Сгенерируй 2 вопроса для следующего:\n", "\n", "## ОТВЕТ (должен быть одинаковым для обоих вопросов): {answer}\n", "## Ключевая информация: {key_info}\n", "## Контекст (main_fact): {main_fact}\n", "\n", "Помни: оба вопроса должны подразумевать ОДИН И ТОТ ЖЕ ответ!\"\"\")\n", "])\n", "\n", "question_chain = question_prompt | llm | question_parser\n", "\n", "print(f\"✅ Двухшаговый агент создан (модель: {MODEL_NAME})\")\n" ] }, { "cell_type": "code", "execution_count": 63, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✅ QA-агент создан\n" ] } ], "source": [ "class QAAgent:\n", " \"\"\"Двухшаговый агент: сначала ответ, потом вопросы\"\"\"\n", " \n", " def __init__(self):\n", " self.answer_chain = answer_chain\n", " self.question_chain = question_chain\n", " self.answer_parser = answer_parser\n", " self.question_parser = question_parser\n", " \n", " def generate(self, row: pd.Series) -> Optional[QAResult]:\n", " \"\"\"Генерирует ответ и вопросы для одной записи\"\"\"\n", " main_fact = row.get(\"main_fact\", \"\")\n", " \n", " # Шаг 1: Извлекаем ответ\n", " try:\n", " answer_result = self.answer_chain.invoke({\n", " \"main_fact\": main_fact,\n", " \"format_instructions\": self.answer_parser.get_format_instructions()\n", " })\n", " except Exception as e:\n", " print(f\"Ошибка извлечения ответа: {e}\")\n", " return None\n", " \n", " # Шаг 2: Генерируем вопросы к этому ответу\n", " try:\n", " questions_result = self.question_chain.invoke({\n", " \"answer\": answer_result.answer,\n", " \"key_info\": answer_result.key_info,\n", " \"main_fact\": main_fact,\n", " \"format_instructions\": self.question_parser.get_format_instructions()\n", " })\n", " except Exception as e:\n", " print(f\"Ошибка генерации вопросов: {e}\")\n", " return None\n", " \n", " return QAResult(answer=answer_result, questions=questions_result)\n", " \n", " def generate_batch(self, df: pd.DataFrame, show_progress: bool = True) -> list[dict]:\n", " \"\"\"Генерирует QA-пары для всего DataFrame\"\"\"\n", " results = []\n", " iterator = tqdm(df.iterrows(), total=len(df), desc=\"Генерация QA\") if show_progress else df.iterrows()\n", " \n", " for idx, row in iterator:\n", " try:\n", " qa_result = self.generate(row)\n", " except KeyboardInterrupt:\n", " break\n", " \n", " if qa_result:\n", " results.append({\n", " \"index\": idx,\n", " \"original_text\": row.get(\"original_text\", \"\"),\n", " \"main_topic\": row.get(\"main_topic\", \"\"),\n", " \"main_fact\": row.get(\"main_fact\", \"\"),\n", " \"answer\": qa_result.answer.answer,\n", " \"answer_type\": qa_result.answer.answer_type,\n", " \"key_info\": qa_result.answer.key_info,\n", " \"strict_question\": qa_result.questions.strict_question,\n", " \"real_question\": qa_result.questions.real_question,\n", " \"question_type\": qa_result.questions.question_type,\n", " })\n", " else:\n", " results.append({\n", " \"index\": idx,\n", " \"original_text\": row.get(\"original_text\", \"\"),\n", " \"main_topic\": row.get(\"main_topic\", \"\"),\n", " \"main_fact\": row.get(\"main_fact\", \"\"),\n", " \"answer\": None,\n", " \"answer_type\": None,\n", " \"key_info\": None,\n", " \"strict_question\": None,\n", " \"real_question\": None,\n", " \"question_type\": None,\n", " })\n", " \n", " return results\n", "\n", "\n", "# Создаем агента\n", "agent = QAAgent()\n", "print(\"✅ QA-агент создан\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Генерация вопросов для отфильтрованных данных\n" ] }, { "cell_type": "code", "execution_count": 64, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "Генерация QA: 100%|██████████| 167/167 [09:09<00:00, 3.29s/it]" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "✅ Сгенерировано QA-пар: 167/167\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\n" ] } ], "source": [ "# Генерация QA-пар для отфильтрованных данных\n", "qa_results = agent.generate_batch(filtered_df)\n", "\n", "print(f\"\\n✅ Сгенерировано QA-пар: {sum(1 for r in qa_results if r['answer'])}/{len(qa_results)}\")\n" ] }, { "cell_type": "code", "execution_count": 85, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "message_id 130738\n", "original_text Итальянский суд принял решение экстрадировать ...\n", "main_topic Экстрадиция Сергея Кузнецова в Германию по под...\n", "key_entities Итальянский суд, Германия, Сергей Кузнецов, Се...\n", "main_fact Итальянский суд принял решение экстрадировать ...\n", "is_unambiguous True\n", "confidence 0.95\n", "category event\n", "search_difficulty easy\n", "ambiguity_reasons NaN\n", "Name: 6, dtype: object" ] }, "execution_count": 85, "metadata": {}, "output_type": "execute_result" } ], "source": [ "filtered_df.iloc[0]" ] }, { "cell_type": "code", "execution_count": 84, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'index': 6,\n", " 'original_text': 'Итальянский суд принял решение экстрадировать в Германию задержанного по подозрению в подрыве «Северных потоков» Сергея Кузнецова, пишет Reuters.\\n\\n🐚 Следить за новостями РБК в Telegram',\n", " 'main_topic': 'Экстрадиция Сергея Кузнецова в Германию по подозрению в подрыве «Северных потоков»',\n", " 'main_fact': 'Итальянский суд принял решение экстрадировать Сергея Кузнецова в Германию, где его подозревают в подрыве газопроводов «Северные потоки».',\n", " 'answer': 'Итальянский суд решил экстрадировать Сергея Кузнецова в Германию, где его подозревают в подрыве газопроводов «Северные потоки».',\n", " 'answer_type': 'action',\n", " 'key_info': 'экстрадировать Сергея Кузнецова в Германию из-за подозрения в подрыве газопроводов «Северные потоки»',\n", " 'strict_question': 'Какое решение приняло итальянское судопроизводство в отношении экстрадиции Сергея Кузнецова в связи с подозрениями в причастности к подрыву газопроводов «Северные потоки»?',\n", " 'real_question': 'Что там с Кузнецовым — его в Германию выдадут за подрыв «Северных потоков»?',\n", " 'question_type': 'what'}" ] }, "execution_count": 84, "metadata": {}, "output_type": "execute_result" } ], "source": [ "qa_results[0]" ] }, { "cell_type": "code", "execution_count": 93, "metadata": {}, "outputs": [], "source": [ "# Преобразование в DataFrame и сохранение\n", "qa_df = pd.DataFrame(qa_results)\n", "qa_df[\"message_id\"] = filtered_df[\"message_id\"].values\n", "qa_df = qa_df[[\"message_id\", \"original_text\", \"strict_question\", \"real_question\"]]\n", "\n", "# Сохраняем в CSV\n", "output_file = \"generated_qa.csv\"\n", "qa_df.to_csv(output_file, index=False)" ] }, { "cell_type": "code", "execution_count": 94, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
| \n", " | message_id | \n", "original_text | \n", "strict_question | \n", "real_question | \n", "
|---|---|---|---|---|
| 0 | \n", "130738 | \n", "Итальянский суд принял решение экстрадировать ... | \n", "Какое решение приняло итальянское судопроизвод... | \n", "Что там с Кузнецовым — его в Германию выдадут ... | \n", "
| 1 | \n", "129361 | \n", "Пять пассажиров автобуса №793 пострадали в ДТП... | \n", "Сколько пассажиров автобуса №793 пострадали в ... | \n", "Сколько человек в автобусе 793 пострадали, ког... | \n", "
| 2 | \n", "133468 | \n", "Владимир Путин утвердил концепцию государствен... | \n", "Кто утвердил концепцию государственной миграци... | \n", "Кто там утвердил новую миграционную концепцию ... | \n", "
| 3 | \n", "123139 | \n", "Генпрокуратура и Минюст подали в Верховный суд... | \n", "Какое юридическое действие предприняли Генерал... | \n", "Что Генпрокуратура и Минюст сделали с сатанист... | \n", "
| 4 | \n", "129894 | \n", "Обломки дрона обнаружили польские пограничники... | \n", "Где и кем был обнаружен непилотируемый летател... | \n", "Что там польские пограничники нашли рядом с Бе... | \n", "
| ... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "
| 162 | \n", "123802 | \n", "Мальчика, которого в Шереметьево мужчина удари... | \n", "Каков исход медицинского случая двухлетнего ма... | \n", "Что случилось с мальчиком, которого бросили в ... | \n", "
| 163 | \n", "124166 | \n", "Почти все виды американского оружия, которые с... | \n", "Каков текущий статус американского оружия, зап... | \n", "Уже есть всё это оружие для Украины, что НАТО ... | \n", "
| 164 | \n", "136058 | \n", "По планам Банка России, массовое внедрение циф... | \n", "Когда начнется массовое внедрение цифрового ру... | \n", "Когда начнут все пользоваться цифровым рублем,... | \n", "
| 165 | \n", "134555 | \n", "В Турции в Гебзе обрушился многоэтажный дом. П... | \n", "В каком городе Турции обрушился семиэтажный до... | \n", "Что там в Гебзе с домом обрушился? Пять челове... | \n", "
| 166 | \n", "123088 | \n", "Современный городской квартал сегодня уже дале... | \n", "Какой девелопер осуществляет строительство жил... | \n", "Кто строит тот самый квартал Soul рядом с метр... | \n", "
167 rows × 4 columns
\n", "