Support triage with speculative fan-out
Route a ticket to the right queue while collecting the signals that may matter later: intent, urgency, frustration, bug severity, reproducibility, and refund intent.
NOUL×3SCORE×2CHOICE×1
01
STATE
state = {
"ticket": {
"subject": "Payments fail at checkout",
"messages": [
{"from": "customer", "text": "Checkout has failed for three days. We are losing sales."},
],
},
"account": {"tier": "business", "region": "us"},
"product": {"known_incidents": ["stripe-connect-degraded"]},
}Use a named object when the decision depends on more than one piece of context.
1 リクエスト / 6 回答
このパターンを一行で
02
QUESTIONS
from typesafe_sdk import Choice, Noul, Score
questions = {
"intent": Choice(
instructions="What is the primary reason for contact in `ticket.messages[0].text`?",
criteria={
"bug_report": "A product defect, outage, or integration failure.",
"billing": "A charge, refund, invoice, or subscription issue.",
"feature_request": "A request for a capability that does not exist yet.",
"information": "A question that can be answered without an incident workflow.",
"other": "None of the options clearly fits.",
},
),
"is_urgent": Noul(
instructions="Does `ticket.messages[0].text` explicitly communicate time pressure or immediate business impact?",
),
"frustration": Score(
instructions="How frustrated does the customer appear in `ticket.messages[0].text`?",
criteria=[
"Calm and neutral",
"Concerned but civil",
"Very angry or using strong language",
],
),
# These are intentionally speculative: code will ignore them for non-bug tickets.
"bug_severity": Score(
instructions="If this is a bug report, how severe is the user impact?",
criteria=[
"Minor inconvenience or workaround available",
"Material degradation affecting some users",
"Critical outage blocking a core workflow",
],
),
"has_reproducible_steps": Noul(
instructions="Does `ticket.messages` contain enough steps or evidence for an engineer to reproduce the issue?",
),
"refund_requested": Noul(
instructions="Does `ticket.messages` request a refund or reversal of a charge?",
),
}03
CODE-OWNED ROUTING
def route_ticket(response) -> str:
intent = response.answers["intent"]
if intent.confidence < 0.6:
return "human_review"
if intent.choice == "bug_report":
severity = response.answers["bug_severity"].score
reproducible = response.answers["has_reproducible_steps"].noul
if severity >= 1.5 and reproducible >= 0.6:
return "engineering_escalation"
return "bug_backlog"
if intent.choice == "billing":
return "billing_with_refund_flag" if response.answers["refund_requested"].noul >= 0.7 else "billing"
if intent.choice == "feature_request":
return "product_feedback"
if intent.choice == "information":
return "self_serve_or_support_llm"
return "human_review"The fan-out avoids serial “classify, then ask about severity, then ask about refund” calls. The unanswered dimensions are cheap to ignore when they are irrelevant. See the official speculative fan-out pattern.