Привет, Хабр! Продуктовая аналитика хорошо работает с событиями и метриками, но ломается на живых коммуникациях. Звонки зачастую остаются неохваченными анализом, хотя именно там слышно как клиент злится или сомневается, но эти сигналы доходят до менеджера продукта хаотично, а не в системном виде.

В этом гайде разберём, как превратить записи звонков в продуктовые инсайты без ручного прослушивания — с помощью Python, звонков от МТС Exolve, интерфейса на Streamlit и нейронкой MWS GPT от МТС.

Архитектура решения

Инструмент построен как простой и прозрачный пайплайн обработки данных. Он не требует отдельного бэкенда, очередей или фронтенда — вся логика укладывается в несколько Python-модулей и управляется из интерфейса Streamlit.

С точки зрения потока данных система состоит из последовательных шагов, каждый из которых решает одну конкретную задачу и не знает о деталях остальных.

  1. Сбор данных. Забираем историю звонков и расшифровки по API с разделением ролей «клиент» и «оператор»

  2. Анализ. Передаём текст диалога в нейронку и получаем структурированный JSON: краткую суть обращения, настроение клиента, риск ухода и продуктовые запросы

  3. Визуализация. Показываем результат в дашборде и сохраняем выбранные инсайты в таблицу

Компоненты

  • МТС Exolve API — источник данных со звонками и транскрипциями

  • MWS GPT — анализ диалога и генерация структурированных данных с помощью модели mws-gpt-alpha

  • Streamlit — интерфейс, фильтры, метрики и кэширование. Позволяет быстро собрать рабочий интерфейс прямо на Python без отдельного фронтенда и API-слоя

  • Google Sheets — простой способ сохранить инсайты из интерфейса и передать их команде

Шаг 1. Забираем диалоги из МТС Exolve

Анализ запускается вручную из интерфейса. Это сделано это для контроля нагрузки и экономии затрат на нейронку.

По кнопке запрашиваем список последних звонков через метод GetList и получаем 10 последних звонков. Лимит можно легко увеличить, например до 100.

Полученный список отображается в интерфейсе. Вы выбираете конкретный звонок и вручную запускаете анализ — только в этот момент начинается дальнейший пайплайн.

Для выбранного звонка по его call_uid запрашивается транскрипция. Она приходит в виде набора фрагментов, привязанных к аудиоканалам. Эти каналы соответствуют ролям участников разговора — клиенту и оператору — и передаются в поле channel_tag.

Эту структуру важно сохранить. Если склеить текст без указания ролей, модель теряет контекст разговора и хуже интерпретирует эмоции, претензии и запросы клиента. Поэтому на этом этапе маппятся каналы на роли Client и Operator и собирается последовательный диалог.

На выходе получается структурирова��ный текст разговора, готовый к анализу нейронкой.

# exolve_provider.py
import requests
from config import Config




class ExolveProvider:
   def __init__(self):
       self.headers = {"Authorization": f"Bearer {Config.EXOLVE_API_KEY}"}


   def get_last_calls(self, limit=10):
       """Запрашивает список последних звонков."""
       url = "https://api.exolve.ru/statistics/call-history/v2/GetList"
       try:
           resp = requests.post(url, headers=self.headers, json={"limit": limit}, timeout=10)
           return resp.json().get('calls', [])
       except Exception as e:
           return []


   def get_transcript(self, call_uid: str) -> str | None:
       """Скачивает и форматирует расшифровку."""
       url = f"{Config.EXOLVE_BASE_URL}/GetTranscribation"
       try:
           resp = requests.post(url, headers=self.headers, json={"uid": int(call_uid)}, timeout=10)
           if resp.status_code != 200: return None


           data = resp.json()
           chunks = data.get('transcribation', [{}])[0].get('chunks', [])


           # Склеиваем диалог: [Role]: Text
           lines = []
           for chunk in chunks:
               role = "Operator" if chunk.get('channel_tag') == 2 else "Client"
               text = chunk.get('text', '')
               lines.append(f"[{role}]: {text}")


           return "\n".join(lines)
       except Exception:
           return None

Шаг 2. Анализируем как продакт менеджер

На этом этапе получаем продуктовые инсайты, с которыми можно работать дальше. Для этого используем LLM в роли Senior Product Manager. Передаём модели полный диалог вместе с промптом, который явно задаёт роль и ожидаемый формат ответа.

Ключевой момент — строгая структура результата. Просим модель вернуть JSON с заранее определёнными полями:

  • краткая суть обращения

  • настроение клиента

  • риск ухода

  • продуктовые запросы

  • цитаты из речи клиента

Такой формат позволяет показывать в интерфейсе, фильтровать, сохранять и передавать в бэклог без дополнительной обработки. 

 # prompts.py
def get_product_analysis_prompt(transcript: str) -> str:
   return f"""
Ты — Senior Product Manager. Твоя цель — найти точки роста продукта на основе звонка в техподдержку.


**Текст звонка:**
{transcript}


**Задача:**
Проанализируй диалог и верни JSON со следующей структурой:
{{
   "summary": "Краткая суть проблемы (1 предложение)",
   "customer_mood": "Настроение клиента (Angry/Neutral/Happy/Frustrated)",
   "key_fear": "Глубинный страх клиента (потеря денег/времени/контроля)",
   "feature_request": "Какую функцию явно или неявно хочет клиент? (или null)",
   "churn_risk": "Риск ухода (Low/Medium/High)",
   "original_phrases": ["Цитата 1", "Цитата 2"],
   "tags": ["Tag1", "Tag2"]
}}


**Важно:**
1. Цитаты должны быть точными выдержками из текста клиента.
2. Отвечай только валидным JSON.
"""

Чтобы ответы были стабильными и воспроизводимыми:

  • Снижаем степень случайности через температуру до 0.1

  • Ограничиваем количество на ответ до 1500 для контроля расходов на нейронку

  • Ограничиваем выборку слов наиболее вероятными, задав top_p 0.1

Чтобы UI не падал с ошибками, используем try/except и возвращаем fallback-результат при неуспехе.

На выходе получаем структурированный результат анализа диалога, готовый к визуализации и сохранению.

# ai_engine.py
def analyze_transcript(self, transcript: str, prompt_func) -> dict:
   # Заглушка на случай сбоя
   fallback_result = {
       "summary": "Ошибка анализа (LLM Error)",
       "customer_mood": "Unknown",
       "churn_risk": "Unknown",
       "tags": ["Error"]
   }


   payload = {
       "model": "mws-gpt-alpha",
       "messages": [...],
       "temperature": 0.1,
       "top_p": 0.1,  # Nucleus Sampling для максимальной строгости
       "max_tokens": 1500
   }


   try:
       resp = requests.post(Config.MTS_AI_URL, headers=self.headers, json=payload, timeout=30)
       resp.raise_for_status()


       content = resp.json()['choices'][0]['message']['content']


       # Очистка от markdown-оберток (AI любит добавлять ```json)
       clean_content = content.replace("```json", "").replace("```", "").strip()


       return json.loads(clean_content)
   except Exception as e:
       logger.error(f"AI Error: {e}")
       return fallback_result

Шаг 3. Собираем дашборд

Streamlit позволяет делать интерфейс прямо на Python без отдельного фронтенда. В нашем случае он показывает список звонков, запускает обработку и отображает результат.

Чтобы интерфейс работал быстро и не дёргал API лишний раз, кэшируем через @st.cache_data. Тяжёлые операции — например, загрузка списка звонков — выполняются один раз и переиспользуются при следующих действиях пользователя.

# app.py
import streamlit as st
import pandas as pd
from exolve_provider import ExolveProvider
from ai_engine import AIAnalyst
from prompts import get_product_analysis_prompt




# Инициализация сервисов (кэшируем, чтобы не пересоздавать объекты)
@st.cache_resource
def get_services():
   return ExolveProvider(), AIAnalyst()




exolve, ai = get_services()




@st.cache_data(ttl=300)  # Кэш данных живет 5 минут
def load_calls(limit):
   return exolve.get_last_calls(limit)




# Основной интерфейс
st.title("📞 Анализ звонков: Поиск инсайтов")


# Сайдбар с настройками
with st.sidebar:
   limit = st.slider("Загружать звонков", 5, 50, 10)
   if st.button("🔄 Сброс кэша"):
       st.cache_data.clear()


# Получаем список звонков
calls = load_calls(limit)
df = pd.DataFrame(calls)


# Выводим таблицу
st.dataframe(df[['date', 'calling_number', 'duration', 'status']], use_container_width=True)


# Выбираем звонок для анализа
selected_uid = st.selectbox("Выберите звонок:", df['uid'].unique())


if st.button("🚀 Запустить анализ"):
   with st.spinner("Магия AI..."):
       text = exolve.get_transcript(selected_uid)
       analysis = ai.analyze_transcript(text, get_product_analysis_prompt)


       # Визуализация метрик (KPI)
       c1, c2, c3 = st.columns(3)
       c1.metric("Настроение", analysis.get("customer_mood"))


       # Подсветка риска
       risk = analysis.get("churn_risk")
       c2.metric("Риск ухода", risk, delta_color="inverse" if risk == "High" else "normal")


       # Карточки с инсайтами (используем HTML/CSS внутри Markdown для красоты)
       st.markdown(f"""
       <div class="insight-box">
           <b>Суть:</b> {analysis.get("summary")}<br>
           <b>Глубинный страх:</b> {analysis.get("key_fear")}
       </div>
       """, unsafe_allow_html=True)

Полный код app.py занимает менее 100 строк, но дает полноценный UI с табами, выпадающими списками и красивыми метриками.

Шаг 4. Сохранение результатов

Если найденный инсайт кажется полезным, его можно сохранить одной кнопкой «Сохранить в Google Sheets». В этом случае результат анализа записывается в Google Sheets — как отдельная строка со структурой JSON от нейросети.

Таблица используется как простой бэклог: сюда попадают продуктовые проблемы, риски ухода и запросы на фичи, которые дальше можно разобрать командой.

expand_less
  if st.button("💾 Сохранить в Google Sheets"):
   if sheets.save_analysis(selected_uid, analysis):
       st.success("Инсайт сохранен в бэклог!")
   else:
       st.error("Ошибка сохранения")

Пример использования

Продакт-менеджер в нашем интерфейсе видит звонок длительностью 15 минут, что аномально долго для нашей поддержки.

  1. Кликает «Анализировать»

  2. Система показывает:

    • Настроение Frustrated (Раздражен)

    • Риск ухода High

    • Суть: «Клиент не смог выгрузить отчет в PDF, кнопка выдает 404»

    • Инсайт: «Клиент боится, что не успеет сдать отчетность налоговой из-за нашего бага»

  3. Продакт нажимает «Сохранить в Google Sheets». Проблема улетает в общий реестр с тегом #critical.

Без этого инструмента звонок мог потеряться, а клиент мог бы молча уйти к конкурентам.

Заключение

Мы разобрали, как превратить записи звонков службы поддержки и отдела продаж в источник продуктовых инсайтов. Связка Python, МТС Exolve, Streamlit и MWS GPT даёт управляемый процесс: анализ запускается по требованию, нагрузка контролируется, а результат готов к дальнейшей работе.

Кстати, если вы не хотите писать код, у MWS GPT есть и No-code платформа, где похожие пайплайны можно собирать визуально. Но для инженера гибкость кода всегда дает преимущество.

Идеи для развития

  • Вместо ручного запуска пайплайн повесить на вебхуки об окончании звонка в МТС Exolve и анализировать диалоги автоматически

  • Собирать проблематику по всем звонкам и раз в сутки агрегировать результаты: формировать краткую выжимку по болям, рискам и запросам на фичи и отправлять её менеджерам продуктов, например, утром перед началом рабочего дня.

  • Отдельное направление — работа с промптами. При повышенной креативности модели можно просить нейронку предлагать 3–5 идей потенциальных фич на основе проблем и страхов клиентов, передавая ей текущий продуктовый контекст

Репозиторий с кодом.