"""Constrained, node-by-node L1 decision-tree generation (spec §4/§5/§6.1). Each call produces ONE node given the problem, category, and full walked path. Generation is constrained to safe/reversible L1 steps and biased to escalate early. normalize_walked_path() turns a resolved walk into a valid tree object for flywheel capture. """ import logging from typing import Any, Optional from uuid import uuid4 from app.core.ai_provider import get_ai_provider from app.core.config import settings from app.services.l1_category_service import HARD_FLOOR_TEXT_PATTERNS from app.services.llm_utils import parse_llm_json logger = logging.getLogger(__name__) MAX_DEPTH = 12 VALID_NODE_TYPES = {"question", "instruction", "resolved", "escalate"} class UnsafeNodeError(ValueError): """Raised when a generated node violates the hard floor or is malformed.""" SYSTEM_PROMPT = """\ You are an L1 helpdesk troubleshooting guide builder. Given a problem and the steps already tried, produce the SINGLE next node of a yes/no decision tree. HARD RULES: - Only safe, reversible, observe-or-restart-class steps: checking status, toggling, restarting, reconnecting, re-entering credentials the USER already knows. - NEVER produce steps that: edit the registry/system files/boot config; delete or format data/disks; change credentials/MFA/security/firewall/AV; run elevated or admin scripts; touch domain controllers/DNS/DHCP or production servers; or have billing/license impact. These are out of L1 scope. - When you run out of safe in-scope steps, DO NOT GUESS. Emit an "escalate" node. Return ONLY a JSON object for ONE node, one of: {"node_type":"question","text":"","yes_label":"