LangfuseLangfuse Python SDK 4.15.4 · Python 3.10+
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
38 small wins to finish your pathNext lesson

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

Exampleshop_model.py
"""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.

Exampletry_model.py
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?"}]))
Example
python try_model.py

Save 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

Examplegeneration.py, after the setup lines with observe
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.

Examplegeneration.py, continued
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 text

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

Examplegeneration.py, continued
@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.

Examplegeneration.py, continued
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])
Example
python generation.py

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

What a generation records
what the generation carriesmessages insystem, ticket, lookup resultwrite-replythe observation for a model callreply outthe text your code returnsmodelshop-modelusage_detailsinput and output countscost_detailsUS dollars, or inferredinput and outputmessages, and the answer
Hover or tap a piece to see what it is and which lesson built it.
Follow one model call

Pick one to watch it run, step by step.

The same step as a decorated function

Examplegeneration.py, ask_model rewritten
@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.

Examplegeneration.py, the new last lines
print(answer("Is B22 on its way?"))

langfuse.flush()
local_langfuse.tree("usage_details")
Example
python generation2.py

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

Try it yourself
  • 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 reply answered.

Little by little, you're building something great.