Skip to content

Commit c60b02f

Browse files
viniciusdsmelloclaude
authored andcommitted
feat(closes OPEN-9557): extend promote to resolve fields from function outputs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ff77ad3 commit c60b02f

File tree

3 files changed

+512
-22
lines changed

3 files changed

+512
-22
lines changed
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
"""
2+
Example: Promoting inputs and outputs to top-level trace columns.
3+
4+
The `promote` parameter on @trace() lets you surface function inputs *and*
5+
output fields as top-level columns in the trace data, so you can create
6+
Openlayer tests against them (e.g. "agent_tool_call_count < 10").
7+
8+
Keys are resolved from **inputs first**, then from the **output** (dict,
9+
Pydantic model, or dataclass). Use a list to keep original names, or a dict
10+
to alias them and avoid collisions between parent and child steps.
11+
"""
12+
13+
import dataclasses
14+
import os
15+
from typing import Any, Dict, List
16+
17+
from pydantic import BaseModel
18+
19+
os.environ["OPENLAYER_API_KEY"] = "your-api-key-here"
20+
os.environ["OPENLAYER_INFERENCE_PIPELINE_ID"] = "your-pipeline-id-here"
21+
22+
from openlayer.lib import trace
23+
from openlayer.lib.tracing import tracer
24+
25+
26+
# ---------------------------------------------------------------------------
27+
# 1. Promote from a Pydantic model output
28+
# ---------------------------------------------------------------------------
29+
30+
class AgentResult(BaseModel):
31+
answer: str
32+
tool_call_count: int
33+
tool_names: List[str]
34+
35+
36+
@trace(promote={
37+
"user_query": "agent_input_query", # from input
38+
"tool_call_count": "agent_tool_calls", # from output
39+
"tool_names": "agent_tools", # from output
40+
})
41+
def run_agent(user_query: str) -> AgentResult:
42+
"""Simulates an agent that uses tools to answer a question.
43+
44+
The trace data will include three top-level columns:
45+
- agent_input_query (from the `user_query` input)
46+
- agent_tool_calls (from the Pydantic output's `tool_call_count`)
47+
- agent_tools (from the Pydantic output's `tool_names`)
48+
"""
49+
# ... agent logic would go here ...
50+
return AgentResult(
51+
answer="Paris is the capital of France.",
52+
tool_call_count=2,
53+
tool_names=["web_search", "summarize"],
54+
)
55+
56+
57+
# ---------------------------------------------------------------------------
58+
# 2. Promote from a dict output (list form -- no aliasing)
59+
# ---------------------------------------------------------------------------
60+
61+
@trace(promote=["score", "confidence"])
62+
def evaluate(text: str) -> Dict[str, Any]:
63+
"""Evaluates text quality. `score` and `confidence` become top-level columns."""
64+
return {"score": 0.95, "confidence": 0.87, "explanation": "Well-structured."}
65+
66+
67+
# ---------------------------------------------------------------------------
68+
# 3. Promote from a dataclass output
69+
# ---------------------------------------------------------------------------
70+
71+
@dataclasses.dataclass
72+
class RetrievalResult:
73+
documents: List[str]
74+
doc_count: int
75+
avg_relevance: float
76+
77+
78+
@trace(promote={"doc_count": "retrieval_doc_count", "avg_relevance": "retrieval_relevance"})
79+
def retrieve(query: str) -> RetrievalResult:
80+
"""Retrieves relevant documents. Promotes doc_count and avg_relevance."""
81+
return RetrievalResult(
82+
documents=["doc_a", "doc_b", "doc_c"],
83+
doc_count=3,
84+
avg_relevance=0.82,
85+
)
86+
87+
88+
# ---------------------------------------------------------------------------
89+
# 4. Nested traces -- child steps promote to the same top-level row
90+
# ---------------------------------------------------------------------------
91+
92+
93+
class ToolResult(BaseModel):
94+
tool_call_count: int
95+
tool_names: List[str]
96+
result: str
97+
98+
99+
@trace(promote={"tool_call_count": "child_tool_calls", "tool_names": "child_tools"})
100+
def inner_agent_step(task: str) -> ToolResult:
101+
"""A child step whose output fields are promoted to the parent trace.
102+
103+
Even though this is a nested step, `promote` writes to the shared Trace
104+
object, so `child_tool_calls` and `child_tools` become top-level columns.
105+
"""
106+
return ToolResult(
107+
tool_call_count=5,
108+
tool_names=["search", "calculator", "code_exec", "summarize", "translate"],
109+
result=f"Completed: {task}",
110+
)
111+
112+
113+
@trace(promote={"user_query": "input_query"})
114+
def orchestrator(user_query: str) -> str:
115+
"""Parent function that delegates to a child step.
116+
117+
After execution the trace will have top-level columns from *both* levels:
118+
- input_query (parent input)
119+
- child_tool_calls (child output)
120+
- child_tools (child output)
121+
"""
122+
step1 = inner_agent_step("look up facts")
123+
step2 = inner_agent_step("summarize findings")
124+
return f"{step1.result} | {step2.result}"
125+
126+
127+
# ---------------------------------------------------------------------------
128+
129+
def main():
130+
print("=== 1. Promote from Pydantic output ===")
131+
result = run_agent("What is the capital of France?")
132+
print(f" Answer: {result.answer}")
133+
print(f" Tool calls: {result.tool_call_count}")
134+
print()
135+
136+
print("=== 2. Promote from dict output (list form) ===")
137+
scores = evaluate("The quick brown fox.")
138+
print(f" Score: {scores['score']}, Confidence: {scores['confidence']}")
139+
print()
140+
141+
print("=== 3. Promote from dataclass output ===")
142+
docs = retrieve("machine learning basics")
143+
print(f" Retrieved {docs.doc_count} docs, avg relevance: {docs.avg_relevance}")
144+
print()
145+
146+
print("=== 4. Nested traces -- child promote to parent row ===")
147+
captured_trace = None
148+
149+
@trace(promote={"user_query": "input_query"})
150+
def traced_orchestrator(user_query: str) -> str:
151+
nonlocal captured_trace
152+
step1 = inner_agent_step("look up facts")
153+
step2 = inner_agent_step("summarize findings")
154+
captured_trace = tracer.get_current_trace()
155+
return f"{step1.result} | {step2.result}"
156+
157+
out = traced_orchestrator("hello world")
158+
print(f" Result: {out}")
159+
print(f" Trace metadata: {captured_trace.metadata}")
160+
assert captured_trace.metadata["input_query"] == "hello world", "parent input promoted"
161+
assert captured_trace.metadata["child_tool_calls"] == 5, "child output promoted"
162+
assert "search" in captured_trace.metadata["child_tools"], "child list output promoted"
163+
print(" All promoted columns verified!")
164+
print()
165+
166+
print("Check your Openlayer dashboard -- promoted fields appear as top-level")
167+
print("columns you can write tests against (e.g. child_tool_calls < 10).")
168+
169+
170+
if __name__ == "__main__":
171+
main()

0 commit comments

Comments
 (0)