2026-09-20 10:05:43 +03:00
|
|
|
|
"""make pregen: таблица реплик звонящего для офлайна.
|
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
|
|
|
|
|
|
|
|
|
|
Зачем не «дерево диалога» в буквальном смысле: ветвление уже делает слот-автомат —
|
2026-09-20 10:05:43 +03:00
|
|
|
|
он решает, какой факт раскрыт, был ли повтор и уточнён ли адрес. Модели остаётся
|
|
|
|
|
|
дать формулировки, поэтому таблица индексируется парой «событие × настроение».
|
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
|
|
|
|
|
|
|
|
|
|
Сеть нужна только здесь. На занятии таблица читается с диска, и звонящий
|
|
|
|
|
|
отвечает мгновенно, без интернета.
|
|
|
|
|
|
|
2026-09-20 10:05:43 +03:00
|
|
|
|
**Один запрос на факт, а не на реплику.** В библиотеке 96 вызовов заказчика,
|
|
|
|
|
|
449 фактов и пять настроений: по реплике за запрос это шесть тысяч обращений
|
|
|
|
|
|
и часы ожидания. Модель отдаёт все настроения одного факта разом, а запросы
|
|
|
|
|
|
идут параллельно — это минуты.
|
|
|
|
|
|
|
|
|
|
|
|
Работа возобновляемая: уже заполненные реплики пропускаются, так что прерванный
|
|
|
|
|
|
прогон продолжается с места остановки.
|
|
|
|
|
|
|
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
|
|
|
|
make pregen все сценарии
|
2026-09-20 10:05:43 +03:00
|
|
|
|
make pregen s=t01-1-fire-container один
|
|
|
|
|
|
make pregen t=19 все вызовы билета 19
|
|
|
|
|
|
make pregen force=1 перегенерировать заполненное
|
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 asyncio
|
2026-09-20 10:05:43 +03:00
|
|
|
|
import json
|
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 os
|
2026-09-20 10:05:43 +03:00
|
|
|
|
import re
|
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 sys
|
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
import yaml
|
|
|
|
|
|
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
|
|
|
|
|
|
|
|
from app.config import get_settings # noqa: E402
|
|
|
|
|
|
from app.dialog.caller import MOOD_WORDS, _prompt # noqa: E402
|
|
|
|
|
|
from app.dialog.llm import LlmClient, LlmRequest, LlmUnavailable # noqa: E402
|
|
|
|
|
|
from app.scenarios.loader import load_library # noqa: E402
|
|
|
|
|
|
|
|
|
|
|
|
LIBRARY = ROOT.parent / "scenarios"
|
|
|
|
|
|
OUTPUT = LIBRARY / "pregenerated"
|
|
|
|
|
|
|
|
|
|
|
|
#: Сколько филлеров «не понял вопрос» на каждое настроение: подряд одинаковая
|
|
|
|
|
|
#: фраза звучит как заевшая пластинка.
|
|
|
|
|
|
FILLERS_PER_MOOD = 3
|
|
|
|
|
|
|
2026-09-20 10:05:43 +03:00
|
|
|
|
#: Одновременных запросов к провайдеру. Восемь — компромисс: быстрее упираемся
|
|
|
|
|
|
#: в ограничение частоты у провайдера, медленнее ждём зря.
|
|
|
|
|
|
CONCURRENCY = int(os.environ.get("PREGEN_CONCURRENCY", "8"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def moods_block() -> str:
|
|
|
|
|
|
return "\n".join(f" {mood.value} — {words}" for mood, words in MOOD_WORDS.items())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse(text: str) -> dict[str, str]:
|
|
|
|
|
|
"""JSON из ответа модели. Рассуждающие модели любят обернуть его в ограду
|
|
|
|
|
|
или дописать фразу вокруг — вынимаем первый объект и не спорим."""
|
|
|
|
|
|
match = re.search(r"\{.*\}", text, re.S)
|
|
|
|
|
|
if not match:
|
|
|
|
|
|
return {}
|
|
|
|
|
|
try:
|
|
|
|
|
|
data = json.loads(match.group(0))
|
|
|
|
|
|
except json.JSONDecodeError:
|
|
|
|
|
|
return {}
|
|
|
|
|
|
return {
|
|
|
|
|
|
key: value.strip()
|
|
|
|
|
|
for key, value in data.items()
|
|
|
|
|
|
if isinstance(value, str) and value.strip()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
|
2026-09-20 10:05:43 +03:00
|
|
|
|
async def ask(client: LlmClient, model: str, scenario_title: str, task: str) -> dict[str, str]:
|
|
|
|
|
|
"""Реплики на все настроения одним запросом."""
|
|
|
|
|
|
prompt = _prompt("pregen.md").format(scenario=scenario_title, task=task, moods=moods_block())
|
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
|
|
|
|
for attempt in range(1, 4):
|
|
|
|
|
|
try:
|
2026-09-20 10:05:43 +03:00
|
|
|
|
answer = await client.complete(
|
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
|
|
|
|
LlmRequest(
|
|
|
|
|
|
messages=[{"role": "user", "content": prompt}],
|
|
|
|
|
|
model=model,
|
|
|
|
|
|
temperature=0.9,
|
2026-09-20 10:05:43 +03:00
|
|
|
|
max_tokens=700,
|
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
|
|
|
|
),
|
|
|
|
|
|
use_cache=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
except LlmUnavailable as exc:
|
2026-09-20 10:05:43 +03:00
|
|
|
|
if attempt == 3:
|
|
|
|
|
|
print(f" не получено: {exc}", flush=True)
|
|
|
|
|
|
return {}
|
|
|
|
|
|
# 429 — не сбой, а просьба подождать. Три секунды провайдеру
|
|
|
|
|
|
# ни о чём не говорят, и прогон сгорает на ровном месте.
|
|
|
|
|
|
pause = (20 if "429" in str(exc) else 3) * attempt
|
|
|
|
|
|
await asyncio.sleep(pause)
|
|
|
|
|
|
continue
|
|
|
|
|
|
lines = parse(answer)
|
|
|
|
|
|
if lines:
|
|
|
|
|
|
return lines
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def tasks_for(scenario, table: dict, force: bool) -> list[tuple[str, str, str]]:
|
|
|
|
|
|
"""Что осталось сгенерировать: (секция, ключ, задание для модели)."""
|
|
|
|
|
|
todo: list[tuple[str, str, str]] = []
|
|
|
|
|
|
known = set(MOOD_WORDS)
|
|
|
|
|
|
|
|
|
|
|
|
def missing(section: str, key: str) -> bool:
|
|
|
|
|
|
if force:
|
|
|
|
|
|
return True
|
|
|
|
|
|
have = table.get(section, {}).get(key) or {}
|
|
|
|
|
|
return not all(have.get(mood.value) for mood in known)
|
|
|
|
|
|
|
|
|
|
|
|
for fact in scenario.facts:
|
|
|
|
|
|
if missing("reveal", fact.id):
|
|
|
|
|
|
todo.append(("reveal", fact.id,
|
|
|
|
|
|
f"Оператор спросил, и ты отвечаешь ему вот этим фактом: «{fact.value}». "
|
|
|
|
|
|
"Скажи это своими словами."))
|
|
|
|
|
|
if missing("repeat", fact.id):
|
|
|
|
|
|
todo.append(("repeat", fact.id,
|
|
|
|
|
|
f"Оператор ПОВТОРНО спрашивает то, что ты уже говорил: «{fact.value}». "
|
|
|
|
|
|
"Ты раздражён: напомни, что уже сказал, и повтори коротко."))
|
|
|
|
|
|
# Уточнение: заявитель назвал не тот адрес и теперь поправляется.
|
|
|
|
|
|
# Без этой секции офлайн-звонящий на уточняющий вопрос ответит
|
|
|
|
|
|
# заготовкой и уточнённого адреса не назовёт (tasks/lct-35).
|
|
|
|
|
|
if fact.refined and missing("refine", fact.id):
|
|
|
|
|
|
todo.append(("refine", fact.id,
|
|
|
|
|
|
f"Оператор переспросил, и выясняется, что ты назвал не то: на самом деле "
|
|
|
|
|
|
f"«{fact.refined}». Поправься — не раздражаясь, ты сам ошибся."))
|
|
|
|
|
|
if force or not all(
|
|
|
|
|
|
len(table.get("fillers", {}).get(mood.value) or []) >= FILLERS_PER_MOOD for mood in known
|
|
|
|
|
|
):
|
|
|
|
|
|
for index in range(FILLERS_PER_MOOD):
|
|
|
|
|
|
todo.append(("fillers", str(index),
|
|
|
|
|
|
"Оператор спросил о том, чего ты не знаешь, или ты не расслышал вопрос. "
|
|
|
|
|
|
"Переспроси или отмахнись — фактов не называй."))
|
|
|
|
|
|
return todo
|
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
|
|
|
|
|
|
|
|
|
|
|
2026-09-20 10:05:43 +03:00
|
|
|
|
async def build(client: LlmClient, model: str, scenario, existing: dict, force: bool,
|
|
|
|
|
|
gate: asyncio.Semaphore) -> tuple[dict, int]:
|
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
|
|
|
|
table = {
|
|
|
|
|
|
"scenario": scenario.id,
|
|
|
|
|
|
"model": model,
|
|
|
|
|
|
"generated_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
|
|
|
|
"first_line": scenario.first_line,
|
|
|
|
|
|
"reveal": dict(existing.get("reveal", {})),
|
|
|
|
|
|
"repeat": dict(existing.get("repeat", {})),
|
2026-09-20 10:05:43 +03:00
|
|
|
|
"refine": dict(existing.get("refine", {})),
|
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
|
|
|
|
"fillers": dict(existing.get("fillers", {})),
|
|
|
|
|
|
}
|
2026-09-20 10:05:43 +03:00
|
|
|
|
todo = tasks_for(scenario, table, force)
|
|
|
|
|
|
if not todo:
|
|
|
|
|
|
return table, 0
|
|
|
|
|
|
|
|
|
|
|
|
async def one(section: str, key: str, task: str):
|
|
|
|
|
|
async with gate:
|
|
|
|
|
|
return section, key, await ask(client, model, scenario.title, task)
|
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
|
|
|
|
|
2026-09-20 10:05:43 +03:00
|
|
|
|
results = await asyncio.gather(*(one(*item) for item in todo))
|
|
|
|
|
|
|
|
|
|
|
|
failed = 0
|
|
|
|
|
|
for section, key, lines in results:
|
|
|
|
|
|
if not lines:
|
|
|
|
|
|
failed += 1
|
|
|
|
|
|
continue
|
|
|
|
|
|
if section == "fillers":
|
|
|
|
|
|
for mood, line in lines.items():
|
|
|
|
|
|
table["fillers"].setdefault(mood, []).append(line)
|
|
|
|
|
|
else:
|
|
|
|
|
|
table[section].setdefault(key, {}).update(lines)
|
|
|
|
|
|
return table, failed
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def main() -> int:
|
|
|
|
|
|
settings = get_settings()
|
|
|
|
|
|
if not settings.llm_api_key:
|
|
|
|
|
|
print("нет LLM_API_KEY в backend/.env — генерировать нечем")
|
|
|
|
|
|
return 1
|
|
|
|
|
|
|
2026-09-20 10:05:43 +03:00
|
|
|
|
only = os.environ.get("s") or ""
|
|
|
|
|
|
ticket = os.environ.get("t") or ""
|
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
|
|
|
|
force = bool(os.environ.get("force"))
|
2026-09-20 10:05:43 +03:00
|
|
|
|
|
|
|
|
|
|
scenarios = load_library(LIBRARY)
|
|
|
|
|
|
if only:
|
|
|
|
|
|
scenarios = [s for s in scenarios if s.id == only]
|
|
|
|
|
|
if ticket:
|
|
|
|
|
|
scenarios = [s for s in scenarios if str(s.ticket) == ticket]
|
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
|
|
|
|
if not scenarios:
|
2026-09-20 10:05:43 +03:00
|
|
|
|
print(f"нет сценариев по условию s={only!r} t={ticket!r}")
|
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
|
|
|
|
return 1
|
|
|
|
|
|
|
2026-09-20 10:05:43 +03:00
|
|
|
|
# Прикидка до запуска: человек должен понимать, что он запускает —
|
|
|
|
|
|
# тысячу обращений к платному провайдеру или десяток.
|
|
|
|
|
|
planned = 0
|
|
|
|
|
|
for scenario in scenarios:
|
|
|
|
|
|
path = OUTPUT / f"{scenario.id}.yaml"
|
|
|
|
|
|
existing = yaml.safe_load(path.read_text(encoding="utf-8")) if path.exists() else {}
|
|
|
|
|
|
planned += len(tasks_for(scenario, existing or {}, force))
|
|
|
|
|
|
# flush обязателен: вывод уходит в трубу, а человек должен увидеть оценку
|
|
|
|
|
|
# объёма до того, как уйдёт первый запрос, а не после всего прогона.
|
|
|
|
|
|
print(f"модель: {settings.llm_model_caller}, сценариев: {len(scenarios)}, "
|
|
|
|
|
|
f"запросов: {planned}, параллельно: {CONCURRENCY}", flush=True)
|
|
|
|
|
|
if not planned:
|
|
|
|
|
|
print("всё уже сгенерировано (перегенерировать: make pregen force=1)", flush=True)
|
|
|
|
|
|
return 0
|
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
|
|
|
|
OUTPUT.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
client = LlmClient(timeout=180) # рассуждающие модели отвечают долго
|
2026-09-20 10:05:43 +03:00
|
|
|
|
gate = asyncio.Semaphore(CONCURRENCY)
|
|
|
|
|
|
started = datetime.now(timezone.utc)
|
|
|
|
|
|
total_failed = 0
|
|
|
|
|
|
|
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
|
|
|
|
try:
|
2026-09-20 10:05:43 +03:00
|
|
|
|
for number, scenario in enumerate(scenarios, 1):
|
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
|
|
|
|
path = OUTPUT / f"{scenario.id}.yaml"
|
|
|
|
|
|
existing = yaml.safe_load(path.read_text(encoding="utf-8")) if path.exists() else {}
|
2026-09-20 10:05:43 +03:00
|
|
|
|
table, failed = await build(client, settings.llm_model_caller, scenario,
|
|
|
|
|
|
existing or {}, force, gate)
|
|
|
|
|
|
total_failed += failed
|
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
|
|
|
|
path.write_text(
|
|
|
|
|
|
yaml.safe_dump(table, allow_unicode=True, sort_keys=False, width=100),
|
|
|
|
|
|
encoding="utf-8",
|
|
|
|
|
|
)
|
2026-09-20 10:05:43 +03:00
|
|
|
|
mark = f", не получено: {failed}" if failed else ""
|
|
|
|
|
|
print(f" [{number}/{len(scenarios)}] {scenario.id}{mark}", flush=True)
|
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
|
|
|
|
finally:
|
|
|
|
|
|
await client.aclose()
|
|
|
|
|
|
|
2026-09-20 10:05:43 +03:00
|
|
|
|
spent = (datetime.now(timezone.utc) - started).total_seconds()
|
|
|
|
|
|
print(f"\nГотово за {spent / 60:.1f} мин. Занятие идёт без сети: "
|
|
|
|
|
|
"звонящий читает таблицу с диска.")
|
|
|
|
|
|
if total_failed:
|
|
|
|
|
|
print(f"Не получено блоков: {total_failed}. Повторный запуск допишет только их.")
|
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
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
raise SystemExit(asyncio.run(main()))
|