# Update a roadside pickup location without sending two jobs

Source: https://rasa.community/library/casebook/insurance-roadside/
Author: Rod Rivera
Published: 2026-10-17T09:00:00Z

A driver asks for roadside assistance, then corrects the pickup location. The assistant must use the new location and check whether a job was already sent.

In this fictional example, you will inspect the corrected location, the question the driver answered and the dispatch record. Those checks distinguish a fresh customer answer from an earlier confirmation.

```text
{"fresh_memory": true, "fresh_question": false, "jobs_sent": 0, "reason": "wrong_incident_location", "status": "blocked"}
```

This is real offline Python output from 8 October 2026. The draft says exit 16;
the caller answered a question about exit 14. Updating memory did not make
that answer apply to the corrected location. The guard sent no job.

Build that distinction into the function that sends the request. Then treat a
repeat of an already sent draft as a receipt lookup. Otherwise a retry can
become a second dispatch, or ask for approval of work already done.

:::callout{type="info" title="What you will test"}
HarborCover and its providers are fictional. Their service rules are assumptions
for this exercise. These checks execute pure Python over synthetic data.
They do not run Rasa, a model or speech recognition, or verify real dispatch.
The optional microphone path below is separate and uses provider APIs.
:::

## Bind the question to the revised draft

```bash
git clone https://github.com/RasaHQ/rasa-community-resources.git
cd rasa-community-resources
git checkout 4aa0c4419dc193fef7a969c12d59edcf720f2606
cd examples/mantle-voice-insurance-roadside-claude
```

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.

::run{cmd="python3 -m unittest discover -s tests -v"}
The recorded offline suite completed without skipped tests:

```text
Ran 30 tests in 0.004s

OK
```

Your timing can differ. The tests include stale confirmation, a completed
request replay and a declined provider retry. See the pinned
[guard tests](https://github.com/RasaHQ/rasa-community-resources/blob/4aa0c4419dc193fef7a969c12d59edcf720f2606/examples/mantle-voice-insurance-roadside-claude/tests/test_guard.py). Their output checks
Python behaviour, rather than whether a model chooses the right tool.

The project separates dialogue from dispatch. Its Rasa skill calls a wrapper;
the wrapper passes memory and conversation events to the dispatch function.
After a draft change, it writes the current draft values back to memory.
The wrapper also sends outcome wording through the tool context.

These are exact excerpts from
[skills/roadside_dispatch/tools.py](https://github.com/RasaHQ/rasa-community-resources/blob/4aa0c4419dc193fef7a969c12d59edcf720f2606/examples/mantle-voice-insurance-roadside-claude/skills/roadside_dispatch/tools.py):

```python
def _memory(context: Optional[ToolContext]) -> dict:
    return {key: (context.memory.get(key) if context is not None else None) or None for key in hc.MEMORY_KEYS}


def _conversation(context: Optional[ToolContext]) -> hc.Conversation:
    return hc.conversation_from_events(context.events) if context is not None else hc.Conversation()


def _write_draft(context: Optional[ToolContext], service: hc.RoadsideService, draft: Optional[hc.Draft]) -> None:
    if context is None or draft is None:
        return
    for key, value in hc.memory_values(service, draft).items():
        context.memory.set(key, value)


async def _send_receipt(context: Optional[ToolContext], tool_name: str, result: dict) -> None:
    text = hc.customer_receipt(tool_name, result) if hc.TOOL_SENDS_RECEIPT else None
    if text and context is not None:
        await context.send(text)
```

The dispatch wrapper then calls the guard:

```python
    result = hc.request_dispatch(_service(), draft_id, _memory(context), _conversation(context))
    await _send_receipt(context, "request_dispatch", result)
    return ToolResult(llm_response=result)
```

Caller words enter the draft through the start or update tool. The guard
resolves a fixture place and checks provider suitability there. The model
does not supply the provider's acceptance or arrival estimate.

The pinned [skill.md](https://github.com/RasaHQ/rasa-community-resources/blob/4aa0c4419dc193fef7a969c12d59edcf720f2606/examples/mantle-voice-insurance-roadside-claude/skills/roadside_dispatch/skill.md) declares the
confirmation question on the request tool:

```yaml
tool_constraints:
  - request_dispatch:
      requires: session.roadside_dispatch.pending_draft_ref
      requires_confirmation:
        enabled: true
        utter_for_confirmation: utter_confirm_location
```

This is project configuration, not a fresh engine test. Read the skill alongside
[responses.yml](https://github.com/RasaHQ/rasa-community-resources/blob/4aa0c4419dc193fef7a969c12d59edcf720f2606/examples/mantle-voice-insurance-roadside-claude/skills/roadside_dispatch/responses.yml): the question reads back
the vehicle, location and service from draft memory. The skill asks the engine
to handle confirmation. A correction calls the update tool before another request.

In [lib/roadside.py](https://github.com/RasaHQ/rasa-community-resources/blob/4aa0c4419dc193fef7a969c12d59edcf720f2606/examples/mantle-voice-insurance-roadside-claude/lib/roadside.py#L533-L581), draft updates increment the
version. The execution check requires the current tag and the current location
and service in an answered question:

```python
def _location_confirmed(service: RoadsideService, draft: Draft, memory: dict, conversation: Conversation) -> bool:
    labels = service.labels(draft)
    question = conversation.confirmation_question or ""
    return (
        (memory.get("pending_draft_ref") or "") == draft.tag
        and labels["location"] in question
        and labels["service"] in question
        and conversation.confirmation_answered
    )

```

The cost of this choice is carrying draft identity through both memory and
event handling. A bare yes/no value is insufficient. Keep the location text
that was read back, rather than replacing it with the latest location when
constructing evidence.

## Cross the old question with new memory

The confirmation and replay rows start with an exit-14 draft, then change it to exit 16. The provider-retry rows use a separate incident at Kestrel Plaza.

| Memory supplied                            | Answered question supplied | Result                        | Jobs sent        |
| ------------------------------------------ | -------------------------- | ----------------------------- | ---------------- |
| Old draft                                  | Exit 14                    | Blocked: wrong location       | 0                |
| Old draft                                  | Exit 16                    | Blocked: wrong location       | 0                |
| Current draft                              | Exit 14                    | Blocked: wrong location       | 0                |
| Current draft                              | Exit 16                    | Succeeded                     | 1                |
| Old evidence after that success            | Exit 14                    | Existing receipt replay       | 1                |
| Declined provider, then next provider      | Same confirmed incident    | Accepted under same reference | 2 total attempts |
| Repeat next-provider call after acceptance | Same incident reference    | Existing receipt replay       | 2 total attempts |

Run the exercise below from this example directory. It adapts the recorded
experiment to your working directory; no private script is required. It renders
the question from the real response file. Setting `confirmation_answered=True`
is deliberate fixture evidence, not a test of spoken consent interpretation.

```bash
python3 - <<'PY'
"""Offline exercise: run from the selected example directory."""
import json
import sys
from pathlib import Path
sys.dont_write_bytecode = True
project = Path.cwd()
from lib import roadside as hc

def question(svc, draft):
    template = (project / 'skills/roadside_dispatch/responses.yml').read_text()
    text = ' '.join(template.split('- text: >', 1)[1].split())
    for key, value in hc.memory_values(svc, draft).items():
        text = text.replace('{' + key + '}', value)
    return hc.Conversation(confirmation_question=text, confirmation_answered=True)

for fresh_memory in (False, True):
    for fresh_question in (False, True):
        svc = hc.RoadsideService('independent-revision-matrix')
        _, draft = hc.start_dispatch_draft(svc, 'HC-AU-517306', 'VEH-5173-1', 'tow', 'exit 14')
        old_memory, old_question = hc.memory_values(svc, draft), question(svc, draft)
        hc.update_dispatch_draft(svc, draft.draft_id, location='exit 16')
        result = hc.request_dispatch(svc, draft.draft_id,
                                     hc.memory_values(svc, draft) if fresh_memory else old_memory,
                                     question(svc, draft) if fresh_question else old_question)
        print(json.dumps({'fresh_memory': fresh_memory, 'fresh_question': fresh_question,
                          'status': result['status'], 'reason': result['reason'],
                          'jobs_sent': len(svc.dispatches)}, sort_keys=True))
        assert len(svc.dispatches) == int(fresh_memory and fresh_question)
        if fresh_memory and fresh_question:
            replay = hc.request_dispatch(svc, draft.draft_id, old_memory, old_question)
            assert replay['replay'] is True and len(svc.dispatches) == 1
            print(json.dumps({'retry_with_old_evidence': replay['status'], 'replay': replay['replay'],
                              'jobs_sent': len(svc.dispatches), 'location_ref': replay['location_ref']}, sort_keys=True))

svc = hc.RoadsideService('independent-provider-retry')
_, draft = hc.start_dispatch_draft(svc, 'HC-AU-440218', 'VEH-4402-1', 'tow', 'Kestrel Plaza')
first = hc.request_dispatch(svc, draft.draft_id, hc.memory_values(svc, draft), question(svc, draft))
second = hc.request_next_provider(svc, first['assistance_ref'])
third = hc.request_next_provider(svc, first['assistance_ref'])
assert first['status'] == 'pending' and second['status'] == 'succeeded'
assert first['assistance_ref'] == second['assistance_ref'] == third['assistance_ref']
assert len(svc.dispatches) == 2 and third['replay']
print(json.dumps({'initial_acceptance': first['provider_acceptance'], 'next_acceptance': second['provider_acceptance'],
                  'same_assistance_ref': first['assistance_ref'] == second['assistance_ref'],
                  'repeat_next_provider_replay': third['replay'], 'jobs_sent': len(svc.dispatches),
                  'places': [job['place_ref'] for job in svc.dispatches]}, sort_keys=True))
PY
```

Recorded stdout from the same exercise logic:

```text
{"fresh_memory": false, "fresh_question": false, "jobs_sent": 0, "reason": "wrong_incident_location", "status": "blocked"}
{"fresh_memory": false, "fresh_question": true, "jobs_sent": 0, "reason": "wrong_incident_location", "status": "blocked"}
{"fresh_memory": true, "fresh_question": false, "jobs_sent": 0, "reason": "wrong_incident_location", "status": "blocked"}
{"fresh_memory": true, "fresh_question": true, "jobs_sent": 1, "reason": "verified_fixture_receipt", "status": "succeeded"}
{"jobs_sent": 1, "location_ref": "R12-N-EXIT16", "replay": true, "retry_with_old_evidence": "succeeded"}
{"initial_acceptance": "declined", "jobs_sent": 2, "next_acceptance": "accepted", "places": ["KESTREL-PLAZA", "KESTREL-PLAZA"], "repeat_next_provider_replay": true, "same_assistance_ref": true}
```

Change just the corrected location back to exit 14 and predict which assertions
will fail. The question checks location and service, while memory checks the
version tag. Even unchanged display text cannot repair an old memory tag.

:::checkpoint{id="roadside-corrected-approval" question="Memory now points to exit 16 but the caller answered an exit 14 question. Can the first dispatch proceed?" options="Yes because memory is current,No because the answered question describes the old place,Yes if the provider covers both exits" answer="1"}
Both memory and the answered question must describe the draft being sent.
Provider coverage is a separate check; it cannot supply caller approval.
:::

## Replay a sent draft before considering another effect

The [request function](https://github.com/RasaHQ/rasa-community-resources/blob/4aa0c4419dc193fef7a969c12d59edcf720f2606/examples/mantle-voice-insurance-roadside-claude/lib/roadside.py#L646-L683) looks for an incident belonging
to this draft before it checks confirmation:

```python
    existing = next((i for i in service.incidents.values() if i["draft_id"] == draft.draft_id), None)
    if existing is not None:
        return _incident_result(service, existing, replay=True)
```

That order explains the fifth output line. Old evidence cannot create a new
job, but it does not prevent retrieving the existing result. Check both the
replay flag and the number of jobs. A success status alone cannot distinguish
an original effect from a replay.

:::diagram{title="A draft correction and a request replay take different paths"}

```dot
rankdir=LR;
request [label="request_dispatch"];
existing [label="Incident for this draft?"];
replay [label="Return existing receipt", class="ok"];
confirm [label="Current tag + answered place/service?"];
blocked [label="No job sent", class="blocked"];
suitable [label="Suitable provider?"];
send [label="Create incident and send job", class="ok"];
request -> existing;
existing -> replay [label="yes"];
existing -> confirm [label="no"];
confirm -> blocked [label="no"];
confirm -> suitable [label="yes"];
suitable -> blocked [label="no"];
suitable -> send [label="yes"];
```

:::

A correction after dispatch returns `already_dispatched`; it needs the desk
route rather than a silent rewrite of an existing job. The replay mechanism
shown here is local to the in-memory service instance. Durable storage and
atomic request handling remain integration work before using a real provider.
These receipts establish neither behaviour across restarts nor concurrent dispatch safety.

## Retry a declined provider under one incident

The final exercise starts an electric-car tow at fictional Kestrel Plaza.
The first suitable provider declines. [request_next_provider](https://github.com/RasaHQ/rasa-community-resources/blob/4aa0c4419dc193fef7a969c12d59edcf720f2606/examples/mantle-voice-insurance-roadside-claude/lib/roadside.py#L686-L710)
uses the existing incident location and assistance reference, excluding declined
providers from the next search. Once a provider accepts, another call replays
the incident instead of sending a third job.

Keep request, acceptance and estimate separate in the caller's answer.
`pending` with a decline does not mean someone is coming. `succeeded` does
not invent an estimate; only the provider fixture can supply one. The tests
exercise pending, accepted and accepted-without-estimate receipts.

:::solution{title="Show what to inspect when the result seems wrong"}
For a blocked first dispatch, inspect the draft tag, the exact question and
whether the conversation records an answer. For a successful replay, inspect
`replay` and the unchanged dispatch count. For a provider retry, compare the
assistance reference and place across both jobs. Changing either means you
are no longer retrying the same incident.
:::

## 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:

```text
Ran 30 tests in 0.004s

OK
```

Validation also printed:

```text
validate_project: ok
```

After installing this selected project, repeat those checks with:

```bash
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 voice path separately

The public [README and Makefile](https://github.com/RasaHQ/rasa-community-resources/blob/4aa0c4419dc193fef7a969c12d59edcf720f2606/examples/mantle-voice-insurance-roadside-claude/README.md) provide the live setup.
With Python 3.12 and uv available, install only this selected example:

```bash
uv sync --locked --python 3.12
make env
```

Fill the local `.env` with `RASA_LICENSE`, `ANTHROPIC_API_KEY` and
`DEEPGRAM_API_KEY`. The project pins Rasa Pro `3.21.0.dev5`.
Its [integrations.yml](https://github.com/RasaHQ/rasa-community-resources/blob/4aa0c4419dc193fef7a969c12d59edcf720f2606/examples/mantle-voice-insurance-roadside-claude/integrations.yml) selects Anthropic
`claude-sonnet-5-5`, Deepgram `flux-general-en` input and
`aura-2-andromeda-en` output, with the Inspector enabled.

```bash
make validate
make train
make inspect
```

Allow microphone access. This optional path uses billed provider APIs; it was
not run for the offline evidence above. Use a short manual acceptance check:
“I am James Whitfield, policy five one seven three zero six. I need a tow at
exit fourteen.” At the location question, say “Actually exit sixteen.” These
are proposed caller lines, not recorded dialogue. Inspect the correction tool
result and the next confirmation before agreeing. Check that the eventual
receipt names exit 16 and that a status check retains the assistance reference.

For the refusal path, leave confirmation unanswered and inspect whether any
job was sent. Finish this small check before designing a wider voice evaluation.
The [quickstart](/quickstart/) covers the general Rasa setup.

:::cta{href="/library/tutorials/guarding-irreversible-actions/" label="Build the guard and retry checks for your own action"}
Carry the draft identity into your service adapter, then test refusals and
replays at the function that performs the side effect.
:::