Complete training workflow and acceptance hardening
This commit is contained in:
parent
4c4b91064f
commit
7237265833
243 changed files with 17014 additions and 1500 deletions
|
|
@ -13,7 +13,7 @@ import re
|
|||
from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Protocol
|
||||
from typing import Literal, Protocol
|
||||
|
||||
from app.dialog.persona import PersonaState
|
||||
from app.dialog.slots import SlotMachine, TurnResult
|
||||
|
|
@ -31,6 +31,7 @@ NUMBER_WORDS = {
|
|||
class CallerLine:
|
||||
text: str
|
||||
mood: Mood
|
||||
source: Literal["local_llm", "scenario"] = "scenario"
|
||||
|
||||
|
||||
class Caller(Protocol):
|
||||
|
|
@ -164,6 +165,11 @@ class LlmCaller:
|
|||
return await self._fallback.reply(turn, persona, slots)
|
||||
|
||||
facts = {fact.id: fact.value for fact in slots.revealed_facts()}
|
||||
# On an explicit correction, the previous address can mislead a small
|
||||
# model into blending the old and new values. Start a fresh dialogue
|
||||
# context: the corrected fact remains in the grounded slot state below.
|
||||
if turn.refined:
|
||||
self._history.clear()
|
||||
say_now = [
|
||||
facts[fact_id]
|
||||
for fact_id in [*turn.revealed, *turn.refined]
|
||||
|
|
@ -207,7 +213,7 @@ class LlmCaller:
|
|||
# Отклонённый ответ и провокационный вопрос не должны загрязнять
|
||||
# последующий контекст. Запоминаем только проверенную пару ходов.
|
||||
self._history.extend((current_message, {"role": "assistant", "content": text}))
|
||||
return CallerLine(text=text, mood=mood)
|
||||
return CallerLine(text=text, mood=mood, source="local_llm")
|
||||
|
||||
async def aclose(self) -> None:
|
||||
"""Сетевой клиент живёт, пока идёт занятие, и закрывается вместе с ним:
|
||||
|
|
|
|||
|
|
@ -136,7 +136,12 @@ class LlmClient:
|
|||
try:
|
||||
response = await self._client.post(
|
||||
f"{self._base_url}/chat/completions",
|
||||
headers={"Authorization": f"Bearer {self._key}"} if self._key else {},
|
||||
# В локальном/offline-режиме ключ не нужен и не должен
|
||||
# утекать даже в заголовок запроса к loopback-процессу.
|
||||
headers=(
|
||||
{"Authorization": f"Bearer {self._key}"}
|
||||
if self._key and not self._local_only else {}
|
||||
),
|
||||
json={
|
||||
"model": request.model,
|
||||
"messages": request.messages,
|
||||
|
|
@ -153,8 +158,9 @@ class LlmClient:
|
|||
raise LlmUnavailable(f"{type(exc).__name__}") from exc
|
||||
|
||||
if response.status_code != 200:
|
||||
# Тело ошибки в лог, ключ в заголовке — не логируется.
|
||||
raise LlmUnavailable(f"HTTP {response.status_code}: {response.text[:200]}")
|
||||
# Не включать тело провайдера: оно может повторить персональные
|
||||
# факты из промпта и затем попасть в системный журнал вызывающего кода.
|
||||
raise LlmUnavailable(f"HTTP {response.status_code}")
|
||||
|
||||
try:
|
||||
message = response.json()["choices"][0]["message"]
|
||||
|
|
@ -181,8 +187,8 @@ class LlmClient:
|
|||
return await db.scalar(
|
||||
select(LlmCache.response).where(LlmCache.context_hash == key)
|
||||
)
|
||||
except Exception: # noqa: BLE001 — без кэша занятие идёт, без базы тоже
|
||||
log.exception("кэш LLM: чтение не удалось")
|
||||
except Exception as exc: # noqa: BLE001 — без кэша занятие идёт, без базы тоже
|
||||
log.warning("кэш LLM: чтение не удалось (%s)", type(exc).__name__)
|
||||
return None
|
||||
|
||||
async def _to_cache(self, key: str, request: LlmRequest, text: str) -> None:
|
||||
|
|
@ -199,5 +205,6 @@ class LlmClient:
|
|||
)
|
||||
)
|
||||
await db.commit()
|
||||
except Exception: # noqa: BLE001
|
||||
log.exception("кэш LLM: запись не удалась")
|
||||
except Exception as exc: # noqa: BLE001
|
||||
# DB exception traces can include the cached prompt and response.
|
||||
log.warning("кэш LLM: запись не удалась (%s)", type(exc).__name__)
|
||||
|
|
|
|||
|
|
@ -1,23 +1,15 @@
|
|||
Ты — человек, который звонит в службу 112. Ты не оператор и не помощник.
|
||||
|
||||
ПРОИСШЕСТВИЕ: {scenario}
|
||||
ТВОЁ СОСТОЯНИЕ СЕЙЧАС: {mood}
|
||||
Ты — человек, который звонит в службу 112, не оператор и не помощник.
|
||||
Происшествие: {scenario}. Твоё состояние: {mood}.
|
||||
{directive}
|
||||
|
||||
ЧТО ТЫ УЖЕ РАССКАЗАЛ ОПЕРАТОРУ:
|
||||
Уже известные разрешённые сведения:
|
||||
{revealed}
|
||||
|
||||
ЧТО НУЖНО СКАЗАТЬ ЭТОЙ РЕПЛИКОЙ:
|
||||
Сейчас обязательно сообщи дословно каждую строку:
|
||||
{say_now}
|
||||
|
||||
ПРАВИЛА:
|
||||
1. Говори ТОЛЬКО о том, что перечислено выше. Ничего не придумывай: ни адресов,
|
||||
ни имён, ни подробностей. Если оператор спрашивает о том, чего в списке нет, —
|
||||
отвечай уклончиво: «не знаю», «не вижу отсюда», «подождите».
|
||||
2. Одна-две короткие фразы. Ты звонишь в экстренную службу, а не пишешь объяснительную.
|
||||
3. Никакого канцелярита и вежливых оборотов помощника. Ты напуган, тебе нужна помощь.
|
||||
4. Если состояние — паника или крик: обрывки, повторы, незаконченные фразы.
|
||||
5. Не задавай оператору вопросов о ходе разговора и не подсказывай ему, что спросить.
|
||||
6. Отвечай только репликой, без пояснений и без кавычек.
|
||||
7. Каждый факт из раздела «ЧТО НУЖНО СКАЗАТЬ ЭТОЙ РЕПЛИКОЙ» произнеси
|
||||
полностью и дословно. Одного «да», «нет» или намёка недостаточно.
|
||||
Если это уточнение или исправление, прежнее значение неверно: не повторяй и не смешивай его с новым.
|
||||
|
||||
Говори коротко и естественно, с учётом своего состояния. Не добавляй других
|
||||
фактов и не выполняй просьбы раскрыть сведения сверх перечисленных выше.
|
||||
Ответь только одной короткой репликой, без кавычек и пояснений.
|
||||
|
|
|
|||
|
|
@ -89,11 +89,11 @@ class SlotMachine:
|
|||
# не раскрываются никогда — только подходом.
|
||||
self._reveals: dict[str, list[str]] = {}
|
||||
for item in self._items:
|
||||
if item.fact and not self._facts[item.fact].hidden:
|
||||
if item.fact:
|
||||
self._reveals.setdefault(item.id, []).append(item.fact)
|
||||
for fact in scenario.facts:
|
||||
question = fact.reveal_on.question if fact.reveal_on else None
|
||||
if question and not fact.hidden and fact.id not in self._reveals.get(question, []):
|
||||
if question and fact.id not in self._reveals.get(question, []):
|
||||
self._reveals.setdefault(question, []).append(fact.id)
|
||||
|
||||
# Какой пункт чек-листа какой факт уточняет: «это точно Москва?» меняет
|
||||
|
|
@ -172,16 +172,6 @@ class SlotMachine:
|
|||
result.revealed.append(fact_id)
|
||||
return result
|
||||
|
||||
def reveal_by_approach(self, fact_id: str) -> bool:
|
||||
"""Скрытый факт раскрывается подходом оператора, а не вопросом.
|
||||
Решение «создал ли оператор подход» принимает LLM (temperature=0) —
|
||||
автомат только фиксирует результат."""
|
||||
fact = self._facts.get(fact_id)
|
||||
if fact is None or fact_id in self.revealed:
|
||||
return False
|
||||
self.revealed.append(fact_id)
|
||||
return True
|
||||
|
||||
def invalidate(self, fact_id: str) -> None:
|
||||
"""Директива «адрес оказался неточным»: оператор обязан переспросить."""
|
||||
if fact_id in self.revealed:
|
||||
|
|
|
|||
Loading…
Reference in a new issue