1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
11 small wins to finish your path
Traced support desk with privacy tests
The final project combines a traced support desk, redaction before any decorated code, and tests for the trace shape and that no card number is exported.
import re
import agentops
from agentops.sdk.decorators import agent, operation, tool
ORDERS = {"A-1001": "shipped on 12 March", "A-1002": "waiting for stock"}
CARD = re.compile(r"\b\d(?:[ -]?\d){12,15}\b")
def redact(text):
return CARD.sub("[card removed]", text)
@tool(cost=0.002)
def lookup_order(order_id):
return ORDERS.get(order_id, "no such order")
@agent(name="desk")
class Desk:
@operation
def answer(self, clean_ticket):
order = re.search(r"A-\d{4}", clean_ticket)
if not order:
return "Could you send your order number?"
return f"Order {order.group()}: {lookup_order(order.group())}"
def handle(ticket):
clean = redact(ticket) # before any decorated function sees the ticket
with agentops.start_trace("support-ticket", tags=["desk"]):
return Desk().answer(clean)redactruns first, undecorated, so the raw ticket never reaches a span (lessons 4 and 8).- The trace is started by hand around the agent, with a tag (lesson 6).
lookup_orderis a tool with a cost (lesson 3).
import agentops
import local_collector
from desk import handle
url = local_collector.start()
agentops.init(endpoint=url, exporter_endpoint=f"{url}/v1/traces", auto_start_session=False, log_level="ERROR")
for ticket in ["Where is A-1001?", "Card 4111 1111 1111 1111 charged twice on A-1002", "Hello?"]:
print(handle(ticket))
local_collector.flush()
local_collector.tree()python app.pyThree tickets, three traces. The ticket without an order number never called the tool, so its trace has no tool span.
Hover or tap a piece to see what it is and which lesson built it.
Send a ticket
Testing the trace and the card number
import agentops
import pytest
import local_collector
from desk import handle
@pytest.fixture(scope="module")
def collector():
url = local_collector.start()
agentops.init(endpoint=url, exporter_endpoint=f"{url}/v1/traces", auto_start_session=False, log_level="ERROR")
return local_collector
def spans_for(collector, ticket):
collector.SPANS.clear()
handle(ticket)
collector.flush()
return collector.SPANS
def test_trace_has_the_expected_shape(collector):
names = [span["name"] for span in spans_for(collector, "Where is A-1001?")]
assert names == ["lookup_order.tool", "answer.task", "desk.agent", "support-ticket.session"]
def test_card_numbers_never_leave_the_process(collector):
spans = spans_for(collector, "Card 4111 1111 1111 1111 charged twice on A-1002")
assert spans
assert not any("4111" in str(span["attributes"]) for span in spans)pytest -q -p no:warningsThe first test pins the trace's structure, so a refactor that drops a decorator is noticed. The second searches every exported attribute for the card number. Both run against the local collector, so they run in CI with no key and no network.
With the hosted service
In app.py, replace the collector and endpoints with agentops.init(api_key=...). Keep the tests on the local collector: they check what your code sends, which does not depend on where it goes.
Where AgentOps goes further
| Topic | What it is for |
|---|---|
| Framework integrations | Automatic spans for CrewAI, LangGraph, OpenAI Agents SDK and others. |
| Other providers | Anthropic, Gemini, LiteLLM and more, instrumented the same way. |
| track_endpoint | Tracing Flask request handlers. |
| TypeScript SDK | AgentOps for Node.js agents. |
| Public API and MCP server | Reading traces back from the service. |
Every expert started right here.