A stand-in model, traced as a generation
A generation is an observation for a model call: it carries the model's name, token usage and cost. The desk gets a stand-in model you write, so no key is needed.
The desk from part 1 answered straight from its dictionary of orders. A real support desk asks a model to write the reply. Langfuse records a model call as a generation, the observation type for model calls, which carries a model name, token counts and cost. To trace one without an API key, you write the model.
A model you can trace without a key
"""A stand-in chat model for the shop's support desk: no key, no network."""
import re
def reply(messages):
"""Answer the last message, and count words as tokens the way a chat model reports usage."""
last = messages[-1]["content"]
lookup = re.search(r"Lookup: (\S+) (.+)", last)
if "refund" in last.lower():
text = "Refunds need approval, so a person will review your request."
elif lookup and lookup.group(2) == "not found":
text = f"I could not find order {lookup.group(1)}. Please check the number."
elif lookup:
text = f"Order {lookup.group(1)} {lookup.group(2)}."
else:
text = "Could you send your order number?"
used = sum(len(message["content"].split()) for message in messages)
return text, {"input": used, "output": len(text.split())}reply takes a list of chat messages, the format every chat model reads, and returns its text with a usage count, the two things a model's API sends back. It reads the last message and decides: a refund question gets the refund rule, a lookup result gets turned into a sentence, and anything else gets a request for the order number. It counts words where a real model counts tokens.
from shop_model import reply
print(reply([{"role": "user", "content": "Where is A17?\nLookup: A17 shipped on 3 March"}]))
print(reply([{"role": "user", "content": "I want a refund"}]))
print(reply([{"role": "user", "content": "Hello?"}]))python try_model.pySave it as shop_model.py. Every lesson from here imports it, and lesson 7 serves it behind an OpenAI-style API.
A generation around the call
import re
from shop_model import reply
ORDERS = {"A17": "shipped on 3 March"}
SYSTEM = "You answer customers of a small online shop in one short sentence."
@observe(name="lookup-order", as_type="tool")
def lookup_order(order_id):
return ORDERS.get(order_id, "not found")The tool is lesson 4's lookup. SYSTEM is the instruction every call to the model starts with.
def ask_model(messages):
with langfuse.start_as_current_observation(
as_type="generation", name="write-reply", model="shop-model", input=messages
) as generation:
text, usage = reply(messages)
generation.update(output=text, usage_details=usage,
cost_details={"input": usage["input"] * 1e-6, "output": usage["output"] * 2e-6})
return textas_type="generation" with model and input opens the observation; update adds what came back. usage_details takes counts by kind, here input and output. cost_details takes the price in US dollars; this made-up price is one dollar per million input words and two per million output words.
@observe(name="answer-ticket", as_type="agent")
def answer(ticket):
order = re.search(r"[A-Z]\d{2}", ticket)
facts = f"\nLookup: {order.group()} {lookup_order(order.group())}" if order else ""
return ask_model([{"role": "system", "content": SYSTEM}, {"role": "user", "content": ticket + facts}])The agent looks up the order, if the ticket has one, and passes the result to the model as a Lookup: line.
print(answer("Where is my order A17?"))
langfuse.flush()
local_langfuse.tree()
generation = next(span for span in local_langfuse.SPANS if span["name"] == "write-reply")
for key in ["model.name", "usage_details", "cost_details"]:
print(key, "=", generation["attributes"]["langfuse.observation." + key])python generation.pyThe generation sits beside the tool, both under the agent: the agent asked for a lookup, then asked the model. Usage and cost arrived as JSON. Langfuse Cloud can also work out cost itself from the model name and a price table, but a price you send takes priority, and a model it has no price for, such as shop-model, gets a cost only if you send one.
A generation is an ordinary observation with a few more fields, filled in around one call.
Pick one to watch it run, step by step.
The same step as a decorated function
@observe(name="write-reply", as_type="generation")
def ask_model(messages):
text, usage = reply(messages)
langfuse.update_current_generation(model="shop-model", usage_details=usage)
return text@observe(as_type="generation") records the call; update_current_generation adds the model and usage from inside. The messages argument becomes the input and the return value the output, as in lesson 3.
print(answer("Is B22 on its way?"))
langfuse.flush()
local_langfuse.tree("usage_details")python generation2.pyB22 is not in ORDERS, so the model was given Lookup: B22 not found and said so. Every model call from here on uses this decorated form.
- Send
usage_details={"input": 10, "output": 5, "cache_read": 3}and print what arrives. - Add
model_parameters={"temperature": 0}to the generation and find it in the attributes. - Ask about a refund and check which branch of
replyanswered.
Little by little, you're building something great.