Skip to content

Commit 7891559

Browse files
docs(examples): add lineage tutorial — data-source refs + agent version on traces
New sync tutorial 00_sync/080_lineage exercising the SGP-6513 capture surface shipped in 0.25.0: @data_sources static refs, an argument resolver, register_tool_sources for unowned (MCP-style) tools, and the AGENT_VERSION env stamp. Verified live against sgp-dev: tool spans carry sgp.lineage.refs and every span carries __agent_version__, both filterable via the spans-search extra_metadata DSL. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 0fa93b6 commit 7891559

10 files changed

Lines changed: 481 additions & 0 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
*.so
6+
.Python
7+
build/
8+
develop-eggs/
9+
dist/
10+
downloads/
11+
eggs/
12+
.eggs/
13+
lib/
14+
lib64/
15+
parts/
16+
sdist/
17+
var/
18+
wheels/
19+
*.egg-info/
20+
.installed.cfg
21+
*.egg
22+
23+
# Environments
24+
.env**
25+
.venv
26+
env/
27+
venv/
28+
ENV/
29+
env.bak/
30+
venv.bak/
31+
32+
# IDE
33+
.idea/
34+
.vscode/
35+
*.swp
36+
*.swo
37+
38+
# Git
39+
.git
40+
.gitignore
41+
42+
# Misc
43+
.DS_Store
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# syntax=docker/dockerfile:1.3
2+
FROM python:3.12-slim
3+
COPY --from=ghcr.io/astral-sh/uv:0.6.4 /uv /uvx /bin/
4+
5+
# Install system dependencies
6+
RUN apt-get update && apt-get install -y \
7+
htop \
8+
vim \
9+
curl \
10+
tar \
11+
python3-dev \
12+
postgresql-client \
13+
build-essential \
14+
libpq-dev \
15+
gcc \
16+
cmake \
17+
netcat-openbsd \
18+
&& apt-get clean \
19+
&& rm -rf /var/lib/apt/lists/*
20+
21+
RUN uv pip install --system --upgrade pip setuptools wheel
22+
23+
ENV UV_HTTP_TIMEOUT=1000
24+
25+
# Copy pyproject.toml and README.md to install dependencies
26+
COPY 00_sync/080_lineage/pyproject.toml /app/080_lineage/pyproject.toml
27+
COPY 00_sync/080_lineage/README.md /app/080_lineage/README.md
28+
29+
WORKDIR /app/080_lineage
30+
31+
# Copy the project code
32+
COPY 00_sync/080_lineage/project /app/080_lineage/project
33+
34+
# Copy the test files
35+
COPY 00_sync/080_lineage/tests /app/080_lineage/tests
36+
37+
# Copy shared test utilities
38+
COPY test_utils /app/test_utils
39+
40+
# Install the required Python packages with dev dependencies
41+
RUN uv pip install --system .[dev]
42+
43+
# Set environment variables
44+
ENV PYTHONPATH=/app
45+
46+
# Set test environment variables
47+
ENV AGENT_NAME=s080-lineage
48+
49+
# The agent build version stamped onto every span as __agent_version__.
50+
# Real deployments pass the image tag or git SHA at build time.
51+
ARG AGENT_VERSION=0.1.0
52+
ENV AGENT_VERSION=${AGENT_VERSION}
53+
54+
# Run the agent using uvicorn
55+
CMD ["uvicorn", "project.acp:acp", "--host", "0.0.0.0", "--port", "8000"]
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Lineage provenance on agent traces
2+
3+
A sync agent (same harness surface as `050_openai_agents`) whose spans carry
4+
the provenance that SGP Lineage derives graph edges from: which data sources
5+
each tool call read, and which agent build produced the trace.
6+
7+
## What this demonstrates
8+
9+
Two span-metadata conventions, both capture-only — nothing here emits to the
10+
lineage service; edges are derived later from materialized traces
11+
([SGP-6513 convention spec](https://github.com/scaleapi/scaleapi/blob/master/packages/sgp-lineage/docs/specs/2026-07-22-sgp-6513-trace-data-source-ref-convention.md)):
12+
13+
- **`sgp.lineage.refs`** — each tool declares the data sources it reads, via
14+
the three capture forms in `agentex.lib.adk.lineage`:
15+
16+
```python
17+
@function_tool
18+
@data_sources(DataSourceRef("elasticsearch://research-cluster", "filings-v1"))
19+
def search_filings(query: str) -> str: ... # static refs
20+
21+
@function_tool
22+
@data_sources(resolver=_kpi_refs)
23+
def read_kpi(table: str) -> str: ... # refs derived from arguments
24+
25+
lineage.register_tool_sources( # tools you don't own (MCP)
26+
"company_profile", [DataSourceRef("mcp://research-mcp", "company-profiles")]
27+
)
28+
```
29+
30+
The harness resolves these on every tool span and merges them into
31+
`span.data["sgp.lineage.refs"]`.
32+
33+
- **`__agent_version__`** — the SGP tracing processor stamps the
34+
`AGENT_VERSION` env var onto every span. The Dockerfile sets it from a
35+
build arg; real deployments pass the image tag or git SHA. For
36+
`agentex agents run`, export it or put it in this directory's `.env`.
37+
38+
Ref namespaces must use the canonical URI forms from the lineage identifier
39+
conventions (`packages/sgp-lineage/docs/event-contract.md` in `scaleapi`);
40+
malformed namespaces raise at import time.
41+
42+
## Run it
43+
44+
```bash
45+
agentex agents run --manifest manifest.yaml
46+
```
47+
48+
Ask it to "research ACME Corp" — the instructions route through all three
49+
tools. With `SGP_API_KEY` / `SGP_ACCOUNT_ID` / `SGP_CLIENT_BASE_URL` set, the
50+
resulting trace's tool spans show `sgp.lineage.refs` (and every span
51+
`__agent_version__`) in their metadata, filterable in the SGP traces UI and
52+
spans-search `extra_metadata` DSL.
53+
54+
## Test it
55+
56+
The offline test verifies all three capture forms resolve without a server or
57+
API key:
58+
59+
```bash
60+
pytest tests/test_agent.py -v
61+
```
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
build:
2+
context:
3+
root: ../../
4+
include_paths:
5+
- 00_sync/080_lineage
6+
- test_utils
7+
dockerfile: 00_sync/080_lineage/Dockerfile
8+
dockerignore: 00_sync/080_lineage/.dockerignore
9+
10+
local_development:
11+
agent:
12+
port: 8000
13+
host_address: host.docker.internal
14+
paths:
15+
acp: project/acp.py
16+
17+
agent:
18+
acp_type: sync
19+
name: s080-lineage
20+
description: A sync agent whose tool calls carry lineage data-source refs and an agent version
21+
22+
temporal:
23+
enabled: false
24+
25+
credentials:
26+
- env_var_name: OPENAI_API_KEY
27+
secret_name: openai-api-key
28+
secret_key: api-key
29+
- env_var_name: REDIS_URL
30+
secret_name: redis-url-secret
31+
secret_key: url
32+
- env_var_name: SGP_API_KEY
33+
secret_name: sgp-api-key
34+
secret_key: api-key
35+
- env_var_name: SGP_ACCOUNT_ID
36+
secret_name: sgp-account-id
37+
secret_key: account-id
38+
- env_var_name: SGP_CLIENT_BASE_URL
39+
secret_name: sgp-client-base-url
40+
secret_key: url
41+
42+
deployment:
43+
image:
44+
repository: ""
45+
tag: "latest"
46+
47+
global:
48+
agent:
49+
name: "s080-lineage"
50+
description: "A sync agent whose tool calls carry lineage data-source refs and an agent version"
51+
replicaCount: 1
52+
resources:
53+
requests:
54+
cpu: "500m"
55+
memory: "1Gi"
56+
limits:
57+
cpu: "1000m"
58+
memory: "2Gi"

examples/tutorials/00_sync/080_lineage/project/__init__.py

Whitespace-only changes.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""ACP handler for the lineage tutorial.
2+
3+
Identical harness wiring to ``00_sync/050_openai_agents``: ``Runner.run_streamed``
4+
wrapped in an ``OpenAITurn``, delivered through ``UnifiedEmitter.yield_turn``.
5+
Lineage capture rides that surface, and the SGP tracing processor stamps
6+
``__agent_version__`` from the ``AGENT_VERSION`` env var on every span.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import os
12+
from typing import AsyncGenerator
13+
14+
from dotenv import load_dotenv
15+
16+
load_dotenv()
17+
18+
from agents import Runner
19+
20+
from agentex.lib import adk
21+
from project.agent import MODEL_NAME, create_agent
22+
from agentex.lib.types.acp import SendMessageParams
23+
from agentex.lib.types.tracing import SGPTracingProcessorConfig
24+
from agentex.lib.utils.logging import make_logger
25+
from agentex.lib.sdk.fastacp.fastacp import FastACP
26+
from agentex.lib.core.harness.emitter import UnifiedEmitter
27+
from agentex.types.task_message_update import TaskMessageUpdate
28+
from agentex.types.task_message_content import TaskMessageContent
29+
from agentex.lib.adk.providers._modules.openai_turn import OpenAITurn
30+
from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_processor_config
31+
32+
logger = make_logger(__name__)
33+
34+
# LiteLLM proxy auth: copy LITELLM_API_KEY to OPENAI_API_KEY for OpenAI client
35+
# compatibility, so the same example works behind the Scale LiteLLM gateway.
36+
_litellm_key = os.environ.get("LITELLM_API_KEY")
37+
if _litellm_key and not os.environ.get("OPENAI_API_KEY"):
38+
os.environ["OPENAI_API_KEY"] = _litellm_key
39+
40+
add_tracing_processor_config(
41+
SGPTracingProcessorConfig(
42+
sgp_api_key=os.environ.get("SGP_API_KEY", ""),
43+
sgp_account_id=os.environ.get("SGP_ACCOUNT_ID", ""),
44+
sgp_base_url=os.environ.get("SGP_CLIENT_BASE_URL", ""),
45+
)
46+
)
47+
48+
acp = FastACP.create(acp_type="sync")
49+
50+
_agent = None
51+
52+
53+
def get_agent():
54+
"""Get or create the OpenAI Agents SDK agent instance."""
55+
global _agent
56+
if _agent is None:
57+
_agent = create_agent()
58+
return _agent
59+
60+
61+
@acp.on_message_send
62+
async def handle_message_send(
63+
params: SendMessageParams,
64+
) -> TaskMessageContent | list[TaskMessageContent] | AsyncGenerator[TaskMessageUpdate, None]:
65+
"""Handle incoming messages, streaming tokens and tool calls via the harness."""
66+
agent = get_agent()
67+
task_id = params.task.id
68+
user_message = params.content.content
69+
logger.info(f"Processing message for task {task_id}")
70+
71+
async with adk.tracing.span(
72+
trace_id=task_id,
73+
task_id=task_id,
74+
name="message",
75+
input={"message": user_message},
76+
data={"__span_type__": "AGENT_WORKFLOW"},
77+
) as turn_span:
78+
result = Runner.run_streamed(starting_agent=agent, input=user_message)
79+
turn = OpenAITurn(result=result, model=MODEL_NAME)
80+
emitter = UnifiedEmitter(
81+
task_id=task_id,
82+
trace_id=task_id,
83+
parent_span_id=turn_span.id if turn_span else None,
84+
)
85+
async for event in emitter.yield_turn(turn):
86+
yield event
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""OpenAI Agents SDK agent whose tool calls carry lineage data-source refs.
2+
3+
Each tool declares the data sources it reads via one of the three capture
4+
forms in ``agentex.lib.adk.lineage`` (see README.md); the harness resolves
5+
them on every tool span into ``span.data["sgp.lineage.refs"]``. Capture only —
6+
nothing here emits to the lineage service.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
from agents import Agent, function_tool, set_tracing_disabled
12+
13+
from project import tools
14+
from agentex.lib.adk import DataSourceRef, lineage, data_sources
15+
16+
# Disable the openai-agents SDK's native tracer so it doesn't ship traces to
17+
# api.openai.com (the key may be a gateway/proxy key). Agentex tracing still
18+
# runs via the harness + tracing manager configured in acp.py.
19+
set_tracing_disabled(True)
20+
21+
MODEL_NAME = "gpt-4o"
22+
INSTRUCTIONS = """You are a market-research assistant with access to tools.
23+
24+
Guidelines:
25+
- To research a company, use all three tools: search_filings, read_kpi
26+
(table="revenue"), and company_profile.
27+
- Be concise, and always report the real tool output back to the user.
28+
"""
29+
30+
31+
@function_tool
32+
@data_sources(DataSourceRef("elasticsearch://research-cluster", "filings-v1"))
33+
def search_filings(query: str) -> str:
34+
"""Search the filings index for documents matching a query."""
35+
return tools.search_filings(query)
36+
37+
38+
def _kpi_refs(args: dict) -> list[DataSourceRef]:
39+
table = args.get("table")
40+
if not table:
41+
return []
42+
return [DataSourceRef("databricks://demo-workspace.cloud.databricks.com", f"main.kpi.{table}")]
43+
44+
45+
@function_tool
46+
@data_sources(resolver=_kpi_refs)
47+
def read_kpi(table: str) -> str:
48+
"""Read a KPI summary row from a metrics table."""
49+
return tools.read_kpi(table)
50+
51+
52+
@function_tool
53+
def company_profile(name: str) -> str:
54+
"""Fetch a company profile."""
55+
return tools.company_profile(name)
56+
57+
58+
# company_profile stands in for a tool this codebase doesn't own (e.g. an MCP
59+
# server's tool), so its refs are registered by name instead of decorating it.
60+
lineage.register_tool_sources(
61+
"company_profile",
62+
[DataSourceRef("mcp://research-mcp", "company-profiles")],
63+
)
64+
65+
66+
def create_agent() -> Agent:
67+
"""Build and return the agent with the three ref-carrying tools."""
68+
return Agent(
69+
name="Lineage Research Assistant",
70+
model=MODEL_NAME,
71+
instructions=INSTRUCTIONS,
72+
tools=[search_filings, read_kpi, company_profile],
73+
)

0 commit comments

Comments
 (0)