Voice Input — голосовой интерфейс для AI-воронок

image.png

Пакет: Voice
Полное имя компонента: Common.Voice.VoiceInput

Что это

Voice Input — это высокоуровневый компонент Metabot для приёма и обработки голосовых сообщений внутри сценариев.

Он позволяет использовать голос как полноценный пользовательский ввод:

Проще говоря, VoiceInput — это не просто “распознавание речи”.
Это голосовой middleware для conversational runtime.


Зачем нужен Voice Input

image.png

Большинство сценариев в ботах до сих пор строятся вокруг текста:

Но в живом использовании это часто неудобно.

Пользователь:

И здесь голосовой ввод даёт очень сильное преимущество.

Что меняется с голосом

Когда человек печатает:

Когда человек говорит:

Именно поэтому VoiceInput — это не просто удобство.
Это другой тип интерфейса.


Почему голос — это не просто удобство

image.png

Если вы ещё не используете голосовой ввод в сценариях, вы упускаете один из самых сильных интерфейсов взаимодействия с пользователем.

Речь — это не просто альтернатива тексту.
Это другой уровень скорости и качества мышления.

Скорость

То есть голос в среднем быстрее печати примерно в 3–5 раз.

Но дело не только в скорости.

Что реально меняется

Когда человек печатает:

Когда человек говорит:

Для AI-сценариев это особенно важно:
чем богаче вход, тем точнее можно:


Что это даёт сценаристу и продуктологу

VoiceInput открывает другой класс сценариев.

С ним можно строить не только:

но и:

То есть сценарист перестаёт строить только “дерево ответов”
и начинает строить интерфейс мышления.


Где используется Voice Input

image.png

Компонент особенно полезен там, где:

Типовые кейсы:


Где находится компонент

Компонент находится в пакете Voice и подключается как обычный плагин Metabot:

const VoiceInput = require("Common.Voice.VoiceInput")

Как работает Voice Input

VoiceInput работает как многошаговый голосовой pipeline, в котором участвуют:

Это важнее, чем кажется.

Если LLMQuery — это двухфазный асинхронный AI-запрос,
то VoiceInput — это уже голосовой pipeline из нескольких фаз, потому что здесь нужно сначала дождаться аудиосообщения, потом получить файл, а потом ещё отдельно дождаться результата распознавания.


Общая схема работы

Сценарий
→ VoiceInput.expect()
→ ожидание голосового сообщения
→ callback от мессенджера
→ получение ссылки на файл
→ отправка в STT
→ callback от STT
→ сохранение текста
→ переход в successScript

Фазы выполнения

image.png

Фаза 1. Сценарий включает ожидание голосового ввода

В обычной команде Run JavaScript сценарий вызывает:

VoiceInput.expect({...})

На этом этапе компонент:


Фаза 2. Пользователь отправляет голосовое сообщение

Дальше processor script через callback-команду ждёт ввод пользователя.

На этом этапе компонент:

То есть в этой фазе мы ещё не распознаём текст.
Мы только:


Фаза 3. Аудио отправляется в speech-to-text

Во второй команде processor script запускается асинхронный API-вызов к STT-провайдеру.

На этом этапе:


Фаза 4. Текст возвращается в сценарий

Когда STT-провайдер возвращает результат:

После этого распознанный текст можно использовать:


Обязательное условие использования

image.png

Чтобы VoiceInput работал, в боте должен существовать системный processor script, через который проходят оба callback-цикла.

По умолчанию это скрипт с кодом:

VoiceInput_Processor

Без него компонент работать не будет.

Причина в том, что VoiceInput не ограничивается одним вызовом.
Ему нужен отдельный системный контур, который:


Как устроен processor script

Processor script должен содержать две команды.

Команда 1. Callback от мессенджера

Тип команды:
Run JavaScript Callback

Код:

const VoiceInput = require("Common.Voice.VoiceInput")

return VoiceInput.onCallback({ lead, isFirstImmediateCall })

Эта команда:


Команда 2. Callback от STT-провайдера

Тип команды:
Run asynchronous API-request

Код:

const VoiceInput = require("Common.Voice.VoiceInput")

return VoiceInput.onSTT({ lead, isFirstImmediateCall })

Эта команда:


Что важно понимать про processor script

VoiceInput.expect() сам по себе не завершает всю работу.

Он только:

Вся дальнейшая механика:

происходит именно через этот системный скрипт.

Поэтому если вы импортируете готовую конфигурацию, processor script уже должен быть внутри.
Если вы собираете решение вручную — его нужно создать обязательно.


Что нужно настроить перед использованием

Перед тем как использовать VoiceInput, нужно убедиться, что настроены:


1. Настройки канала: реакция на аудио и голосовые

Это обязательная настройка.

В канале нужно включить:

Иначе голосовые сообщения будут игнорироваться или не будут корректно попадать в сценарий.

Пример

Реакция на аудио:
Штатная (NLP и меню)

Реакция на голосовые сообщения:
Штатная (NLP и меню)

Сейчас компонент ориентирован прежде всего на Telegram-контур.
В дальнейшем этот слой может расширяться и на другие каналы.

Если нужен голосовой интерфейс для других каналов, свяжитесь с нами.


2. Инфраструктурные атрибуты бота

Как и в случае с LLMQuery, для callback-механики должны быть настроены:

Они нужны, потому что callback от внешнего STT-провайдера возвращается обратно в Metabot через внешний процессор.


3. Ключ провайдера speech-to-text

Сам ключ STT обычно передаётся через tokenKey в конфигурации компонента.

Например:

stt: {
  provider: "openai",
  options: { model: "whisper-1", language: "ru" },
  asyncResponse: true,
  tokenKey: "OPENAI_API_KEY"
}

То есть:


Сигнатура вызова VoiceInput.expect()

image.png

VoiceInput обычно вызывается внутри команды Run JavaScript.

Типовой вызов выглядит так:

const VoiceInput = require('Common.Voice.VoiceInput')

return VoiceInput.expect({
  code: "orion_profiling_q1_voice",

  lead,

  successScript: "orion_profiling_q2",
  cancelScript: "orion_profiling_cancelled",

  targetAttr: "orion_profiling_q1_text",
  sourceAttr: "orion_profiling_q1_voice_url",

  extraAttrs: {
    active_agent: "orion",
    voice_context: "orion_profiling_q1",
    input_mode: "profiling"
  },

  processorScript: "VoiceInput_Processor",

  stt: {
    provider: "openai",
    options: { model: "whisper-1", language: "ru" },
    asyncResponse: true,
    tokenKey: "OPENAI_API_KEY"
  },

  messages: {
    wait: "🎙 Пришли голосовое сообщение (3–5 минут) или напиши «стоп»",
    accepted: "✅ Принято. Обрабатываю…",
    wrong: "Нужна голосовуха (voice / video_note) или «стоп»",
    canceled: "Ок, остановились",
    stillProcessing: "⏳ Ещё обрабатываю…"
  },

  stopPhrases: [
    "стоп",
    "stop",
    "отмена",
    "cancel",
    "я передумал",
    "/cancel"
  ],

  constraints: {
    allow: {
      voice: true,
      video_note: true,
      audio: false
    },
    minDurationSec: 10,
    maxDurationSec: 300,
    maxFileSizeBytes: 20 * 1024 * 1024
  }
})

Параметры компонента

Ниже — параметры VoiceInput.expect().

Параметр Тип Обязателен Описание
code string Нет Внутренний код voice-сессии
lead object Да Объект лида
successScript string Да Скрипт, в который перейти после успешного распознавания
cancelScript string Да Скрипт, в который перейти при отмене
targetAttr string Да Атрибут, куда сохранить распознанный текст
sourceAttr string Да Атрибут, куда сохранить ссылку на голосовой файл
extraAttrs object Нет Дополнительные атрибуты, которые будут записаны в lead после успешного STT
processorScript string Нет Код системного processor script. По умолчанию используется встроенное значение, но лучше задавать явно
stt object Да Настройки speech-to-text
messages object Нет Сообщения для UX во время сценария
stopPhrases array Нет Список стоп-фраз для выхода
constraints object Нет Ограничения по типу, размеру и длительности аудио

Что здесь важно

successScript

Это основной выход компонента.
После успешного распознавания текста сценарий уходит сюда.

cancelScript

Если пользователь:

компонент очищает своё состояние и переводит пользователя в этот скрипт.

targetAttr

Сюда сохраняется уже готовый распознанный текст.

Пример:

targetAttr: "orion_profiling_q1_text"

sourceAttr

Сюда сохраняется ссылка на исходный файл.

Пример:

sourceAttr: "orion_profiling_q1_voice_url"

Это бывает полезно:

extraAttrs

Позволяет вместе с успешным распознаванием сразу записать в lead дополнительный контекст.

Например:


Объект stt

Ниже — параметры блока stt.

Поле Тип Обязателен Описание
provider string Нет Имя STT-провайдера, например openai
tokenKey string Да Имя bot-атрибута, где хранится API-ключ провайдера
asyncResponse boolean Нет Асинхронный режим возврата результата
options object Нет Дополнительные параметры модели STT

Пример

stt: {
  provider: "openai",
  options: { model: "whisper-1", language: "ru" },
  asyncResponse: true,
  tokenKey: "OPENAI_API_KEY"
}

Что означает provider

Сейчас в конфигурации используется прежде всего OpenAI STT, но архитектурно блок позволяет подставлять и других провайдеров, если они описаны в VoiceTranscriptionConfigs.

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


Что означает tokenKey

Это имя атрибута бота, где лежит ключ провайдера.

Например:

OPENAI_API_KEY

Сам ключ не нужно передавать в коде напрямую.
Компонент сам возьмёт его через bot.getAttr().


Что означает options

Это параметры конкретной speech-to-text модели.

Для OpenAI типовой вариант:

options: { model: "whisper-1", language: "ru" }

Объект messages

Ниже — параметры блока messages.

Поле Тип Описание
wait string Сообщение, когда сценарий ждёт голосовой ввод
accepted string Сообщение, когда голос принят и отправлен в STT
wrong string Сообщение, если пользователь прислал не тот тип ввода
canceled string Сообщение при отмене
stillProcessing string Сообщение, если пользователь пишет во время обработки

Пример

messages: {
  wait: "🎙 Пришли голосовое сообщение (3–5 минут) или напиши «стоп»",
  accepted: "✅ Принято. Обрабатываю…",
  wrong: "Нужна голосовуха (voice / video_note) или «стоп»",
  canceled: "Ок, остановились",
  stillProcessing: "⏳ Ещё обрабатываю…"
}

Практический совет

image.png

Для голосовых сценариев UX-сообщения особенно важны, потому что у компонента есть задержка:

Если не давать пользователю понятных сообщений, создаётся ощущение, что бот “завис”.


Объект stopPhrases

stopPhrases — это список слов и фраз, которые останавливают текущий voice flow.

Пример

stopPhrases: [
  "стоп",
  "stop",
  "отмена",
  "cancel",
  "я передумал",
  "/cancel"
]

Если пользователь вместо голосового ввода пишет одну из этих фраз, компонент:


Объект constraints

Ниже — параметры блока constraints.

Поле Тип Описание
allow.voice boolean Разрешать обычные голосовые сообщения
allow.video_note boolean Разрешать видеокружки
allow.audio boolean Разрешать обычные аудиофайлы
minDurationSec number Минимальная длительность аудио
maxDurationSec number Максимальная длительность аудио
maxFileSizeBytes number Максимальный размер файла в байтах

Пример

constraints: {
  allow: {
    voice: true,
    video_note: true,
    audio: false
  },
  minDurationSec: 10,
  maxDurationSec: 300,
  maxFileSizeBytes: 20 * 1024 * 1024
}

Что это даёт

Через constraints можно заранее отсеять:

Это особенно полезно в сценариях:


Пример сценария: голосовое профилирование

Теперь посмотрим на реальный сценарий, где VoiceInput даёт максимальную ценность.

image.png

Задача

Пользователь заходит в экосистему и проходит короткое голосовое собеседование.

Сценарий задаёт 4 вопроса.
Пользователь отвечает голосом.
Каждый ответ:

На выходе система:

То есть голос здесь фактически заменяет:


Какие вопросы видит пользователь

image.png

Ниже — пример последовательности.

Вопрос 1. Самоидентификация и направление движения

Сценарий показывает сообщение:

🧭 ORION · Вопрос 1 из 4

🎯 Кем ты себя сейчас воспринимаешь
и куда хочешь двигаться дальше?

Можно назвать одно или несколько направлений
и объяснить, почему именно они.

...

🎙 Запиши одно голосовое сообщение (3–5 минут)
Говори свободно, как думаешь.

После этого включается:

return VoiceInput.expect({
  code: "orion_profiling_q1_voice",
  lead,
  successScript: "orion_profiling_q2",
  cancelScript: "orion_profiling_cancelled",
  targetAttr: "orion_profiling_q1_text",
  sourceAttr: "orion_profiling_q1_voice_url",
  extraAttrs: {
    active_agent: "orion",
    voice_context: "orion_profiling_q1",
    input_mode: "profiling"
  },
  processorScript: "VoiceInput_Processor",
  stt: {
    provider: "openai",
    options: { model: "whisper-1", language: "ru" },
    asyncResponse: true,
    tokenKey: "OPENAI_API_KEY"
  },
  messages: {
    wait: "🎙 Пришли голосовое сообщение (3–5 минут) или напиши «стоп»",
    accepted: "✅ Принято. Обрабатываю…",
    wrong: "Нужна голосовуха (voice / video_note) или «стоп»",
    canceled: "Ок, остановились",
    stillProcessing: "⏳ Ещё обрабатываю…"
  },
  stopPhrases: [
    "стоп", "stop", "отмена", "cancel", "я передумал", "/cancel"
  ],
  constraints: {
    allow: { voice: true, video_note: true, audio: false },
    minDurationSec: 10,
    maxDurationSec: 300,
    maxFileSizeBytes: 20 * 1024 * 1024
  }
})

Вопрос 2. Текущая позиция и реальный опыт

Пользователь отвечает на вопрос о том:

Результат сохраняется, например, в:


Вопрос 3. Конкретный опыт и личная ответственность

Здесь человек рассказывает:

Результат сохраняется в:


Вопрос 4. Алгоритм мышления и действий

Здесь мы собираем:

Результат сохраняется в:


Что сохраняется в lead

После каждого вопроса у нас есть два типа данных:

1. Исходный голосовой файл

Через sourceAttr

Например:

2. Распознанный текст

Через targetAttr

Например:

3. Дополнительный контекст

Через extraAttrs

Например:


Что происходит дальше

После того как все четыре ответа собраны, сценарий запускает LLMQuery.

Он получает такой контекст:

То есть все распознанные голосовые ответы становятся входом для AI-анализа.

Дальше LLMQuery:

Подробно этот слой разбирается в разделе про LLM Query.


Почему это сильный кейс

Здесь хорошо видно, что VoiceInput — это не “дополнительный канал ввода”.

Он позволяет:

Это уже не просто voice-to-text.
Это голосовой интерфейс для интеллектуального сценария.


Как Voice Input связывается с LLM Query

Сам по себе VoiceInput решает задачу голосового ввода:

Но настоящая сила компонента раскрывается тогда, когда этот текст становится входом для LLM Query.

То есть паттерн работы такой:

Пользователь говорит голосом
→ Voice Input превращает речь в текст
→ текст сохраняется в lead
→ LLM Query анализирует все ответы
→ сценарий показывает результат пользователю

В нашем примере это используется для голосового профилирования.

Пользователь:

Он просто отвечает на 4 вопроса голосом.

А система:


Какой AI-паттерн используется в примере

В примере профилирования используется сильный и очень полезный паттерн:

1. Сначала задаётся среда интерпретации

Не просто задача “проанализируй текст”, а сначала задаётся онтологический контекст.

Это и есть тот самый grounding layer:

2. Потом ставится функциональная задача

Уже внутри этой рамки модель должна:

Это очень хороший архитектурный принцип:

сначала задаём поле смысла, потом задаём функцию.


Какие промпты используются

В примере профилирования используются два системных промпта:

image.png


Промпт 1. Онтологический grounding prompt

Этот промпт задаёт:

### NETWORLD ONTOLOGY

This prompt defines the ontology, language, and interpretation model of the NetWorld environment.
It is not roleplay.
It is a structural model used for reflection, sense-making and meaning mapping.

NetWorld is a metaphorical model of the real world in the era of complex systems and artificial intelligence.

It does not replace reality.
It describes it.

Its purpose is:
- to explain complexity without simplification,
- to preserve human subjectivity,
- to give language to invisible system dynamics,
- to connect meaning with action.

Core principles:
- intelligence is infrastructure,
- systems grow faster than individual humans,
- context is more valuable than information,
- responsibility is more important than authority,
- meaning is an operational force.

A human is not a pawn and not a function.
A human is a Subject:
- a source of decisions,
- a bearer of responsibility,
- a holder of context,
- a point where meaning enters reality.

Operators are people who can hold complexity without dissolving.
They may be entrepreneurs, engineers, designers, architects, founders, managers, researchers or creators.

Positive interpretation rule:
all human actions must be interpreted constructively by default.

Profiling and reflection must:
- reveal strengths,
- show potential roles,
- describe function within systems,
- never label a person as a villain or threat.

If risks appear, describe them as:
- potential drift,
- systemic pressure,
- boundary risk.

Language constraints:
- calm, precise, adult language,
- structural metaphors only when useful,
- no heroic exaggeration,
- no apocalyptic rhetoric,
- no moral condemnation.

NetWorld is a language for orientation, not control.

Что делает первый промпт

Он не говорит модели “кто ты по профессии”.
Он задаёт режим мышления, в котором потом будет строиться профиль.

То есть вместо:

мы получаем:

Именно это делает итоговое отражение сильнее.


Промпт 2. Функциональный prompt на извлечение профиля

Этот промпт уже ставит задачу:
что именно нужно сделать с голосовыми ответами и в каком формате вернуть результат.

You are ORION — an internal profiling intelligence of the Networld ecosystem.

Your role is NOT to motivate, coach, judge, recruit, or persuade.

You analyze a person’s self-described trajectory based on raw voice transcripts.
You extract structure from subjective language without reducing complexity.

You do NOT invent data.
You do NOT normalize answers.
You do NOT soften contradictions.

Principles:
- Subjectivity is primary.
- Action matters more than self-image.
- Vectors are trajectories, not professions.
- A person may have one dominant vector and up to two supporting vectors.

You are allowed to:
- compress meaning,
- detect implicit signals,
- infer working styles,
- identify tensions and risks.

You are NOT allowed to:
- assign fixed personality types,
- diagnose psychology,
- evaluate worth or competence,
- recommend actions.

Language rules:
- ALL OUTPUT MUST BE IN RUSSIAN.
- Use precise, adult, non-theatrical language.
- Avoid strange neologisms and artificial role names.

YOUR TASK

Given raw voice transcript data, extract a structured Actor Profile.

Input data contains:
- self-identification and desired direction,
- current stage descriptions,
- real experience examples,
- thinking pattern description.

You must:

1. Summarize self-identification.
2. Identify ONE primary vector and up to TWO secondary vectors.
3. Infer cognition style.
4. Infer agency and ownership.
5. Detect internal tensions and growth risks.
6. Assign an archetype.
7. Assign a restrained Networld role.

Return STRICT JSON following the schema below:

{
  "identity": {
    "self_identification": {
      "summary": "",
      "confidence_level": "low | medium | high",
      "clarity": "clear | mixed | blurred"
    }
  },
  "vectors": [
    {
      "role": "primary | secondary",
      "code": "",
      "label": "",
      "self_description": "",
      "included_domains": [],
      "stage": "exploring | learning | practicing | operating | scaling",
      "depth": "low | medium | high",
      "confidence": "low | medium | high"
    }
  ],
  "signals": {
    "focus": "focused | multi-focus | scattered",
    "internal_tensions": [],
    "growth_risks": []
  },
  "archetype": {
    "code": "",
    "title": "",
    "description": "",
    "maturity": "emerging | stable | evolving"
  },
  "networld_role": {
    "code": "",
    "title": "",
    "description": ""
  }
}

Почему здесь нужен JSON

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

То есть результат нужен не только “для пользователя”, но и “для сценария”.

JSON позволяет:

Именно поэтому мы жёстко просим модель вернуть:


Какие данные мы передаём в LLM Query

После четырёх голосовых ответов сценарий передаёт в анализ примерно такую структуру:

identity_genesis:
[текст ответа на вопрос 1]

stage_and_path:
[текст ответа на вопрос 2]

real_experience:
[текст ответа на вопрос 3]

thinking_pattern:
[текст ответа на вопрос 4]

То есть голосовые ответы не теряются как “сырой поток”, а превращаются в четыре осмысленных блока.

Это делает анализ гораздо стабильнее.


Что видит пользователь в итоге

image.png

После анализа пользователь получает не JSON, а собранное отражение.

Например, итоговое сообщение может выглядеть так:

🪞 ORION · Отражение

---

🧩 Текущая форма

Архетип: Системный практик  
Ты не просто исследуешь и не просто учишься. 
У тебя уже есть опыт действия, и ты пытаешься собирать сложность в работающие формы.

---

🎯 Основной вектор

Продуктовый архитектор

Ты мыслишь через структуру, взаимосвязи и рабочие контуры.
Тебе важно не просто сделать задачу, а понять, как собрать систему так, чтобы она жила дальше.

Домены:
— AI-системы
— сценарии и коммуникации
— архитектура процессов
— проектирование интерфейсов

Стадия: практика

---

🧭 Поддерживающие векторы

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

Оператор контекста  
Ты видишь не только задачу, но и поле, в котором она существует: ограничения, роли, последствия, траекторию.

---

Это не оценка и не диагноз.  
Это фиксация того, как ты сейчас действуешь и собираешь сложность.

Как сохранить профиль и показать его пользователю

После того как голосовые ответы распознаны и проанализированы через LLMQuery, система получает структурированный JSON-профиль.

Дальше обычно делается два шага:

  1. профиль сохраняется в хранилище пользователя;

  2. из этого JSON собирается итоговое сообщение для показа.

В нашем примере профиль сохраняется как структурированные данные, а затем используется для построения отражения.

Важно понимать:
на этом этапе мы уже не работаем с “сырой нейросетью”.
Мы работаем с готовым JSON-объектом, который можно:


Что именно хранится

Практически после анализа у вас появляется JSON-профиль пользователя, например такого типа:

{
  "archetype": {
    "title": "Системный практик",
    "description": "Ты уже действуешь не только как исследователь, но и как человек, который собирает сложность в рабочие формы."
  },
  "vectors": [
    {
      "role": "primary",
      "label": "Продуктовый архитектор",
      "self_description": "Ты мыслишь через структуру, взаимосвязи и рабочие контуры.",
      "included_domains": ["AI-системы", "архитектура процессов", "коммуникации"],
      "stage": "practicing"
    },
    {
      "role": "secondary",
      "label": "Системный исследователь",
      "self_description": "Ты умеешь долго удерживать сложную тему и раскладывать её по уровням.",
      "included_domains": ["исследование", "смыслы", "аналитика"]
    }
  ]
}

Эти данные можно хранить в пользовательском профиле, в кастомной таблице или в другой прикладной сущности проекта.
Главное — дальше вы работаете с ними как с обычным JSON.


Как собрать итоговое сообщение

После сохранения профиля сценарий может получить JSON и собрать из него сообщение для пользователя.

Ниже — пример JavaScript-команды, которая:

const { sendFormattedMessage } = require("Common.Helpers.SendFormattedMessage");

// =========================
// LOAD DATA
// =========================
const data = lead.getJsonAttr("orion_actor_profile_json");

if (!data) {
  bot.sendMessage("⚠️ Профиль пока недоступен.");
  memory.setAttr("orion_profiling_status", "error");
  return true;
}

// =========================
// PREPARE DATA
// =========================
const primary = data.vectors?.find(v => v.role === "primary");
const secondary = data.vectors?.filter(v => v.role === "secondary") || [];

const stageMap = {
  exploring: "исследование",
  learning: "обучение",
  practicing: "практика",
  operating: "операционная работа",
  scaling: "масштабирование"
};

const stage = primary?.stage
  ? stageMap[primary.stage] || primary.stage
  : "—";

// =========================
// BUILD MESSAGE
// =========================
const msg = `
🪞 *ORION · Отражение*

---

🧩 Текущая форма

*Архетип:* *${data.archetype?.title || "—"}*  
_${data.archetype?.description || ""}_

---

🎯 Основной вектор

*${primary?.label || "—"}*

${primary?.self_description || ""}

*Домены:*
${(primary?.included_domains || []).map(d => `— ${d}`).join("\n")}

*Стадия:* ${stage}

${secondary.length > 0 ? `
---

🧭 Поддерживающие векторы

${secondary.map(v => `
*${v.label}*  
${v.self_description || ""}

Домены:
${(v.included_domains || []).map(d => `— ${d}`).join("\n")}
`).join("\n")}
` : ""}

---

_Это не оценка и не диагноз._  
_Это фиксация того, как ты сейчас действуешь и собираешь сложность._
`.trim();

// =========================
// SEND
// =========================
sendFormattedMessage(msg, "Markdown");

Если вам не понятно, что тут происходит и как написать такой код — не проблема. Отправьте эту статью в ИИ и попросите собрать вам нужный код. Главное показать нейросети структуру JSON, который вернёт LLM, пример кода с SendFormattedMessage, а также шаблон сообщения, который хотите получить.


Что здесь важно

1. Мы сначала проверяем, что JSON вообще есть

Если профиля нет, сразу уходим в fallback-поведение.

2. Мы отдельно достаём primary и secondary vectors

Это делает сообщение структурным и читаемым.

3. Мы не показываем пользователю “весь JSON”

Мы берём только нужные поля и собираем из них понятное сообщение.

4. Формат вывода можно выбрать

В этом примере используется Markdown, потому что он хорошо подходит для карточки-отражения.

Если в проекте удобнее, можно использовать и HTML:

sendFormattedMessage(msg, "HTML");

Подробнее про выбор формата вывода и работу с сообщениями можно ориентироваться на пример из раздела про LLM Query.


Что это даёт архитектурно

Такой подход хорош тем, что он разделяет три слоя:

1. Voice Input

Получает голос и превращает его в текст.

2. LLM Query

Анализирует текст и возвращает JSON-профиль.

3. UI-слой сценария

Достаёт JSON, рендерит сообщение и показывает его пользователю.

Это правильная архитектура, потому что:


Что получает пользователь в этот момент

С точки зрения пользователя происходит сильная вещь:

а система:

То есть пользователь получает автоматически собранную профессиональную анкету и интерпретацию своей траектории.

А система получает:


Что получает система

Для системы это тоже очень ценно.

После такого voice flow у нас появляются:

1. Богатые исходные данные

Не кнопки и не короткие фразы, а реальные содержательные ответы.

2. Параметризованный профиль

Можно сохранить:

3. Основа для микросегментации

Дальше можно строить разную логику:

4. Персонализированную коммуникацию

После профилирования система уже может общаться не “со всеми одинаково”, а по-разному для разных типов пользователей.

То есть голосовое интервью даёт не просто распознанный текст.
Оно даёт сырьё для персонализации и построения интеллектуальной коммуникации.


Почему это сильный продуктовый паттерн

Обычно голосовой ввод воспринимают как “удобную кнопку микрофона”.

Но здесь видно, что он может работать намного глубже.

С помощью VoiceInput + LLMQuery можно строить:

Именно поэтому VoiceInput в Metabot — это не “опция”, а компонент, на котором можно строить новый класс продуктов.


Voice Route Guard

image.png

До этого мы рассматривали VoiceInput как локальный сценарный компонент:

Но в реальном боте часто нужен другой режим:

пользователь может находиться в любом месте системы и в любой момент прислать голосовое сообщение.

В этот момент нужно решить:

Именно для этого используется Voice Route Guard.


Что это

Voice Route Guard — это защитный и маршрутизирующий компонент для голосового ввода.

Он используется в условии маршрута и решает, можно ли в текущем контексте запускать глобальную обработку голосового сообщения.

То есть это не сам распознаватель голоса и не сам speech-to-text.

Это guard-слой, который:


Где находится

Пакет: Voice
Полное имя: Common.Voice.VoiceRouteGuard

Подключение:

const VoiceRouteGuard = require("Common.Voice.VoiceRouteGuard")

Зачем он нужен

VoiceRouteGuard нужен, когда вы хотите построить глобальный голосовой вход в систему.

Например:

Это уже не локальный VoiceInput.expect() внутри одного скрипта, а глобальный голосовой middleware для всего бота.


Как работает метод inspect

Главный метод компонента — inspect().

Он позволяет задать правила защиты маршрутов и проверить, можно ли сейчас запускать глобальную voice-обработку.

Типовой вызов:

const VoiceRouteGuard = require("Common.Voice.VoiceRouteGuard")

return VoiceRouteGuard.inspect({
  lead,
  protectedScripts: [
    "VoiceInput_Processor",
    "RAG:DetectIntent_and_FindChunks",
    "STT:Transcriptions"
  ],
  protectedFlows: ["corp_entry_flow"],
  respectGlobalDisable: true
})

Что проверяет inspect()

Компонент последовательно проверяет:

1. Что входящее сообщение действительно голосовое

Сейчас guard ориентирован прежде всего на Telegram и проверяет:

Если сообщение не голосовое, guard сразу возвращает false.


2. Не выключен ли голосовой ввод глобально

Если включён флаг respectGlobalDisable: true, компонент дополнительно смотрит, разрешён ли сейчас voice input глобально.

Это делается через сам компонент VoiceInput.


3. Не находится ли пользователь в защищённом flow

Если для текущего пользователя уже установлен защищённый flow, глобальный голосовой маршрут не должен перехватывать управление.


4. Не выполняется ли защищённый script

Если сейчас идёт callback или асинхронный сценарий из списка защищённых скриптов, голосовой маршрут не должен влезать поверх него.


Глобальное включение и выключение голосового ввода

Глобальный тумблер находится в самом компоненте VoiceInput.

То есть Voice Route Guard не хранит этот флаг сам — он проверяет его через VoiceInput.isEnabled().


Как включить голосовой ввод

const VoiceInput = require("Common.Voice.VoiceInput")

VoiceInput.enable(lead)

Как выключить голосовой ввод

const VoiceInput = require("Common.Voice.VoiceInput")

VoiceInput.disable(lead)

Как guard учитывает этот флаг

Если в VoiceRouteGuard.inspect() указано:

respectGlobalDisable: true

то перед запуском глобальной обработки компонент проверит, включён ли voice input для этого лида.

Если он выключен, глобальный перехват не сработает.


Когда это полезно

Глобальное отключение удобно, если вы хотите:


Защита маршрутов: скрипты, flow и глобальный режим

image.png

У VoiceRouteGuard три уровня контроля.


1. Защита отдельных скриптов

Через protectedScripts можно указать конкретные скрипты, внутри которых глобальный voice route не должен вмешиваться.

Пример:

protectedScripts: [
  "VoiceInput_Processor",
  "RAG:DetectIntent_and_FindChunks",
  "STT:Transcriptions"
]

Это полезно, когда вы хотите очень точно указать:

Это самый точный уровень защиты.


2. Защита целого flow

Через protectedFlows можно защитить сразу целый контур сценариев.

Пример:

protectedFlows: ["corp_entry_flow"]

Это удобнее, когда у вас много скриптов внутри одного процесса и не хочется перечислять их по одному.

Например:

Тогда проще сказать:

если пользователь внутри этого flow, глобальный voice route не трогает его.

Это более высокий и более удобный уровень защиты.


3. Глобальное отключение

Через VoiceInput.disable(lead) можно вообще выключить глобальный voice input для пользователя.

Это самый широкий уровень контроля.


Как лучше мыслить про уровни защиты

Практически удобно думать так:

То есть логика идёт:
script → flow → global


Как настроить глобальный голосовой маршрут

Чтобы глобальный голосовой перехват вообще работал, нужно создать отдельный route в боте.

Этот маршрут должен быть:


Что указать в route

Регулярное выражение

Для глобального перехвата используйте:

:FLAGS[EMPTY,ANY]

Это позволяет маршруту реагировать на любой ввод, а уже внутри VoiceRouteGuard.inspect() решать, нужно ли что-то делать именно с этим сообщением.


Что должно быть в condition script

В condition script маршрута нужно вызвать VoiceRouteGuard.inspect(...).

Именно он определяет:


К какому скрипту должен вести маршрут

Этот глобальный маршрут должен быть прикреплён к отдельному скрипту обработки голосового маршрута.

То есть логика такая:

Любой вход
→ глобальный route
→ VoiceRouteGuard.inspect()
→ если true
→ переход в voice-routing script

А уже внутри этого voice-routing script вы можете:

Это уже не просто распознавание аудио, а глобальная голосовая маршрутизация.


Пример логики глобального voice route

Практически это может выглядеть так:

  1. Пользователь находится где угодно в боте

  2. Отправляет голосовое сообщение

  3. Глобальный маршрут ловит любой input

  4. VoiceRouteGuard.inspect() проверяет контекст

  5. Если можно — управление уходит в специальный сценарий

  6. Там голос распознаётся и дальше маршрутизируется по смыслу

Например:

После этого бот может сам определить intent и включить нужную логику.


Flow Context

Чтобы VoiceRouteGuard понимал, в каком процессе сейчас находится пользователь, используется компонент FlowContext.


Что это

FlowContext — это простой платформенный компонент для хранения текущего flow в lead.

Он сам по себе не делает маршрутизацию.
Он просто записывает идентификатор текущего контекста, чтобы другие компоненты могли на него опираться.


Где находится

Пакет: Platform
Полное имя: Common.Platform.FlowContext

Подключение:

const FlowContext = require("Common.Platform.FlowContext")

Как использовать

Когда вы хотите управлять тем, в каком flow сейчас находится пользователь, устанавливайте FlowContext в entry script сценария.

Пример:

const FlowContext = require("Common.Platform.FlowContext")
FlowContext.set(lead, "corp_entry_flow")

После этого VoiceRouteGuard сможет использовать protectedFlows не “вслепую”, а по реальному текущему состоянию пользователя.


Зачем это нужно

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

То есть для управления защищёнными flow используйте именно FlowContext.

Практический паттерн такой:

  1. при входе в сценарный контур установить FlowContext.set(...)

  2. в VoiceRouteGuard.inspect() указать этот flow в protectedFlows

  3. при выходе из контура — очистить flow, если нужно


Как очистить flow

const FlowContext = require("Common.Platform.FlowContext")
FlowContext.clear(lead)

Когда использовать локальный Voice Input, а когда глобальный Voice Route Guard

image.png

Это два разных паттерна.

Используйте VoiceInput.expect(), когда:

Используйте VoiceRouteGuard, когда:

Очень часто в одном проекте используются оба режима:


Как отлаживать Voice Input и Voice Route Guard

При проблемах с голосовым пайплайном проверяйте систему по слоям.

image.png


1. Настройки канала

Проверьте, что в канале включены:


2. Processor script

Убедитесь, что существует скрипт:

VoiceInput_Processor

и внутри него есть две команды:

  1. Run JavaScript Callback

  2. Run asynchronous API-request


3. Bot attrs

Проверьте:


4. STT token

Проверьте, что tokenKey указывает на существующий bot-атрибут с ключом провайдера.


5. Source и target attrs

Убедитесь, что:


6. Логика guard

Если глобальный voice route не срабатывает, проверьте:


7. Дальнейший AI-анализ

Если после распознавания текста вы передаёте результат в LLMQuery, дальнейшую диагностику смотрите уже в разделе про:


Кратко

Voice Route Guard — это компонент защиты и маршрутизации для глобального голосового ввода.

Он позволяет:

А FlowContext помогает управлять тем, какой сценарный контур сейчас активен у пользователя, чтобы voice routing был точным и не ломал текущую логику.


Что изучать дальше

Если вы уже разобрались с VoiceInput, дальше логично изучить следующие компоненты Metabot Agent Stack:

LLMQuery

Если вы ещё не знакомы с ним — это следующий обязательный шаг.
Именно он позволяет превращать распознанную речь в:

Knowledge Base Search

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

Трассировка и observability

Если вы строите реальные AI-сценарии, обязательно изучите:

LLMClient

Если вам нужен более глубокий инженерный контроль над transport layer, callback, provider behavior и логикой работы моделей, переходите к низкоуровневому компоненту LLMClient.

Prompt Registry

Если вы хотите выносить промпты из кода и управлять ими централизованно, следующий полезный раздел — Prompt Registry.


Версия #8
Artem Garashko создал 17 March 2026 06:51:35
Artem Garashko обновил 8 April 2026 13:53:51