{ "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", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
message_idoriginal_textstrict_questionreal_question
0130738Итальянский суд принял решение экстрадировать ...Какое решение приняло итальянское судопроизвод...Что там с Кузнецовым — его в Германию выдадут ...
1129361Пять пассажиров автобуса №793 пострадали в ДТП...Сколько пассажиров автобуса №793 пострадали в ...Сколько человек в автобусе 793 пострадали, ког...
2133468Владимир Путин утвердил концепцию государствен...Кто утвердил концепцию государственной миграци...Кто там утвердил новую миграционную концепцию ...
3123139Генпрокуратура и Минюст подали в Верховный суд...Какое юридическое действие предприняли Генерал...Что Генпрокуратура и Минюст сделали с сатанист...
4129894Обломки дрона обнаружили польские пограничники...Где и кем был обнаружен непилотируемый летател...Что там польские пограничники нашли рядом с Бе...
...............
162123802Мальчика, которого в Шереметьево мужчина удари...Каков исход медицинского случая двухлетнего ма...Что случилось с мальчиком, которого бросили в ...
163124166Почти все виды американского оружия, которые с...Каков текущий статус американского оружия, зап...Уже есть всё это оружие для Украины, что НАТО ...
164136058По планам Банка России, массовое внедрение циф...Когда начнется массовое внедрение цифрового ру...Когда начнут все пользоваться цифровым рублем,...
165134555В Турции в Гебзе обрушился многоэтажный дом. П...В каком городе Турции обрушился семиэтажный до...Что там в Гебзе с домом обрушился? Пять челове...
166123088Современный городской квартал сегодня уже дале...Какой девелопер осуществляет строительство жил...Кто строит тот самый квартал Soul рядом с метр...
\n", "

167 rows × 4 columns

\n", "
" ], "text/plain": [ " message_id original_text \\\n", "0 130738 Итальянский суд принял решение экстрадировать ... \n", "1 129361 Пять пассажиров автобуса №793 пострадали в ДТП... \n", "2 133468 Владимир Путин утвердил концепцию государствен... \n", "3 123139 Генпрокуратура и Минюст подали в Верховный суд... \n", "4 129894 Обломки дрона обнаружили польские пограничники... \n", ".. ... ... \n", "162 123802 Мальчика, которого в Шереметьево мужчина удари... \n", "163 124166 Почти все виды американского оружия, которые с... \n", "164 136058 По планам Банка России, массовое внедрение циф... \n", "165 134555 В Турции в Гебзе обрушился многоэтажный дом. П... \n", "166 123088 Современный городской квартал сегодня уже дале... \n", "\n", " strict_question \\\n", "0 Какое решение приняло итальянское судопроизвод... \n", "1 Сколько пассажиров автобуса №793 пострадали в ... \n", "2 Кто утвердил концепцию государственной миграци... \n", "3 Какое юридическое действие предприняли Генерал... \n", "4 Где и кем был обнаружен непилотируемый летател... \n", ".. ... \n", "162 Каков исход медицинского случая двухлетнего ма... \n", "163 Каков текущий статус американского оружия, зап... \n", "164 Когда начнется массовое внедрение цифрового ру... \n", "165 В каком городе Турции обрушился семиэтажный до... \n", "166 Какой девелопер осуществляет строительство жил... \n", "\n", " real_question \n", "0 Что там с Кузнецовым — его в Германию выдадут ... \n", "1 Сколько человек в автобусе 793 пострадали, ког... \n", "2 Кто там утвердил новую миграционную концепцию ... \n", "3 Что Генпрокуратура и Минюст сделали с сатанист... \n", "4 Что там польские пограничники нашли рядом с Бе... \n", ".. ... \n", "162 Что случилось с мальчиком, которого бросили в ... \n", "163 Уже есть всё это оружие для Украины, что НАТО ... \n", "164 Когда начнут все пользоваться цифровым рублем,... \n", "165 Что там в Гебзе с домом обрушился? Пять челове... \n", "166 Кто строит тот самый квартал Soul рядом с метр... \n", "\n", "[167 rows x 4 columns]" ] }, "execution_count": 94, "metadata": {}, "output_type": "execute_result" } ], "source": [ "qa_df" ] } ], "metadata": { "kernelspec": { "display_name": "venv", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.3" } }, "nbformat": 4, "nbformat_minor": 2 }