{ "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 }