LangChain (YT style)LangChain 1.4 · Python 3.12+
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
46 small wins to finish your pathNext lesson →

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

python
@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.

python
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 code

The 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.

python
@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.

python
@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

A stand-in from here on. From this point the desk runs on 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.

python
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
Project files used on this pageThis lesson builds on a project from earlier lessons. The code below imports these files. Click a file to see its code, or follow the link to the lesson that wrote it. To run the code yourself, keep them in the same folder.
View the code here
policies.py
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)
word_embeddings.py
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]
search.py
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.

python
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.

ExampleAPI key
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)

How the desk answered each question

  • Where is A17? matched an order Ravi owns, so lookup_order answered.
  • 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_order refused it, even for Ravi.
  • No rules yet, and nothing stopping a refund; the next lesson adds both.

The three tools

ToolAnswers aboutChecks the owner
lookup_orderOne order's statusYes
refund_orderRefunding one orderYes
search_policiesRefunds, shipping, accountsNo, 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.
Watch out. The owner check lives inside the tool, not in the prompt. Move it into the model's instructions and a customer can talk the model into looking up someone else's order.
Try it yourself
  • Ask about an order id the shop has never heard of.
  • Take search_policies out 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.