AI product engineer · Retail & e-commerce (external)
Explain “label created” in parcel tracking
Use tracking evidence to tell a customer whether an order was delivered or only had a shipping label created.
Key takeaways (3)
- A warehouse label cannot prove the carrier collected or delivered a parcel.
- Keep the time of a parcel event separate from the time the feed was observed.
- Offer support for a selected parcel even when its tracking cannot support a current answer.
A customer asks where their order is. The latest warehouse event says a shipping label was created two days ago. That event tells the assistant something about preparation, but it does not say the parcel was delivered.
You will test a fictional order-status service and inspect the evidence behind its answer. The example also shows when to route a customer’s question for support instead of turning an old event into a delivery promise.
The first output below is a different control: it takes a delivery record and changes only its source to warehouse. The service must refuse that mis-sourced delivery claim. The companion separately tests the legitimate label-created event; these two inputs must not be treated as the same result.
{"event_age_hours": 48, "event_source": "warehouse", "observation_age_seconds": 21599, "reason": "label_as_delivery", "status": "blocked", "support_prediction": null, "support_status": "routed"}
The feed observation is fresh, yet the delivery answer is refused. This real
offline output changes only the observer of a delivery record to warehouse.
A recent poll cannot turn a warehouse assertion into a carrier scan.
Keep the event’s source and time separate from the feed’s observation time. Then give a refused status answer its own support route. The customer still needs help when tracking cannot justify an answer.
Require the observer that can support the milestone
git clone https://github.com/RasaHQ/rasa-community-resources.git
cd rasa-community-resources
git checkout 4aa0c4419dc193fef7a969c12d59edcf720f2606
cd examples/mantle-voice-retail-order-status-gemini
The full clone also keeps the neighbouring case contract used by a unit test. Stay in this example directory for every command below. Python’s standard library is enough for the offline path; it needs no licence, API key or package install.
- Any system
python3 -m unittest discover -s tests -v
The recorded suite completed without skips:
Ran 24 tests in 0.002s
OK
Your timing can differ. The pinned guard tests
check label-only status, source mismatches, stale feeds, split parcels and
support recovery. Start with test_label_is_never_a_delivery: the label is
answerable as a label, with both possession and delivery false.
The distinction is between a known warehouse milestone and an unsupported
claim about a carrier milestone. label_as_delivery also names the refusal
for a delivery record with the wrong source; it is not limited to printed labels.
The source check in lib/orders.py is exact:
def milestone_explicit(event: Optional[dict]) -> bool:
if not event:
return False
milestone = event.get("milestone")
return milestone in MILESTONE_SOURCES and event.get("source") == MILESTONE_SOURCES[milestone]
A label needs a warehouse source. Collection, transit and delivery need the carrier source in the milestone mapping. Re-labelling a poll as recent leaves that source requirement untouched. The unit test checks both a carrier label and a warehouse delivery as invalid combinations.
The Rasa tool wrapper takes only an order number and an optional parcel number from the model:
def _customer(context: Optional[ToolContext]) -> str:
return (
context.memory.get("project.customer_id") if context is not None else None
) or orders.SESSION_CUSTOMER_ID
async def track_order(
order_number: str, parcel_number: Optional[int] = None, context: ToolContext = None
) -> ToolResult:
return ToolResult(llm_response=orders.track_order(_customer(context), order_number, parcel_number))
The customer comes from project memory in a Rasa session. Offline calls use the synthetic session customer. Your application must supply the authenticated customer when replacing this fixture adapter. Do not make customer identity, tracking facts or arrival predictions model arguments.
The pinned skill.md tells the model to say the milestone, observer, event time and status reference when the tool answers. It also requires another tool call when the caller changes parcels. This is source evidence of the intended conversation, not a fresh model result.
Age the observation independently of the event
The fixture in lib/fixtures/orders.json fixes its clock at
2026-09-30T12:00:00-04:00 and its freshness window at six hours. Repeating
these checks later does not move that clock to today.
The relevant part of facts_for compares the observation time with that clock:
window = timedelta(hours=data["carrier_freshness_hours"])
observed = parcel.get("carrier_observed_at")
latest = parcel["events"][-1] if parcel.get("events") else None
return {
"order_subject_resolved": True,
"carrier_event_current": bool(observed) and _parse(data["as_of"]) - _parse(observed) <= window,
"milestone_type_explicit": milestone_explicit(latest),
}
There are two questions. When did the carrier event happen? When was the feed observed? A historical delivery can remain the latest event in a freshly observed feed. Refreshing the event’s timestamp would misstate when delivery happened; rejecting it solely because delivery happened earlier would confuse event age with feed freshness.
Test the exact freshness boundary
Hold the delivery event 48 hours in the past. Vary only the observation age and observer. This is the recorded matrix from 8 October 2026:
| Event age | Feed observation age | Event observer | Status answer | Support route |
|---|---|---|---|---|
| 48 hours | 21,599 seconds | Carrier | Answered | Routed; no prediction |
| 48 hours | 21,599 seconds | Warehouse | Blocked: wrong source | Routed; no prediction |
| 48 hours | 21,600 seconds | Carrier | Answered | Routed; no prediction |
| 48 hours | 21,600 seconds | Warehouse | Blocked: wrong source | Routed; no prediction |
| 48 hours | 21,601 seconds | Carrier | Blocked: stale feed | Routed; no prediction |
| 48 hours | 21,601 seconds | Warehouse | Blocked: stale feed | Routed; no prediction |
At exactly six hours, the carrier observation is within the configured window. One second later, freshness fails. On the last row, the freshness refusal comes first; it does not make the warehouse delivery source valid.
Paste this complete exercise from the selected example directory. It mutates a newly loaded fixture for each case, leaving the committed data untouched. The assertions test the boundary, source and recovery path together.
python3 - <<'PY'
"""Offline exercise: run from the selected example directory."""
import json
import sys
from datetime import datetime, timedelta
from pathlib import Path
sys.dont_write_bytecode = True
project = Path.cwd()
from lib import orders as ws
for observation_seconds in (21599, 21600, 21601):
for event_source in ('carrier', 'warehouse'):
data = ws.load_data()
now = datetime.fromisoformat(data['as_of'])
parcel = data['orders']['WS-10482']['parcels'][0]
parcel['carrier_observed_at'] = (now - timedelta(seconds=observation_seconds)).isoformat()
parcel['events'][-1]['at'] = (now - timedelta(hours=48)).isoformat()
parcel['events'][-1]['source'] = event_source
result = ws.track_order(ws.SESSION_CUSTOMER_ID, '10482', data=data)
help_result = ws.delivery_help(ws.SESSION_CUSTOMER_ID, '10482', data=data)
expected = 'answered' if observation_seconds <= 21600 and event_source == 'carrier' else 'blocked'
assert result['status'] == expected and help_result['status'] == 'routed'
assert help_result['arrival_prediction'] is None
print(json.dumps({'observation_age_seconds': observation_seconds, 'event_age_hours': 48,
'event_source': event_source, 'status': result['status'],
'reason': result.get('reason'), 'support_status': help_result['status'],
'support_prediction': help_result['arrival_prediction']}, sort_keys=True))
first = ws.track_order(ws.SESSION_CUSTOMER_ID, '10539', 1)
second = ws.track_order(ws.SESSION_CUSTOMER_ID, '10539', 2)
assert first['status_reference'] != second['status_reference']
print(json.dumps({'parcel_1': first['milestone'], 'parcel_2': second['milestone'],
'distinct_references': first['status_reference'] != second['status_reference']}, sort_keys=True))
PY
Actual recorded stdout from the same exercise logic:
{"event_age_hours": 48, "event_source": "carrier", "observation_age_seconds": 21599, "reason": null, "status": "answered", "support_prediction": null, "support_status": "routed"}
{"event_age_hours": 48, "event_source": "warehouse", "observation_age_seconds": 21599, "reason": "label_as_delivery", "status": "blocked", "support_prediction": null, "support_status": "routed"}
{"event_age_hours": 48, "event_source": "carrier", "observation_age_seconds": 21600, "reason": null, "status": "answered", "support_prediction": null, "support_status": "routed"}
{"event_age_hours": 48, "event_source": "warehouse", "observation_age_seconds": 21600, "reason": "label_as_delivery", "status": "blocked", "support_prediction": null, "support_status": "routed"}
{"event_age_hours": 48, "event_source": "carrier", "observation_age_seconds": 21601, "reason": "stale_carrier_event", "status": "blocked", "support_prediction": null, "support_status": "routed"}
{"event_age_hours": 48, "event_source": "warehouse", "observation_age_seconds": 21601, "reason": "stale_carrier_event", "status": "blocked", "support_prediction": null, "support_status": "routed"}
{"distinct_references": true, "parcel_1": "delivered", "parcel_2": "in_transit"}
Now change the event age alone from 48 hours to another historical age. Predict
whether the six status results should change before running it again. The
freshness predicate reads carrier_observed_at, so event age alone does not
change that decision.
Keep every answer scoped to one parcel
The last line of the exercise reads the same split order twice, selecting a different parcel each time. Their milestones and references differ:
| Fixture order | Selected parcel | Milestone | Reference relationship |
|---|---|---|---|
| WS-10539 | 1: desk chair | Delivered | Different from parcel 2 |
| WS-10539 | 2: cushion set | In transit | Different from parcel 1 |
The companion test_each_parcel_has_its_own_status confirms those values.
Without a parcel selection, a split order returns parcel_not_selected.
Resolve that question before describing either parcel’s status. If a caller
asks about the other parcel, call the tracking tool again; carrying over the
first result can produce an answer that is true for the wrong item.
The status reference belongs to the selected result. Preserve it with the milestone and source when displaying or logging the answer. Do not build an order-wide delivery flag by taking the first delivered parcel you find.
Let refusal lead to support without a prediction
The delivery_help function retains ownership and parcel
resolution checks. It can route a selected customer’s parcel when tracking
is stale or its latest milestone is unusable. It takes the last explicit
event, if one exists, and returns arrival_prediction: None.
The wrapper maps the skill’s open_delivery_help tool to this function and
returns its payload as a ToolResult. The support reference and review step
come from that payload. They are not permission to promise a new arrival time.

The support route does not weaken the status check. It answers a different question: where can this customer’s parcel problem go next? The matrix proves that distinction for the fixture cases. A real service still needs its own support adapter, current observation clock and authenticated customer binding. The exercise establishes no production delivery accuracy or behaviour across restarts and concurrent requests.
Show the checks for a refused tracking answer
Inspect the selected order and parcel first. Then compare the feed observation time with the configured window, without changing the event time. Check the milestone’s expected source. If status is refused, request support for that same parcel and inspect its reference, last explicit event and absent prediction. A support route should not reuse a refused estimate.
Check the project with the pinned SDK
A separate offline check on 8 October 2026 used Rasa 3.21.0.dev5. Its full suite passed without skips, and project validation loaded the tools and skills. The test summary was:
Ran 24 tests in 0.002s
OK
Validation also printed:
validate_project: ok
After installing this selected project, repeat those checks with:
uv run --locked python -m unittest discover -s tests -v
make validate
This adds evidence for SDK imports and project validation. It does not run training, a model or speech providers. The independent property exercise above still tests pure Python rather than engine confirmation or spoken behaviour.
Try the configured microphone path separately
The public README and Makefile document the optional Rasa run. Use Python 3.12 and uv in this example directory:
uv sync --locked --python 3.12
make env
Fill the local .env with RASA_LICENSE, GEMINI_API_KEY,
DEEPGRAM_API_KEY and RIME_API_KEY. The project pins
Rasa Pro 3.21.0.dev5. Its integrations.yml selects
gemini-3.8-flash, Deepgram flux-general-en input and Rime mistv3 output
with the lagoon voice. The Inspector channel is enabled.
make validate
make train
make inspect
Allow microphone access. This path uses billed provider APIs and was not run
for the offline receipts above. For a bounded manual check, say “Where is
order one zero five one seven?” Then ask “So it has been delivered?” These
are proposed caller lines. Inspect that the tool result is label_created,
with possession and delivery false, and that the spoken answer preserves that
meaning. In a separate short check, ask about order 10560 and request help;
look for a support reference rather than a newly predicted arrival date.
Use the quickstart if you need the general Rasa setup first. Take observations from these calls as individual acceptance checks, rather than as a reliability rate or evidence about other speech configurations.