1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
24 small wins to finish your pathNext lesson →
BaseLM: a readable keyword model for DSPy
A DSPy language model is any class with a forward method. This course uses one written in Python: it reads DSPy's prompt and answers with keyword rules.
A mock returns the same text for every question. To sort tickets for real, the model has to read the prompt. dspy.BaseLM is the base class DSPy's own LM classes use, and a subclass only has to implement forward:
import json
import re
import dspy
from dspy.clients.openai_format import to_openai_chat_request
KEYWORDS = {
"billing": ["charged", "refund", "invoice", "payment"],
"shipping": ["parcel", "delivery", "courier", "arrived"],
"account": ["password", "login", "email", "account"],
}
def read_fields(text):
"""The [[ ## name ## ]] blocks of a message, as a dictionary."""
parts = re.split(r"\[\[ ## (\w+) ## \]\]\n", text)
return {name: value.split("\n\nRespond with")[0].strip() for name, value in zip(parts[1::2], parts[2::2])}
def words(text):
return {w for w in re.findall(r"[a-z]+", text.lower()) if len(w) > 3}
def sort_ticket(ticket, demos, attempt=0):
# With examples in the prompt: copy the label of the one that shares most words.
scored = [(len(words(ticket) & words(d["ticket"])), d["category"]) for d in demos if "category" in d]
best = max(scored, default=(0, None))
if best[0] > 0:
return best[1], f"It shares the most words with an example labelled {best[1]}."
# Without: keyword rules.
for category, keys in KEYWORDS.items():
found = [k for k in keys if k in ticket.lower()]
if found:
return category, f"The ticket mentions '{found[0]}'."
guess = ["account", "billing", "shipping"][attempt % 3]
return guess, "No keyword matched, so I guessed."
class ShopLM(dspy.BaseLM):
"""A stand-in language model: keyword rules instead of a neural network."""
forward_contract = "typed_lm"
def forward(self, request):
messages = to_openai_chat_request(request)["messages"]
wanted = re.findall(r"^\d+\. `(\w+)`", messages[0]["content"].split("Your output fields are:")[1], re.M)
inputs = read_fields(messages[-1]["content"])
demos = [read_fields(u["content"]) | read_fields(a["content"]) for u, a in zip(messages[1:-1:2], messages[2:-1:2])]
ticket = inputs.get("ticket", "")
# DSPy numbers repeated tries of the same request (rollout_id); each try guesses differently.
cache = request.config.cache
attempt = cache.rollout_id if cache and cache.rollout_id else 0
category, why = sort_ticket(ticket, demos, attempt)
order = re.search(r"A-\d{4}", ticket)
# A ReAct trajectory arrives as more [[ ## ]] blocks: observation_0 is the first tool's result.
looked_up = inputs.get("observation_0") if inputs.get("tool_name_0") == "lookup_order" else None
tool_used = "tool_name_0" in inputs
call_tool = bool(order) and not tool_used
answers = {
"category": category,
"reasoning": why,
"reply": f"Your order is {looked_up}." if looked_up else f"Thanks for your message. Our {category} team will help.",
"next_thought": "I should look the order up." if call_tool else "I have what I need.",
"next_tool_name": "lookup_order" if call_tool else "finish",
"next_tool_args": json.dumps({"order_id": order.group()} if call_tool else {}),
}
text = "\n\n".join(f"[[ ## {name} ## ]]\n{answers.get(name, '')}" for name in wanted)
return dspy.LMResponse.from_text(text + "\n\n[[ ## completed ## ]]", model=self.model)forwardgets adspy.LMRequest.to_openai_chat_requestturns it into the chat messages from lesson 3.wantedis the list of output fields from the system message, andread_fieldspulls the inputs out of their markers.sort_ticketpicks a category: from examples in the prompt if there are any (lesson 15), from keywords if not, and otherwise a guess.- It answers each wanted field under its marker, the reply format
ChatAdapter.parseexpects. The ReAct lines are for lesson 10. forward_contract = "typed_lm"tells DSPy this class takes anLMRequestand returns anLMResponse, the contract DSPy's docs recommend for new LMs.
import dspy
from shop_lm import ShopLM
dspy.configure(lm=ShopLM(model="shop/keywords"))
sort = dspy.Predict("ticket -> category")
for ticket in ["I was charged twice", "My parcel never arrived", "I want my money back"]:
print(ticket, "->", sort(ticket=ticket).category)The first two tickets contain charged and parcel. The third has no keyword, so the stand-in guessed account, which is wrong. That mistake is useful: the second half of the course measures it and fixes it.
What the stand-in is for
Keyword rules are not a language model, and the scores in this course are scores for this stand-in, not for GPT or Claude. What stays the same with a real model is everything around it: signatures, modules, metrics, optimizers, saving and tests. Swapping models is one line, lesson 6.
Try it yourself
- Add
"money"to the billing keywords and sort the third ticket again. - Print
dspy.settings.lm.history[-1].responseafter a call. - Ask
dspy.Predict("question -> answer")something. What does the stand-in answer, and why?
This is what real progress feels like.