diff --git "a/news_classification_pipeline.ipynb" "b/news_classification_pipeline.ipynb" new file mode 100644--- /dev/null +++ "b/news_classification_pipeline.ipynb" @@ -0,0 +1,2116 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "7217e456", + "metadata": {}, + "source": [ + "# Пайплайн классификации новостных постов\n", + "\n", + "Этот пайплайн состоит из двух агентов:\n", + "1. **Агент извлечения** - вычленяет основную мысль/сообщение из новостного поста\n", + "2. **Агент классификации** - определяет, является ли основная тема однозначной при поиске\n", + "\n", + "Неоднозначные тексты сложны для поиска, так как вопрос может иметь несколько противоречивых ответов в новостном потоке.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "0721134b", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/incllude/dev/rag_tg_2025/venv/lib/python3.13/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + }, + { + "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", + "# Проверка наличия API ключа\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", + "id": "049045bd", + "metadata": {}, + "source": [ + "## Определение структурированных моделей вывода" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "9a669035", + "metadata": {}, + "outputs": [], + "source": [ + "class MainMessage(BaseModel):\n", + " \"\"\"Основная мысль/сообщение новостного поста\"\"\"\n", + " \n", + " main_topic: str = Field(\n", + " description=\"Основная тема или предмет новостного поста (например: 'Выпуск iPhone 17', 'Высказывание политика А о политике Б')\"\n", + " )\n", + " key_entities: list[str] = Field(\n", + " description=\"Ключевые сущности, упомянутые в посте (люди, организации, события, даты, места)\"\n", + " )\n", + " main_fact_or_statement: str = Field(\n", + " description=\"Основной факт или утверждение, содержащееся в посте\"\n", + " )\n", + "\n", + "\n", + "class ClassificationResult(BaseModel):\n", + " \"\"\"Результат классификации новостного поста по однозначности поиска\"\"\"\n", + " \n", + " is_unambiguous: bool = Field(\n", + " description=\"Является ли основная тема поста однозначной при поиске. True - однозначная (конкретный факт), False - неоднозначная (могут быть противоречивые ответы)\"\n", + " )\n", + " confidence: float = Field(\n", + " description=\"Уверенность в классификации от 0.0 до 1.0\",\n", + " ge=0.0,\n", + " le=1.0\n", + " )\n", + " category: Literal[\"fact\", \"opinion\", \"statement\", \"event\", \"mixed\"] = Field(\n", + " description=\"Категория контента: fact - чистый факт, opinion - мнение, statement - высказывание/заявление, event - событие, mixed - смешанный\"\n", + " )\n", + " search_difficulty: Literal[\"easy\", \"medium\", \"hard\"] = Field(\n", + " description=\"Сложность поиска: easy - простой уникальный факт, medium - требует временного контекста, hard - неоднозначный, может иметь противоречивые ответы\"\n", + " )\n", + " ambiguity_reasons: list[str] = Field(\n", + " default_factory=list,\n", + " description=\"Причины неоднозначности (если есть): изменчивость позиции, множественные источники, субъективность и т.д.\"\n", + " )\n", + " reasoning: str = Field(\n", + " description=\"Подробное обоснование классификации\"\n", + " )\n", + "\n", + "\n", + "class PipelineResult(BaseModel):\n", + " \"\"\"Полный результат работы пайплайна\"\"\"\n", + " \n", + " original_text: str\n", + " main_message: MainMessage\n", + " classification: ClassificationResult\n" + ] + }, + { + "cell_type": "markdown", + "id": "c96b15db", + "metadata": {}, + "source": [ + "## Настройка LLM через OpenRouter\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "f669da89", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✅ Используемая модель: qwen/qwen3-next-80b-a3b-instruct\n" + ] + } + ], + "source": [ + "# Настройка LLM через OpenRouter\n", + "# Используем модель с хорошей поддержкой русского языка и структурированного вывода\n", + "\n", + "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", + "# Можно использовать разные модели для разных агентов\n", + "# Альтернативы: \"anthropic/claude-3-haiku\", \"google/gemini-flash-1.5\", \"meta-llama/llama-3.1-70b-instruct\"\n", + "MODEL_NAME = \"qwen/qwen3-next-80b-a3b-instruct\"\n", + "\n", + "print(f\"✅ Используемая модель: {MODEL_NAME}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "7dc42e5b", + "metadata": {}, + "source": [ + "## Агент 1: Извлечение основной мысли" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "1fdbd5f5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✅ Агент извлечения создан\n" + ] + } + ], + "source": [ + "class ExtractionAgent:\n", + " \"\"\"Агент для извлечения основной мысли из новостного поста\"\"\"\n", + " \n", + " def __init__(self, model: str = MODEL_NAME):\n", + " self.llm = create_llm(model, temperature=0.0)\n", + " self.parser = PydanticOutputParser(pydantic_object=MainMessage)\n", + " \n", + " self.prompt = ChatPromptTemplate.from_messages([\n", + " (\"system\", \"\"\"Ты - эксперт по анализу новостного контента. Твоя задача - извлечь основную мысль и ключевую информацию из новостного поста.\n", + "\n", + "Анализируй текст внимательно и выдели:\n", + "1. Основную тему поста\n", + "2. Все ключевые сущности (люди, организации, места, даты, события)\n", + "3. Главный факт или утверждение\n", + "4. Временной контекст (когда это произошло/происходит)\n", + "5. Дополнительный контекст для понимания\n", + "\n", + "{format_instructions}\"\"\"),\n", + " (\"human\", \"Проанализируй следующий новостной пост и извлеки основную мысль:\\n\\n{text}\")\n", + " ])\n", + " \n", + " def extract(self, text: str) -> MainMessage:\n", + " \"\"\"Извлекает основную мысль из текста\"\"\"\n", + " chain = self.prompt | self.llm | self.parser\n", + " \n", + " result = chain.invoke({\n", + " \"text\": text,\n", + " \"format_instructions\": self.parser.get_format_instructions()\n", + " })\n", + " \n", + " return result\n", + "\n", + "\n", + "# Создаем экземпляр агента\n", + "extraction_agent = ExtractionAgent()\n", + "print(\"✅ Агент извлечения создан\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "9b7fb05f", + "metadata": {}, + "source": [ + "## Агент 2: Классификация по однозначности" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "82eec007", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✅ Агент классификации создан\n" + ] + } + ], + "source": [ + "class ClassificationAgent:\n", + " \"\"\"Агент для классификации новостного поста по однозначности поиска\"\"\"\n", + " \n", + " def __init__(self, model: str = MODEL_NAME):\n", + " self.llm = create_llm(model, temperature=0.0)\n", + " self.parser = PydanticOutputParser(pydantic_object=ClassificationResult)\n", + " \n", + " self.prompt = ChatPromptTemplate.from_messages([\n", + " (\"system\", \"\"\"Ты - эксперт по классификации новостного контента для поисковых систем.\n", + "\n", + "Твоя задача - определить, является ли новостной пост ОДНОЗНАЧНЫМ или НЕОДНОЗНАЧНЫМ для поиска.\n", + "\n", + "## Критерии ОДНОЗНАЧНОГО контента (is_unambiguous=True):\n", + "- Конкретные факты с точными датами и цифрами (\"Apple выпустила iPhone 17 15 сентября 2025\")\n", + "- Уникальные события, которые произошли один раз\n", + "- Официальные решения, законы, назначения\n", + "- Результаты спортивных событий, выборов\n", + "- Финансовые показатели за конкретный период\n", + "\n", + "## Критерии НЕОДНОЗНАЧНОГО контента (is_unambiguous=False):\n", + "- Высказывания и мнения, которые могут меняться со временем\n", + "- Позиции персон по вопросам (\"персона А заявила о персоне Б\")\n", + "- Прогнозы и ожидания\n", + "- События без точной привязки ко времени\n", + "- Темы, где возможны противоречивые источники\n", + "\n", + "## Сложность поиска:\n", + "- easy: Уникальный факт, легко найти один правильный ответ\n", + "- medium: Требует временного/контекстного уточнения\n", + "- hard: Высокая вероятность найти противоречивые ответы\n", + "\n", + "{format_instructions}\"\"\"),\n", + " (\"human\", \"\"\"Проклассифицируй следующий новостной контент:\n", + "\n", + "## Извлечённая основная мысль:\n", + "- Основной факт/утверждение: {main_fact}\n", + "\n", + "Определи, является ли этот контент однозначным для поиска.\"\"\")\n", + " ])\n", + " \n", + " def classify(self, original_text: str, main_message: MainMessage) -> ClassificationResult:\n", + " \"\"\"Классифицирует контент по однозначности поиска\"\"\"\n", + " chain = self.prompt | self.llm | self.parser\n", + " \n", + " result = chain.invoke({\n", + " \"main_fact\": main_message.main_fact_or_statement,\n", + " \"format_instructions\": self.parser.get_format_instructions()\n", + " })\n", + " \n", + " return result\n", + "\n", + "\n", + "# Создаем экземпляр агента\n", + "classification_agent = ClassificationAgent()\n", + "print(\"✅ Агент классификации создан\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "f14beb72", + "metadata": {}, + "source": [ + "## Полный пайплайн" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "321233be", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✅ Пайплайн создан и готов к работе\n" + ] + } + ], + "source": [ + "class NewsClassificationPipeline:\n", + " \"\"\"Полный пайплайн классификации новостных постов\"\"\"\n", + " \n", + " def __init__(self, model: str = MODEL_NAME):\n", + " self.extraction_agent = ExtractionAgent(model)\n", + " self.classification_agent = ClassificationAgent(model)\n", + " \n", + " def process(self, text: str) -> PipelineResult:\n", + " \"\"\"Обрабатывает один новостн��й пост\"\"\"\n", + " # Шаг 1: Извлечение основной мысли\n", + " main_message = self.extraction_agent.extract(text)\n", + " \n", + " # Шаг 2: Классификация\n", + " classification = self.classification_agent.classify(text, main_message)\n", + " \n", + " return PipelineResult(\n", + " original_text=text,\n", + " main_message=main_message,\n", + " classification=classification\n", + " )\n", + " \n", + " def process_batch(self, texts: list[str], show_progress: bool = True) -> list[PipelineResult]:\n", + " \"\"\"Обрабатывает список постов\"\"\"\n", + " results = []\n", + " iterator = tqdm(texts, desc=\"Обработка постов\") if show_progress else texts\n", + " \n", + " for text in iterator:\n", + " try:\n", + " result = self.process(text)\n", + " results.append(result)\n", + " except Exception as e:\n", + " print(f\"Ошибка при обработке: {e}\")\n", + " results.append(None)\n", + " \n", + " return results\n", + "\n", + "\n", + "# Создаем пайплайн\n", + "pipeline = NewsClassificationPipeline()\n", + "print(\"✅ Пайплайн создан и готов к работе\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "50013fda", + "metadata": {}, + "source": [ + "## Демонстрация работы пайплайна\n" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "45f07523", + "metadata": {}, + "outputs": [], + "source": [ + "# Примеры для тестирования\n", + "test_posts = [\n", + " # Пример однозначного факта\n", + " \"\"\"▪️Apple представила iPhone 17 на презентации 10 сентября 2025 года. \n", + " Новый смартфон получил процессор A19 Bionic и камеру на 200 мегапикселей. \n", + " Цена в России начинается от 129 990 рублей.\"\"\",\n", + " \n", + " # Пример неоднозначного высказывания\n", + " \"\"\"▪️Путин заявил о готовности к переговорам по Украине.\n", + " «Мы всегда открыты к диалогу», – подчеркнул президент на встрече с журналистами.\n", + " При этом он отметил, что условия для переговоров должны учитывать интересы России.\"\"\",\n", + " \n", + " # Пример события с контекстом\n", + " \"\"\"▪️Роскомнадзор сообщил об ограничении звонков через Telegram и WhatsApp.\n", + " «По данным правоохранительных органов, иностранные мессенджеры стали основными \n", + " голосовыми сервисами для обмана граждан», – пояснили в пресс-службе ведомства.\"\"\",\n", + " \n", + " # Пример финансовой новости\n", + " \"\"\"▪️Индекс Мосбиржи упал на 3,2% по итогам торгов 13 марта 2025 года.\n", + " Основными аутсайдерами стали акции Сбербанка (-4,5%) и Газпрома (-3,8%).\n", + " Аналитики связывают падение с геополитической напряжённостью.\"\"\"\n", + "]\n" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "f755fa7a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "================================================================================\n", + "📰 ПОСТ #1\n", + "================================================================================\n", + "▪️Apple представила iPhone 17 на презентации 10 сентября 2025 года. \n", + " Новый смартфон получил процессор A19 Bionic и камеру на 200 мегапикселей. \n", + " Цена в России начинается от 129 990 рублей.\n", + "\n", + "📋 ОСНОВНАЯ МЫСЛЬ:\n", + " Тема: Презентация iPhone 17\n", + " Сущности: Apple, iPhone 17, 10 сентября 2025 года, A19 Bionic, 200 мегапикселей, 129 990 рублей\n", + " Факт: Apple представила iPhone 17 10 сентября 2025 года с процессором A19 Bionic, камерой на 200 мегапикселей и ценой в России от 129 990 рублей.\n", + "\n", + "🎯 КЛАССИФИКАЦИЯ:\n", + " Статус: ✅ ОДНОЗНАЧНЫЙ\n", + " Уверенность: 98%\n", + " Категория: fact\n", + " Сложность поиска: easy\n", + " Обоснование: Представленный контент содержит конкретные, измеримые и объективные факты: точная дата презентации (10 сентября 2025 года), конкретная модель устройства (iPhone 17), точные технические характеристики (процессор A19 Bionic, камера 200 МП) и четко указанная цена в рублях (от 129 990). Все эти данные являются однозначными и не подвержены интерпретации. Такой набор параметров легко проверяется через официальные источники Apple, пресс-релизы или архивы новостей, что делает поиск однозначным и не оставляет места для противоречивых толкований. Следовательно, это чистый факт с низкой сложностью поиска.\n", + "\n", + "================================================================================\n", + "📰 ПОСТ #2\n", + "================================================================================\n", + "▪️Путин заявил о готовности к переговорам по Украине.\n", + " «Мы всегда открыты к диалогу», – подчеркнул президент на встрече с журналистами.\n", + " При этом он отметил, что условия для переговоров должны у...\n", + "\n", + "📋 ОСНОВНАЯ МЫСЛЬ:\n", + " Тема: Готовность России к переговорам по Украине\n", + " Сущности: Владимир Путин, Россия, Украина\n", + " Факт: Путин заявил, что Россия открыта к переговорам по Украине, но только при условии, что они будут учитывать интересы России.\n", + "\n", + "🎯 КЛАССИФИКАЦИЯ:\n", + " Статус: ⚠️ НЕОДНОЗНАЧНЫЙ\n", + " Уверенность: 95%\n", + " Категория: statement\n", + " Сложность поиска: medium\n", + " Причины неоднозначности: Заявление может меняться со временем в зависимости от политической ситуации, Могут существовать различные интерпретации формулировки 'учитывать интересы России', Возможны противоречивые источники: официальные трансляции vs. переводы и комментарии западных СМИ, Не указано конкретное время заявления — может относиться к разным выступлениям Путина\n", + " Обоснование: Заявление Владимира Путина о готовности России к переговорам по Украине при условии учёта российских интересов является политическим высказыванием, а не фиксированным фактом. Такие заявления носят условный и изменчивый характер — они могут быть переформулированы, дополнены или отменены в будущем. Кроме того, фраза 'учитывать интересы России' является политически многозначной и может интерпретироваться по-разному в зависимости от источника. Отсутствие точной даты заявления усложняет идентификацию конкретного события. Хотя само высказывание может быть найдено в архивах, его смысл и контекст подвержены субъективной интерпретации, что делает поиск неоднозначным. Сложность поиска — средняя, так как требует уточнения времени и контекста для точной идентификации.\n", + "\n", + "================================================================================\n", + "📰 ПОСТ #3\n", + "================================================================================\n", + "▪️Роскомнадзор сообщил об ограничении звонков через Telegram и WhatsApp.\n", + " «По данным правоохранительных органов, иностранные мессенджеры стали основными \n", + " голосовыми сервисами для обмана граждан...\n", + "\n", + "📋 ОСНОВНАЯ МЫСЛЬ:\n", + " Тема: Ограничение голосовых звонков через Telegram и WhatsApp в России\n", + " Сущности: Роскомнадзор, Telegram, WhatsApp, иностранные мессенджеры\n", + " Факт: Роскомнадзор сообщил об ограничении голосовых звонков через Telegram и WhatsApp, поскольку эти сервисы стали основными инструментами для мошенничества с гражданами, согласно данным правоохранительных органов.\n", + "\n", + "🎯 КЛАССИФИКАЦИЯ:\n", + " Статус: ✅ ОДНОЗНАЧНЫЙ\n", + " Уверенность: 95%\n", + " Категория: fact\n", + " Сложность поиска: easy\n", + " Обоснование: Контент содержит конкретный официальный факт: Роскомнадзор сообщил о введении ограничений на голосовые звонки в Telegram и WhatsApp на основании данных правоохранительных органов. Это не мнение или прогноз, а зафиксированное административное действие, связанное с официальным заявлением государственного органа. Такое решение подлежит проверке через официальные источники Роскомнадзора, СМИ и протоколы заседаний, и не допускает множества интерпретаций. Даже если детали реализации могут меняться, само сообщение о введении ограничений — это однозначный, однократный факт, который можно однозначно подтвердить или опровергнуть. Поэтому поиск по этому запросу является простым (easy), так как существует один проверяемый ответ.\n", + "\n", + "================================================================================\n", + "📰 ПОСТ #4\n", + "================================================================================\n", + "▪️Индекс Мосбиржи упал на 3,2% по итогам торгов 13 марта 2025 года.\n", + " Основными аутсайдерами стали акции Сбербанка (-4,5%) и Газпрома (-3,8%).\n", + " Аналитики связывают падение с геополитической напря...\n", + "\n", + "📋 ОСНОВНАЯ МЫСЛЬ:\n", + " Тема: Падение индекса Мосбиржи на фоне геополитической напряжённости\n", + " Сущности: Мосбиржа, 13 марта 2025 года, Сбербанк, Газпром, геополитическая напряжённость\n", + " Факт: Индекс Мосбиржи упал на 3,2% 13 марта 2025 года, причём акции Сбербанка и Газпрома показали наиболее значительные потери — на 4,5% и 3,8% соответственно — что аналитики связывают с ростом геополитической напряжённости.\n", + "\n", + "🎯 КЛАССИФИКАЦИЯ:\n", + " Статус: ✅ ОДНОЗНАЧНЫЙ\n", + " Уверенность: 98%\n", + " Категория: fact\n", + " Сложность поиска: easy\n", + " Обоснование: Контент содержит конкретные, измеримые и временно привязанные факты: точная дата (13 марта 2025 года), точные процентные изменения индекса Мосбиржи (−3,2%) и акций двух компаний (Сбербанк −4,5%, Газпром −3,8%). Эти данные являются объективными рыночными показателями, которые фиксируются биржевыми систем��ми и публикуются официальными источниками. Хотя упоминается причина (рост геополитической напряжённости), она является общепринятой аналитической интерпретацией, не оспаривающей сам факт падения индекса и акций. Все числовые данные однозначны и могут быть проверены через архивы биржи или финансовые новостные агентства, что делает поиск простым и неоднозначным ответом не допускает.\n" + ] + } + ], + "source": [ + "# Обработка тестовых примеров\n", + "results = []\n", + "\n", + "for i, post in enumerate(test_posts, 1):\n", + " print(f\"\\n{'='*80}\")\n", + " print(f\"📰 ПОСТ #{i}\")\n", + " print(f\"{'='*80}\")\n", + " print(post[:200] + \"...\" if len(post) > 200 else post)\n", + " \n", + " result = pipeline.process(post)\n", + " results.append(result)\n", + " \n", + " print(f\"\\n📋 ОСНОВНАЯ МЫСЛЬ:\")\n", + " print(f\" Тема: {result.main_message.main_topic}\")\n", + " print(f\" Сущности: {', '.join(result.main_message.key_entities)}\")\n", + " print(f\" Факт: {result.main_message.main_fact_or_statement}\")\n", + " \n", + " print(f\"\\n🎯 КЛАССИФИКАЦИЯ:\")\n", + " status = \"✅ ОДНОЗНАЧНЫЙ\" if result.classification.is_unambiguous else \"⚠️ НЕОДНОЗНАЧНЫЙ\"\n", + " print(f\" Статус: {status}\")\n", + " print(f\" Уверенность: {result.classification.confidence:.0%}\")\n", + " print(f\" Категория: {result.classification.category}\")\n", + " print(f\" Сложность поиска: {result.classification.search_difficulty}\")\n", + " if result.classification.ambiguity_reasons:\n", + " print(f\" Причины неоднозначности: {', '.join(result.classification.ambiguity_reasons)}\")\n", + " print(f\" Обоснование: {result.classification.reasoning}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "4a66b86d", + "metadata": {}, + "source": [ + "## Преобразование результатов в DataFrame\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "b4d69cbc", + "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", + " \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", + "
original_textmain_topickey_entitiesmain_factis_unambiguousconfidencecategorysearch_difficultyambiguity_reasons
0Российские войска применяют модернизированные ...Модернизация российских авиационных бомб и бал...Российские войска, Корректируемые планирующие ...Россия модернизировала советские КАБ, добавив ...False0.40mixedhardОтсутствие независимых подтверждений данных о ...
1Ким Чен Ын на своем спецпоезде отправился в Пе...Поездка Ким Чен Ына в Пекин для участия в воен...Ким Чен Ын, Пекин, Вторая мировая война, 80-ле...Ким Чен Ын совершил первую за год зарубежную п...False0.75statementmediumУтверждение о 'подчеркивании укрепления связей...
2В министерстве обороны Камбоджи отрицают наруш...Оспаривание обвинений в нарушении режима прекр...Министерство обороны Камбоджи, Тайская сторона...Министерство обороны Камбоджи отрицает обвинен...False0.85statementhardОбвинения Таиланда и опровержение Камбоджи пре...
3⬜⬜⬜⬜⬜⬜ План Трампа – это хорошая основа для ра...Позиция России по урегулированию украинского к...Дмитрий Песков, Дональд Трамп, Россия, Украина...Дмитрий Песков заявил, что план Трампа являетс...False0.95statementhardЗаявление Пескова отражает официальную позицию...
4Ограничения полетов ввели в аэропорту Ульяновс...Ограничения полетов в аэропортах нескольких го...Росавиация, Ульяновск, Пенза, Нижний Новгород,...В аэропортах Ульяновска, Пензы, Нижнего Новгор...False0.75eventmediumОтсутствует точная дата и время введения огран...
..............................
294Импортируемые в Россию автомобили предложили о...Предложение обязать импортируемые в Россию авт...Россия, Концепция развития телерадиовещания до...Российские радиохолдинги предлагают включить в...False0.85statementmediumПредложение ещё не принято и не включено в офи...
295По планам Банка России, массовое внедрение циф...Внедрение цифрового рубля в РоссииБанк России, цифровой рубль, сентябрь 2026 год...Массовое внедрение цифрового рубля в России на...True0.95eventmedium
296В Турции в Гебзе обрушился многоэтажный дом. П...Обрушение многоэтажного дома в Гебзе, ТурцияТурция, Гебзе, Зиннура Бюйюкгеза, NTV, IHA, ме...В Гебзе, Турция, обрушился семиэтажный дом, по...True0.95eventeasy
297Современный городской квартал сегодня уже дале...Создание жилого квартала с развитой инфраструк...Soul, Часовая улица, метро «Аэропорт», Forma, ...Девелопер Forma создает жилой квартал Soul на ...True0.95eventeasy
298Сейчас не время просить российского президента...Отношение Дональда Трампа к запросам о прекращ...Дональд Трамп, Владимир Путин, Россия, Air For...Дональд Трамп считает, что сейчас не время про...False0.95statementhardВысказывание отражает личное мнение Дональда Т...
\n", + "

299 rows × 9 columns

\n", + "
" + ], + "text/plain": [ + " original_text \\\n", + "0 Российские войска применяют модернизированные ... \n", + "1 Ким Чен Ын на своем спецпоезде отправился в Пе... \n", + "2 В министерстве обороны Камбоджи отрицают наруш... \n", + "3 ⬜⬜⬜⬜⬜⬜ План Трампа – это хорошая основа для ра... \n", + "4 Ограничения полетов ввели в аэропорту Ульяновс... \n", + ".. ... \n", + "294 Импортируемые в Россию автомобили предложили о... \n", + "295 По планам Банка России, массовое внедрение циф... \n", + "296 В Турции в Гебзе обрушился многоэтажный дом. П... \n", + "297 Современный городской квартал сегодня уже дале... \n", + "298 Сейчас не время просить российского президента... \n", + "\n", + " main_topic \\\n", + "0 Модернизация российских авиационных бомб и бал... \n", + "1 Поездка Ким Чен Ына в Пекин для участия в воен... \n", + "2 Оспаривание обвинений в нарушении режима прекр... \n", + "3 Позиция России по урегулированию украинского к... \n", + "4 Ограничения полетов в аэропортах нескольких го... \n", + ".. ... \n", + "294 Предложение обязать импортируемые в Россию авт... \n", + "295 Внедрение цифрового рубля в России \n", + "296 Обрушение многоэтажного дома в Гебзе, Турция \n", + "297 Создание жилого квартала с развитой инфраструк... \n", + "298 Отношение Дональда Трампа к запросам о прекращ... \n", + "\n", + " key_entities \\\n", + "0 Российские войска, Корректируемые планирующие ... \n", + "1 Ким Чен Ын, Пекин, Вторая мировая война, 80-ле... \n", + "2 Министерство обороны Камбоджи, Тайская сторона... \n", + "3 Дмитрий Песков, Дональд Трамп, Россия, Украина... \n", + "4 Росавиация, Ульяновск, Пенза, Нижний Новгород,... \n", + ".. ... \n", + "294 Россия, Концепция развития телерадиовещания до... \n", + "295 Банк России, цифровой рубль, сентябрь 2026 год... \n", + "296 Турция, Ге��зе, Зиннура Бюйюкгеза, NTV, IHA, ме... \n", + "297 Soul, Часовая улица, метро «Аэропорт», Forma, ... \n", + "298 Дональд Трамп, Владимир Путин, Россия, Air For... \n", + "\n", + " main_fact is_unambiguous \\\n", + "0 Россия модернизировала советские КАБ, добавив ... False \n", + "1 Ким Чен Ын совершил первую за год зарубежную п... False \n", + "2 Министерство обороны Камбоджи отрицает обвинен... False \n", + "3 Дмитрий Песков заявил, что план Трампа являетс... False \n", + "4 В аэропортах Ульяновска, Пензы, Нижнего Новгор... False \n", + ".. ... ... \n", + "294 Российские радиохолдинги предлагают включить в... False \n", + "295 Массовое внедрение цифрового рубля в России на... True \n", + "296 В Гебзе, Турция, обрушился семиэтажный дом, по... True \n", + "297 Девелопер Forma создает жилой квартал Soul на ... True \n", + "298 Дональд Трамп считает, что сейчас не время про... False \n", + "\n", + " confidence category search_difficulty \\\n", + "0 0.40 mixed hard \n", + "1 0.75 statement medium \n", + "2 0.85 statement hard \n", + "3 0.95 statement hard \n", + "4 0.75 event medium \n", + ".. ... ... ... \n", + "294 0.85 statement medium \n", + "295 0.95 event medium \n", + "296 0.95 event easy \n", + "297 0.95 event easy \n", + "298 0.95 statement hard \n", + "\n", + " ambiguity_reasons \n", + "0 Отсутствие независимых подтверждений данных о ... \n", + "1 Утверждение о 'подчеркивании укрепления связей... \n", + "2 Обвинения Таиланда и опровержение Камбоджи пре... \n", + "3 Заявление Пескова отражает официальную позицию... \n", + "4 Отсутствует точная дата и время введения огран... \n", + ".. ... \n", + "294 Предложение ещё не принято и не включено в офи... \n", + "295 \n", + "296 \n", + "297 \n", + "298 Высказывание отражает личное мнение Дональда Т... \n", + "\n", + "[299 rows x 9 columns]" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def results_to_dataframe(results: list[PipelineResult]) -> pd.DataFrame:\n", + " \"\"\"Преобразует результаты в pandas DataFrame\"\"\"\n", + " rows = []\n", + " \n", + " for r in results:\n", + " if r is None:\n", + " continue\n", + " \n", + " rows.append({\n", + " \"original_text\": r.original_text,\n", + " \"main_topic\": r.main_message.main_topic,\n", + " \"key_entities\": \", \".join(r.main_message.key_entities),\n", + " \"main_fact\": r.main_message.main_fact_or_statement,\n", + " \"is_unambiguous\": r.classification.is_unambiguous,\n", + " \"confidence\": r.classification.confidence,\n", + " \"category\": r.classification.category,\n", + " \"search_difficulty\": r.classification.search_difficulty,\n", + " \"ambiguity_reasons\": \", \".join(r.classification.ambiguity_reasons),\n", + " })\n", + " \n", + " return pd.DataFrame(rows)\n", + "\n", + "\n", + "df_results = results_to_dataframe(results)\n", + "df_results\n" + ] + }, + { + "cell_type": "markdown", + "id": "eefcb6da", + "metadata": {}, + "source": [ + "## Применение к реал��ным данным\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "adbc18e0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Загружено 4847 постов\n", + "Период: 2025-04-15 00:00:00 - 2025-12-03 00:00:00\n" + ] + }, + { + "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", + "
message_dtmessage_idchannel_idcontentviewsoriginal_author
48462025-04-15116039rbc_newsПервоклассники не должны заниматься уроками бо...156002.0NaN
48222025-04-15116076rbc_newsКоманда Трампа рассматривает варианты ужесточе...117161.0NaN
48212025-04-15116077rbc_newsНа эмблему Минспорта вернулись христианские кр...109451.0rbc_sport
48202025-04-15116078rbc_newsВ Китае в расцвете сил умерли несколько учёных...134105.0NaN
48182025-04-15116080rbc_news«Яндекс» договаривается с операторами систем «...112462.0NaN
\n", + "
" + ], + "text/plain": [ + " message_dt message_id channel_id \\\n", + "4846 2025-04-15 116039 rbc_news \n", + "4822 2025-04-15 116076 rbc_news \n", + "4821 2025-04-15 116077 rbc_news \n", + "4820 2025-04-15 116078 rbc_news \n", + "4818 2025-04-15 116080 rbc_news \n", + "\n", + " content views \\\n", + "4846 Первоклассники не должны заниматься уроками бо... 156002.0 \n", + "4822 Команда Трампа рассматривает варианты ужесточе... 117161.0 \n", + "4821 На эмблему Минспорта вернулись христианские кр... 109451.0 \n", + "4820 В Китае в расцвете сил умерли несколько учёных... 134105.0 \n", + "4818 «Яндекс» договаривается с операторами систем «... 112462.0 \n", + "\n", + " original_author \n", + "4846 NaN \n", + "4822 NaN \n", + "4821 rbc_sport \n", + "4820 NaN \n", + "4818 NaN " + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Загрузка реальных данных\n", + "data = pd.read_csv('src/dataset/rbc/channel_rbc_news_posts.csv')\n", + "data[\"message_dt\"] = pd.to_datetime(data[\"message_dt\"])\n", + "data = data.sort_values(\"message_dt\")\n", + "\n", + "print(f\"Загружено {len(data)} постов\")\n", + "print(f\"Период: {data['message_dt'].min()} - {data['message_dt'].max()}\")\n", + "data.head()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "81facdd5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Обрабатываем 300 постов...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Обработка постов: 0%| | 0/300 [00:00\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", + " \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", + "
original_textmain_topickey_entitiesmain_factis_unambiguousconfidencecategorysearch_difficultyambiguity_reasons
0Российские войска применяют модернизированные ...Модернизация российских авиационных бомб и бал...Российские войска, Корректируемые планирующие ...Россия модернизировала советские КАБ, добавив ...False0.40mixedhardОтсутствие независимых подтверждений данных о ...
1Ким Чен Ын на своем спецпоезде отправился в Пе...Поездка Ким Чен Ына в Пекин для участия в воен...Ким Чен Ын, Пекин, Вторая мировая война, 80-ле...Ким Чен Ын совершил первую за год зарубежную п...False0.75statementmediumУтверждение о 'подчеркивании укрепления связей...
2В министерстве обороны Камбоджи отрицают наруш...Оспаривание обвинений в нарушении режима прекр...Министерство обороны Камбоджи, Тайская сторона...Министерство обороны Камбоджи отрицает обвинен...False0.85statementhardОбвинения Таиланда и опровержение Камбоджи пре...
3⬜⬜⬜⬜⬜⬜ План Трампа – это хорошая основа для ра...Позиция России по урегулированию украинского к...Дмитрий Песков, Дональд Трамп, Россия, Украина...Дмитрий Песков заявил, что план Трампа являетс...False0.95statementhardЗаявление Пескова отражает официальную позицию...
4Ограничения полетов ввели в аэропорту Ульяновс...Ограничения полетов в аэропортах нескольких го...Росавиация, Ульяновск, Пенза, Нижний Новгород,...В аэропортах Ульяновска, Пензы, Нижнего Новгор...False0.75eventmediumОтсутствует точная дата и время введения огран...
..............................
294Импортируемые в Россию автомобили предложили о...Предложение обязать импортируемые в Россию авт...Россия, Концепция развития телерадиовещания до...Российские радиохолдинги предлагают включить в...False0.85statementmediumПредложение ещё не принято и не включено в офи...
295По планам Банка России, массовое внедрение циф...Внедрение цифрового рубля в РоссииБанк России, цифровой рубль, сентябрь 2026 год...Массовое внедрение цифрового рубля в России на...True0.95eventmedium
296В Турции в Гебзе обрушился многоэтажный дом. П...Обрушение многоэтажного дома в Гебзе, ТурцияТурция, Гебзе, Зиннура Бюйюкгеза, NTV, IHA, ме...В Гебзе, Турция, обрушился семиэтажный дом, по...True0.95eventeasy
297Современный городской квартал сегодня уже дале...Создание жилого квартала с развитой инфраструк...Soul, Часовая улица, метро «Аэропорт», Forma, ...Девелопер Forma создает жилой квартал Soul на ...True0.95eventeasy
298Сейчас не время просить российского президента...Отношение Дональда Трампа к запросам о прекращ...Дональд Трамп, Владимир Путин, Россия, Air For...Дональд Трамп считает, что сейчас не время про...False0.95statementhardВысказывание отражает личное мнение Дональда Т...
\n", + "

299 rows × 9 columns

\n", + "" + ], + "text/plain": [ + " original_text \\\n", + "0 Российские войска применяют модернизированные ... \n", + "1 Ким Чен Ын на своем спецпоезде отправился в Пе... \n", + "2 В министерстве обороны Камбоджи отрицают наруш... \n", + "3 ⬜⬜⬜⬜⬜⬜ План Трампа – это хорошая основа для ра... \n", + "4 Ограничения полетов ввели в аэропо��ту Ульяновс... \n", + ".. ... \n", + "294 Импортируемые в Россию автомобили предложили о... \n", + "295 По планам Банка России, массовое внедрение циф... \n", + "296 В Турции в Гебзе обрушился многоэтажный дом. П... \n", + "297 Современный городской квартал сегодня уже дале... \n", + "298 Сейчас не время просить российского президента... \n", + "\n", + " main_topic \\\n", + "0 Модернизация российских авиационных бомб и бал... \n", + "1 Поездка Ким Чен Ына в Пекин для участия в воен... \n", + "2 Оспаривание обвинений в нарушении режима прекр... \n", + "3 Позиция России по урегулированию украинского к... \n", + "4 Ограничения полетов в аэропортах нескольких го... \n", + ".. ... \n", + "294 Предложение обязать импортируемые в Россию авт... \n", + "295 Внедрение цифрового рубля в России \n", + "296 Обрушение многоэтажного дома в Гебзе, Турция \n", + "297 Создание жилого квартала с развитой инфраструк... \n", + "298 Отношение Дональда Трампа к запросам о прекращ... \n", + "\n", + " key_entities \\\n", + "0 Российские войска, Корректируемые планирующие ... \n", + "1 Ким Чен Ын, Пекин, Вторая мировая война, 80-ле... \n", + "2 Министерство обороны Камбоджи, Тайская сторона... \n", + "3 Дмитрий Песков, Дональд Трамп, Россия, Украина... \n", + "4 Росавиация, Ульяновск, Пенза, Нижний Новгород,... \n", + ".. ... \n", + "294 Россия, Концепция развития телерадиовещания до... \n", + "295 Банк России, цифровой рубль, сентябрь 2026 год... \n", + "296 Турция, Гебзе, Зиннура Бюйюкгеза, NTV, IHA, ме... \n", + "297 Soul, Часовая улица, метро «Аэропорт», Forma, ... \n", + "298 Дональд Трамп, Владимир Путин, Россия, Air For... \n", + "\n", + " main_fact is_unambiguous \\\n", + "0 Россия модернизировала советские КАБ, добавив ... False \n", + "1 Ким Чен Ын совершил первую за год зарубежную п... False \n", + "2 Министерство обороны Камбоджи отрицает обвинен... False \n", + "3 Дмитрий Песков заявил, что план Трампа являетс... False \n", + "4 В аэропортах Ульяновска, Пензы, Нижнего Новгор... False \n", + ".. ... ... \n", + "294 Российские радиохолдинги предлагают включить в... False \n", + "295 Массовое внедрение цифрового рубля в России на... True \n", + "296 В Гебзе, Турция, обрушился семиэтажный дом, по... True \n", + "297 Девелопер Forma создает жилой квартал Soul на ... True \n", + "298 Дональд Трамп считает, что сейчас не время про... False \n", + "\n", + " confidence category search_difficulty \\\n", + "0 0.40 mixed hard \n", + "1 0.75 statement medium \n", + "2 0.85 statement hard \n", + "3 0.95 statement hard \n", + "4 0.75 event medium \n", + ".. ... ... ... \n", + "294 0.85 statement medium \n", + "295 0.95 event medium \n", + "296 0.95 event easy \n", + "297 0.95 event easy \n", + "298 0.95 statement hard \n", + "\n", + " ambiguity_reasons \n", + "0 Отсутствие независимых подтверждений данных о ... \n", + "1 Утверждение о 'подчеркивании укрепления связей... \n", + "2 Обвинения Таиланда и опровержение Камбоджи пре... \n", + "3 Заявление Пескова отражает официальную позицию... \n", + "4 Отсутствует точная дата и время введения огран... \n", + ".. ... \n", + "294 Предложение ещё не принято и не включено в офи... \n", + "295 \n", + "296 \n", + "297 \n", + "298 Высказывание отражает личное мнение Дональда Т... \n", + "\n", + "[299 rows x 9 columns]" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df_sample_results" + ] + }, + { + "cell_type": "markdown", + "id": "c407f477", + "metadata": {}, + "source": [ + "## Утилиты для интеграции\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "280b0062", + "metadata": {}, + "outputs": [], + "source": [ + "def classify_single_post(text: str, pipeline: NewsClassificationPipeline = None) -> dict:\n", + " \"\"\"\n", + " Удобная функция для классификации одного поста.\n", + " Возвращает словарь с результатами.\n", + " \"\"\"\n", + " if pipeline is None:\n", + " pipeline = NewsClassificationPipeline()\n", + " \n", + " result = pipeline.process(text)\n", + " \n", + " return {\n", + " \"is_unambiguous\": result.classification.is_unambiguous,\n", + " \"confidence\": result.classification.confidence,\n", + " \"category\": result.classification.category,\n", + " \"search_difficulty\": result.classification.search_difficulty,\n", + " \"main_topic\": result.main_message.main_topic,\n", + " \"suggested_query\": result.classification.suggested_search_query,\n", + " \"reasoning\": result.classification.reasoning\n", + " }\n", + "\n", + "\n", + "# Пример использования\n", + "test_text = \"\"\"Центробанк повысил ключевую ставку до 21% годовых.\n", + "Это максимальный уровень с 2022 года.\"\"\"\n", + "\n", + "result = classify_single_post(test_text, pipeline)\n", + "print(\"Результат классификации:\")\n", + "for key, value in result.items():\n", + " print(f\" {key}: {value}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "e0d7c811", + "metadata": {}, + "outputs": [], + "source": [ + "# Пайплайн классификации новостных постов\n", + "# Два агента: извлечение основной мысли и классификация однозначности\n", + "\n", + "import os\n", + "from dotenv import load_dotenv\n", + "from pydantic import BaseModel, Field\n", + "from typing import Literal\n", + "import pandas as pd\n", + "\n", + "load_dotenv()\n", + "\n", + "# Проверка наличия API ключа\n", + "OPENROUTER_API_KEY = os.getenv(\"OPENROUTER_API_KEY\")\n", + "if not OPENROUTER_API_KEY:\n", + " raise ValueError(\"OPENROUTER_API_KEY не найден в переменных окружения\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b68c90b0", + "metadata": {}, + "outputs": [], + "source": [ + "# Установка зависимостей (если нужно)\n", + "# !pip install langchain langchain-openai langchain-core\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "b7a3b041", + "metadata": {}, + "outputs": [ + { + "ename": "ModuleNotFoundError", + "evalue": "No module named 'langchain_openai'", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mModuleNotFoundError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[3]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mlangchain_openai\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m ChatOpenAI\n\u001b[32m 2\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mlangchain_core\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mprompts\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m ChatPromptTemplate\n\u001b[32m 3\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mlangchain_core\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01moutput_parsers\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m PydanticOutputParser\n", + "\u001b[31mModuleNotFoundError\u001b[39m: No module named 'langchain_openai'" + ] + } + ], + "source": [ + "from langchain_openai import ChatOpenAI\n", + "from langchain_core.prompts import ChatPromptTemplate\n", + "from langchain_core.output_parsers import PydanticOutputParser\n", + "from langchain_core.runnables import RunnablePassthrough\n", + "from langchain_core.pydantic_v1 import BaseModel as LangChainBaseModel\n", + "from typing import List" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37727f77", + "metadata": {}, + "outputs": [], + "source": [ + "# Определение Pydantic моделей для структурированного вывода\n", + "\n", + "class MainMessage(BaseModel):\n", + " \"\"\"Основная мысль/сообщение новостного поста\"\"\"\n", + " main_topic: str = Field(\n", + " description=\"Основная тема или предмет новостного поста (например: 'Выпуск iPhone 17', 'Высказывание политика А о политике Б')\"\n", + " )\n", + " key_entities: List[str] = Field(\n", + " description=\"Ключевые сущности, упомянутые в посте (люди, организации, события, даты)\"\n", + " )\n", + " main_fact_or_statement: str = Field(\n", + " description=\"Основной факт или утверждение, содержащееся в посте\"\n", + " )\n", + " context: str = Field(\n", + " description=\"Дополнительный контекст, необходимый для понимания основной мысли\"\n", + " )\n", + "\n", + "\n", + "class ClassificationResult(BaseModel):\n", + " \"\"\"Результат классификации новостного поста\"\"\"\n", + " is_unambiguous: bool = Field(\n", + " description=\"Является ли основная тема поста однозначной при поиске. True - однозначная (факт), False - неоднозначная (могут быть противоречивые ответы)\"\n", + " )\n", + " confidence: float = Field(\n", + " description=\"Уверенность в классификации от 0.0 до 1.0\",\n", + " ge=0.0,\n", + " le=1.0\n", + " )\n", + " reasoning: str = Field(\n", + " description=\"Обоснование классификации: почему пост считается однозначным или неоднозначным\"\n", + " )\n", + " search_difficulty: Literal[\"easy\", \"medium\", \"hard\"] = Field(\n", + " description=\"Сложность поиска: easy - простой факт, medium - требует контекста, hard - неоднозначный, может иметь противоречивые ответы\"\n", + " )\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "3b336614", + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'ChatOpenAI' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mNameError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[2]\u001b[39m\u001b[32m, line 4\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;66;03m# Инициализация LLM через OpenRouter\u001b[39;00m\n\u001b[32m 2\u001b[39m \u001b[38;5;66;03m# Используем ChatOpenAI с настройкой для OpenRouter\u001b[39;00m\n\u001b[32m----> \u001b[39m\u001b[32m4\u001b[39m llm = \u001b[43mChatOpenAI\u001b[49m(\n\u001b[32m 5\u001b[39m model=\u001b[33m\"\u001b[39m\u001b[33mqwen/qwen3-235b-a22b:free\u001b[39m\u001b[33m\"\u001b[39m, \u001b[38;5;66;03m# Можно выбрать другую модель из OpenRouter\u001b[39;00m\n\u001b[32m 6\u001b[39m temperature=\u001b[32m0.3\u001b[39m,\n\u001b[32m 7\u001b[39m base_url=\u001b[33m\"\u001b[39m\u001b[33mhttps://openrouter.ai/api/v1\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 8\u001b[39m api_key=OPENROUTER_API_KEY\n\u001b[32m 9\u001b[39m )\n", + "\u001b[31mNameError\u001b[39m: name 'ChatOpenAI' is not defined" + ] + } + ], + "source": [ + "# Инициализация LLM через OpenRouter\n", + "# Используем ChatOpenAI с настройкой для OpenRouter\n", + "\n", + "llm = ChatOpenAI(\n", + " model=\"qwen/qwen3-235b-a22b:free\", # Можно выбрать другую модель из OpenRouter\n", + " temperature=0.3,\n", + " base_url=\"https://openrouter.ai/api/v1\",\n", + " api_key=OPENROUTER_API_KEY\n", + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "483f3981", + "metadata": {}, + "outputs": [], + "source": [ + "# АГЕНТ 1: Извлечение основной мысли из новостного поста\n", + "\n", + "extraction_prompt = ChatPromptTemplate.from_messages([\n", + " (\"system\", \"\"\"Ты - эксперт по анализу новостных текстов. Твоя задача - извлечь основную мысль и ключевую информацию из новостного поста.\n", + "\n", + "Извлеки:\n", + "1. Основную тему поста\n", + "2. Ключевые сущности (люди, организации, события, даты)\n", + "3. Основной факт или утверждение\n", + "4. Дополнительный контекст, необходимый для понимания\n", + "\n", + "Будь точным и лаконичным.\"\"\"),\n", + " (\"human\", \"Проанализируй следующий новостной пост и извлеки основную мысль:\\n\\n{post_content}\")\n", + "])\n", + "\n", + "# Создаем парсер для структурированного вывода\n", + "extraction_parser = PydanticOutputParser(pydantic_object=MainMessage)\n", + "\n", + "# Создаем цепочку для извлечения\n", + "extraction_chain = (\n", + " extraction_prompt \n", + " | llm \n", + " | extraction_parser\n", + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a0ee9e21", + "metadata": {}, + "outputs": [], + "source": [ + "# АГЕНТ 2: Классификация однозначности темы для поиска\n", + "\n", + "classification_prompt = ChatPromptTemplate.from_messages([\n", + " (\"system\", \"\"\"Ты - эксперт по классификации новостных постов для систем поиска информации.\n", + "\n", + "Твоя задача - определить, является ли основная тема новостного поста однозначной для поиска.\n", + "\n", + "ОДНОЗНАЧНЫЕ посты (is_unambiguous=True):\n", + "- Конкретные факты и события (например: \"Apple выпустила iPhone 17\", \"Компания X объявила о слиянии с компанией Y\")\n", + "- Четкие даты, цифры, статистика\n", + "- Конкретные действия организаций или людей в определенный момент времени\n", + "- Новости о продуктах, релизах, запусках\n", + "\n", + "НЕОДНОЗНАЧНЫЕ посты (is_unambiguous=False):\n", + "- Высказывания политиков, мнения, комментарии (например: \"Политик А высказался насчет политика Б\")\n", + "- Тексты, где позиция может меняться со временем\n", + "- Интерпретации событий, которые могут иметь разные трактовки\n", + "- Новости, где без дополнительного контекста может быть несколько противоречивых ответов\n", + "\n", + "Оцени также:\n", + "- confidence: уверенность в классификации (0.0-1.0)\n", + "- search_difficulty: easy (простой факт), medium (требует контекста), hard (неоднозначный)\n", + "\n", + "Будь внимательным и обоснуй свое решение.\"\"\"),\n", + " (\"human\", \"\"\"Классифицируй следующий новостной пост:\n", + "\n", + "Оригинальный текст поста:\n", + "{post_content}\n", + "\n", + "Извлеченная основная мысль:\n", + "Тема: {main_topic}\n", + "Сущности: {key_entities}\n", + "Факт/утверждение: {main_fact}\n", + "Контекст: {context}\n", + "\n", + "Определи, является ли тема однозначной для поиска.\"\"\")\n", + "])\n", + "\n", + "# Создаем парсер для классификации\n", + "classification_parser = PydanticOutputParser(pydantic_object=ClassificationResult)\n", + "\n", + "# Создаем цепочку для классификации\n", + "classification_chain = (\n", + " classification_prompt \n", + " | llm \n", + " | classification_parser\n", + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b0bbad4b", + "metadata": {}, + "outputs": [], + "source": [ + "# Объединенный пайплайн: извлечение + классификация\n", + "\n", + "def process_post(post_content: str):\n", + " \"\"\"\n", + " Обрабатывает новостной пост через два агента:\n", + " 1. Извлекает основную мысль\n", + " 2. Классифицирует однозначность\n", + " \n", + " Args:\n", + " post_content: Текст новостного поста\n", + " \n", + " Returns:\n", + " dict: Словарь с результатами извлечения и классификации\n", + " \"\"\"\n", + " # Шаг 1: Извлечение основной мысли\n", + " try:\n", + " main_message = extraction_chain.invoke({\"post_content\": post_content})\n", + " except Exception as e:\n", + " print(f\"Ошибка при извлечении основной мысли: {e}\")\n", + " import traceback\n", + " traceback.print_exc()\n", + " return None\n", + " \n", + " # Шаг 2: Классификация\n", + " try:\n", + " classification = classification_chain.invoke({\n", + " \"post_content\": post_content,\n", + " \"main_topic\": main_message.main_topic,\n", + " \"key_entities\": \", \".join(main_message.key_entities),\n", + " \"main_fact\": main_message.main_fact_or_statement,\n", + " \"context\": main_message.context\n", + " })\n", + " except Exception as e:\n", + " print(f\"Ошибка при классификации: {e}\")\n", + " import traceback\n", + " traceback.print_exc()\n", + " return None\n", + " \n", + " return {\n", + " \"main_message\": main_message,\n", + " \"classification\": classification\n", + " }\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a9937d7b", + "metadata": {}, + "outputs": [], + "source": [ + "# Пример использования\n", + "\n", + "# Загружаем данные\n", + "data = pd.read_csv('src/dataset/rbc/channel_rbc_news_posts.csv')\n", + "\n", + "# Пример 1: Простой факт (однозначный)\n", + "example_1 = \"Apple выпустила iPhone 17 с новым чипом A18 Pro. Стоимость начинается от 999 долларов.\"\n", + "\n", + "# Пример 2: Неоднозначный текст (высказывание политика)\n", + "example_2 = \"Политик А высказался насчет политика Б, заявив о необходимости пересмотра текущей политики.\"\n", + "\n", + "print(\"=\" * 80)\n", + "print(\"ПРИМЕР 1: Простой факт\")\n", + "print(\"=\" * 80)\n", + "print(f\"Текст: {example_1}\\n\")\n", + "\n", + "result_1 = process_post(example_1)\n", + "if result_1:\n", + " print(\"Извлеченная основная мысль:\")\n", + " print(f\" Тема: {result_1['main_message'].main_topic}\")\n", + " print(f\" Сущности: {result_1['main_message'].key_entities}\")\n", + " print(f\" Факт: {result_1['main_message'].main_fact_or_statement}\")\n", + " print(f\"\\nКлассификация:\")\n", + " print(f\" Однозначный: {result_1['classification'].is_unambiguous}\")\n", + " print(f\" Уверенность: {result_1['classification'].confidence:.2f}\")\n", + " print(f\" Сложность поиска: {result_1['classification'].search_difficulty}\")\n", + " print(f\" Обоснование: {result_1['classification'].reasoning}\")\n", + "\n", + "print(\"\\n\" + \"=\" * 80)\n", + "print(\"ПРИМЕР 2: Неоднозначный текст\")\n", + "print(\"=\" * 80)\n", + "print(f\"Текст: {example_2}\\n\")\n", + "\n", + "result_2 = process_post(example_2)\n", + "if result_2:\n", + " print(\"Извлеченная основ��ая мысль:\")\n", + " print(f\" Тема: {result_2['main_message'].main_topic}\")\n", + " print(f\" Сущности: {result_2['main_message'].key_entities}\")\n", + " print(f\" Факт: {result_2['main_message'].main_fact_or_statement}\")\n", + " print(f\"\\nКлассификация:\")\n", + " print(f\" Однозначный: {result_2['classification'].is_unambiguous}\")\n", + " print(f\" Уверенность: {result_2['classification'].confidence:.2f}\")\n", + " print(f\" Сложность поиска: {result_2['classification'].search_difficulty}\")\n", + " print(f\" Обоснование: {result_2['classification'].reasoning}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c9de8fee", + "metadata": {}, + "outputs": [], + "source": [ + "# Обработка реальных данных из датасета\n", + "\n", + "# Выбираем несколько постов для тестирования\n", + "sample_posts = data.head(5)\n", + "\n", + "results = []\n", + "for idx, row in sample_posts.iterrows():\n", + " post_content = row['content']\n", + " print(f\"\\n{'='*80}\")\n", + " print(f\"Обработка поста {idx + 1}/{len(sample_posts)}\")\n", + " print(f\"{'='*80}\")\n", + " print(f\"Текст: {post_content[:200]}...\\n\")\n", + " \n", + " result = process_post(post_content)\n", + " if result:\n", + " results.append({\n", + " 'post_id': row.get('message_id', idx),\n", + " 'content': post_content,\n", + " 'main_topic': result['main_message'].main_topic,\n", + " 'key_entities': result['main_message'].key_entities,\n", + " 'is_unambiguous': result['classification'].is_unambiguous,\n", + " 'confidence': result['classification'].confidence,\n", + " 'search_difficulty': result['classification'].search_difficulty,\n", + " 'reasoning': result['classification'].reasoning\n", + " })\n", + " print(f\"✓ Результат: Однозначный={result['classification'].is_unambiguous}, \"\n", + " f\"Уверенность={result['classification'].confidence:.2f}, \"\n", + " f\"Сложность={result['classification'].search_difficulty}\")\n", + " else:\n", + " print(\"✗ Ошибка при обработке\")\n", + " \n", + " print()\n", + "\n", + "# Создаем DataFrame с результатами\n", + "if results:\n", + " results_df = pd.DataFrame(results)\n", + " print(\"\\n\" + \"=\"*80)\n", + " print(\"СВОДКА РЕЗУЛЬТАТОВ\")\n", + " print(\"=\"*80)\n", + " print(results_df[['post_id', 'main_topic', 'is_unambiguous', 'confidence', 'search_difficulty']])\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7e41ac98", + "metadata": {}, + "outputs": [], + "source": [ + "# Функция для пакетной обработки\n", + "\n", + "def process_batch(posts: list, batch_size: int = 10):\n", + " \"\"\"\n", + " Обрабатывает список постов пакетами\n", + " \n", + " Args:\n", + " posts: Список текстов постов\n", + " batch_size: Размер пакета\n", + " \n", + " Returns:\n", + " list: Список результатов обработки\n", + " \"\"\"\n", + " all_results = []\n", + " \n", + " for i in range(0, len(posts), batch_size):\n", + " batch = posts[i:i+batch_size]\n", + " print(f\"Обработка пакета {i//batch_size + 1}/{(len(posts)-1)//batch_size + 1}\")\n", + " \n", + " for post in batch:\n", + " result = process_post(post)\n", + " if result:\n", + " all_results.append(result)\n", + " \n", + " return all_results\n", + "\n", + "# Пример использования пакетной обработки\n", + "# batch_results = process_batch(data['content'].head(20).tolist(), batch_size=5)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "98ac70d8", + "metadata": {}, + "outputs": [], + "source": [ + "# Визуализация результатов классификации\n", + "\n", + "import matplotlib.pyplot as plt\n", + "\n", + "if results:\n", + " results_df = pd.DataFrame(results)\n", + " \n", + " # График распределения по однозначности\n", + " fig, axes = plt.subplots(1, 2, figsize=(12, 5))\n", + " \n", + " # Распределение однозначных/неоднозначных\n", + " unambiguous_counts = results_df['is_unambiguous'].value_counts()\n", + " axes[0].bar(['Неоднозначные', 'Однозначные'], \n", + " [unambiguous_counts.get(False, 0), unambiguous_counts.get(True, 0)],\n", + " color=['orange', 'green'])\n", + " axes[0].set_title('Распределение по однозначности')\n", + " axes[0].set_ylabel('Количество постов')\n", + " \n", + " # Распределение по сложности поиска\n", + " difficulty_counts = results_df['search_difficulty'].value_counts()\n", + " axes[1].bar(difficulty_counts.index, difficulty_counts.values, \n", + " color=['green', 'yellow', 'red'])\n", + " axes[1].set_title('Распределение по сложности поиска')\n", + " axes[1].set_ylabel('Количество постов')\n", + " axes[1].set_xlabel('Сложность')\n", + " \n", + " plt.tight_layout()\n", + " plt.show()\n", + " \n", + " # Статистика по уверенности\n", + " print(\"\\nСтатистика по уверенности:\")\n", + " print(results_df['confidence'].describe())\n" + ] + }, + { + "cell_type": "markdown", + "id": "49962e02", + "metadata": {}, + "source": [ + "# Описание пайплайна\n", + "\n", + "Этот пайплайн состоит из двух агентов, построенных на LangChain:\n", + "\n", + "1. **Агент извлечения основной мысли** - вычленяет основную мысль/сообщение из новостного поста\n", + " - Извлекает тему, ключевые сущности, основной факт и контекст\n", + " \n", + "2. **Агент классификации** - определяет, является ли тема поста однозначной для поиска\n", + " - Классифицирует на однозначные/неоднозначные\n", + " - Оценивает уверенность и сложность поиска\n", + " - Предоставляет обоснование\n", + "\n", + "Оба агента используют структурированный вывод через Pydantic модели и LLM из OpenRouter.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21c69837", + "metadata": {}, + "outputs": [], + "source": [ + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "011ae2c6", + "metadata": {}, + "outputs": [], + "source": [ + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "54eac0d6", + "metadata": {}, + "source": [ + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c9a44014", + "metadata": {}, + "outputs": [], + "source": [ + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "7b5b2c5d", + "metadata": {}, + "source": [ + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6a0b5264", + "metadata": {}, + "outputs": [], + "source": [ + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7b2e1a1a", + "metadata": {}, + "outputs": [], + "source": [ + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "438f56aa", + "metadata": {}, + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b5e93a1b", + "metadata": {}, + "outputs": [], + "source": [ + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "8c9e50a8", + "metadata": {}, + "source": [ + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e58e5659", + "metadata": {}, + "outputs": [], + "source": [ + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ede3e803", + "metadata": {}, + "outputs": [], + "source": [ + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0435c31d", + "metadata": {}, + "outputs": [], + "source": [ + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34160d69", + "metadata": {}, + "outputs": [], + "source": [ + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "c952fb01", + "metadata": {}, + "source": [ + "# Примечания\n", + "\n", + "1. **Настройка модели**: Измените `model` в инициализации `ChatOpenAI` для использования другой модели из OpenRouter\n", + "2. **API ключ**: Убедитесь, что `OPENROUTER_API_KEY` установлен в `.env` файле\n", + "3. **Структурированный вывод**: Используется `PydanticOutputParser` для гарантированного соответствия Pydantic моделям\n", + "4. **Температура**: Настроена низкая температура (0.3) для более детерминированных результатов\n", + "5. **LangChain**: Пайплайн построен на LangChain для удобства композиции и расширения\n" + ] + } + ], + "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": 5 +}