2026-09-24 01:10:49 +03:00
|
|
|
|
"""Кто играет звонящего: локальная LLM, таблица или заготовки.
|
lct-07 и lct-19: звонящий на LLM, офлайн-таблица, предгенерация
Что можно сказать, решает слот-автомат, а не модель: в промпт попадают
только раскрытые факты. Иначе LLM услужливо назовёт адрес, которого не
спрашивали. Промпты лежат файлами — их правит тот, кто ведёт занятия.
Отказ провайдера не роняет занятие: звонящий откатывается на заготовки.
Молчащий звонящий хуже шаблонной фразы.
Офлайн — таблица, а не локальная модель. Ветвление уже делает слот-автомат,
поэтому таблица индексируется парой «событие × настроение» и выходит
небольшой: для пожарного сценария 62 реплики, около 36 секунд генерации.
Формулировки в ней от облачной модели, а задержка на занятии нулевая —
локальная 3–4B дала бы формулировки хуже и за 2.5 секунды.
Найдено на бесплатной модели OpenRouter: рассуждающие модели тратят
сотни токенов на размышление и при малом бюджете возвращают пустой
content с finish_reason=length. Пустой ответ теперь честный отказ,
бюджет токенов поднят, рассуждение из ответа исключается.
Живые запросы к LLM вынесены из общего прогона: они требуют сети,
а рассуждающая модель отвечает десятками секунд.
2026-09-17 23:50:19 +03:00
|
|
|
|
|
|
|
|
|
|
Провайдер и модель меняются значением в конфиге, а не кодом. Заготовки —
|
|
|
|
|
|
не запасной костыль, а рабочий режим: занятие идёт и без сети.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import logging
|
|
|
|
|
|
|
|
|
|
|
|
from app.config import get_settings
|
|
|
|
|
|
from app.dialog.caller import Caller, LlmCaller, TemplateCaller
|
|
|
|
|
|
from app.dialog.llm import LlmClient
|
2026-09-24 01:10:49 +03:00
|
|
|
|
from app.dialog.llm import is_loopback_url
|
lct-07 и lct-19: звонящий на LLM, офлайн-таблица, предгенерация
Что можно сказать, решает слот-автомат, а не модель: в промпт попадают
только раскрытые факты. Иначе LLM услужливо назовёт адрес, которого не
спрашивали. Промпты лежат файлами — их правит тот, кто ведёт занятия.
Отказ провайдера не роняет занятие: звонящий откатывается на заготовки.
Молчащий звонящий хуже шаблонной фразы.
Офлайн — таблица, а не локальная модель. Ветвление уже делает слот-автомат,
поэтому таблица индексируется парой «событие × настроение» и выходит
небольшой: для пожарного сценария 62 реплики, около 36 секунд генерации.
Формулировки в ней от облачной модели, а задержка на занятии нулевая —
локальная 3–4B дала бы формулировки хуже и за 2.5 секунды.
Найдено на бесплатной модели OpenRouter: рассуждающие модели тратят
сотни токенов на размышление и при малом бюджете возвращают пустой
content с finish_reason=length. Пустой ответ теперь честный отказ,
бюджет токенов поднят, рассуждение из ответа исключается.
Живые запросы к LLM вынесены из общего прогона: они требуют сети,
а рассуждающая модель отвечает десятками секунд.
2026-09-17 23:50:19 +03:00
|
|
|
|
from app.dialog.tree import TreeCaller, has_table
|
|
|
|
|
|
from app.db.base import get_sessionmaker
|
|
|
|
|
|
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-24 01:10:49 +03:00
|
|
|
|
def build_caller(
|
|
|
|
|
|
scenario_id: str | None = None,
|
|
|
|
|
|
sessionmaker=None,
|
|
|
|
|
|
*,
|
|
|
|
|
|
use_pregenerated: bool = False,
|
|
|
|
|
|
) -> Caller:
|
lct-07 и lct-19: звонящий на LLM, офлайн-таблица, предгенерация
Что можно сказать, решает слот-автомат, а не модель: в промпт попадают
только раскрытые факты. Иначе LLM услужливо назовёт адрес, которого не
спрашивали. Промпты лежат файлами — их правит тот, кто ведёт занятия.
Отказ провайдера не роняет занятие: звонящий откатывается на заготовки.
Молчащий звонящий хуже шаблонной фразы.
Офлайн — таблица, а не локальная модель. Ветвление уже делает слот-автомат,
поэтому таблица индексируется парой «событие × настроение» и выходит
небольшой: для пожарного сценария 62 реплики, около 36 секунд генерации.
Формулировки в ней от облачной модели, а задержка на занятии нулевая —
локальная 3–4B дала бы формулировки хуже и за 2.5 секунды.
Найдено на бесплатной модели OpenRouter: рассуждающие модели тратят
сотни токенов на размышление и при малом бюджете возвращают пустой
content с finish_reason=length. Пустой ответ теперь честный отказ,
бюджет токенов поднят, рассуждение из ответа исключается.
Живые запросы к LLM вынесены из общего прогона: они требуют сети,
а рассуждающая модель отвечает десятками секунд.
2026-09-17 23:50:19 +03:00
|
|
|
|
settings = get_settings()
|
|
|
|
|
|
|
2026-09-24 01:10:49 +03:00
|
|
|
|
local = settings.llm_provider == "local"
|
|
|
|
|
|
mode = settings.dialogue_model_mode
|
|
|
|
|
|
if mode not in {"dialogue", "russian_control"}:
|
|
|
|
|
|
raise ValueError(f"неизвестный DIALOGUE_MODEL_MODE: {mode}")
|
|
|
|
|
|
model = settings.llm_model_control if mode == "russian_control" else settings.llm_model_caller
|
|
|
|
|
|
base_url = settings.llm_control_base_url if mode == "russian_control" else settings.llm_base_url
|
|
|
|
|
|
model_allowed = settings.llm_provider != "disabled" and bool(model and base_url and (
|
|
|
|
|
|
is_loopback_url(base_url, allow_docker_host=settings.allow_docker_host_models)
|
|
|
|
|
|
if settings.offline or local else settings.llm_api_key
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
# Локальный loopback не считается внешней сетью. Если сервер модели не
|
|
|
|
|
|
# отвечает, LlmCaller откатится на проверенные заготовки этой же реплики.
|
|
|
|
|
|
if model_allowed:
|
|
|
|
|
|
client = LlmClient(sessionmaker=sessionmaker or _safe_sessionmaker(), base_url=base_url)
|
|
|
|
|
|
log.info("звонящий: режим %s, модель %s, адрес %s", mode, model, base_url)
|
|
|
|
|
|
return LlmCaller(client, model=model)
|
|
|
|
|
|
|
|
|
|
|
|
if use_pregenerated and scenario_id and has_table(scenario_id):
|
lct-07 и lct-19: звонящий на LLM, офлайн-таблица, предгенерация
Что можно сказать, решает слот-автомат, а не модель: в промпт попадают
только раскрытые факты. Иначе LLM услужливо назовёт адрес, которого не
спрашивали. Промпты лежат файлами — их правит тот, кто ведёт занятия.
Отказ провайдера не роняет занятие: звонящий откатывается на заготовки.
Молчащий звонящий хуже шаблонной фразы.
Офлайн — таблица, а не локальная модель. Ветвление уже делает слот-автомат,
поэтому таблица индексируется парой «событие × настроение» и выходит
небольшой: для пожарного сценария 62 реплики, около 36 секунд генерации.
Формулировки в ней от облачной модели, а задержка на занятии нулевая —
локальная 3–4B дала бы формулировки хуже и за 2.5 секунды.
Найдено на бесплатной модели OpenRouter: рассуждающие модели тратят
сотни токенов на размышление и при малом бюджете возвращают пустой
content с finish_reason=length. Пустой ответ теперь честный отказ,
бюджет токенов поднят, рассуждение из ответа исключается.
Живые запросы к LLM вынесены из общего прогона: они требуют сети,
а рассуждающая модель отвечает десятками секунд.
2026-09-17 23:50:19 +03:00
|
|
|
|
log.info("звонящий по предгенерированной таблице сценария %s", scenario_id)
|
|
|
|
|
|
return TreeCaller(scenario_id)
|
|
|
|
|
|
|
2026-09-24 01:10:49 +03:00
|
|
|
|
log.info("звонящий отвечает фактами сценария: модель отключена или предгенерация не разрешена")
|
|
|
|
|
|
return TemplateCaller()
|
lct-07 и lct-19: звонящий на LLM, офлайн-таблица, предгенерация
Что можно сказать, решает слот-автомат, а не модель: в промпт попадают
только раскрытые факты. Иначе LLM услужливо назовёт адрес, которого не
спрашивали. Промпты лежат файлами — их правит тот, кто ведёт занятия.
Отказ провайдера не роняет занятие: звонящий откатывается на заготовки.
Молчащий звонящий хуже шаблонной фразы.
Офлайн — таблица, а не локальная модель. Ветвление уже делает слот-автомат,
поэтому таблица индексируется парой «событие × настроение» и выходит
небольшой: для пожарного сценария 62 реплики, около 36 секунд генерации.
Формулировки в ней от облачной модели, а задержка на занятии нулевая —
локальная 3–4B дала бы формулировки хуже и за 2.5 секунды.
Найдено на бесплатной модели OpenRouter: рассуждающие модели тратят
сотни токенов на размышление и при малом бюджете возвращают пустой
content с finish_reason=length. Пустой ответ теперь честный отказ,
бюджет токенов поднят, рассуждение из ответа исключается.
Живые запросы к LLM вынесены из общего прогона: они требуют сети,
а рассуждающая модель отвечает десятками секунд.
2026-09-17 23:50:19 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _safe_sessionmaker():
|
|
|
|
|
|
"""Кэш в Postgres — приятный бонус, а не условие работы звонящего."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
return get_sessionmaker()
|
|
|
|
|
|
except Exception: # noqa: BLE001
|
|
|
|
|
|
log.warning("кэш LLM выключен: база недоступна")
|
|
|
|
|
|
return None
|