Three tools and a model that picks
The desk is an agent with three tools that answers two kinds of question: where an order is, and what the shop's policies say.
Last updated: 27 Sep, 2026 · LangChain 1.4
The tests in the testing lesson proved the pieces work. Now assemble the desk itself. Each order carries an owner as well as a status, and Customer is the runtime context: the name comes from your code, never from the model. Each of the three tools builds on one you wrote earlier.
Reading the runtime context in a tool
@tool
def a_tool(order_id: str, runtime: ToolRuntime[Customer]) -> str:
"""One line the model reads to know when to call this."""
name = runtime.context.name # who is asking, from your code not the model
...The orders and the customer context
The orders map an id to an owner and a status. Customer is the runtime context, a small object your code fills in. This section's code goes in a new file, desk_tools.py, holding the orders, Customer and both order tools.
from dataclasses import dataclass
from langchain.tools import ToolRuntime, tool
ORDERS = {"A17": ("ravi", "shipped on 3 March"), "C40": ("mei", "waiting for stock")}
@dataclass
class Customer:
name: str # who is asking, set by your codeThe order lookup, scoped to the owner
The lookup answers only about the asking customer's orders. A question about someone else's order gets the same reply as one about an order that does not exist, which is the answer a shop should give.
@tool
def lookup_order(order_id: str, runtime: ToolRuntime[Customer]) -> str:
"""Look up one of the customer's orders by its id, such as A17."""
owner, status = ORDERS.get(order_id, (None, None))
if owner != runtime.context.name: # someone else's order
return f"{order_id} is not one of your orders."
return f"{order_id} {status}."The refund tool, scoped too
refund_order checks the owner too, so a customer cannot refund someone else's order even if a reviewer approves the call by mistake. It is the last part of desk_tools.py. The third tool is search_policies from search.py, the one over the vector store, unchanged.
@tool
def refund_order(order_id: str, runtime: ToolRuntime[Customer]) -> str:
"""Refund one of the customer's orders in full. This cannot be undone."""
owner, _ = ORDERS.get(order_id, (None, None))
if owner != runtime.context.name:
return f"{order_id} is not one of your orders, so it cannot be refunded."
return f"Refunded {order_id}."A model that picks between them
DeskModel, a scripted stand-in you write below, so the outputs in the next lessons are exact and the desk's tests run without a key. The Groq run at the end of this lesson shows the real model making the same tool choices, and the last part of the course, Off the stand-ins, runs the desk once on the real model, then swaps in a real store and saver.The rule is simple: an order id means the order tools; anything else is a question for the policies. DeskModel extends ShopModel, the stand-in from Several tool calls at once, and overrides its decide method. Save it as desk_model.py. The desk gets its own lookup_order in desk_tools.py, with owner checks; orders.py stays only for the router tests.
import re
from langchain.messages import AIMessage
from shop_model import ShopModel
class DeskModel(ShopModel):
def decide(self, messages):
last = messages[-1]
if last.type == "tool" and last.text == "No policy covers this.":
return AIMessage("Our policies do not cover that. A person will reply.")
if last.type == "tool" or re.findall(r"\b[A-Z]\d+\b", last.text):
return super().decide(messages) # order id or tool result
query = {"name": "search_policies", "args": {"query": last.text}, "id": "call_p"}
return AIMessage("", tool_calls=[query]) # otherwise search policies- written in Documents and splitting
- written in Embeddings and a vector store
- written in Retrieval as a tool
View the code here
from langchain_core.documents import Document
from langchain_text_splitters import RecursiveCharacterTextSplitter
POLICIES = {
"refunds.md": "Refunds go back to the card you paid with. They take up to 5 working days to arrive."
"\n\nYou can ask for a refund within 30 days of delivery. Opened items can be refunded if they are faulty.",
"shipping.md": "Standard shipping takes 3 to 5 working days. Shipping is free on orders over 50 euros."
"\n\nExpress shipping arrives the next working day and costs 9 euros.",
"accounts.md": "To reset your password, use the reset link on the sign-in page. Support staff never ask for your password.",
}
docs = [Document(page_content=text, metadata={"source": name}) for name, text in POLICIES.items()]
splitter = RecursiveCharacterTextSplitter(chunk_size=120, chunk_overlap=0, add_start_index=True)
chunks = splitter.split_documents(docs)
import re
import zlib
from langchain_core.embeddings import Embeddings
COMMON = {"a", "an", "and", "are", "can", "do", "does", "for", "how", "i",
"if", "is", "it", "my", "of", "on", "the", "to", "what", "with", "you", "your"}
class WordEmbeddings(Embeddings):
def embed_query(self, text):
vector = [0.0] * 256
for word in re.findall(r"[a-z]+", text.lower()):
if word not in COMMON:
vector[zlib.crc32(word.rstrip("s").encode()) % 256] += 1.0
return vector
def embed_documents(self, texts):
return [self.embed_query(text) for text in texts]
from langchain.tools import tool
from langchain_core.vectorstores import InMemoryVectorStore
from policies import chunks
from word_embeddings import WordEmbeddings
store = InMemoryVectorStore(WordEmbeddings())
store.add_documents(chunks)
@tool
def search_policies(query: str) -> str:
"""Search the shop's policies on refunds, shipping and accounts.
Pass the customer's question, word for word, as the query."""
found = [doc for doc, score in store.similarity_search_with_score(query, k=2) if score >= 0.3]
if not found:
return "No policy covers this."
return "\n".join(f"[{doc.metadata['source']}] {doc.page_content}" for doc in found)
When a tool has answered, its text is the reply. An order id or a tool result goes to ShopModel, which picks the order tool; anything else searches the policies.
Build the desk with the three tools and the customer context. context_schema tells the agent what shape Customer has.
from langchain.agents import create_agent
from desk_model import DeskModel
from search import search_policies
from desk_tools import Customer, lookup_order, refund_order
desk = create_agent(DeskModel(), tools=[lookup_order, refund_order, search_policies],
context_schema=Customer)Running three questions from Ravi
The same desk on a real model: three questions from Ravi, each passing his name as the context. The system prompt adds two sentences to the usual one: say a refusal exactly as the tool gave it, and add nothing the tools did not say. A hosted model routes by choosing the tool itself, the same way DeskModel's rule does.
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from search import search_policies
from desk_tools import Customer, lookup_order, refund_order
desk = create_agent(init_chat_model("groq:openai/gpt-oss-120b", temperature=0), # uses your GROQ_API_KEY
system_prompt="You are the support assistant for a small online shop. Answer in one or two short sentences, using only what the tools returned. If a tool says an order is not the customer's, say exactly that. Add nothing the tools did not say.",
tools=[lookup_order, refund_order, search_policies],
context_schema=Customer)
for text in ["Where is A17?", "Is shipping free?", "Where is C40?"]:
result = desk.invoke({"messages": [{"role": "user", "content": text}]},
context=Customer("ravi"))
print(text, "->", result["messages"][-1].text)Where is A17? -> A17 shipped on 3 March. Is shipping free? -> Shipping is free on orders over 50 euros. Where is C40? -> C40 is not one of your orders.
How the desk answered each question
- Where is A17? matched an order Ravi owns, so
lookup_orderanswered. - Is shipping free? had no order id, so the model searched the policies and answered from the shipping document.
- Where is C40? is Mei's order, so
lookup_orderrefused it, even for Ravi. - No rules yet, and nothing stopping a refund; the next lesson adds both.
The three tools
| Tool | Answers about | Checks the owner |
|---|---|---|
| lookup_order | One order's status | Yes |
| refund_order | Refunding one order | Yes |
| search_policies | Refunds, shipping, accounts | No, policies are public |
When a desk mixes lookups and policy
- A single desk that mixes account-specific lookups with public policy answers.
- Any tool that must act only on the asking user's own data.
Related
- Previous: Testing an agent
- Next: The desk's guardrails
- Reference: Tools
- Ask about an order id the shop has never heard of.
- Take
search_policiesout of the tool list and ask the shipping question again. - Print the whole message list for one question and count the steps.
You understood something today that you didn't yesterday.