The lookup is a plain local tool in the skill’s own folder — only this skill calls it, so it belongs there and travels with it.
# skills/default_session_start/tools.py
from datetime import datetime
from rasa.mantle.tools.decorator import ToolContext, tool
from rasa.mantle.tools.result import ToolResult
_CUSTOMER_PROFILE = {
"preferred_name": "Jordan",
"tier": "Premier",
"member_since": "2019",
"default_account_id": "acc_checking",
"default_account_label": "Everyday Checking",
}
@tool(description="Look up the signed-in customer's profile before greeting.")
async def get_customer_profile(context: ToolContext = None) -> ToolResult:
if context is not None:
context.memory.set("customer_name", _CUSTOMER_PROFILE["preferred_name"])
context.memory.set("customer_tier", _CUSTOMER_PROFILE["tier"])
context.memory.set("member_since", _CUSTOMER_PROFILE["member_since"])
context.memory.set("time_of_day", _time_of_day(datetime.now().hour))
context.memory.set("default_account_id", _CUSTOMER_PROFILE["default_account_id"])
context.memory.set(
"default_account_label", _CUSTOMER_PROFILE["default_account_label"]
)
return ToolResult(llm_response={"ok": True})
The profile is hard-coded so the pattern runs with no backend. In a real deployment this is a query against your customer store, keyed on whoever the channel says is signed in. The shape of the tool does not change.
Write with bare names.
context.memory.set("customer_name", …)resolves to the project entry. Writing"project.customer_name"is rejected at train time as an undeclared memory write — the qualified form is for reads only. This catches people out, because reads accept both.
Declare what you store
Nothing can be written that has not been declared. In the root memory.yml:
customer_name:
type: text
description: The name to address the customer by. Set at session start.
customer_tier:
type: text
description: The customer's membership tier, e.g. Premier.
member_since:
type: text
description: The year the customer joined.
time_of_day:
type: categorical
enum_values: [morning, afternoon, evening]
description: Part of day, computed at session start.
default_account_id:
type: text
description: Account id of the customer's usual account.
default_account_label:
type: text
description: Human-readable label of the usual account.
Two decisions are encoded here, and both matter more than they look.
Project scope, not skill scope. These live at the project level so every skill can read them. The greeting needs the name; the transactions skill needs the default account. Putting them in one skill’s memory would hide them from the other.
Not llm_settable. Only the tool writes here. The agent cannot decide the
customer has been a member since 2015 because it sounded plausible. If a fact is
supposed to come from your systems, never mark it settable.
Keeping something private
If the profile carries a field the customer must never be told, project memory is the wrong place — it can flow into model context.
Declare it in the owning skill’s memory.yml under private: instead. Private
entries are readable only by that skill, and the runtime enforces it rather than
trusting an instruction. Chapter 4 covers the second gate: what the response
template lets through.
