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",
+ " original_text | \n",
+ " main_topic | \n",
+ " key_entities | \n",
+ " main_fact | \n",
+ " is_unambiguous | \n",
+ " confidence | \n",
+ " category | \n",
+ " search_difficulty | \n",
+ " ambiguity_reasons | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | 0 | \n",
+ " Российские войска применяют модернизированные ... | \n",
+ " Модернизация российских авиационных бомб и бал... | \n",
+ " Российские войска, Корректируемые планирующие ... | \n",
+ " Россия модернизировала советские КАБ, добавив ... | \n",
+ " False | \n",
+ " 0.40 | \n",
+ " mixed | \n",
+ " hard | \n",
+ " Отсутствие независимых подтверждений данных о ... | \n",
+ "
\n",
+ " \n",
+ " | 1 | \n",
+ " Ким Чен Ын на своем спецпоезде отправился в Пе... | \n",
+ " Поездка Ким Чен Ына в Пекин для участия в воен... | \n",
+ " Ким Чен Ын, Пекин, Вторая мировая война, 80-ле... | \n",
+ " Ким Чен Ын совершил первую за год зарубежную п... | \n",
+ " False | \n",
+ " 0.75 | \n",
+ " statement | \n",
+ " medium | \n",
+ " Утверждение о 'подчеркивании укрепления связей... | \n",
+ "
\n",
+ " \n",
+ " | 2 | \n",
+ " В министерстве обороны Камбоджи отрицают наруш... | \n",
+ " Оспаривание обвинений в нарушении режима прекр... | \n",
+ " Министерство обороны Камбоджи, Тайская сторона... | \n",
+ " Министерство обороны Камбоджи отрицает обвинен... | \n",
+ " False | \n",
+ " 0.85 | \n",
+ " statement | \n",
+ " hard | \n",
+ " Обвинения Таиланда и опровержение Камбоджи пре... | \n",
+ "
\n",
+ " \n",
+ " | 3 | \n",
+ " ⬜⬜⬜⬜⬜⬜ План Трампа – это хорошая основа для ра... | \n",
+ " Позиция России по урегулированию украинского к... | \n",
+ " Дмитрий Песков, Дональд Трамп, Россия, Украина... | \n",
+ " Дмитрий Песков заявил, что план Трампа являетс... | \n",
+ " False | \n",
+ " 0.95 | \n",
+ " statement | \n",
+ " hard | \n",
+ " Заявление Пескова отражает официальную позицию... | \n",
+ "
\n",
+ " \n",
+ " | 4 | \n",
+ " Ограничения полетов ввели в аэропорту Ульяновс... | \n",
+ " Ограничения полетов в аэропортах нескольких го... | \n",
+ " Росавиация, Ульяновск, Пенза, Нижний Новгород,... | \n",
+ " В аэропортах Ульяновска, Пензы, Нижнего Новгор... | \n",
+ " False | \n",
+ " 0.75 | \n",
+ " event | \n",
+ " medium | \n",
+ " Отсутствует точная дата и время введения огран... | \n",
+ "
\n",
+ " \n",
+ " | ... | \n",
+ " ... | \n",
+ " ... | \n",
+ " ... | \n",
+ " ... | \n",
+ " ... | \n",
+ " ... | \n",
+ " ... | \n",
+ " ... | \n",
+ " ... | \n",
+ "
\n",
+ " \n",
+ " | 294 | \n",
+ " Импортируемые в Россию автомобили предложили о... | \n",
+ " Предложение обязать импортируемые в Россию авт... | \n",
+ " Россия, Концепция развития телерадиовещания до... | \n",
+ " Российские радиохолдинги предлагают включить в... | \n",
+ " False | \n",
+ " 0.85 | \n",
+ " statement | \n",
+ " medium | \n",
+ " Предложение ещё не принято и не включено в офи... | \n",
+ "
\n",
+ " \n",
+ " | 295 | \n",
+ " По планам Банка России, массовое внедрение циф... | \n",
+ " Внедрение цифрового рубля в России | \n",
+ " Банк России, цифровой рубль, сентябрь 2026 год... | \n",
+ " Массовое внедрение цифрового рубля в России на... | \n",
+ " True | \n",
+ " 0.95 | \n",
+ " event | \n",
+ " medium | \n",
+ " | \n",
+ "
\n",
+ " \n",
+ " | 296 | \n",
+ " В Турции в Гебзе обрушился многоэтажный дом. П... | \n",
+ " Обрушение многоэтажного дома в Гебзе, Турция | \n",
+ " Турция, Гебзе, Зиннура Бюйюкгеза, NTV, IHA, ме... | \n",
+ " В Гебзе, Турция, обрушился семиэтажный дом, по... | \n",
+ " True | \n",
+ " 0.95 | \n",
+ " event | \n",
+ " easy | \n",
+ " | \n",
+ "
\n",
+ " \n",
+ " | 297 | \n",
+ " Современный городской квартал сегодня уже дале... | \n",
+ " Создание жилого квартала с развитой инфраструк... | \n",
+ " Soul, Часовая улица, метро «Аэропорт», Forma, ... | \n",
+ " Девелопер Forma создает жилой квартал Soul на ... | \n",
+ " True | \n",
+ " 0.95 | \n",
+ " event | \n",
+ " easy | \n",
+ " | \n",
+ "
\n",
+ " \n",
+ " | 298 | \n",
+ " Сейчас не время просить российского президента... | \n",
+ " Отношение Дональда Трампа к запросам о прекращ... | \n",
+ " Дональд Трамп, Владимир Путин, Россия, Air For... | \n",
+ " Дональд Трамп считает, что сейчас не время про... | \n",
+ " False | \n",
+ " 0.95 | \n",
+ " statement | \n",
+ " hard | \n",
+ " Высказывание отражает личное мнение Дональда Т... | \n",
+ "
\n",
+ " \n",
+ "
\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",
+ " message_dt | \n",
+ " message_id | \n",
+ " channel_id | \n",
+ " content | \n",
+ " views | \n",
+ " original_author | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | 4846 | \n",
+ " 2025-04-15 | \n",
+ " 116039 | \n",
+ " rbc_news | \n",
+ " Первоклассники не должны заниматься уроками бо... | \n",
+ " 156002.0 | \n",
+ " NaN | \n",
+ "
\n",
+ " \n",
+ " | 4822 | \n",
+ " 2025-04-15 | \n",
+ " 116076 | \n",
+ " rbc_news | \n",
+ " Команда Трампа рассматривает варианты ужесточе... | \n",
+ " 117161.0 | \n",
+ " NaN | \n",
+ "
\n",
+ " \n",
+ " | 4821 | \n",
+ " 2025-04-15 | \n",
+ " 116077 | \n",
+ " rbc_news | \n",
+ " На эмблему Минспорта вернулись христианские кр... | \n",
+ " 109451.0 | \n",
+ " rbc_sport | \n",
+ "
\n",
+ " \n",
+ " | 4820 | \n",
+ " 2025-04-15 | \n",
+ " 116078 | \n",
+ " rbc_news | \n",
+ " В Китае в расцвете сил умерли несколько учёных... | \n",
+ " 134105.0 | \n",
+ " NaN | \n",
+ "
\n",
+ " \n",
+ " | 4818 | \n",
+ " 2025-04-15 | \n",
+ " 116080 | \n",
+ " rbc_news | \n",
+ " «Яндекс» договаривается с операторами систем «... | \n",
+ " 112462.0 | \n",
+ " NaN | \n",
+ "
\n",
+ " \n",
+ "
\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, ?it/s]"
+ ]
+ },
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "Обработка постов: 89%|████████▉ | 268/300 [23:58<02:26, 4.57s/it]"
+ ]
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Ошибка при обработке: Invalid json output: {\n",
+ " \"is_unambiguous\": false,\n",
+ " \"confidence\": 0.75,\n",
+ " \"category\": \"statement\",\n",
+ " \"search_difficulty\": \"medium\",\n",
+ " \"ambiguity_reasons\": [\n",
+ " \"Последовательность событий (совещание → решение об отставке) может интерпретироваться по-разному в зависимости от источника\",\n",
+ " \"Отсутствие официального подтверждения точного времени принятия решения об отставке\",\n",
+ " \"Возможны противоречивые версии: некоторые источники могут утверждать, что решение было принято до совещания, другие — что оно было следствием совещания\",\n",
+ " \"Информация о \"непрямом контакте\" с Ермаком — субъективная интерпретация действий администрации, не всегда прямо подтверждаемая документами\"\n",
+ " ],\n",
+ " \"reasoning\": \"Несмотря на то, что в утверждении приводятся конкретные лица, события и временные рамки (совещание, отставка, обыски), ключевая деталь — последовательность и причины принятия решения об отставке Ермака — основана на внутренних процессах, которые не всегда документированы публично и могут интерпретироваться по-разному. Разные источники (официальные заявления, анонимные источники, СМИ) могут подавать эту последовательность как причинно-следственную, как формальную процедуру или как политический манёвр. Точное время принятия решения без прямого контакта с Ермаком не подтверждено официальными документами в публичном доступе, что делает эту информацию уязвимой для противоречивых трактовок. Это не чистый факт, а интерпретация событий, основанная на косвенных данных, что требует контекстного анализа и делает поиск сложным (medium).\"\n",
+ "}\n",
+ "For troubleshooting, visit: https://python.langchain.com/docs/troubleshooting/errors/OUTPUT_PARSING_FAILURE \n"
+ ]
+ },
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "Обработка постов: 100%|██████████| 300/300 [26:47<00:00, 5.36s/it]\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Обработка выборки постов (для экономии API вызовов)\n",
+ "SAMPLE_SIZE = 300 # Измените для обработки большего количества\n",
+ "\n",
+ "sample_data = data.sample(n=min(SAMPLE_SIZE, len(data)), random_state=42)\n",
+ "sample_texts = sample_data[\"content\"].dropna().tolist()\n",
+ "\n",
+ "print(f\"Обрабатываем {len(sample_texts)} постов...\")\n",
+ "\n",
+ "# sample_results = pipeline.process_batch(sample_texts)\n",
+ "results = []\n",
+ "iterator = tqdm(sample_texts, desc=\"Обработка постов\")\n",
+ "\n",
+ "for text in iterator:\n",
+ " try:\n",
+ " result = pipeline.process(text)\n",
+ " results.append(result)\n",
+ " except KeyboardInterrupt:\n",
+ " continue\n",
+ " except Exception as e:\n",
+ " print(f\"Ошибка при обработке: {e}\")\n",
+ " results.append(None)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "5e8c3791",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ "📊 СТАТИСТИКА КЛАССИФИКАЦИИ:\n",
+ " Однозначных постов: 167 (56%)\n",
+ " Неоднозначных постов: 132 (44%)\n",
+ "\n",
+ "📈 РАСПРЕДЕЛЕНИЕ ПО СЛОЖНОСТИ:\n",
+ "search_difficulty\n",
+ "easy 154\n",
+ "hard 77\n",
+ "medium 68\n",
+ "Name: count, dtype: int64\n",
+ "\n",
+ "📂 РАСПРЕДЕЛЕНИЕ ПО КАТЕГОРИЯМ:\n",
+ "category\n",
+ "event 135\n",
+ "statement 86\n",
+ "fact 55\n",
+ "mixed 21\n",
+ "opinion 2\n",
+ "Name: count, dtype: int64\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Анализ результатов\n",
+ "df_sample_results = results_to_dataframe([r for r in results if r is not None])\n",
+ "\n",
+ "print(\"\\n📊 СТАТИСТИКА КЛАССИФИКАЦИИ:\")\n",
+ "print(f\" Однозначных постов: {df_sample_results['is_unambiguous'].sum()} ({df_sample_results['is_unambiguous'].mean():.0%})\")\n",
+ "print(f\" Неоднозначных постов: {(~df_sample_results['is_unambiguous']).sum()} ({(~df_sample_results['is_unambiguous']).mean():.0%})\")\n",
+ "\n",
+ "print(\"\\n📈 РАСПРЕДЕЛЕНИЕ ПО СЛОЖНОСТИ:\")\n",
+ "print(df_sample_results['search_difficulty'].value_counts())\n",
+ "\n",
+ "print(\"\\n📂 РАСПРЕДЕЛЕНИЕ ПО КАТЕГОРИЯМ:\")\n",
+ "print(df_sample_results['category'].value_counts())\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 26,
+ "id": "605c2741",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "'Глава Грайворонского городского округа Белгородской области Геннадий Бондарев покидает свою должность, сообщил губернатор региона Вячеслав Гладков. Последний накануне посетил округ и раскритиковал работу местной администрации. \\n\\n«К сожалению, иногда вижу слабое реагирование властей округа. Это приводит к дестабилизации и так крайне сложной обстановки в населенных пунктах. Плохая работа с населением, и поэтому жители начинают нервничать», — отметил Гладков.\\n\\nОн добавил, что решение Бондарева о закрытии администрации одного из сел привело к ощущению брошенности у жителей. \\n\\n🐚 Читать РБК в Telegram'"
+ ]
+ },
+ "execution_count": 26,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "df_sample_results.loc[df_sample_results['is_unambiguous'], \"original_text\"].sample(4).iloc[0]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 31,
+ "id": "e3839a4f",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Обломки дрона обнаружили польские пограничники в деревне возле границы с Белоруссией, сообщает Reuters.\n",
+ "\n",
+ "«Этот дрон рухнул у границы, примерно в 300 метрах от пограничного перехода, в деревне Полатыче. Дрон не вооружен, на корпусе имеются надписи на кириллице», — рассказала Агнешка Кепка из прокуратуры города Люблин на пресс-конференции.\n",
+ "\n",
+ "Военные полицейские опрашивают свидетелей и проверяют видеозаписи, чтобы установить траекторию полета дрона, добавила она. Никто не пострадал, подчеркнули в полиции.\n",
+ "\n",
+ "Фото: Kuba Stezycki / Reuters\n",
+ "\n",
+ "🐚 Следить за новостями РБК в Telegram\n"
+ ]
+ }
+ ],
+ "source": [
+ "print(df_sample_results.loc[df_sample_results[\"is_unambiguous\"] & df_sample_results[\"category\"].isin([\"event\", \"statement\", \"fact\"])].iloc[4][\"original_text\"])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "id": "333d1225",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "✅ Результаты сохранены в classification_results.csv\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Сохранение результатов\n",
+ "df_sample_results.to_csv('classification_results.csv', index=False)\n",
+ "print(\"✅ Результаты сохранены в classification_results.csv\")\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 32,
+ "id": "7e709faf",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " original_text | \n",
+ " main_topic | \n",
+ " key_entities | \n",
+ " main_fact | \n",
+ " is_unambiguous | \n",
+ " confidence | \n",
+ " category | \n",
+ " search_difficulty | \n",
+ " ambiguity_reasons | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | 0 | \n",
+ " Российские войска применяют модернизированные ... | \n",
+ " Модернизация российских авиационных бомб и бал... | \n",
+ " Российские войска, Корректируемые планирующие ... | \n",
+ " Россия модернизировала советские КАБ, добавив ... | \n",
+ " False | \n",
+ " 0.40 | \n",
+ " mixed | \n",
+ " hard | \n",
+ " Отсутствие независимых подтверждений данных о ... | \n",
+ "
\n",
+ " \n",
+ " | 1 | \n",
+ " Ким Чен Ын на своем спецпоезде отправился в Пе... | \n",
+ " Поездка Ким Чен Ына в Пекин для участия в воен... | \n",
+ " Ким Чен Ын, Пекин, Вторая мировая война, 80-ле... | \n",
+ " Ким Чен Ын совершил первую за год зарубежную п... | \n",
+ " False | \n",
+ " 0.75 | \n",
+ " statement | \n",
+ " medium | \n",
+ " Утверждение о 'подчеркивании укрепления связей... | \n",
+ "
\n",
+ " \n",
+ " | 2 | \n",
+ " В министерстве обороны Камбоджи отрицают наруш... | \n",
+ " Оспаривание обвинений в нарушении режима прекр... | \n",
+ " Министерство обороны Камбоджи, Тайская сторона... | \n",
+ " Министерство обороны Камбоджи отрицает обвинен... | \n",
+ " False | \n",
+ " 0.85 | \n",
+ " statement | \n",
+ " hard | \n",
+ " Обвинения Таиланда и опровержение Камбоджи пре... | \n",
+ "
\n",
+ " \n",
+ " | 3 | \n",
+ " ⬜⬜⬜⬜⬜⬜ План Трампа – это хорошая основа для ра... | \n",
+ " Позиция России по урегулированию украинского к... | \n",
+ " Дмитрий Песков, Дональд Трамп, Россия, Украина... | \n",
+ " Дмитрий Песков заявил, что план Трампа являетс... | \n",
+ " False | \n",
+ " 0.95 | \n",
+ " statement | \n",
+ " hard | \n",
+ " Заявление Пескова отражает официальную позицию... | \n",
+ "
\n",
+ " \n",
+ " | 4 | \n",
+ " Ограничения полетов ввели в аэропорту Ульяновс... | \n",
+ " Ограничения полетов в аэропортах нескольких го... | \n",
+ " Росавиация, Ульяновск, Пенза, Нижний Новгород,... | \n",
+ " В аэропортах Ульяновска, Пензы, Нижнего Новгор... | \n",
+ " False | \n",
+ " 0.75 | \n",
+ " event | \n",
+ " medium | \n",
+ " Отсутствует точная дата и время введения огран... | \n",
+ "
\n",
+ " \n",
+ " | ... | \n",
+ " ... | \n",
+ " ... | \n",
+ " ... | \n",
+ " ... | \n",
+ " ... | \n",
+ " ... | \n",
+ " ... | \n",
+ " ... | \n",
+ " ... | \n",
+ "
\n",
+ " \n",
+ " | 294 | \n",
+ " Импортируемые в Россию автомобили предложили о... | \n",
+ " Предложение обязать импортируемые в Россию авт... | \n",
+ " Россия, Концепция развития телерадиовещания до... | \n",
+ " Российские радиохолдинги предлагают включить в... | \n",
+ " False | \n",
+ " 0.85 | \n",
+ " statement | \n",
+ " medium | \n",
+ " Предложение ещё не принято и не включено в офи... | \n",
+ "
\n",
+ " \n",
+ " | 295 | \n",
+ " По планам Банка России, массовое внедрение циф... | \n",
+ " Внедрение цифрового рубля в России | \n",
+ " Банк России, цифровой рубль, сентябрь 2026 год... | \n",
+ " Массовое внедрение цифрового рубля в России на... | \n",
+ " True | \n",
+ " 0.95 | \n",
+ " event | \n",
+ " medium | \n",
+ " | \n",
+ "
\n",
+ " \n",
+ " | 296 | \n",
+ " В Турции в Гебзе обрушился многоэтажный дом. П... | \n",
+ " Обрушение многоэтажного дома в Гебзе, Турция | \n",
+ " Турция, Гебзе, Зиннура Бюйюкгеза, NTV, IHA, ме... | \n",
+ " В Гебзе, Турция, обрушился семиэтажный дом, по... | \n",
+ " True | \n",
+ " 0.95 | \n",
+ " event | \n",
+ " easy | \n",
+ " | \n",
+ "
\n",
+ " \n",
+ " | 297 | \n",
+ " Современный городской квартал сегодня уже дале... | \n",
+ " Создание жилого квартала с развитой инфраструк... | \n",
+ " Soul, Часовая улица, метро «Аэропорт», Forma, ... | \n",
+ " Девелопер Forma создает жилой квартал Soul на ... | \n",
+ " True | \n",
+ " 0.95 | \n",
+ " event | \n",
+ " easy | \n",
+ " | \n",
+ "
\n",
+ " \n",
+ " | 298 | \n",
+ " Сейчас не время просить российского президента... | \n",
+ " Отношение Дональда Трампа к запросам о прекращ... | \n",
+ " Дональд Трамп, Владимир Путин, Россия, Air For... | \n",
+ " Дональд Трамп считает, что сейчас не время про... | \n",
+ " False | \n",
+ " 0.95 | \n",
+ " statement | \n",
+ " hard | \n",
+ " Высказывание отражает личное мнение Дональда Т... | \n",
+ "
\n",
+ " \n",
+ "
\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
+}