tracely-ai 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tracely_ai-0.1.0/.gitignore +29 -0
- tracely_ai-0.1.0/LICENSE +21 -0
- tracely_ai-0.1.0/PKG-INFO +266 -0
- tracely_ai-0.1.0/README.md +176 -0
- tracely_ai-0.1.0/example.py +35 -0
- tracely_ai-0.1.0/examples/README.md +81 -0
- tracely_ai-0.1.0/examples/_fake_db.py +219 -0
- tracely_ai-0.1.0/examples/auto_agent.py +128 -0
- tracely_ai-0.1.0/examples/auto_anthropic.py +108 -0
- tracely_ai-0.1.0/examples/auto_bedrock.py +111 -0
- tracely_ai-0.1.0/examples/auto_claude_agent.py +105 -0
- tracely_ai-0.1.0/examples/auto_crewai.py +133 -0
- tracely_ai-0.1.0/examples/auto_gemini.py +124 -0
- tracely_ai-0.1.0/examples/auto_google_adk.py +103 -0
- tracely_ai-0.1.0/examples/auto_langchain.py +93 -0
- tracely_ai-0.1.0/examples/auto_langgraph.py +110 -0
- tracely_ai-0.1.0/examples/auto_litellm.py +101 -0
- tracely_ai-0.1.0/examples/auto_llama_index.py +85 -0
- tracely_ai-0.1.0/examples/auto_mistral.py +118 -0
- tracely_ai-0.1.0/examples/auto_openai.py +112 -0
- tracely_ai-0.1.0/examples/auto_openai_agents.py +89 -0
- tracely_ai-0.1.0/examples/auto_openrouter.py +102 -0
- tracely_ai-0.1.0/examples/dropin_anthropic.py +106 -0
- tracely_ai-0.1.0/examples/dropin_openai.py +101 -0
- tracely_ai-0.1.0/examples/manual_spans.py +116 -0
- tracely_ai-0.1.0/examples/seed_conversations.py +809 -0
- tracely_ai-0.1.0/examples/seed_handler.py +38 -0
- tracely_ai-0.1.0/examples/seed_multicall.py +77 -0
- tracely_ai-0.1.0/examples/seed_multiturn.py +121 -0
- tracely_ai-0.1.0/examples/seed_regression.py +168 -0
- tracely_ai-0.1.0/examples/weather_agent.py +80 -0
- tracely_ai-0.1.0/examples/weather_agent_cli.py +31 -0
- tracely_ai-0.1.0/pyproject.toml +67 -0
- tracely_ai-0.1.0/tests/test_auto_tracing.py +447 -0
- tracely_ai-0.1.0/tests/test_redaction.py +72 -0
- tracely_ai-0.1.0/tests/test_replay_autoinstrument.py +183 -0
- tracely_ai-0.1.0/tests/test_replay_bridge.py +152 -0
- tracely_ai-0.1.0/tests/test_replay_real_providers.py +175 -0
- tracely_ai-0.1.0/tracely_sdk/__init__.py +1289 -0
- tracely_ai-0.1.0/tracely_sdk/_wrap.py +77 -0
- tracely_ai-0.1.0/tracely_sdk/anthropic.py +97 -0
- tracely_ai-0.1.0/tracely_sdk/cli.py +473 -0
- tracely_ai-0.1.0/tracely_sdk/google.py +135 -0
- tracely_ai-0.1.0/tracely_sdk/mistral.py +107 -0
- tracely_ai-0.1.0/tracely_sdk/openai.py +87 -0
- tracely_ai-0.1.0/tracely_sdk/openrouter.py +42 -0
- tracely_ai-0.1.0/tracely_sdk/xai.py +45 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.venv/
|
|
6
|
+
.uv/
|
|
7
|
+
.pytest_cache/
|
|
8
|
+
.ruff_cache/
|
|
9
|
+
.mypy_cache/
|
|
10
|
+
|
|
11
|
+
# Node / Next
|
|
12
|
+
node_modules/
|
|
13
|
+
.next/
|
|
14
|
+
out/
|
|
15
|
+
.turbo/
|
|
16
|
+
|
|
17
|
+
# Env / secrets
|
|
18
|
+
.env
|
|
19
|
+
.env.local
|
|
20
|
+
*.local
|
|
21
|
+
|
|
22
|
+
# Data / infra
|
|
23
|
+
.data/
|
|
24
|
+
*.log
|
|
25
|
+
|
|
26
|
+
# OS / editor
|
|
27
|
+
.DS_Store
|
|
28
|
+
.idea/
|
|
29
|
+
.vscode/
|
tracely_ai-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Julien Wuthrich
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tracely-ai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Tracely Python SDK: automatic + manual agent tracing, exported to Tracely over OTLP.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Jwuthri/Tracely
|
|
6
|
+
Project-URL: Documentation, https://github.com/Jwuthri/Tracely/blob/master/sdk/README.md
|
|
7
|
+
Project-URL: Source, https://github.com/Jwuthri/Tracely
|
|
8
|
+
Project-URL: Issues, https://github.com/Jwuthri/Tracely/issues
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agents,evals,llm,observability,opentelemetry,tracing
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: System :: Monitoring
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.25
|
|
18
|
+
Requires-Dist: opentelemetry-sdk>=1.25
|
|
19
|
+
Provides-Extra: all
|
|
20
|
+
Requires-Dist: anthropic; extra == 'all'
|
|
21
|
+
Requires-Dist: claude-agent-sdk; extra == 'all'
|
|
22
|
+
Requires-Dist: crewai; extra == 'all'
|
|
23
|
+
Requires-Dist: google-adk; extra == 'all'
|
|
24
|
+
Requires-Dist: google-genai>=0.3; extra == 'all'
|
|
25
|
+
Requires-Dist: langchain-openrouter>=0.1.0; extra == 'all'
|
|
26
|
+
Requires-Dist: litellm>=1.40; extra == 'all'
|
|
27
|
+
Requires-Dist: llama-index; extra == 'all'
|
|
28
|
+
Requires-Dist: mistralai>=1.0; extra == 'all'
|
|
29
|
+
Requires-Dist: openai-agents; extra == 'all'
|
|
30
|
+
Requires-Dist: openinference-instrumentation-anthropic>=0.1.0; extra == 'all'
|
|
31
|
+
Requires-Dist: openinference-instrumentation-bedrock>=0.1.0; extra == 'all'
|
|
32
|
+
Requires-Dist: openinference-instrumentation-claude-agent-sdk>=0.1.0; extra == 'all'
|
|
33
|
+
Requires-Dist: openinference-instrumentation-crewai>=0.1.0; extra == 'all'
|
|
34
|
+
Requires-Dist: openinference-instrumentation-google-adk>=0.1.0; extra == 'all'
|
|
35
|
+
Requires-Dist: openinference-instrumentation-google-genai>=0.1.0; extra == 'all'
|
|
36
|
+
Requires-Dist: openinference-instrumentation-groq>=0.1.0; extra == 'all'
|
|
37
|
+
Requires-Dist: openinference-instrumentation-langchain>=0.1.0; extra == 'all'
|
|
38
|
+
Requires-Dist: openinference-instrumentation-llama-index>=0.1.0; extra == 'all'
|
|
39
|
+
Requires-Dist: openinference-instrumentation-mistralai>=0.1.0; extra == 'all'
|
|
40
|
+
Requires-Dist: openinference-instrumentation-openai-agents>=0.1.0; extra == 'all'
|
|
41
|
+
Requires-Dist: openinference-instrumentation-openai>=0.1.0; extra == 'all'
|
|
42
|
+
Provides-Extra: anthropic
|
|
43
|
+
Requires-Dist: anthropic; extra == 'anthropic'
|
|
44
|
+
Requires-Dist: openinference-instrumentation-anthropic>=0.1.0; extra == 'anthropic'
|
|
45
|
+
Provides-Extra: bedrock
|
|
46
|
+
Requires-Dist: openinference-instrumentation-bedrock>=0.1.0; extra == 'bedrock'
|
|
47
|
+
Provides-Extra: claude-agent-sdk
|
|
48
|
+
Requires-Dist: claude-agent-sdk; extra == 'claude-agent-sdk'
|
|
49
|
+
Requires-Dist: openinference-instrumentation-claude-agent-sdk>=0.1.0; extra == 'claude-agent-sdk'
|
|
50
|
+
Provides-Extra: crewai
|
|
51
|
+
Requires-Dist: crewai; extra == 'crewai'
|
|
52
|
+
Requires-Dist: openinference-instrumentation-crewai>=0.1.0; extra == 'crewai'
|
|
53
|
+
Provides-Extra: google
|
|
54
|
+
Requires-Dist: google-genai>=0.3; extra == 'google'
|
|
55
|
+
Requires-Dist: openinference-instrumentation-google-genai>=0.1.0; extra == 'google'
|
|
56
|
+
Provides-Extra: google-adk
|
|
57
|
+
Requires-Dist: google-adk; extra == 'google-adk'
|
|
58
|
+
Requires-Dist: openinference-instrumentation-google-adk>=0.1.0; extra == 'google-adk'
|
|
59
|
+
Provides-Extra: google-genai
|
|
60
|
+
Requires-Dist: google-genai>=0.3; extra == 'google-genai'
|
|
61
|
+
Provides-Extra: groq
|
|
62
|
+
Requires-Dist: openinference-instrumentation-groq>=0.1.0; extra == 'groq'
|
|
63
|
+
Provides-Extra: langchain
|
|
64
|
+
Requires-Dist: openinference-instrumentation-langchain>=0.1.0; extra == 'langchain'
|
|
65
|
+
Provides-Extra: litellm
|
|
66
|
+
Requires-Dist: litellm>=1.40; extra == 'litellm'
|
|
67
|
+
Provides-Extra: llama-index
|
|
68
|
+
Requires-Dist: llama-index; extra == 'llama-index'
|
|
69
|
+
Requires-Dist: openinference-instrumentation-llama-index>=0.1.0; extra == 'llama-index'
|
|
70
|
+
Provides-Extra: mistral
|
|
71
|
+
Requires-Dist: mistralai>=1.0; extra == 'mistral'
|
|
72
|
+
Requires-Dist: openinference-instrumentation-mistralai>=0.1.0; extra == 'mistral'
|
|
73
|
+
Provides-Extra: mistral-sdk
|
|
74
|
+
Requires-Dist: mistralai>=1.0; extra == 'mistral-sdk'
|
|
75
|
+
Provides-Extra: openai
|
|
76
|
+
Requires-Dist: openinference-instrumentation-openai>=0.1.0; extra == 'openai'
|
|
77
|
+
Provides-Extra: openai-agents
|
|
78
|
+
Requires-Dist: openai-agents; extra == 'openai-agents'
|
|
79
|
+
Requires-Dist: openinference-instrumentation-openai-agents>=0.1.0; extra == 'openai-agents'
|
|
80
|
+
Provides-Extra: openllmetry
|
|
81
|
+
Requires-Dist: opentelemetry-instrumentation-anthropic>=0.30b0; extra == 'openllmetry'
|
|
82
|
+
Requires-Dist: opentelemetry-instrumentation-openai-v2>=2.0b0; extra == 'openllmetry'
|
|
83
|
+
Provides-Extra: openrouter
|
|
84
|
+
Requires-Dist: langchain-openrouter>=0.1.0; extra == 'openrouter'
|
|
85
|
+
Provides-Extra: openrouter-openai
|
|
86
|
+
Requires-Dist: openai>=1.0; extra == 'openrouter-openai'
|
|
87
|
+
Provides-Extra: xai
|
|
88
|
+
Requires-Dist: openai>=1.0; extra == 'xai'
|
|
89
|
+
Description-Content-Type: text/markdown
|
|
90
|
+
|
|
91
|
+
# `tracely-ai` — instrument agents + the CI gate CLI
|
|
92
|
+
|
|
93
|
+
Two things in one small package:
|
|
94
|
+
|
|
95
|
+
1. an **instrumentation SDK** — `tracely.init()` activates the OpenAI/Anthropic/LangChain/LiteLLM auto-instrumentors so your existing code is traced with **zero span code** (the default path); a custom `SpanProcessor` stamps the active `tracely.trace()` run context onto every span — including the zero-touch provider spans. Manual context managers remain the escape hatch. Everything emits standard `gen_ai.*` / OpenInference attributes **plus** Tracely's first-class `tracely.*` hints, so the backend can populate its agent-semantic columns; and
|
|
96
|
+
2. the **`tracely` CLI** — `tracely gate` and `tracely replay`, which run an agent's promoted regression suite in CI and gate the PR (exit 0/1 + GitHub status/comment).
|
|
97
|
+
|
|
98
|
+
The core depends only on `opentelemetry-sdk` + the OTLP HTTP exporter (Python ≥ 3.10); a provider extra adds that provider's auto-instrumentor.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pip install "tracely-ai[openai]" # provider extras: [openai] [anthropic] [langchain] [litellm] [all]
|
|
102
|
+
# or: pip install tracely-ai # core only (manual API + CLI)
|
|
103
|
+
# import name is `tracely_sdk`; the CLI is `tracely` (entry point tracely_sdk.cli:main)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
> Already using OpenTelemetry / OpenInference / LangGraph instrumentation? You don't need the instrumentation half — point your existing OTLP exporter at `POST {endpoint}/v1/traces` with `Authorization: Bearer <ingest-key>` and set the `tracely.*` attributes below. This SDK is just the ergonomic path. The **CLI**, however, is how you wire Tracely into CI.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 1. Instrument an agent
|
|
111
|
+
|
|
112
|
+
### Automatic (the default — zero span code)
|
|
113
|
+
|
|
114
|
+
`init()` activates the auto-instrumentors; `trace()` attaches the run context; `@observe` adds
|
|
115
|
+
function-level spans. No manual span code.
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
import tracely_sdk as tracely
|
|
119
|
+
from openai import OpenAI
|
|
120
|
+
|
|
121
|
+
tracely.init(endpoint="http://localhost:8000", api_key="tracely_dev_key",
|
|
122
|
+
service_name="support-agent", env="prod", instrument="auto")
|
|
123
|
+
|
|
124
|
+
with tracely.trace(agent="support-agent", conversation="conv-1", user="u_42"):
|
|
125
|
+
OpenAI().chat.completions.create(model="gpt-4o", messages=[...]) # GENERATION span, captured
|
|
126
|
+
|
|
127
|
+
@tracely.observe(as_type="agent") # args→input, return→output, auto-nested
|
|
128
|
+
def plan(goal): ...
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`instrument` is `"auto"` (every importable provider SDK), an explicit list (`["openai",
|
|
132
|
+
"anthropic"]`), or `False`. The `tracely.trace()` hints flow onto **every** span inside it — including
|
|
133
|
+
the provider spans the instrumentor creates — via a custom `SpanProcessor`. Streaming token usage
|
|
134
|
+
needs `stream_options={"include_usage": True}`.
|
|
135
|
+
|
|
136
|
+
**Also covered:** LangChain/LangGraph (`[langchain]` — graphs nest, node names become steps), LiteLLM
|
|
137
|
+
(`instrument=["litellm"]` — 100+ providers via one callback), and a non-patching drop-in
|
|
138
|
+
(`from tracely_sdk.openai import OpenAI` / `wrap_openai`). Under `"auto"`, when the LangChain
|
|
139
|
+
instrumentor is present it owns LLM spans and the provider instrumentors are skipped to avoid
|
|
140
|
+
duplicate spans (override with an explicit list). Full guide: the docs [Automatic instrumentation](https://github.com/Jwuthri/Tracely/blob/master/docs/pages/automatic.mdx) page.
|
|
141
|
+
|
|
142
|
+
### Manual / custom spans (the escape hatch)
|
|
143
|
+
|
|
144
|
+
For anything the auto path doesn't cover. Each `with` block is a span; nesting builds the tree.
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
import tracely_sdk as tracely
|
|
148
|
+
|
|
149
|
+
tracely.init(endpoint="http://localhost:8000", api_key="tracely_dev_key",
|
|
150
|
+
service_name="support-agent", env="prod")
|
|
151
|
+
|
|
152
|
+
with tracely.agent("support-agent", version="v3", conversation="conv-1", turn=0) as a: # AGENT span = run root
|
|
153
|
+
tracely.set_io(a, input=user_msg, output=answer) # what the agent received / returned
|
|
154
|
+
with tracely.thinking(agent="support-agent") as th: # THINKING span (reasoning)
|
|
155
|
+
tracely.set_io(th, output=reasoning); tracely.set_usage(th, thinking_tokens=120)
|
|
156
|
+
with tracely.llm("gpt-4o", agent="support-agent") as g: # GENERATION span
|
|
157
|
+
tracely.set_io(g, input=messages, output=completion)
|
|
158
|
+
tracely.set_usage(g, input_tokens=812, output_tokens=96)
|
|
159
|
+
with tracely.tool("get_order", agent="support-agent") as t: # TOOL span
|
|
160
|
+
try:
|
|
161
|
+
result = get_order(order_id)
|
|
162
|
+
tracely.set_io(t, input={"order_id": order_id}, output=result)
|
|
163
|
+
except Exception as e:
|
|
164
|
+
tracely.error(t, str(e)) # level=ERROR → the failure signal
|
|
165
|
+
|
|
166
|
+
tracely.flush() # force-flush the exporter (call before the process exits)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Span context managers — one per observation type
|
|
170
|
+
| Call | Span type | Sets |
|
|
171
|
+
|---|---|---|
|
|
172
|
+
| `agent(slug, *, version, run_id, role, conversation, turn, user, trace_name, handoff_from, edge="delegate")` | `AGENT` (run root) | `tracely.agent.id`/`.version`/`.run_id`/`.role`, `tracely.conversation.id` + `session.id`, `tracely.turn.index`, `tracely.env`; `user`→`tracely.user.id`, `trace_name`→`tracely.trace.name`; `handoff_from`→ a handoff edge (`caller`→this agent, `edge.type`). |
|
|
173
|
+
| `llm(model, *, agent, temperature, top_p, max_tokens, frequency_penalty, presence_penalty, seed, tool_calls, metadata)` | `GENERATION` | `gen_ai.request.model` + the sampling params as `gen_ai.request.*`; `tool_calls`→`tracely.tool_calls` (tools the model **requested**); `metadata`→`tracely.metadata.*`. |
|
|
174
|
+
| `tool(name, *, agent)` | `TOOL` | `gen_ai.operation.name=execute_tool`, `gen_ai.tool.name`. |
|
|
175
|
+
| `thinking(name="thinking", *, agent, model)` | `THINKING` | reasoning emitted as its own span; optional `model`. |
|
|
176
|
+
| `retriever(name="retrieve", *, agent)` | `RETRIEVER` | a retrieval step — query in `set_io(input=)`, hits in `set_io(output=)`. |
|
|
177
|
+
| `embedding(model, *, agent)` | `EMBEDDING` | `gen_ai.request.model`; record tokens with `set_usage(input_tokens=)`. |
|
|
178
|
+
| `guardrail(name="guardrail", *, agent)` | `GUARDRAIL` | a safety/policy check — verdict in `set_io(output={"action": "allow"\|"block"})`. |
|
|
179
|
+
| `chain(name, *, agent)` | `CHAIN` | a grouping span (e.g. a RAG pipeline) — nest other spans inside it. |
|
|
180
|
+
| `turn(turn_id, *, index)` / `step(name, *, step_id)` | marker / generic | `tracely.turn.*` / `tracely.step.*`. |
|
|
181
|
+
|
|
182
|
+
### Annotating spans
|
|
183
|
+
- `set_io(span, *, input=None, output=None)` → `tracely.input` / `tracely.output` (objects are JSON-encoded; message content is a `{role, content:[blocks]}` object or a content-block list).
|
|
184
|
+
- `set_usage(span, *, input_tokens=None, output_tokens=None, thinking_tokens=None)` → `gen_ai.usage.input_tokens` / `output_tokens` / `reasoning_tokens`.
|
|
185
|
+
- `set_metadata(span, **kv)` → `tracely.metadata.<key>` — arbitrary tags (e.g. prompt version, tenant), surfaced in the span's Metadata and searchable.
|
|
186
|
+
- `error(span, message="")` → marks the span `StatusCode.ERROR` (→ `level=ERROR` in Tracely) — this is *the* failure-detection signal.
|
|
187
|
+
- `flush()` → force-flush the OTLP exporter.
|
|
188
|
+
|
|
189
|
+
### What Tracely reads
|
|
190
|
+
Standard `gen_ai.*` / OpenInference attributes, plus first-class hints that become **indexed columns** on the span row: `tracely.agent.id`/`.version`/`.role`, `tracely.user.id`, `tracely.trace.name`, `tracely.conversation.id`, `tracely.turn.id`/`.index`, `tracely.step.id`/`.name`, `tracely.observation.type`, `tracely.tool_calls`, `tracely.handoff.*` + `tracely.edge.type`, the `gen_ai.request.*` sampling params, and `tracely.env` (`prod|staging|ci|dev` — the gating axis). Agent slug + version are auto-registered into the Postgres registry on ingest.
|
|
191
|
+
|
|
192
|
+
### Declaring a conversation's agent catalog
|
|
193
|
+
Pass `agents=[{name, description, tools:[...]}]` to `tracely.trace(...)` (or call `set_agents([...])`) to declare the agents/tools a conversation uses — emitted once as `tracely.agents`. The backend stores this per conversation; the UI's **Conversation Agents** panel renders it (with per-tool run counts) and the LLM judge can read it as `@LIST_AGENT`. Without it, Tracely still derives the agent view from the spans.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## 2. Hermetic replay (`call_tool` / `call_llm` / `fixtures`)
|
|
198
|
+
|
|
199
|
+
These let the **same agent code** run live in production and deterministically offline in CI. Wrap each external call:
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
def run(user_input: str):
|
|
203
|
+
with tracely.agent("support-agent"):
|
|
204
|
+
# In prod: calls the real fn and records the output. In replay: serves the recorded output.
|
|
205
|
+
order = tracely.call_tool("get_order", lambda: get_order(order_id), args={"order_id": order_id})
|
|
206
|
+
answer = tracely.call_llm("gpt-4o", lambda: chat(messages), input=messages, usage=(812, 96))
|
|
207
|
+
return answer
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
- **Production (`--live` or no fixtures active):** `call_tool`/`call_llm` invoke your `fn`, record the output (and any error) on the span, and return it.
|
|
211
|
+
- **CI replay:** `tracely replay` activates the case's recorded **fixture bundle** via `with tracely.fixtures(bundle): ...`; `call_tool`/`call_llm` then **serve the recorded outputs in order** (or by `args` match) and never call your `fn`. A call that errored in production is reproduced on the span **and raised as `tracely.ToolError`**, so the agent's own `try/except` runs exactly as it would live — and the gate sees the same failure condition.
|
|
212
|
+
|
|
213
|
+
**Auto-instrument / drop-in code replays too** (no manual seam required): inside a `fixtures()` block Tracely class-patches the provider's create-method, so code that calls the SDK directly — `client.chat.completions.create(...)` under `instrument="auto"` or the `tracely_sdk.openai` drop-in — is served the recorded completion (reconstructed into a provider-shaped response) and never hits the network. Covered today: **OpenAI `chat.completions`** and **Anthropic `messages`**; other providers fall back to live in replay until added. The manual `call_llm` seam remains the way to get provider-agnostic hermetic replay everywhere.
|
|
214
|
+
|
|
215
|
+
This is what makes replay deterministic, offline, and free (no API keys, no cost). See [regression-testing design](https://github.com/Jwuthri/Tracely/blob/master/design/part2-tracely/05-regression-testing.md).
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## 3. The CI gate CLI
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
tracely gate <agent> [--env ci] [--api …] [--key …] [--pr N] [--sha …] [--github]
|
|
223
|
+
tracely replay <agent> (--entrypoint module:func | --cmd "…") [--live] [--github]
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
- **`tracely gate <agent>`** — gate a PR against **pre-emitted** `env=ci` traces (your CI already ran the agent and emitted traces); cases are matched to candidates by `input_digest`.
|
|
227
|
+
- **`tracely replay <agent>`** — re-run the agent **on each promoted case's recorded input** (fetched from `GET /api/gate/suite`), then gate. `--entrypoint module:func` calls a Python function per case; `--cmd "…"` runs a shell command per case (gets `TRACELY_INPUT`) that emits its own trace. Hermetic by default; `--live` makes real tool/LLM calls.
|
|
228
|
+
|
|
229
|
+
Both **exit 0 (PASS) / 1 (FAIL)** and, inside GitHub Actions (or with `--github`), post a **commit status + PR comment** with per-case results and soft warnings (latency/token deltas vs the last green gate). `--dry-run` prints the GitHub calls instead of sending; `--no-github` never touches GitHub. Config via flags or env (`TRACELY_API`, `TRACELY_KEY`, `TRACELY_AGENT`, `TRACELY_GATE_ENV`, `TRACELY_WEB_URL`, `GITHUB_TOKEN`). A reusable composite action lives at `.github/actions/tracely-gate/`.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Examples (`sdk/examples/` + `sdk/example.py`)
|
|
234
|
+
|
|
235
|
+
[`examples/README.md`](examples/README.md) is the full index — **one runnable file per way of
|
|
236
|
+
tracing**, all the same fake-DB **two-agent conversation** (a Support Agent that hands the pricing
|
|
237
|
+
turn off to a Billing Agent, declaring its catalog via `tracely.trace(agents=...)`): each frontier
|
|
238
|
+
provider (OpenAI, Anthropic, Gemini, Mistral, Bedrock) + OpenRouter, each harness (LangChain
|
|
239
|
+
`create_agent`, LangGraph, LiteLLM, LlamaIndex, CrewAI), each first-party agent SDK (OpenAI Agents,
|
|
240
|
+
Claude Agent SDK, Google ADK), and each approach (`@observe`+`trace`, the `wrap_openai`/`wrap_anthropic`
|
|
241
|
+
drop-ins, manual spans). Highlights:
|
|
242
|
+
|
|
243
|
+
| File | Shows |
|
|
244
|
+
|---|---|
|
|
245
|
+
| `../example.py` | the minimal demo trace (agent → llm → failing tool). `make sdk-example`. |
|
|
246
|
+
| `examples/auto_openai.py` · `auto_anthropic.py` · `auto_gemini.py` · … | **automatic** provider tracing — zero span code (one file per frontier provider + OpenRouter). |
|
|
247
|
+
| `examples/auto_langchain.py` (`create_agent`) · `auto_langgraph.py` · `auto_litellm.py` · … | **automatic** harness tracing (one file per framework, current APIs). |
|
|
248
|
+
| `examples/auto_openai_agents.py` · `auto_claude_agent.py` · `auto_google_adk.py` | **automatic** first-party agent-SDK tracing (OpenAI Agents / Claude Agent SDK / Google ADK). |
|
|
249
|
+
| `examples/auto_agent.py` | **automatic** `@observe` + `trace()` agent → thinking/gen/tool tree. `make auto-agent`. |
|
|
250
|
+
| `examples/dropin_openai.py` · `dropin_anthropic.py` | non-patching `wrap_openai` / `wrap_anthropic` drop-ins. |
|
|
251
|
+
| `examples/manual_spans.py` | the manual escape-hatch API as a full agent (no provider/key needed). |
|
|
252
|
+
| `examples/weather_agent.py` / `weather_agent_cli.py` | a real agent wired with `call_tool`/`call_llm` for `tracely replay --entrypoint` / `--cmd`. |
|
|
253
|
+
| `examples/seed_conversations.py` | rich demo data using **every** SDK helper — single/multi-turn, multi-agent + handoffs, RAG (guardrail→embed→retrieve→chain), thinking, multimodal, structured output, multi-model. `make seed-demo`. |
|
|
254
|
+
| `examples/seed_multiturn.py` | one multi-turn conversation (manual API, no key) — the showcase for the **rolling summary** + **declared agents** (`@LIST_AGENT`). |
|
|
255
|
+
| `examples/seed_regression.py` | promote a failing trace → run red→green CI gates (fills Cases + Gates). `make seed-regression`. |
|
|
256
|
+
| `examples/seed_multicall.py` / `seed_handler.py` | repeated-call + handler examples for fixture replay. |
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Key decisions (and why)
|
|
261
|
+
|
|
262
|
+
1. **Thin wrapper, not a framework.** It only sets attributes on OTel spans — anyone already on OpenTelemetry/OpenInference can skip it and just emit the `tracely.*` hints. No lock-in.
|
|
263
|
+
2. **First-class `tracely.*` hints.** Agent/conversation/turn/step/env are emitted as semantic attributes so they become indexed columns server-side — the basis for agent-level evals and PR gating.
|
|
264
|
+
3. **`THINKING` is a span type, not a field.** Reasoning is its own observation, so it renders distinctly and carries its own token usage.
|
|
265
|
+
4. **Record-replay in the SDK.** `call_tool`/`call_llm` are the seam that makes "the recorded run is the test" real: the same code path records in prod and replays in CI, reproducing recorded errors via `ToolError` so error-handling behaviour is gated faithfully.
|
|
266
|
+
5. **The CLI is the CI contract.** `gate`/`replay` exit 0/1 and speak GitHub — so wiring Tracely into a pipeline is one step, with the hard gate being fail-to-pass and everything else advisory.
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# `tracely-ai` — instrument agents + the CI gate CLI
|
|
2
|
+
|
|
3
|
+
Two things in one small package:
|
|
4
|
+
|
|
5
|
+
1. an **instrumentation SDK** — `tracely.init()` activates the OpenAI/Anthropic/LangChain/LiteLLM auto-instrumentors so your existing code is traced with **zero span code** (the default path); a custom `SpanProcessor` stamps the active `tracely.trace()` run context onto every span — including the zero-touch provider spans. Manual context managers remain the escape hatch. Everything emits standard `gen_ai.*` / OpenInference attributes **plus** Tracely's first-class `tracely.*` hints, so the backend can populate its agent-semantic columns; and
|
|
6
|
+
2. the **`tracely` CLI** — `tracely gate` and `tracely replay`, which run an agent's promoted regression suite in CI and gate the PR (exit 0/1 + GitHub status/comment).
|
|
7
|
+
|
|
8
|
+
The core depends only on `opentelemetry-sdk` + the OTLP HTTP exporter (Python ≥ 3.10); a provider extra adds that provider's auto-instrumentor.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install "tracely-ai[openai]" # provider extras: [openai] [anthropic] [langchain] [litellm] [all]
|
|
12
|
+
# or: pip install tracely-ai # core only (manual API + CLI)
|
|
13
|
+
# import name is `tracely_sdk`; the CLI is `tracely` (entry point tracely_sdk.cli:main)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
> Already using OpenTelemetry / OpenInference / LangGraph instrumentation? You don't need the instrumentation half — point your existing OTLP exporter at `POST {endpoint}/v1/traces` with `Authorization: Bearer <ingest-key>` and set the `tracely.*` attributes below. This SDK is just the ergonomic path. The **CLI**, however, is how you wire Tracely into CI.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 1. Instrument an agent
|
|
21
|
+
|
|
22
|
+
### Automatic (the default — zero span code)
|
|
23
|
+
|
|
24
|
+
`init()` activates the auto-instrumentors; `trace()` attaches the run context; `@observe` adds
|
|
25
|
+
function-level spans. No manual span code.
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import tracely_sdk as tracely
|
|
29
|
+
from openai import OpenAI
|
|
30
|
+
|
|
31
|
+
tracely.init(endpoint="http://localhost:8000", api_key="tracely_dev_key",
|
|
32
|
+
service_name="support-agent", env="prod", instrument="auto")
|
|
33
|
+
|
|
34
|
+
with tracely.trace(agent="support-agent", conversation="conv-1", user="u_42"):
|
|
35
|
+
OpenAI().chat.completions.create(model="gpt-4o", messages=[...]) # GENERATION span, captured
|
|
36
|
+
|
|
37
|
+
@tracely.observe(as_type="agent") # args→input, return→output, auto-nested
|
|
38
|
+
def plan(goal): ...
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`instrument` is `"auto"` (every importable provider SDK), an explicit list (`["openai",
|
|
42
|
+
"anthropic"]`), or `False`. The `tracely.trace()` hints flow onto **every** span inside it — including
|
|
43
|
+
the provider spans the instrumentor creates — via a custom `SpanProcessor`. Streaming token usage
|
|
44
|
+
needs `stream_options={"include_usage": True}`.
|
|
45
|
+
|
|
46
|
+
**Also covered:** LangChain/LangGraph (`[langchain]` — graphs nest, node names become steps), LiteLLM
|
|
47
|
+
(`instrument=["litellm"]` — 100+ providers via one callback), and a non-patching drop-in
|
|
48
|
+
(`from tracely_sdk.openai import OpenAI` / `wrap_openai`). Under `"auto"`, when the LangChain
|
|
49
|
+
instrumentor is present it owns LLM spans and the provider instrumentors are skipped to avoid
|
|
50
|
+
duplicate spans (override with an explicit list). Full guide: the docs [Automatic instrumentation](https://github.com/Jwuthri/Tracely/blob/master/docs/pages/automatic.mdx) page.
|
|
51
|
+
|
|
52
|
+
### Manual / custom spans (the escape hatch)
|
|
53
|
+
|
|
54
|
+
For anything the auto path doesn't cover. Each `with` block is a span; nesting builds the tree.
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import tracely_sdk as tracely
|
|
58
|
+
|
|
59
|
+
tracely.init(endpoint="http://localhost:8000", api_key="tracely_dev_key",
|
|
60
|
+
service_name="support-agent", env="prod")
|
|
61
|
+
|
|
62
|
+
with tracely.agent("support-agent", version="v3", conversation="conv-1", turn=0) as a: # AGENT span = run root
|
|
63
|
+
tracely.set_io(a, input=user_msg, output=answer) # what the agent received / returned
|
|
64
|
+
with tracely.thinking(agent="support-agent") as th: # THINKING span (reasoning)
|
|
65
|
+
tracely.set_io(th, output=reasoning); tracely.set_usage(th, thinking_tokens=120)
|
|
66
|
+
with tracely.llm("gpt-4o", agent="support-agent") as g: # GENERATION span
|
|
67
|
+
tracely.set_io(g, input=messages, output=completion)
|
|
68
|
+
tracely.set_usage(g, input_tokens=812, output_tokens=96)
|
|
69
|
+
with tracely.tool("get_order", agent="support-agent") as t: # TOOL span
|
|
70
|
+
try:
|
|
71
|
+
result = get_order(order_id)
|
|
72
|
+
tracely.set_io(t, input={"order_id": order_id}, output=result)
|
|
73
|
+
except Exception as e:
|
|
74
|
+
tracely.error(t, str(e)) # level=ERROR → the failure signal
|
|
75
|
+
|
|
76
|
+
tracely.flush() # force-flush the exporter (call before the process exits)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Span context managers — one per observation type
|
|
80
|
+
| Call | Span type | Sets |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| `agent(slug, *, version, run_id, role, conversation, turn, user, trace_name, handoff_from, edge="delegate")` | `AGENT` (run root) | `tracely.agent.id`/`.version`/`.run_id`/`.role`, `tracely.conversation.id` + `session.id`, `tracely.turn.index`, `tracely.env`; `user`→`tracely.user.id`, `trace_name`→`tracely.trace.name`; `handoff_from`→ a handoff edge (`caller`→this agent, `edge.type`). |
|
|
83
|
+
| `llm(model, *, agent, temperature, top_p, max_tokens, frequency_penalty, presence_penalty, seed, tool_calls, metadata)` | `GENERATION` | `gen_ai.request.model` + the sampling params as `gen_ai.request.*`; `tool_calls`→`tracely.tool_calls` (tools the model **requested**); `metadata`→`tracely.metadata.*`. |
|
|
84
|
+
| `tool(name, *, agent)` | `TOOL` | `gen_ai.operation.name=execute_tool`, `gen_ai.tool.name`. |
|
|
85
|
+
| `thinking(name="thinking", *, agent, model)` | `THINKING` | reasoning emitted as its own span; optional `model`. |
|
|
86
|
+
| `retriever(name="retrieve", *, agent)` | `RETRIEVER` | a retrieval step — query in `set_io(input=)`, hits in `set_io(output=)`. |
|
|
87
|
+
| `embedding(model, *, agent)` | `EMBEDDING` | `gen_ai.request.model`; record tokens with `set_usage(input_tokens=)`. |
|
|
88
|
+
| `guardrail(name="guardrail", *, agent)` | `GUARDRAIL` | a safety/policy check — verdict in `set_io(output={"action": "allow"\|"block"})`. |
|
|
89
|
+
| `chain(name, *, agent)` | `CHAIN` | a grouping span (e.g. a RAG pipeline) — nest other spans inside it. |
|
|
90
|
+
| `turn(turn_id, *, index)` / `step(name, *, step_id)` | marker / generic | `tracely.turn.*` / `tracely.step.*`. |
|
|
91
|
+
|
|
92
|
+
### Annotating spans
|
|
93
|
+
- `set_io(span, *, input=None, output=None)` → `tracely.input` / `tracely.output` (objects are JSON-encoded; message content is a `{role, content:[blocks]}` object or a content-block list).
|
|
94
|
+
- `set_usage(span, *, input_tokens=None, output_tokens=None, thinking_tokens=None)` → `gen_ai.usage.input_tokens` / `output_tokens` / `reasoning_tokens`.
|
|
95
|
+
- `set_metadata(span, **kv)` → `tracely.metadata.<key>` — arbitrary tags (e.g. prompt version, tenant), surfaced in the span's Metadata and searchable.
|
|
96
|
+
- `error(span, message="")` → marks the span `StatusCode.ERROR` (→ `level=ERROR` in Tracely) — this is *the* failure-detection signal.
|
|
97
|
+
- `flush()` → force-flush the OTLP exporter.
|
|
98
|
+
|
|
99
|
+
### What Tracely reads
|
|
100
|
+
Standard `gen_ai.*` / OpenInference attributes, plus first-class hints that become **indexed columns** on the span row: `tracely.agent.id`/`.version`/`.role`, `tracely.user.id`, `tracely.trace.name`, `tracely.conversation.id`, `tracely.turn.id`/`.index`, `tracely.step.id`/`.name`, `tracely.observation.type`, `tracely.tool_calls`, `tracely.handoff.*` + `tracely.edge.type`, the `gen_ai.request.*` sampling params, and `tracely.env` (`prod|staging|ci|dev` — the gating axis). Agent slug + version are auto-registered into the Postgres registry on ingest.
|
|
101
|
+
|
|
102
|
+
### Declaring a conversation's agent catalog
|
|
103
|
+
Pass `agents=[{name, description, tools:[...]}]` to `tracely.trace(...)` (or call `set_agents([...])`) to declare the agents/tools a conversation uses — emitted once as `tracely.agents`. The backend stores this per conversation; the UI's **Conversation Agents** panel renders it (with per-tool run counts) and the LLM judge can read it as `@LIST_AGENT`. Without it, Tracely still derives the agent view from the spans.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 2. Hermetic replay (`call_tool` / `call_llm` / `fixtures`)
|
|
108
|
+
|
|
109
|
+
These let the **same agent code** run live in production and deterministically offline in CI. Wrap each external call:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
def run(user_input: str):
|
|
113
|
+
with tracely.agent("support-agent"):
|
|
114
|
+
# In prod: calls the real fn and records the output. In replay: serves the recorded output.
|
|
115
|
+
order = tracely.call_tool("get_order", lambda: get_order(order_id), args={"order_id": order_id})
|
|
116
|
+
answer = tracely.call_llm("gpt-4o", lambda: chat(messages), input=messages, usage=(812, 96))
|
|
117
|
+
return answer
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
- **Production (`--live` or no fixtures active):** `call_tool`/`call_llm` invoke your `fn`, record the output (and any error) on the span, and return it.
|
|
121
|
+
- **CI replay:** `tracely replay` activates the case's recorded **fixture bundle** via `with tracely.fixtures(bundle): ...`; `call_tool`/`call_llm` then **serve the recorded outputs in order** (or by `args` match) and never call your `fn`. A call that errored in production is reproduced on the span **and raised as `tracely.ToolError`**, so the agent's own `try/except` runs exactly as it would live — and the gate sees the same failure condition.
|
|
122
|
+
|
|
123
|
+
**Auto-instrument / drop-in code replays too** (no manual seam required): inside a `fixtures()` block Tracely class-patches the provider's create-method, so code that calls the SDK directly — `client.chat.completions.create(...)` under `instrument="auto"` or the `tracely_sdk.openai` drop-in — is served the recorded completion (reconstructed into a provider-shaped response) and never hits the network. Covered today: **OpenAI `chat.completions`** and **Anthropic `messages`**; other providers fall back to live in replay until added. The manual `call_llm` seam remains the way to get provider-agnostic hermetic replay everywhere.
|
|
124
|
+
|
|
125
|
+
This is what makes replay deterministic, offline, and free (no API keys, no cost). See [regression-testing design](https://github.com/Jwuthri/Tracely/blob/master/design/part2-tracely/05-regression-testing.md).
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## 3. The CI gate CLI
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
tracely gate <agent> [--env ci] [--api …] [--key …] [--pr N] [--sha …] [--github]
|
|
133
|
+
tracely replay <agent> (--entrypoint module:func | --cmd "…") [--live] [--github]
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
- **`tracely gate <agent>`** — gate a PR against **pre-emitted** `env=ci` traces (your CI already ran the agent and emitted traces); cases are matched to candidates by `input_digest`.
|
|
137
|
+
- **`tracely replay <agent>`** — re-run the agent **on each promoted case's recorded input** (fetched from `GET /api/gate/suite`), then gate. `--entrypoint module:func` calls a Python function per case; `--cmd "…"` runs a shell command per case (gets `TRACELY_INPUT`) that emits its own trace. Hermetic by default; `--live` makes real tool/LLM calls.
|
|
138
|
+
|
|
139
|
+
Both **exit 0 (PASS) / 1 (FAIL)** and, inside GitHub Actions (or with `--github`), post a **commit status + PR comment** with per-case results and soft warnings (latency/token deltas vs the last green gate). `--dry-run` prints the GitHub calls instead of sending; `--no-github` never touches GitHub. Config via flags or env (`TRACELY_API`, `TRACELY_KEY`, `TRACELY_AGENT`, `TRACELY_GATE_ENV`, `TRACELY_WEB_URL`, `GITHUB_TOKEN`). A reusable composite action lives at `.github/actions/tracely-gate/`.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Examples (`sdk/examples/` + `sdk/example.py`)
|
|
144
|
+
|
|
145
|
+
[`examples/README.md`](examples/README.md) is the full index — **one runnable file per way of
|
|
146
|
+
tracing**, all the same fake-DB **two-agent conversation** (a Support Agent that hands the pricing
|
|
147
|
+
turn off to a Billing Agent, declaring its catalog via `tracely.trace(agents=...)`): each frontier
|
|
148
|
+
provider (OpenAI, Anthropic, Gemini, Mistral, Bedrock) + OpenRouter, each harness (LangChain
|
|
149
|
+
`create_agent`, LangGraph, LiteLLM, LlamaIndex, CrewAI), each first-party agent SDK (OpenAI Agents,
|
|
150
|
+
Claude Agent SDK, Google ADK), and each approach (`@observe`+`trace`, the `wrap_openai`/`wrap_anthropic`
|
|
151
|
+
drop-ins, manual spans). Highlights:
|
|
152
|
+
|
|
153
|
+
| File | Shows |
|
|
154
|
+
|---|---|
|
|
155
|
+
| `../example.py` | the minimal demo trace (agent → llm → failing tool). `make sdk-example`. |
|
|
156
|
+
| `examples/auto_openai.py` · `auto_anthropic.py` · `auto_gemini.py` · … | **automatic** provider tracing — zero span code (one file per frontier provider + OpenRouter). |
|
|
157
|
+
| `examples/auto_langchain.py` (`create_agent`) · `auto_langgraph.py` · `auto_litellm.py` · … | **automatic** harness tracing (one file per framework, current APIs). |
|
|
158
|
+
| `examples/auto_openai_agents.py` · `auto_claude_agent.py` · `auto_google_adk.py` | **automatic** first-party agent-SDK tracing (OpenAI Agents / Claude Agent SDK / Google ADK). |
|
|
159
|
+
| `examples/auto_agent.py` | **automatic** `@observe` + `trace()` agent → thinking/gen/tool tree. `make auto-agent`. |
|
|
160
|
+
| `examples/dropin_openai.py` · `dropin_anthropic.py` | non-patching `wrap_openai` / `wrap_anthropic` drop-ins. |
|
|
161
|
+
| `examples/manual_spans.py` | the manual escape-hatch API as a full agent (no provider/key needed). |
|
|
162
|
+
| `examples/weather_agent.py` / `weather_agent_cli.py` | a real agent wired with `call_tool`/`call_llm` for `tracely replay --entrypoint` / `--cmd`. |
|
|
163
|
+
| `examples/seed_conversations.py` | rich demo data using **every** SDK helper — single/multi-turn, multi-agent + handoffs, RAG (guardrail→embed→retrieve→chain), thinking, multimodal, structured output, multi-model. `make seed-demo`. |
|
|
164
|
+
| `examples/seed_multiturn.py` | one multi-turn conversation (manual API, no key) — the showcase for the **rolling summary** + **declared agents** (`@LIST_AGENT`). |
|
|
165
|
+
| `examples/seed_regression.py` | promote a failing trace → run red→green CI gates (fills Cases + Gates). `make seed-regression`. |
|
|
166
|
+
| `examples/seed_multicall.py` / `seed_handler.py` | repeated-call + handler examples for fixture replay. |
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Key decisions (and why)
|
|
171
|
+
|
|
172
|
+
1. **Thin wrapper, not a framework.** It only sets attributes on OTel spans — anyone already on OpenTelemetry/OpenInference can skip it and just emit the `tracely.*` hints. No lock-in.
|
|
173
|
+
2. **First-class `tracely.*` hints.** Agent/conversation/turn/step/env are emitted as semantic attributes so they become indexed columns server-side — the basis for agent-level evals and PR gating.
|
|
174
|
+
3. **`THINKING` is a span type, not a field.** Reasoning is its own observation, so it renders distinctly and carries its own token usage.
|
|
175
|
+
4. **Record-replay in the SDK.** `call_tool`/`call_llm` are the seam that makes "the recorded run is the test" real: the same code path records in prod and replays in CI, reproducing recorded errors via `ToolError` so error-handling behaviour is gated faithfully.
|
|
176
|
+
5. **The CLI is the CI contract.** `gate`/`replay` exit 0/1 and speak GitHub — so wiring Tracely into a pipeline is one step, with the hard gate being fail-to-pass and everything else advisory.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Emit the demo agent trace via the Tracely SDK.
|
|
2
|
+
|
|
3
|
+
uv run python sdk/example.py (API + worker must be running)
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import tracely_sdk as tracely
|
|
7
|
+
|
|
8
|
+
tracely.init(endpoint="http://localhost:8000", api_key="tracely_dev_key", service_name="demo-agent")
|
|
9
|
+
|
|
10
|
+
with tracely.agent("planner", version="v1"):
|
|
11
|
+
with tracely.llm("gpt-4o") as g:
|
|
12
|
+
tracely.set_io(
|
|
13
|
+
g,
|
|
14
|
+
input=[
|
|
15
|
+
{"role": "system", "content": "You are a helpful weather assistant."},
|
|
16
|
+
{"role": "user", "content": "weather in SF?"},
|
|
17
|
+
],
|
|
18
|
+
output={
|
|
19
|
+
"role": "assistant",
|
|
20
|
+
"content": None,
|
|
21
|
+
"finish_reason": "tool_calls",
|
|
22
|
+
"tool_calls": [{
|
|
23
|
+
"id": "call_1",
|
|
24
|
+
"type": "function",
|
|
25
|
+
"function": {"name": "get_weather", "arguments": '{"city": "SF"}'},
|
|
26
|
+
}],
|
|
27
|
+
},
|
|
28
|
+
)
|
|
29
|
+
tracely.set_usage(g, input_tokens=812, output_tokens=96)
|
|
30
|
+
with tracely.tool("get_weather") as t:
|
|
31
|
+
tracely.set_io(t, input={"city": "SF"})
|
|
32
|
+
tracely.error(t, "upstream timeout") # demo failure signal (level=ERROR)
|
|
33
|
+
|
|
34
|
+
tracely.flush()
|
|
35
|
+
print("sent demo trace via SDK")
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# `sdk/examples/` — one runnable example per way of tracing
|
|
2
|
+
|
|
3
|
+
Every example is the **same realistic two-agent conversation**: a **Support Agent** answers
|
|
4
|
+
order/inventory questions over several turns by calling tools against a fake e-commerce DB
|
|
5
|
+
([`_fake_db.py`](_fake_db.py)) — `get_order_status` + `check_inventory` — then **hands off** the final
|
|
6
|
+
pricing-comparison turn to a **Billing Agent** (`compare_prices`). So each file shows a real
|
|
7
|
+
multi-turn, multi-agent tool-calling loop (plus thinking, in the `@observe`/manual ones), not a toy
|
|
8
|
+
single call. Each guards on its instrumentor + API key, printing setup hints instead of crashing when
|
|
9
|
+
a dependency or key is missing.
|
|
10
|
+
|
|
11
|
+
Each run also declares its two-agent catalog once via `tracely.trace(agents=AGENTS)` (so the
|
|
12
|
+
Conversation Agents panel + the judge's `@LIST_AGENT` see it) and tags itself with its own filename
|
|
13
|
+
via `example=os.path.basename(__file__)`, so each span carries `tracely.metadata.example = <file>.py`
|
|
14
|
+
— filter on it in the Tracely UI to find the traces a given example produced.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install "tracely-ai[<extra>]" # the extra named in each file's header
|
|
18
|
+
export TRACELY_API=http://localhost:8000 # your Tracely API (default shown)
|
|
19
|
+
export OPENAI_API_KEY=sk-... # (or the provider's key)
|
|
20
|
+
uv run python sdk/examples/<file>.py
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Ways of sending traces (the SDK layers)
|
|
24
|
+
|
|
25
|
+
| File | Layer | Shows |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| [`auto_agent.py`](auto_agent.py) | L2 + L3 | `@observe` agent + `trace()` → **AGENT → thinking · 2 generations · 2 tools** tree |
|
|
28
|
+
| [`dropin_openai.py`](dropin_openai.py) | R13 | non-patching `wrap_openai` — the tool-calling agent, nothing patched globally |
|
|
29
|
+
| [`dropin_anthropic.py`](dropin_anthropic.py) | R13 | non-patching `wrap_anthropic` — the Claude tool-calling agent |
|
|
30
|
+
| [`manual_spans.py`](manual_spans.py) | L4 | the manual escape hatch — full agent: thinking → llm → tools (one **errors**) → answer (no key needed) |
|
|
31
|
+
|
|
32
|
+
## Frontier providers (L1 — auto-instrumented, zero span code)
|
|
33
|
+
|
|
34
|
+
| File | `instrument=` | Extra | Key |
|
|
35
|
+
|---|---|---|---|
|
|
36
|
+
| [`auto_openai.py`](auto_openai.py) | `["openai"]` | `[openai]` | `OPENAI_API_KEY` |
|
|
37
|
+
| [`auto_anthropic.py`](auto_anthropic.py) | `["anthropic"]` | `[anthropic]` | `ANTHROPIC_API_KEY` |
|
|
38
|
+
| [`auto_gemini.py`](auto_gemini.py) | `["gemini"]` | `[google]` | `GEMINI_API_KEY` |
|
|
39
|
+
| [`auto_mistral.py`](auto_mistral.py) | `["mistral"]` | `[mistral]` | `MISTRAL_API_KEY` |
|
|
40
|
+
| [`auto_bedrock.py`](auto_bedrock.py) | `["bedrock"]` | `[bedrock]` | AWS creds + `AWS_REGION` |
|
|
41
|
+
| [`auto_openrouter.py`](auto_openrouter.py) | `["langchain"]` | `[langchain,openrouter]` | `OPENROUTER_API_KEY` |
|
|
42
|
+
|
|
43
|
+
> **OpenRouter** routes one API to 100+ models. `auto_openrouter.py` uses LangChain's first-party
|
|
44
|
+
> `ChatOpenRouter` (`langchain-openrouter`) inside `create_agent`, traced by the LangChain
|
|
45
|
+
> instrumentor. (OpenRouter is also OpenAI-wire-compatible, so pointing the OpenAI SDK at its
|
|
46
|
+
> `base_url` works too — traced by the OpenAI instrumentor.)
|
|
47
|
+
|
|
48
|
+
## Harnesses (L1 — orchestration frameworks)
|
|
49
|
+
|
|
50
|
+
| File | `instrument=` | Extra | Agent pattern (current API) |
|
|
51
|
+
|---|---|---|---|
|
|
52
|
+
| [`auto_langchain.py`](auto_langchain.py) | `["langchain"]` | `[langchain]` | `langchain.agents.create_agent` (LangChain 1.0+; replaces `AgentExecutor`/`create_react_agent`) |
|
|
53
|
+
| [`auto_langgraph.py`](auto_langgraph.py) | `["langchain"]` | `[langchain]` | a custom `StateGraph` + `ToolNode` + `tools_condition` (hand-built ReAct loop) |
|
|
54
|
+
| [`auto_litellm.py`](auto_litellm.py) | `["litellm"]` | `[litellm]` | OpenAI-shaped tool-calling loop via one callback |
|
|
55
|
+
| [`auto_llama_index.py`](auto_llama_index.py) | `["llama-index"]` | `[llama-index]` | `ReActAgent` over `FunctionTool`s |
|
|
56
|
+
| [`auto_crewai.py`](auto_crewai.py) | `["crewai"]` | `[crewai]` | a `Crew` whose agent is equipped with the tools |
|
|
57
|
+
|
|
58
|
+
> `instrument="auto"` activates whichever of these are importable; when a harness instrumentor (e.g.
|
|
59
|
+
> LangChain) is present it owns the LLM spans and the provider instrumentors are skipped to avoid
|
|
60
|
+
> duplicate spans (override with an explicit list). See the docs [Automatic instrumentation](../../docs/pages/automatic.mdx) page.
|
|
61
|
+
|
|
62
|
+
## Agent frameworks — first-party SDKs (L1)
|
|
63
|
+
|
|
64
|
+
The big labs now ship their own agent harnesses; each has an OpenInference instrumentor that
|
|
65
|
+
`init(instrument=[...])` activates, emitting AGENT/TOOL/LLM spans to Tracely.
|
|
66
|
+
|
|
67
|
+
| File | `instrument=` | Extra (+ SDK) | Framework |
|
|
68
|
+
|---|---|---|---|
|
|
69
|
+
| [`auto_openai_agents.py`](auto_openai_agents.py) | `["openai-agents"]` | `[openai-agents]` + `openai-agents` | OpenAI Agents SDK (`agents`: `Agent`/`Runner`/`@function_tool`) |
|
|
70
|
+
| [`auto_claude_agent.py`](auto_claude_agent.py) | `["claude-agent-sdk"]` | `[claude-agent-sdk]` + `claude-agent-sdk` | Anthropic Claude Agent SDK (`@tool`/`create_sdk_mcp_server`/`ClaudeSDKClient`; needs the Claude Code CLI) |
|
|
71
|
+
| [`auto_google_adk.py`](auto_google_adk.py) | `["google-adk"]` | `[google-adk]` + `google-adk` | Google ADK (`google.adk.agents.Agent` + `InMemoryRunner`; instrument **before** importing `google.adk`) |
|
|
72
|
+
|
|
73
|
+
## Demo data & CI-gate examples (not tracing how-tos)
|
|
74
|
+
|
|
75
|
+
| File | Shows |
|
|
76
|
+
|---|---|
|
|
77
|
+
| [`seed_conversations.py`](seed_conversations.py) | rich manual-API demo data — every observation type (`make seed-demo`) |
|
|
78
|
+
| [`seed_multiturn.py`](seed_multiturn.py) | one multi-turn conversation via the manual API (no key) — the showcase for the **rolling summary** + **declared agents** (`tracely.trace(agents=...)`) |
|
|
79
|
+
| [`seed_regression.py`](seed_regression.py) | promote a failing trace → red→green CI gates (`make seed-regression`) |
|
|
80
|
+
| [`seed_multicall.py`](seed_multicall.py) / [`seed_handler.py`](seed_handler.py) | repeated-call + handler fixtures for hermetic replay |
|
|
81
|
+
| [`weather_agent.py`](weather_agent.py) / [`weather_agent_cli.py`](weather_agent_cli.py) | a real agent wired for `tracely replay --entrypoint` / `--cmd` |
|