Skip to content
RasaGet a free licence
Guides for AI teams

Guide · AI product engineer

Connect an Agno shopping adviser to Rasa

Help returning shoppers use their saved preferences through a Rasa assistant, while keeping each customer’s information separate.

by Rod Rivera

About 6 minutes

Key takeaways (3)
  • Retain the Agno adviser’s tools and saved customer preferences.
  • Give each customer their own saved preferences and each conversation its own history.
  • Check what the model receives, as well as what it answers.

A returning shopper expects the adviser to remember their preferences. Another shopper should receive their own advice. This tutorial adds a Rasa conversation to an Agno product adviser and keeps saved preferences tied to the right customer.

Our test shows why checking the answer alone is insufficient. The adviser returns ready and recommends the right product even when its input includes another fictional customer’s saved preference.

The native ownership mutation changes user_id=scope.user_id to user_id=None in the actual handoff function. Both runs start with identical public seeded records and empty history in independent databases. Both run the native product tool and return checked advice. Their initial inputs differ:

Handoff argumentResultProductCurrent user messagesA’s preference supplied to B
user_id=scope.user_idreadyWS-HAND-011No
user_id=NonereadyWS-HAND-011Yes

The result schema checks the advice after the model has received its input. Bind the memory owner before that request. Rasa can expose the existing Agno adviser while application code supplies the customer key from its channel.

A new thread and a new Agent leave saved memory intact

The next conversation contains no earlier question. It can still contain a saved preference from a previous interaction. Creating a fresh Agno Agent leaves that result unchanged: the preference lives in SQLite.

That persistence is useful when the same customer returns through another channel. It needs a stable customer key. Use the conversation key instead and the returning customer loses their preference. Omit the customer key in this seeded exercise and both synthetic customers receive the default owner’s fact.

The native controls inspect the next model input under six configurations:

Agent lifetimeMemory owner supplied by applicationA’s new conversation receives A’s preferenceB receives A’s preferencePrevious question present
ReusedStable customer keyYesNoNo
Fresh per requestStable customer keyYesNoNo
ReusedConversation key used as customer keyNoNoNo
Fresh per requestConversation key used as customer keyNoNoNo
ReusedOmitted; fact seeded under default ownerYesYesNo
Fresh per requestOmitted; fact seeded under default ownerYesYesNo

Each row runs the actual Agno Agent and product tool. The preference is seeded through the native database API, and replies are scripted. Automatic memory extraction is disabled so the control isolates storage ownership. The measured observation is what enters the model request. Assess generated wording when you run your own model conversation.

Keep the preference, change the conversation

Willow Shop is fictional. Its existing adviser has a public hand-cream tool, a model and SQLite storage. Its synthetic customer A prefers fragrance-free products. This lesson adds a Rasa conversation around that adviser. Agno still runs the product tool and supplies its memory to its model.

The adviser factory keeps two context settings enabled: add_memories_to_context and add_history_to_context. The Agno memory guide states, “user_id identifies whose memories should be loaded.” Our native controls inspect the actual next model input under three owner strategies. Memory follows user_id; history follows session_id. A new conversation needs a new thread, while its returning customer keeps the same memory owner.

The native call supplies these two keys separately:

result = agent.run(question, user_id=scope.user_id, session_id=scope.session_id)

Keep scope.user_id stable when the same customer starts another conversation. Change scope.session_id to give that conversation its own history. Reconstructing agent changes neither key nor the saved records.

The exercise holds both settings fixed. It changes the ownership argument and agent lifetime, then checks both context sources. A test that checks only for an earlier chat question would pass every row, including the omitted-owner case. Conversely, replacing the customer key with the new thread key removes the fact the returning customer wanted to retain.

Two selected fields from returning A’s fresh-Agent observations make the continuity difference visible. Both inputs contain just the current question:

A new thread should retain the returning customer's preference

Prefer: Stable customer key

{ "preference_in_initial_input": true, "user_messages_in_initial_input": 1 }

The new conversation retains A’s stored preference.

Avoid: Thread used as memory owner

{ "preference_in_initial_input": false, "user_messages_in_initial_input": 1 }

Changing the owner loses A’s preference, even though the thread is new in both cases.

Customer A A's chat conversation A's next conversation A's stored preference Existing Agno product adviser user_id = customer-a Customer B B's conversation Public product tool Rasa skill: explain checked advice
  1. A’s customer key remains stable across the two conversations. Their thread keys differ, so the earlier question stays out of the next conversation.
  2. B supplies another customer key. The native input check inspects both the stored preference and the previous question.
FigureOne customer can have two conversations

Bind customer and conversation in application code

The demo registry maps three public channel sender names to two synthetic customers:

Public demo senderAgno user keyAgno conversation key
willow-a-chatcustomer-aa-chat
willow-a-voicecustomer-aa-voice
willow-b-chatcustomer-bb-chat

The second name simulates another channel using the same customer; the supplied REST exercise sends text. The service creates a fresh native Agent for each request while keeping its database. The observed fresh-agent rows show why this can retain useful memory, and why ownership must still be supplied.

The application passes the resolved user_id and session_id to agent.run. The question remains text. In the hostile-text control, B asks the adviser to use customer-a as its user ID. The actual initial model input still excludes A’s seeded preference: the question cannot change the application’s selected arguments. This checks dispatch and supplied context. A model could still invent a preference; assess its reply separately.

Unknown demo senders and invalid questions return before a model is created. The ready result must name public product WS-HAND-01 and fit the required advice schema. Actual native runs with malformed JSON, an unlisted product or a model exception return advice_unavailable in the negative controls. The schema checks product identity and shape. Verify the explanation against the product record when assessing the conversation.

Let Rasa expose the existing product task

The Mantle tool accepts only the question. The decorated binding controls exercise its arguments and native result. It resolves the channel sender through the current turn context and calls the native service in a worker thread. The skill tells Mantle to explain a ready result and offer a retry when advice is unavailable. Product lookup and memory remain in Agno.

The pinned tool takes the question from Mantle and the sender from the current turn. It sends both to the existing adviser service:

@tool(
    description="Ask the existing Agno adviser about the public synthetic Willow Shop hand cream. Supply the question only; application code resolves its demo profile and conversation. Does not place orders or update preferences."
)
async def ask_product_adviser(question: str, context: ToolContext = None) -> ToolResult:
    from rasa.mantle.orchestration.turn_context import current_turn_context

    sender = current_turn_context().sender_id
    return ToolResult(
        llm_response=await asyncio.to_thread(service().answer, sender, question)
    )

The service’s native handoff resolves that sender before constructing the two storage keys. This excerpt is from shop_adviser/adviser.py:

scope = resolve_demo_scope(sender_id)
if scope is None:
    return {"status": "unknown_demo_sender"}
if not isinstance(question, str) or not question.strip() or len(question) > 1000:
    return {"status": "invalid_question"}

The agent.run call above uses the resolved scope. The model receives the question; it does not choose its own customer or conversation key.

When adapting this project to your own adviser, these Mantle files define the handoff. Paths are relative to the companion project:

Files in this step

Modified: skills/product_advice/tools.py, skills/product_advice/skill.md, integrations.yml

Keep your existing service entry point in tools.py, and keep identity resolution in application code. Describe its accepted result and unavailable outcome in skill.md. Configure the conversational model in integrations.yml, while retaining the adviser’s own model and memory settings. Re-run the native context checks against your chosen configuration.

The native adviser returns structured advice with a product ID and explanation. Rasa makes that existing task available in its conversation. There is no order, payment or profile-update tool. To extend the task, retain the adviser’s tools and define the result your conversation needs before it replies.

The service serializes native requests with a thread lock. Cancelling the async caller can leave the native thread running; it holds the lock until it finishes. A ten-second model request timeout and zero SDK retries are separate from an overall operation deadline. Keep that distinction when deciding how your interface reports waiting, cancellation and retry.

There are two model stages: the retained adviser and Mantle’s conversation. SHOP_MODEL selects the former; integrations.yml selects the latter. Track both providers’ requests when adapting the example. The native controls use scripted responses and make zero provider model requests.

Run the native exercise, then assess a conversation

Start with the Rasa quickstart if Mantle is new to you. This combined project includes the Agno package. Use Python 3.12.

From a fresh checkout:

git clone https://github.com/RasaHQ/rasa-community-resources.git
cd rasa-community-resources
git fetch origin b921c6650a207359bac010585520ed884f924c20
git checkout b921c6650a207359bac010585520ed884f924c20
python3 scripts/workspace.py start tutorials/frameworks-with-rasa/agno-mantle --owner willow-conversation-demo
cd tutorials/frameworks-with-rasa/agno-mantle
make test
make validate
.venv/bin/python ../agno/demo_memory_scopes.py
.venv/bin/python ../agno/demo_handoff_owner.py

The 11 October scripted run used these commands from agno-mantle. Each summary below is a separate, literal excerpt from its recorded output. First, the native adviser controls:

.venv/bin/python -B -m unittest discover -s ../agno/tests -p 'test_*.py' -v
Ran 15 tests in 0.214s

OK

Then the decorated Mantle handoff controls:

.venv/bin/python -B -m unittest discover -s tests -p 'test_*.py' -v
Ran 5 tests in 1.688s

OK

The mutation demo prints ready and WS-HAND-01 in both observations, with preference_in_initial_input changing from false to true. Its source seeds the default-owner fact deliberately; it illustrates a missing argument and a response check that cannot detect the resulting input change.

The six JSON records correspond to the memory table under “A new thread and a new Agent leave saved memory intact”. Each contains two observations, one for returning A and one for B. Inspect preference_in_initial_input and previous_question_in_initial_input together. user_messages_in_initial_input is one in every new conversation: its current question. native_product_tool_seen confirms the product tool ran in each case.

The assertions below check fresh history and exclusion of another customer’s preference. Returning A’s retained preference is printed for comparison.

Show the memory ownership assertions

From the Mantle project directory, compare the two assertions on B’s actual native input. Each strategy uses a separate temporary database:

.venv/bin/python - <<'PYCODE'
import tempfile
from pathlib import Path
from shop_adviser.native_fixture import observe_memory_strategy

for strategy in ("omitted", "correct", "session-as-user"):
    with tempfile.TemporaryDirectory() as directory:
        result = observe_memory_strategy(
            Path(directory) / "memory.sqlite", strategy, reuse_agent=False
        )
    returning_a, b = result["observations"]
    assert not b["previous_question_in_initial_input"]
    assert b["user_messages_in_initial_input"] == 1
    print(strategy, "history check: PASS")
    try:
        assert not b["preference_in_initial_input"]
    except AssertionError:
        print("  other-customer memory check: FAIL")
    else:
        print("  other-customer memory check: PASS")
    print("  returning A retains preference:",
          returning_a["preference_in_initial_input"])
PYCODE

The history check passes three times. With omitted ownership, the separate memory check fails: B receives the seeded default-owner preference. The stable customer key passes both separation checks and retains A’s preference. The thread-as-owner variant passes separation but loses that useful continuity.

This is a configured regression exercise with seeded memory and scripted replies. Assess production access and generated answer quality separately.

Export your own RASA_PRO_LICENSE and OPENAI_API_KEY using the companion .env.example as the variable list. Then train and start the local REST channel:

make train
make run

In another terminal, ask from A’s first conversation, then their new one:

curl -sS http://localhost:5020/webhooks/rest/webhook \
  -H 'Content-Type: application/json' \
  -d '{"sender":"willow-a-chat","message":"Tell me about the hand cream."}'
curl -sS http://localhost:5020/webhooks/rest/webhook \
  -H 'Content-Type: application/json' \
  -d '{"sender":"willow-a-voice","message":"Tell me about the hand cream."}'

Repeat with willow-b-chat. Check product WS-HAND-01, the explanation and how each reply uses the supplied preference. The qualification records twenty native controls, project validation, licensed training and a cold archive load. The automated context checks and the reader’s model conversation answer different questions.

Stop your own server and complete outstanding native operations. The SQLite file under .state/public-shop.sqlite contains the demo’s stored memory and history. Preserve it if you need it for later work. Retire only the selected environment, from the repository root:

python3 scripts/workspace.py retire tutorials/frameworks-with-rasa/agno-mantle --owner willow-conversation-demo --inactive

Source, lock and stored data remain. Retire a separately created standalone Agno environment too once its experiment ends, using its original owner.