feat(sprint6b-F): guards в new_booking — require_legal_rep

- check_guards() в state_machine.py: проверяет guards_json шага при переходе;
  trigger_slot/trigger_value/required_slots; нормализует "true"/"false"-строки
- qualify step: guard require_legal_rep — блокирует переход в present, если
  is_child=true и не заполнены legal_rep_name / legal_rep_phone
- Промпт qualify обновлён: инструкции по is_child, legal_rep, requested_doctor,
  waitlist_flag, needs_surgologist_first
- ensure_seed_guards() патчит guards_json существующих шагов при старте
- Sandbox: блок валидации показывает guard_name + missing_slots + description
- Settings: обновлён лейбл поля guards с примером формата

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
AR 15 M4
2026-04-26 18:27:10 +05:00
parent 45832e2b37
commit 4977199cd4
7 changed files with 171 additions and 13 deletions
+48
View File
@@ -152,3 +152,51 @@ def validate_transition(
False,
f"requested {requested_step!r} not in allowed_next {allowed_next!r} of {current_step!r}",
)
def check_guards(
*,
current_step_code: str,
requested_step_code: str,
slots: dict,
guards: dict,
) -> tuple[bool, str | None, list[str], str | None]:
"""Проверяет guards шага: можно ли перейти с учётом текущих слотов.
Guards проверяются только при реальном переходе (requested != current).
Возвращает (ok, guard_name, missing_slots, guard_description).
Формат guards:
{
"require_legal_rep": {
"description": "...",
"trigger_slot": "is_child",
"trigger_value": true,
"required_slots": ["legal_rep_name", "legal_rep_phone"]
}
}
"""
if requested_step_code == current_step_code or not guards:
return True, None, [], None
for guard_name, guard_def in guards.items():
if not isinstance(guard_def, dict):
continue
trigger_slot = guard_def.get("trigger_slot")
trigger_value = guard_def.get("trigger_value")
required_slots: list[str] = guard_def.get("required_slots", [])
slot_val = slots.get(trigger_slot) if trigger_slot else None
# Нормализация: модель может вернуть "true"/"false" как строки.
if isinstance(slot_val, str) and slot_val.lower() in ("true", "false"):
slot_val = slot_val.lower() == "true"
triggered = (trigger_slot is None) or (slot_val == trigger_value)
if not triggered:
continue
missing = [s for s in required_slots if not slots.get(s)]
if missing:
desc = guard_def.get("description", "")
return False, guard_name, missing, desc
return True, None, [], None