Skip to content
RasaGet a free licence
Menu
AI team casebook

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.

by Rod Rivera

About 11 minutesHands-on tutorial

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 ageFeed observation ageEvent observerStatus answerSupport route
48 hours21,599 secondsCarrierAnsweredRouted; no prediction
48 hours21,599 secondsWarehouseBlocked: wrong sourceRouted; no prediction
48 hours21,600 secondsCarrierAnsweredRouted; no prediction
48 hours21,600 secondsWarehouseBlocked: wrong sourceRouted; no prediction
48 hours21,601 secondsCarrierBlocked: stale feedRouted; no prediction
48 hours21,601 secondsWarehouseBlocked: stale feedRouted; 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 orderSelected parcelMilestoneReference relationship
WS-105391: desk chairDeliveredDifferent from parcel 2
WS-105392: cushion setIn transitDifferent 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.

Status refusal preserves a separate support route
FigureStatus refusal preserves a separate support route

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.