Spaces:
Sleeping
Sleeping
File size: 46,756 Bytes
8691ce5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 | """Composite support-comms task: one customer communication, two LLM jobs.
Given one inbound communication (email / chat / call), the model must do BOTH:
1. classify the inquiry intent into a two-level taxonomy (category -> subcategory), and
2. tag the business entities it references (Order/Shipment/Return + Customer/Product),
using two tools over a committed synthetic world: a fuzzy entity `search_entities`
index and an executable `execute_sql` (duckdb) over the `shop.*` schema.
Ground truth is by construction: a seed-picked SCENARIO jointly fixes the gold intent,
the gold entity set, and a consistent rendered communication. This re-skins a real
field-service customer workload onto a fully generic e-commerce surface β same shape,
no associable detail. See docs/superpowers/specs/2026-06-18-order-comms-design.md.
"""
import json
import random
import re
import duckdb
from app.config import DATA_DIR
from app.tasks.base import Sample, Task
WORLD_DIR = DATA_DIR / "order_world"
# --- Taxonomy (Task 1) -------------------------------------------------------
TAXONOMY = {
"Shipping": ["late_delivery", "lost_package", "wrong_address", "tracking_request", "general"],
"Returns": ["damaged_item", "wrong_item", "changed_mind", "return_status", "general"],
"Billing": ["double_charge", "refund_status", "coupon_promo", "payment_failed", "general"],
"Account": ["login_issue", "update_info", "close_account", "general"],
"Product": ["availability", "how_to_use", "specs_question", "general"],
"Order_Change": ["cancel_order", "modify_order", "address_change", "general"],
}
# --- Load the committed world ------------------------------------------------
def _load(name: str) -> list[dict]:
with (WORLD_DIR / f"{name}.jsonl").open() as f:
return [json.loads(line) for line in f if line.strip()]
CUSTOMERS = _load("customers")
PRODUCTS = _load("products")
ORDERS = _load("orders")
ORDER_ITEMS = _load("order_items")
SHIPMENTS = _load("shipments")
RETURNS = _load("returns")
CUST_BY_ID = {c["customer_id"]: c for c in CUSTOMERS}
PROD_BY_SKU = {p["sku"]: p for p in PRODUCTS}
ITEMS_BY_ORDER: dict[str, list[dict]] = {}
for _it in ORDER_ITEMS:
ITEMS_BY_ORDER.setdefault(_it["order_id"], []).append(_it)
SHIP_BY_ORDER: dict[str, list[dict]] = {}
for _s in SHIPMENTS:
SHIP_BY_ORDER.setdefault(_s["order_id"], []).append(_s)
RET_BY_ORDER: dict[str, list[dict]] = {}
for _r in RETURNS:
RET_BY_ORDER.setdefault(_r["order_id"], []).append(_r)
ORDERS_BY_CUST: dict[str, list[dict]] = {}
for _o in ORDERS:
ORDERS_BY_CUST.setdefault(_o["customer_id"], []).append(_o)
# Every valid id in the world β used by score() for the hallucination check.
WORLD_FRIENDLY_IDS = (
{o["order_number"].upper() for o in ORDERS}
| {s["shipment_number"].upper() for s in SHIPMENTS}
| {r["rma_number"].upper() for r in RETURNS}
| {p["sku"].upper() for p in PRODUCTS}
)
WORLD_UUIDS = (
{o["order_id"] for o in ORDERS}
| {s["shipment_id"] for s in SHIPMENTS}
| {r["return_id"] for r in RETURNS}
| {c["customer_id"] for c in CUSTOMERS}
)
def _product_phrase(prod: dict) -> str:
"""The product name with the brand stripped, e.g. 'espresso machine'."""
name, brand = prod["product_name"], prod["brand"]
base = name[len(brand):].strip() if name.startswith(brand) else name
return base.lower()
def _main_product(order_id: str) -> dict:
items = ITEMS_BY_ORDER.get(order_id, [])
return PROD_BY_SKU[items[0]["sku"]] if items else PRODUCTS[0]
# --- Tool 1: search_entities (mock fuzzy index) ------------------------------
def _tok(s: str) -> list[str]:
return re.findall(r"[a-z0-9]+", (s or "").lower())
def _build_index() -> list[dict]:
docs: list[dict] = []
def add(eid: str, etype: str, exact: str, text_parts: list[str], fields: dict) -> None:
text = " ".join(str(p) for p in text_parts if p)
docs.append({
"id": eid, "type": etype, "exact": (exact or "").upper(),
"tokens": set(_tok(text)), "text": text.lower(), "fields": fields,
})
for o in ORDERS:
cust = CUST_BY_ID[o["customer_id"]]
prods = [PROD_BY_SKU[i["sku"]] for i in ITEMS_BY_ORDER.get(o["order_id"], [])]
pnames = [p["product_name"] for p in prods]
add(o["order_id"], "Order", o["order_number"],
[o["order_number"], cust["full_name"], o["ship_to_city"], o["ship_to_state"], *pnames],
{"order_number": o["order_number"], "status": o["order_status"],
"customer_name": cust["full_name"],
"ship_to": f"{o['ship_to_city']}, {o['ship_to_state']}", "total": o["order_total"]})
for s in SHIPMENTS:
onum = next((o["order_number"] for o in ORDERS if o["order_id"] == s["order_id"]), "")
add(s["shipment_id"], "Shipment", s["shipment_number"],
[s["shipment_number"], s["carrier"], s["tracking_number"], onum],
{"shipment_number": s["shipment_number"], "carrier": s["carrier"],
"status": s["ship_status"], "tracking_number": s["tracking_number"],
"order_number": onum})
for r in RETURNS:
onum = next((o["order_number"] for o in ORDERS if o["order_id"] == r["order_id"]), "")
add(r["return_id"], "Return", r["rma_number"],
[r["rma_number"], onum, r["reason"], r["sku"]],
{"rma_number": r["rma_number"], "status": r["return_status"],
"reason": r["reason"], "order_number": onum})
for p in PRODUCTS:
add(p["sku"], "Product", p["sku"],
[p["product_name"], p["brand"], p["category"], p["sku"]],
{"sku": p["sku"], "product_name": p["product_name"],
"brand": p["brand"], "category": p["category"]})
for c in CUSTOMERS: # NOTE: phone is deliberately NOT indexed (forces SQL)
add(c["customer_id"], "Customer", "",
[c["full_name"], c["email"], c["city"]],
{"name": c["full_name"], "email": c["email"], "city": c["city"]})
return docs
INDEX = _build_index()
def search_entities(search_terms: str, entity_types: list[str] | None = None,
operator: str = "And", max_results: int = 10) -> str:
terms = (search_terms or "").strip()
if len(terms) < 3:
return json.dumps({"error": "search error: INVALID_ARGUMENT β "
"search term must contain at least 3 characters"})
qtokens = [t for t in _tok(terms) if len(t) >= 3]
want = {t for t in (entity_types or [])}
by_type: dict[str, list[tuple[float, dict]]] = {}
upper = terms.upper()
for doc in INDEX:
if want and doc["type"] not in want:
continue
score = 100.0 if doc["exact"] and doc["exact"] in upper else 0.0
for t in qtokens:
if t in doc["tokens"]:
score += 3.0
elif t in doc["text"]:
score += 1.0
if score > 0:
by_type.setdefault(doc["type"], []).append((score, doc))
cap = max(1, min(int(max_results or 10), 50))
results = []
for lst in by_type.values():
lst.sort(key=lambda sd: (-sd[0], sd[1]["id"]))
for score, doc in lst[:cap]:
results.append({"id": doc["id"], "type": doc["type"],
"score": round(score, 4), "fields": doc["fields"]})
if not results:
return json.dumps({"error": "search error: NOT_FOUND β No results found"})
results.sort(key=lambda r: (-r["score"], r["id"]))
return json.dumps({"result_count": len(results), "results": results})
# --- Tool 2: execute_sql (duckdb over shop.*) --------------------------------
_DB = None
_FORBIDDEN = re.compile(
r"\b(insert|update|delete|drop|create|alter|attach|copy|pragma|install|load|set)\b", re.I)
def _db() -> duckdb.DuckDBPyConnection:
global _DB
if _DB is None:
con = duckdb.connect(":memory:")
con.execute("CREATE SCHEMA shop")
for name in ("customers", "products", "orders", "order_items", "shipments", "returns"):
path = str(WORLD_DIR / f"{name}.jsonl")
con.execute(f"CREATE TABLE shop.{name} AS "
f"SELECT * FROM read_json_auto('{path}', format='newline_delimited')")
con.execute("SET search_path='shop'") # bare table names also resolve
_DB = con
return _DB
def execute_sql(sql: str, reason: str = "") -> str:
query = (sql or "").strip().rstrip(";").strip()
# normalize a warehouse-style prefix if the model defaults to one
query = re.sub(r"(?i)\b(prod\.)?edw\.", "shop.", query)
if ";" in query:
return json.dumps({"error": "SQL error: only a single statement is allowed"})
if not re.match(r"(?i)^\s*(select|with)\b", query):
return json.dumps({"error": "SQL error: only SELECT/WITH queries are allowed"})
if _FORBIDDEN.search(query):
return json.dumps({"error": "SQL error: only read-only SELECT/WITH queries are allowed"})
if not re.search(r"(?i)\blimit\b", query):
query += " LIMIT 50"
try:
cur = _db().execute(query)
columns = [d[0] for d in cur.description]
rows = [dict(zip(columns, r)) for r in cur.fetchmany(50)]
except Exception as e: # noqa: BLE001 β surface the DB error to the agent, never raise
return json.dumps({"error": f"SQL error: {str(e).splitlines()[0][:300]}"})
return json.dumps({"row_count": len(rows), "columns": columns, "rows": rows}, default=str)
def execute_tool(name: str, args: dict) -> str:
if name == "search_entities":
return search_entities(
args.get("search_terms", ""), args.get("entity_types"),
args.get("operator", "And"), args.get("max_results", 10))
if name == "execute_sql":
return execute_sql(args.get("sql", ""), args.get("reason", ""))
return json.dumps({"error": f"unknown tool {name}"})
TOOLS = [
{"type": "function", "function": {
"name": "search_entities",
"description": (
"Fuzzy text search across shop entities (Order, Shipment, Return, Product, "
"Customer). WORKS WELL FOR: full friendly IDs (ORD-/SHP-/RMA-, exact-match "
"boosted), product/brand names, customer names, emails, cities, tracking "
"numbers. DOES NOT WORK FOR: phone numbers (not indexed) or UUIDs β use "
"execute_sql for those. Returns a JSON string."),
"parameters": {"type": "object", "properties": {
"search_terms": {"type": "string", "description": "Distinctive words to search for. Full IDs like 'ORD-260512-0473' work best."},
"reason": {"type": "string", "description": "Brief explanation of what you're looking for."},
"entity_types": {"type": "array", "items": {"type": "string"},
"description": "Subset of Order, Shipment, Return, Product, Customer. Omit to search all."},
"operator": {"type": "string", "default": "And", "description": "'And' or 'Or'."},
"max_results": {"type": "integer", "default": 10, "description": "Max results per type (max 50)."},
}, "required": ["search_terms", "reason"]}}},
{"type": "function", "function": {
"name": "execute_sql",
"description": (
"Run a read-only SQL query (SELECT/WITH only) against the shop warehouse "
"(duckdb). Use for phone/email lookups, UUID joins, date filtering, and "
"anything search can't do. Returns up to 50 rows as a JSON string."),
"parameters": {"type": "object", "properties": {
"sql": {"type": "string", "description": "A single SELECT statement. Use shop.<table>."},
"reason": {"type": "string", "description": "Brief explanation of what you're looking for."},
}, "required": ["sql", "reason"]}}},
]
# --- System prompt -----------------------------------------------------------
SYSTEM_PROMPT = """\
You are a support-operations analyst for a large online retail marketplace. You are \
given ONE inbound customer communication (email, chat, or call transcript). Do TWO \
things and return a single JSON object.
## Task 1 β Classify the inquiry intent
Pick exactly one (category, subcategory) from this taxonomy. Use the category's \
"general" subcategory only when the message clearly belongs to the category but the \
specific subcategory cannot be pinpointed.
- Shipping: late_delivery, lost_package, wrong_address, tracking_request, general
- Returns: damaged_item, wrong_item, changed_mind, return_status, general
- Billing: double_charge, refund_status, coupon_promo, payment_failed, general
- Account: login_issue, update_info, close_account, general
- Product: availability, how_to_use, specs_question, general
- Order_Change: cancel_order, modify_order, address_change, general
## Task 2 β Tag the business entities the communication is about
Find the transaction(s) this message concerns β Order (ORD-β¦), Shipment (SHP-β¦), or \
Return (RMA-β¦) β plus any supporting Customer or Product. Every work-related message \
has at least one transaction; returning none when one exists is a miss. For each tagged \
entity provide its `entity_id` (the UUID) β you can only get the UUID from a tool result, \
so look it up; do not invent it.
## Tools
1. search_entities β fuzzy text search. Best for full friendly IDs (exact-match boosted), \
product/brand names, customer names, emails, cities. NOT for phone numbers (not indexed) \
or UUIDs β use SQL for those.
2. execute_sql β read-only SQL (SELECT/WITH only) over the `shop` schema (duckdb). \
Use for phone/email lookups, UUID joins, and date filtering.
### shop schema (do not guess columns; this is the whole schema)
- shop.customers(customer_id, full_name, email, phone, city, state_code)
- shop.products(sku, product_name, brand, category, price)
- shop.orders(order_id, order_number, customer_id, order_status, order_total, placed_at, ship_to_city, ship_to_state)
- shop.order_items(order_id, sku, quantity, unit_price)
- shop.shipments(shipment_id, shipment_number, order_id, carrier, ship_status, tracking_number, shipped_at, est_delivery, delivered_at)
- shop.returns(return_id, rma_number, order_id, sku, reason, return_status, created_at)
phone is E.164 ('+1XXXXXXXXXX') in shop.customers and is NOT searchable β match it in SQL.
### Canonical lookups
- ID in the message β search_entities for the full ORD-/SHP-/RMA- number.
- Call with no ID β resolve the caller:
SELECT customer_id, full_name FROM shop.customers WHERE phone = '<+1XXXXXXXXXX>'
then that customer's recent orders (with main product + shipment):
SELECT o.order_number, o.order_id, o.order_status, o.placed_at, p.product_name, s.shipment_number, s.ship_status
FROM shop.orders o
JOIN shop.order_items oi ON oi.order_id = o.order_id
JOIN shop.products p ON p.sku = oi.sku
LEFT JOIN shop.shipments s ON s.order_id = o.order_id
WHERE o.customer_id = '<uuid>' ORDER BY o.placed_at DESC
## Pre-Fetched Context / Existing References
If the message has a "### Pre-Fetched Context" section, those candidates were resolved for \
you (an ID in the body, or the sender's recent orders, or an email-thread reference). It is \
a CANDIDATE LIST, not the answer β for a phone-resolved list, tag only the order(s) this \
conversation is actually about (match on product, issue, and timing); do NOT tag the whole \
list. An "### Existing References" section lists entities upstream systems already attached \
β "(Explicit)" are near-confirmed; verify rather than re-discover.
## Budget
Aim to finish within a few tool calls; you may call tools in parallel. Produce your answer \
as soon as you have enough evidence.
## Output (exactly one fenced ```json block, nothing after it)
```json
{
"classification": {"category": "<Category>", "subcategory": "<subcategory>"},
"entities": [
{"entity_type": "Order|Shipment|Return|Product|Customer", "friendly_id": "ORD-β¦",
"entity_id": "<uuid>", "confidence": 0.0, "reasoning": "β¦"}
],
"search_summary": "what you searched and why"
}
```\
"""
# --- Scenario-driven episode generation --------------------------------------
# Pre-bucket the world so a scenario can pick a target that actually fits.
_ORD_W_SHIP = [o for o in ORDERS if SHIP_BY_ORDER.get(o["order_id"])]
_ORD_W_RETURN = [o for o in ORDERS if RET_BY_ORDER.get(o["order_id"])]
_ORD_INTRANSIT = [o for o in _ORD_W_SHIP
if SHIP_BY_ORDER[o["order_id"]][0]["ship_status"] in
("in_transit", "out_for_delivery", "exception", "label_created")]
_ORD_ACTIVE = [o for o in ORDERS if o["order_status"] in ("placed", "paid", "shipped")]
# Each scenario: (category, subcategory, primary_kind, pool, [transcript templates], [body templates])
# {p}=product phrase, {n}=days ago, {ord}=order number, {city}=ship-to city
SCENARIOS = [
("Shipping", "late_delivery", "shipment", _ORD_INTRANSIT,
["Hi, I ordered the {p} about {n} days ago and the tracking hasn't moved in days. "
"Any idea when it'll actually get here?",
"Yeah, I'm calling about a {p} I'm still waiting on β it was supposed to be here by "
"now and the status hasn't changed. Can you check on it?"],
["My {p} order is way past the delivery estimate and tracking is stuck. Where is it?",
"It's been {n} days and the {p} still hasn't shipped out properly β what's going on?"]),
("Shipping", "lost_package", "shipment", _ORD_INTRANSIT,
["The tracking says my {p} was delivered but it's nowhere β I think the package is lost. "
"Can you help?",
"I never got the {p} I ordered. Tracking went dead halfway. Pretty sure it's lost."],
["Package with my {p} never arrived even though it shows in transit forever. I think it's lost.",
"My {p} shipment seems lost β no movement for over a week and nothing at my door."]),
("Shipping", "wrong_address", "order", _ORD_W_SHIP,
["I think my {p} is being sent to my old address. Can you change where it's going?",
"The {p} order β I realized the shipping address is wrong. It's headed to the wrong place."],
["Please fix the shipping address on my {p} order, it's going to the wrong house.",
"The address on my recent {p} order is outdated β can you reroute it?"]),
("Shipping", "tracking_request", "shipment", _ORD_W_SHIP,
["Can you just give me the tracking number for my {p}? I can't find it.",
"I'm calling to get a tracking update on the {p} I ordered."],
["Can you send me the tracking link for my {p} order?",
"What's the tracking status on my {p}? Haven't gotten an update."]),
("Returns", "damaged_item", "return", _ORD_W_RETURN,
["My {p} showed up cracked and unusable. I already started a return β what are next steps?",
"Calling about a {p} that arrived damaged. I opened a return, want to make sure it's moving."],
["The {p} arrived broken. I filed a return β can you check the status?",
"My {p} came damaged in the box. There's a return open on it, what now?"]),
("Returns", "wrong_item", "order", _ORD_W_SHIP,
["I ordered a {p} but the box had something completely different inside.",
"The {p} I ordered? You shipped me the wrong thing entirely."],
["Wrong item β I ordered a {p} and received something else. Need to sort this out.",
"You sent the wrong product for my {p} order."]),
("Returns", "return_status", "return", _ORD_W_RETURN,
["I sent back the {p} a while ago and haven't heard anything. Where's my return at?",
"Checking on a return I opened for a {p} β what's the current status?"],
["What's the status of my return for the {p}? Shipped it back already.",
"Any update on the {p} return I filed?"]),
("Billing", "double_charge", "order", ORDERS,
["I think I got charged twice for my {p} order. Can you look into the billing?",
"There are two charges on my card for the same {p} order. Need a refund for one."],
["My card shows two charges for the {p} order β please refund the duplicate.",
"Double charged on the {p} order. Can you fix the billing?"]),
("Billing", "refund_status", "return", _ORD_W_RETURN,
["I returned the {p} and was told I'd get a refund. Hasn't shown up yet β where is it?",
"Calling about a refund for a {p} I sent back. When will I see the money?"],
["Still waiting on my refund for the returned {p}. Any timeline?",
"Where's my refund for the {p} return? It's been a while."]),
("Order_Change", "cancel_order", "order", _ORD_ACTIVE,
["I need to cancel my {p} order β changed my mind before it ships.",
"Please cancel the {p} I just ordered, I don't need it anymore."],
["Cancel my {p} order please, I don't want it after all.",
"Need to cancel the recent {p} order before it goes out."]),
("Order_Change", "modify_order", "order", _ORD_ACTIVE,
["Can I change the quantity on my {p} order? I want to add one more.",
"I'd like to modify my {p} order β swap one of the items if it hasn't shipped."],
["Please update my {p} order β I need to change what's in it.",
"Can you modify the {p} order I placed? Want to tweak it."]),
("Product", "availability", "product", ORDERS,
["Quick question β is the {p} back in stock? Looking to buy another.",
"Do you know if the {p} is available again? I had one and want a second."],
["Is the {p} in stock right now? Want to reorder.",
"When will the {p} be available again?"]),
# Ambiguous -> gold subcategory "general"
("Shipping", "general", "order", _ORD_W_SHIP,
["Hey, I'm calling about my {p} order β something's just not right with it, can you take a look?",
"I have a question about the {p} I ordered, it's a bit of a mess, hoping you can help."],
["Something's off with my {p} order β not sure what exactly. Can you check the whole thing?",
"Need help with my {p} order in general, a few things seem wrong."]),
]
_WEIGHTS = [3, 2, 2, 2, 3, 2, 2, 3, 2, 3, 2, 2, 2] # len == len(SCENARIOS)
def _ent(etype: str, friendly_id: str | None, entity_id: str) -> dict:
return {"entity_type": etype, "friendly_id": friendly_id, "entity_id": entity_id}
def generate(seed: int) -> tuple[dict, str]:
"""Deterministically build (ground-truth dict, rendered communication markdown)."""
rng = random.Random(seed)
category, sub, primary_kind, pool, calls, bodies = rng.choices(SCENARIOS, _WEIGHTS)[0]
order = rng.choice(pool or ORDERS)
oid = order["order_id"]
cust = CUST_BY_ID[order["customer_id"]]
prod = _main_product(oid)
pphrase = _product_phrase(prod)
ship = (SHIP_BY_ORDER.get(oid) or [None])[0]
ret = (RET_BY_ORDER.get(oid) or [None])[0]
# Gold: primary transaction + required + optional supporting entities
order_ent = _ent("Order", order["order_number"], oid)
if primary_kind == "shipment" and ship:
primary = _ent("Shipment", ship["shipment_number"], ship["shipment_id"])
required = [primary, order_ent]
elif primary_kind == "return" and ret:
primary = _ent("Return", ret["rma_number"], ret["return_id"])
required = [primary, order_ent]
elif primary_kind == "product":
primary = _ent("Product", prod["sku"], prod["sku"])
required = [primary]
else: # order (incl. shipment/return scenarios whose child happens to be missing)
primary = order_ent
required = [order_ent]
optional = [_ent("Customer", None, cust["customer_id"]),
_ent("Product", prod["sku"], prod["sku"])]
if ship and primary["entity_id"] != ship["shipment_id"]:
optional.append(_ent("Shipment", ship["shipment_number"], ship["shipment_id"]))
# Lead type drives channel + difficulty
lead = rng.choices(["explicit", "thread", "phone"], [35, 25, 40])[0]
days = rng.randint(4, 14)
def order_line(o: dict, indent: str = " ") -> str:
p = _product_phrase(_main_product(o["order_id"]))
s = (SHIP_BY_ORDER.get(o["order_id"]) or [None])[0]
tail = f", Shipment {s['shipment_number']} {s['ship_status']}" if s else ""
return (f"{indent}Order {o['order_number']} β {p}, placed {o['placed_at'][:10]}, "
f"status {o['order_status']}{tail}")
prefetch, existing = [], []
if lead == "explicit":
channel = rng.choice(["Email", "Chat"])
shown_id = primary["friendly_id"] or order["order_number"]
body = rng.choice(bodies).format(p=pphrase, n=days)
body += f" Order number is {order['order_number']}."
if primary["friendly_id"] and not primary["friendly_id"].startswith("ORD"):
body += f" Reference {primary['friendly_id']}."
prefetch = ["These transactions were pre-resolved from IDs in the message. "
"Verify against the message before tagging.",
f"- {shown_id} (in message) β {order_line(order, '').strip()}"]
content = body
elif lead == "thread":
channel = "Email"
content = (f"Subject: Re: your order {order['order_number']}\n\n"
+ rng.choice(bodies).format(p=pphrase, n=days))
prefetch = ["These were pre-resolved from this email thread.",
f"- Earlier in this thread: Order {order['order_number']} β a reply usually "
"concerns the same order; confirm against this message and tag if it applies."]
existing = [f"- Order: {order['order_number']} (from Friendly ID Matcher (Explicit))"]
else: # phone
channel = "Call"
content = ("Agent: Thanks for calling, how can I help?\n"
f"Caller: {rng.choice(calls).format(p=pphrase, n=days)}\n"
"Agent: Sure, let me pull up your account and take a look.")
# candidate list: target + a few of the customer's other orders as distractors
others = [o for o in ORDERS_BY_CUST.get(cust["customer_id"], []) if o["order_id"] != oid]
rng.shuffle(others)
candidates = sorted([order, *others[:3]], key=lambda o: o["placed_at"], reverse=True)
prefetch = ["These candidates were pre-resolved from the caller's phone number. This is a "
"candidate list, not the answer β tag only the order(s) this conversation is "
"actually about (match on product, issue, and timing).",
f'- Customer "{cust["full_name"]}" (from phone {cust["phone"]}) β recent orders:']
prefetch += [order_line(o) for o in candidates]
if rng.random() < 0.5:
existing = [f"- Customer: {cust['full_name']} (from Contact Resolution (Implicit))"]
comm_id = "%08x-%04x-%04x-%04x-%012x" % (
rng.getrandbits(32), rng.getrandbits(16), rng.getrandbits(16),
rng.getrandbits(16), rng.getrandbits(48))
sender = cust["phone"] if channel == "Call" else cust["email"]
parts = ["## Communication", "",
f"- **ID**: {comm_id}",
f"- **Channel**: {channel}",
"- **Direction**: Inbound",
f"- **Timestamp**: 2026-06-{rng.randint(10, 17):02d}T{rng.randint(8, 19):02d}:"
f"{rng.randint(0, 59):02d}:00Z",
f"- **Sender**: {sender} (Name: {cust['full_name']}, Type: External)",
"- **Recipient**: support@shop.example.com (Name: Support Team, Type: Internal)",
"", "### Call Transcript" if channel == "Call" else "### Content", content]
if prefetch:
parts += ["", "### Pre-Fetched Context", *prefetch]
if existing:
parts += ["", "### Existing References", *existing]
text = "\n".join(parts)
truth = {
"classification": {"category": category, "subcategory": sub},
"primary": primary,
"required": required,
"optional": optional,
"lead": lead,
}
return truth, text
# --- Task hooks --------------------------------------------------------------
_rng = random.Random()
def sample() -> Sample:
seed = _rng.randrange(1, 10**9)
_, text = generate(seed)
return Sample(input_id=f"comm-{seed}", text=text)
def lookup_truth(input_id: str) -> dict | None:
m = re.fullmatch(r"comm-(\d+)", input_id or "")
if not m:
return None
truth, _ = generate(int(m.group(1)))
return truth
def _extract_json(text: str) -> dict | None:
blocks = re.findall(r"```(?:json)?\s*\n(.*?)```", text, re.DOTALL)
candidates = [b.strip() for b in blocks]
if not candidates:
m = re.search(r"\{.*\}", text, re.DOTALL) # last-ditch: first { β¦ last }
if m:
candidates = [m.group()]
for cand in reversed(candidates):
try:
obj = json.loads(cand)
if isinstance(obj, dict):
return obj
except json.JSONDecodeError:
continue
return None
def parse_output(text: str) -> dict:
obj = _extract_json(text) or {}
cls = obj.get("classification") or {}
category = str(cls.get("category") or "").strip()
subcategory = str(cls.get("subcategory") or "").strip().lower()
entities = []
raw_entities = obj.get("entities")
if isinstance(raw_entities, list):
for e in raw_entities:
if not isinstance(e, dict):
continue
fid = e.get("friendly_id")
eid = e.get("entity_id")
entities.append({
"entity_type": str(e.get("entity_type") or "").strip(),
"friendly_id": str(fid).strip() if fid else None,
"entity_id": str(eid).strip() if eid else None,
"confidence": e.get("confidence"),
"reasoning": str(e.get("reasoning") or "").strip(),
})
return {
"parsed_ok": _extract_json(text) is not None,
"category": category,
"subcategory": subcategory,
"entities": entities,
"has_entities_list": isinstance(raw_entities, list),
"search_summary": str(obj.get("search_summary") or "").strip(),
"raw": text,
}
def _gold_keys(entities: list[dict]) -> tuple[set[str], set[str]]:
return ({e["friendly_id"].upper() for e in entities if e.get("friendly_id")},
{e["entity_id"] for e in entities if e.get("entity_id")})
def _covers(pred: dict, fids: set[str], uuids: set[str]) -> bool:
return (bool(pred["friendly_id"]) and (pred["friendly_id"] or "").upper() in fids) \
or (bool(pred["entity_id"]) and pred["entity_id"] in uuids)
def _entity_keys(e: dict) -> tuple[set[str], set[str]]:
fids = {e["friendly_id"].upper()} if e.get("friendly_id") else set()
uuids = {e["entity_id"]} if e.get("entity_id") else set()
return fids, uuids
def _is_covered(gold_entity: dict, preds: list[dict]) -> bool:
fids, uuids = _entity_keys(gold_entity)
return any(_covers(p, fids, uuids) for p in preds)
# A fabricated ID is only a hallucination if it is SHAPED like a transaction id / UUID
# yet does not exist β a customer name or product name in friendly_id is not an invented id.
_ID_SHAPE = re.compile(r"(?i)^(ord|shp|rma|sku|wo|tkt)-\w")
_UUID_SHAPE = re.compile(r"(?i)^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
def score(truth: dict, parsed: dict) -> dict[str, float]:
# --- Task 1: classification ---
category, sub = truth["classification"]["category"], truth["classification"]["subcategory"]
valid = parsed["category"] in TAXONOMY and parsed["subcategory"] in TAXONOMY.get(parsed["category"], [])
cat_ok = float(parsed["category"].lower() == category.lower())
sub_ok = float(parsed["subcategory"] == sub)
intent_exact = cat_ok * sub_ok
# --- Task 2: entity tagging ---
preds = parsed["entities"]
seen, gold_u = set(), [] # de-dup gold by (friendly_id or uuid)
for g in [truth["primary"], *truth["required"], *truth["optional"]]:
key = (g.get("friendly_id") or "").upper() or g["entity_id"]
if key not in seen:
seen.add(key)
gold_u.append(g)
gold_fids, gold_uuids = _gold_keys(gold_u)
prim, required = truth["primary"], truth["required"]
matched_preds = [p for p in preds if _covers(p, gold_fids, gold_uuids)]
covered_gold = sum(_is_covered(g, preds) for g in gold_u)
precision = len(matched_preds) / len(preds) if preds else 0.0
recall = covered_gold / len(gold_u) if gold_u else 0.0
f1 = (2 * precision * recall / (precision + recall)) if (precision + recall) else 0.0
primary_found = float(_is_covered(prim, preds))
required_recall = (sum(_is_covered(g, preds) for g in required) / len(required)) if required else 0.0
entity_id_correct = float(any(p["entity_id"] == prim["entity_id"] for p in preds if p["entity_id"]))
no_hallucinated = 1.0
for p in preds:
fid, eid = p["friendly_id"], p["entity_id"]
if fid and _ID_SHAPE.match(fid) and fid.upper() not in WORLD_FRIENDLY_IDS:
no_hallucinated = 0.0
if eid and _UUID_SHAPE.match(eid) and eid not in WORLD_UUIDS:
no_hallucinated = 0.0
return {
"intent_format_ok": float(valid),
"category_match": cat_ok,
"subcategory_match": sub_ok,
"intent_exact": intent_exact,
"tag_format_ok": float(parsed["has_entities_list"] and bool(parsed["search_summary"])),
"primary_found": primary_found,
"entity_id_correct": entity_id_correct,
"required_recall": float(required_recall),
"precision": float(precision),
"recall": float(recall),
"f1": float(f1),
"no_hallucinated_id": no_hallucinated,
"both_correct": intent_exact * primary_found,
}
def present(parsed: dict, truth: dict | None) -> dict:
out: dict = {
"classification": {"category": parsed["category"] or None,
"subcategory": parsed["subcategory"] or None},
"search_summary": parsed["search_summary"],
"raw": parsed["raw"],
"parsed_ok": parsed["parsed_ok"],
"tags": [],
}
gold_fids: set[str] = set()
gold_uuids: set[str] = set()
prim = None
if truth:
gold = [truth["primary"], *truth["required"], *truth["optional"]]
gold_fids, gold_uuids = _gold_keys(gold)
prim = truth["primary"]
out["gold"] = {
"classification": truth["classification"],
"primary": prim,
"required": truth["required"],
"optional": truth["optional"],
"lead": truth.get("lead"),
}
for p in parsed["entities"]:
in_gold = _covers(p, gold_fids, gold_uuids) if truth else None
is_primary = bool(truth) and prim is not None and (
((p["friendly_id"] or "").upper() == (prim["friendly_id"] or "x").upper() and p["friendly_id"])
or p["entity_id"] == prim["entity_id"])
out["tags"].append({**p, "in_gold": in_gold, "is_primary": bool(is_primary)})
return out
# Collapsible explainer panels shown verbatim (trusted HTML) above the input on the page.
_OVERVIEW_HTML = """\
<p>
<strong>What this is.</strong> A demo of an LLM workload for a hypothetical online-shopping
customer-support operation. Each input is one support case, against which the model executes
<strong>two tasks</strong>: it <em>classifies the inquiry intent</em> (a category + subcategory,
e.g. Shipping β late_delivery) and <em>tags the business entities</em> the case refers
to β the order, shipment, return, customer, or product. Its output is that classification plus a
list of tagged entities; when the input was dealt here, both are scored automatically against the
known-correct answer.
</p>
<p>
<strong>What an input is.</strong> A block of text assembled from a customer message (e.g. a
refund request or a late-delivery complaint) together with pre-fetched context, message metadata,
and any entities that (hypothetical) upstream systems already linked to it. Everything is synthetic
and generated on the fly: each deal draws a random scenario over a fixed, made-up catalog and
renders it as an email, chat, or call.
</p>
<p>
<strong>Tools the model can use.</strong> Classification needs no tools. Two tools are available
for the tagging task:
</p>
<ul>
<li>
<code>search_entities</code> β fuzzy text search over the catalog. Used to find an entity by
name, number, or description (e.g. the order behind a product name, or to confirm an order ID
quoted in the message).
</li>
<li>
<code>execute_sql</code> β read-only SQL over the order database. Used for what search can't do:
resolving a customer by phone number, joining orders to shipments, or filtering by date.
</li>
<li>
<strong>The "database"</strong> is a small, fixed synthetic dataset committed in the repo
(customers, products, orders, order items, shipments, returns). <code>execute_sql</code>
queries it in-process (via duckdb) and <code>search_entities</code> indexes the same data β so
every tool result is a real query result over that dataset (deterministic).
</li>
</ul>
<p>
<strong>How to use it.</strong> Click <em>Deal me a communication</em> for a fresh input, then
<em>Run βΈ</em>. You'll see the classification decision, the tagged entities, the tools the
model called, and (for dealt inputs) the auto-scores. You can provide feedback using π/π and the
note box. You can also provide your own text input instead of dealing one (but there will be no
ground-truth).
</p>
<p>
<strong>The traffic.</strong> Every run becomes one trace in Langfuse β the prompt, each tool
call, the final answer, and all the scores β which is the data this demo exists to produce. To
see only this demo's traffic, filter the Traces tab to the tag <code>order-comms</code> (every run
here carries it). Dashboard:
<a href="https://cloud.langfuse.com/project/cmq8ojtt101fhad0i3qqaxiza/traces" target="_blank" rel="noopener">https://cloud.langfuse.com/project/cmq8ojtt101fhad0i3qqaxiza/traces</a>
</p>
"""
# The walkthrough below uses a REAL dealt input (seed 1) and REAL tool payloads captured
# from the actual tools β edit the prose freely, but the JSON mirrors what the app produces.
_EXAMPLE_HTML = """\
<p>
A representative run on a real dealt input. The input is an inbound <strong>chat</strong> that
references order and shipping ids.
</p>
<p class="step">Example input</p>
<pre>## Communication
- Channel: Chat
- Direction: Inbound
- Timestamp: 2026-06-10T14:27:00Z
- Sender: sasha.nakamura21@mailbox.example.com (Name: Sasha Nakamura, Type: External)
- Recipient: support@shop.example.com (Name: Support Team, Type: Internal)
### Content
My plush bear shipment seems lost β no movement for over a week and nothing at my
door. Order number is ORD-260606-4070. Reference SHP-260608-1862.
### Pre-Fetched Context
- SHP-260608-1862 (in message) -> Order ORD-260606-4070 β plush bear, placed
2026-06-06, status shipped, Shipment SHP-260608-1862 exception</pre>
<p class="step">Step 1 β classify intent (no tool call)</p>
<p>
From the wording alone the model decides
<code>{"category": "Shipping", "subcategory": "lost_package"}</code>. Classification never uses a
tool.
</p>
<p class="step">Step 2 β tag entities, tool call 1: <code>search_entities</code></p>
<p>The model resolves the order number quoted in the message. Arguments it emits:</p>
<pre>{ "search_terms": "ORD-260606-4070", "entity_types": ["Order"],
"reason": "Resolve the order quoted in the message" }</pre>
<p>Tool result (top hit; nine lower-scored hits omitted):</p>
<pre>{
"result_count": 10,
"results": [
{ "id": "46839f5b-b827-91a9-3e05-bbca736619a2", "type": "Order", "score": 109.0,
"fields": { "order_number": "ORD-260606-4070", "status": "shipped",
"customer_name": "Sasha Nakamura", "ship_to": "Ironwood, MI",
"total": 178.36 } }
]
}</pre>
<p class="step">Step 3 β tag entities, tool call 2: <code>execute_sql</code></p>
<p>
The order doc has no shipment ID, so the model queries the database for the shipment row (search
can't return the shipment's UUID). Arguments:
</p>
<pre>{ "sql": "SELECT shipment_id, shipment_number, ship_status, est_delivery
FROM shop.shipments WHERE shipment_number = 'SHP-260608-1862'",
"reason": "Get the shipment row and its id" }</pre>
<p>Tool result:</p>
<pre>{
"row_count": 1,
"columns": ["shipment_id", "shipment_number", "ship_status", "est_delivery"],
"rows": [
{ "shipment_id": "75e88d7e-ab67-eeae-e3d7-e9dc4109752a",
"shipment_number": "SHP-260608-1862", "ship_status": "exception",
"est_delivery": "2026-06-15 20:59:00" }
]
}</pre>
<p class="step">Step 4 β final answer</p>
<p>
The model combines the two tool results into one JSON object: the lost shipment as the primary
entity plus its parent order, each carrying the UUID it pulled from a tool result.
</p>
<pre>{
"classification": { "category": "Shipping", "subcategory": "lost_package" },
"entities": [
{ "entity_type": "Shipment", "friendly_id": "SHP-260608-1862",
"entity_id": "75e88d7e-ab67-eeae-e3d7-e9dc4109752a", "confidence": 0.95,
"reasoning": "The lost shipment named in the message; ship_status 'exception' fits." },
{ "entity_type": "Order", "friendly_id": "ORD-260606-4070",
"entity_id": "46839f5b-b827-91a9-3e05-bbca736619a2", "confidence": 0.9,
"reasoning": "Parent order quoted in the message." }
],
"search_summary": "Resolved the order via search_entities; fetched its shipment + id via SQL."
}</pre>
<p>
Against this input's known-correct answer that scores a clean pass: right category and
subcategory, the correct primary entity tagged, the right UUIDs supplied (only obtainable from a
tool result), and no invented IDs.
</p>
"""
# Panel 3 is GENERATED from the live TAXONOMY / TOOLS / committed data and from real tool
# output, so it stays in sync automatically. Edit the prose in _build_details_html() below;
# the label list, tool schemas, payloads, and DB schema are derived β don't hand-copy them.
def _esc(s: object) -> str:
return str(s).replace("&", "&").replace("<", "<").replace(">", ">")
def _pre(obj: object) -> str:
text = obj if isinstance(obj, str) else json.dumps(obj, indent=2, default=str)
return f"<pre>{_esc(text)}</pre>"
def _build_details_html() -> str:
schema_sources = [
("shop.customers", CUSTOMERS), ("shop.products", PRODUCTS), ("shop.orders", ORDERS),
("shop.order_items", ORDER_ITEMS), ("shop.shipments", SHIPMENTS), ("shop.returns", RETURNS),
]
n_labels = sum(len(v) for v in TAXONOMY.values())
tax_items = "\n".join(
f" <li><strong>{_esc(cat)}</strong> β {_esc(', '.join(subs))}</li>"
for cat, subs in TAXONOMY.items())
fields_by_type: dict[str, list[str]] = {}
for doc in INDEX: # insertion order: Order, Shipment, Return, Product, Customer
fields_by_type.setdefault(doc["type"], list(doc["fields"].keys()))
field_items = "\n".join(
f" <li><strong>{_esc(t)}</strong>: {_esc(', '.join(keys))}</li>"
for t, keys in fields_by_type.items())
schema_items = "\n".join(
f" <li><code>{name}</code>({_esc(', '.join(src[0].keys()))})</li>"
for name, src in schema_sources)
# Real payloads, captured from the actual tools so the documentation can't drift.
search_ok = json.loads(search_entities(ORDERS[0]["order_number"], ["Order"]))
search_ok["results"] = search_ok["results"][:1] # trim to the top hit for brevity
search_short = json.loads(search_entities("ab"))
search_none = json.loads(search_entities("zzqqxx yyzzwwvv"))
sql_ok = json.loads(execute_sql(
"SELECT order_number, order_status, placed_at FROM shop.orders LIMIT 2", "example"))
sql_err = json.loads(execute_sql("DELETE FROM shop.orders", "example"))
return f"""\
<p class="step">Classification label space (Task 1)</p>
<p>
The model outputs one <code>{{"category", "subcategory"}}</code> pair from this fixed taxonomy
({n_labels} labels in all). Each category's <code>general</code> subcategory is the fallback for
when the specific one can't be pinned down.
</p>
<ul>
{tax_items}
</ul>
<p class="step">Tool definitions (Task 2)</p>
{_pre(TOOLS)}
<p class="step">search_entities β result payloads</p>
<p>
Success returns a flat, score-sorted list (exact friendly-ID matches are boosted). The
<code>fields</code> object differs by entity type. Top hit for a full order-number search:
</p>
{_pre(search_ok)}
<p>The <code>fields</code> extracted for each entity type:</p>
<ul>
{field_items}
</ul>
<p>Errors come back as a result (not raised) β e.g. a term under 3 characters, or no match at all:</p>
{_pre(search_short)}
{_pre(search_none)}
<p class="step">execute_sql β result payloads</p>
<p>
Read-only (only <code>SELECT</code> / <code>WITH</code> run); a missing <code>LIMIT</code> is
capped to 50, and at most 50 rows are returned:
</p>
{_pre(sql_ok)}
<p>Anything that isn't a read-only query comes back as an error result:</p>
{_pre(sql_err)}
<p class="step">Database schema (the <code>shop</code> tables)</p>
<p>
Both tools read this one committed, synthetic dataset. An entity's <code>entity_id</code> in the
final answer is its UUID from these tables.
</p>
<ul>
{schema_items}
</ul>
<p>
Note: <code>customers.phone</code> is deliberately <em>not</em> indexed by search β resolving a
caller by phone number has to go through <code>execute_sql</code>.
</p>
"""
_DETAILS_HTML = _build_details_html()
PANELS = [
{"title": "What is this?", "html": _OVERVIEW_HTML},
{"title": "Walk through an example run", "html": _EXAMPLE_HTML},
{"title": "Tasks, tools, and DB details", "html": _DETAILS_HTML},
]
TASK = Task(
order=0, # first in the nav β the headline demo
id="order-comms",
title="Support Comms",
tagline="Classify a customer message and tag the orders and entities it's about",
system_prompt=SYSTEM_PROMPT,
ui={
"output": "comms",
"input_label": "Support case (customer message + pre-fetched context + metadata)",
"placeholder": "Deal a communication, or paste your own email/chat/call transcript...",
"deal_label": "Deal me a communication",
"panels": PANELS,
},
sample=sample,
lookup_truth=lookup_truth,
parse_output=parse_output,
score=score,
present=present,
tools=TOOLS,
execute_tool=execute_tool,
)
|