spanforge 2.0.0__py3-none-any.whl
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.
- spanforge/__init__.py +695 -0
- spanforge/_batch_exporter.py +322 -0
- spanforge/_cli.py +3081 -0
- spanforge/_hooks.py +340 -0
- spanforge/_server.py +953 -0
- spanforge/_span.py +1015 -0
- spanforge/_store.py +287 -0
- spanforge/_stream.py +654 -0
- spanforge/_trace.py +334 -0
- spanforge/_tracer.py +253 -0
- spanforge/actor.py +141 -0
- spanforge/alerts.py +464 -0
- spanforge/auto.py +181 -0
- spanforge/baseline.py +336 -0
- spanforge/config.py +460 -0
- spanforge/consent.py +227 -0
- spanforge/consumer.py +379 -0
- spanforge/core/__init__.py +5 -0
- spanforge/core/compliance_mapping.py +1060 -0
- spanforge/cost.py +597 -0
- spanforge/debug.py +514 -0
- spanforge/drift.py +488 -0
- spanforge/egress.py +63 -0
- spanforge/eval.py +575 -0
- spanforge/event.py +1052 -0
- spanforge/exceptions.py +246 -0
- spanforge/explain.py +181 -0
- spanforge/export/__init__.py +50 -0
- spanforge/export/append_only.py +342 -0
- spanforge/export/cloud.py +349 -0
- spanforge/export/datadog.py +495 -0
- spanforge/export/grafana.py +331 -0
- spanforge/export/jsonl.py +198 -0
- spanforge/export/otel_bridge.py +291 -0
- spanforge/export/otlp.py +817 -0
- spanforge/export/otlp_bridge.py +231 -0
- spanforge/export/redis_backend.py +282 -0
- spanforge/export/webhook.py +302 -0
- spanforge/exporters/__init__.py +29 -0
- spanforge/exporters/console.py +271 -0
- spanforge/exporters/jsonl.py +144 -0
- spanforge/hitl.py +297 -0
- spanforge/inspect.py +429 -0
- spanforge/integrations/__init__.py +39 -0
- spanforge/integrations/_pricing.py +277 -0
- spanforge/integrations/anthropic.py +388 -0
- spanforge/integrations/bedrock.py +306 -0
- spanforge/integrations/crewai.py +251 -0
- spanforge/integrations/gemini.py +349 -0
- spanforge/integrations/groq.py +444 -0
- spanforge/integrations/langchain.py +349 -0
- spanforge/integrations/llamaindex.py +370 -0
- spanforge/integrations/ollama.py +286 -0
- spanforge/integrations/openai.py +370 -0
- spanforge/integrations/together.py +485 -0
- spanforge/metrics.py +393 -0
- spanforge/metrics_export.py +342 -0
- spanforge/migrate.py +278 -0
- spanforge/model_registry.py +282 -0
- spanforge/models.py +407 -0
- spanforge/namespaces/__init__.py +215 -0
- spanforge/namespaces/audit.py +253 -0
- spanforge/namespaces/cache.py +209 -0
- spanforge/namespaces/chain.py +74 -0
- spanforge/namespaces/confidence.py +69 -0
- spanforge/namespaces/consent.py +85 -0
- spanforge/namespaces/cost.py +175 -0
- spanforge/namespaces/decision.py +135 -0
- spanforge/namespaces/diff.py +146 -0
- spanforge/namespaces/drift.py +79 -0
- spanforge/namespaces/eval_.py +232 -0
- spanforge/namespaces/fence.py +180 -0
- spanforge/namespaces/guard.py +104 -0
- spanforge/namespaces/hitl.py +92 -0
- spanforge/namespaces/latency.py +69 -0
- spanforge/namespaces/prompt.py +185 -0
- spanforge/namespaces/redact.py +172 -0
- spanforge/namespaces/template.py +197 -0
- spanforge/namespaces/tool_call.py +76 -0
- spanforge/namespaces/trace.py +1006 -0
- spanforge/normalizer.py +183 -0
- spanforge/presidio_backend.py +149 -0
- spanforge/processor.py +258 -0
- spanforge/prompt_registry.py +415 -0
- spanforge/py.typed +0 -0
- spanforge/redact.py +780 -0
- spanforge/sampling.py +500 -0
- spanforge/schemas/v1.0/schema.json +170 -0
- spanforge/schemas/v2.0/schema.json +536 -0
- spanforge/signing.py +1152 -0
- spanforge/stream.py +559 -0
- spanforge/testing.py +376 -0
- spanforge/trace.py +199 -0
- spanforge/types.py +696 -0
- spanforge/ulid.py +304 -0
- spanforge/validate.py +383 -0
- spanforge-2.0.0.dist-info/METADATA +1777 -0
- spanforge-2.0.0.dist-info/RECORD +101 -0
- spanforge-2.0.0.dist-info/WHEEL +4 -0
- spanforge-2.0.0.dist-info/entry_points.txt +5 -0
- spanforge-2.0.0.dist-info/licenses/LICENSE +21 -0
spanforge/_trace.py
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""spanforge._trace — :class:`Trace` object and :func:`start_trace` entry point.
|
|
2
|
+
|
|
3
|
+
A :class:`Trace` wraps a *root* :class:`~spanforge._span.AgentRunContextManager`
|
|
4
|
+
and gives callers a convenient imperative handle to the entire agent execution::
|
|
5
|
+
|
|
6
|
+
trace = spanforge.start_trace("research_agent")
|
|
7
|
+
|
|
8
|
+
with trace.llm_call(model="gpt-4o") as s:
|
|
9
|
+
s.set_attribute("query", "latest AI papers")
|
|
10
|
+
|
|
11
|
+
with trace.tool_call("search") as s:
|
|
12
|
+
s.set_attribute("results_count", 5)
|
|
13
|
+
|
|
14
|
+
trace.end()
|
|
15
|
+
|
|
16
|
+
The :class:`Trace` object also provides convenience methods for serialisation
|
|
17
|
+
(:meth:`to_json`, :meth:`save`) that will become richer in later phases when
|
|
18
|
+
debug utilities (:meth:`print_tree`, :meth:`summary`) are added.
|
|
19
|
+
|
|
20
|
+
Design notes
|
|
21
|
+
------------
|
|
22
|
+
* :func:`start_trace` opens the underlying :class:`AgentRunContextManager`
|
|
23
|
+
immediately (``__enter__``), so all subsequently created spans automatically
|
|
24
|
+
inherit the trace's ``trace_id``.
|
|
25
|
+
* :meth:`Trace.end` closes that context manager (``__exit__``). Callers
|
|
26
|
+
*must* call :meth:`end` or use the :class:`Trace` as a context manager::
|
|
27
|
+
|
|
28
|
+
with spanforge.start_trace("my-agent") as trace:
|
|
29
|
+
...
|
|
30
|
+
# trace.end() called automatically on exit
|
|
31
|
+
|
|
32
|
+
* :class:`Trace` collects emitted :class:`~spanforge._span.Span` instances via
|
|
33
|
+
:meth:`_record_span` which is called by the stream module when a
|
|
34
|
+
``_TRACE_COLLECTOR`` attribute is present on the active
|
|
35
|
+
:class:`~spanforge._span.AgentRunContext`. This allows :meth:`to_json` and
|
|
36
|
+
:meth:`save` to operate on in-memory data without re-reading from a file.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import json
|
|
42
|
+
import time
|
|
43
|
+
from dataclasses import dataclass, field
|
|
44
|
+
from typing import TYPE_CHECKING, Any
|
|
45
|
+
|
|
46
|
+
from spanforge._span import (
|
|
47
|
+
AgentRunContext,
|
|
48
|
+
AgentRunContextManager,
|
|
49
|
+
Span,
|
|
50
|
+
SpanContextManager,
|
|
51
|
+
_span_id,
|
|
52
|
+
_trace_id,
|
|
53
|
+
_now_ns,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if TYPE_CHECKING:
|
|
57
|
+
from types import TracebackType
|
|
58
|
+
|
|
59
|
+
__all__ = ["Trace", "start_trace"]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class Trace:
|
|
64
|
+
"""A handle to a complete agent trace.
|
|
65
|
+
|
|
66
|
+
Created by :func:`start_trace`; do not construct directly.
|
|
67
|
+
|
|
68
|
+
Attributes:
|
|
69
|
+
agent_name: Name of the agent being traced.
|
|
70
|
+
trace_id: 32-char hex OTel-compatible trace ID.
|
|
71
|
+
start_time: Unix timestamp (float seconds) when the trace started.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
agent_name: str
|
|
75
|
+
trace_id: str
|
|
76
|
+
start_time: float
|
|
77
|
+
_run_ctx: AgentRunContext = field(repr=False)
|
|
78
|
+
_run_cm: AgentRunContextManager = field(repr=False)
|
|
79
|
+
attributes: dict[str, Any] = field(default_factory=dict)
|
|
80
|
+
_spans: list[Span] = field(default_factory=list, repr=False)
|
|
81
|
+
_ended: bool = field(default=False, init=False, repr=False)
|
|
82
|
+
|
|
83
|
+
# ------------------------------------------------------------------
|
|
84
|
+
# Span convenience methods
|
|
85
|
+
# ------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
def llm_call(
|
|
88
|
+
self,
|
|
89
|
+
model: str | None = None,
|
|
90
|
+
*,
|
|
91
|
+
operation: str = "chat",
|
|
92
|
+
temperature: float | None = None,
|
|
93
|
+
top_p: float | None = None,
|
|
94
|
+
max_tokens: int | None = None,
|
|
95
|
+
attributes: dict[str, Any] | None = None,
|
|
96
|
+
) -> SpanContextManager:
|
|
97
|
+
"""Open a child span for an LLM call within this trace.
|
|
98
|
+
|
|
99
|
+
Example::
|
|
100
|
+
|
|
101
|
+
with trace.llm_call(model="gpt-4o", temperature=0.7) as s:
|
|
102
|
+
s.set_attribute("prompt_tokens", 512)
|
|
103
|
+
"""
|
|
104
|
+
name = f"llm_call:{model}" if model else "llm_call"
|
|
105
|
+
return SpanContextManager(
|
|
106
|
+
name=name,
|
|
107
|
+
model=model,
|
|
108
|
+
operation=operation,
|
|
109
|
+
temperature=temperature,
|
|
110
|
+
top_p=top_p,
|
|
111
|
+
max_tokens=max_tokens,
|
|
112
|
+
attributes=attributes,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def tool_call(
|
|
116
|
+
self,
|
|
117
|
+
tool_name: str,
|
|
118
|
+
*,
|
|
119
|
+
attributes: dict[str, Any] | None = None,
|
|
120
|
+
) -> SpanContextManager:
|
|
121
|
+
"""Open a child span for a tool call within this trace.
|
|
122
|
+
|
|
123
|
+
Example::
|
|
124
|
+
|
|
125
|
+
with trace.tool_call("search") as s:
|
|
126
|
+
results = search(query)
|
|
127
|
+
"""
|
|
128
|
+
return SpanContextManager(
|
|
129
|
+
name=f"tool_call:{tool_name}",
|
|
130
|
+
operation="tool",
|
|
131
|
+
attributes=attributes,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def span(
|
|
135
|
+
self,
|
|
136
|
+
name: str,
|
|
137
|
+
*,
|
|
138
|
+
model: str | None = None,
|
|
139
|
+
operation: str = "chat",
|
|
140
|
+
temperature: float | None = None,
|
|
141
|
+
top_p: float | None = None,
|
|
142
|
+
max_tokens: int | None = None,
|
|
143
|
+
attributes: dict[str, Any] | None = None,
|
|
144
|
+
) -> SpanContextManager:
|
|
145
|
+
"""Open a generic child span within this trace."""
|
|
146
|
+
return SpanContextManager(
|
|
147
|
+
name=name,
|
|
148
|
+
model=model,
|
|
149
|
+
operation=operation,
|
|
150
|
+
temperature=temperature,
|
|
151
|
+
top_p=top_p,
|
|
152
|
+
max_tokens=max_tokens,
|
|
153
|
+
attributes=attributes,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# ------------------------------------------------------------------
|
|
157
|
+
# Internal span collection
|
|
158
|
+
# ------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
def _record_span(self, span: Span) -> None:
|
|
161
|
+
"""Called by _stream.emit_span when this trace is the active run context."""
|
|
162
|
+
self._spans.append(span)
|
|
163
|
+
|
|
164
|
+
# ------------------------------------------------------------------
|
|
165
|
+
# Lifecycle
|
|
166
|
+
# ------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
def end(self) -> None:
|
|
169
|
+
"""Close the trace and emit the agent-run completion event.
|
|
170
|
+
|
|
171
|
+
Idempotent — subsequent calls are no-ops.
|
|
172
|
+
"""
|
|
173
|
+
if self._ended:
|
|
174
|
+
return
|
|
175
|
+
self._ended = True
|
|
176
|
+
self._run_cm.__exit__(None, None, None)
|
|
177
|
+
|
|
178
|
+
# ------------------------------------------------------------------
|
|
179
|
+
# Context manager protocol (allows ``with start_trace(...) as trace:``)
|
|
180
|
+
# ------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
def __enter__(self) -> "Trace":
|
|
183
|
+
return self
|
|
184
|
+
|
|
185
|
+
def __exit__(
|
|
186
|
+
self,
|
|
187
|
+
exc_type: type[BaseException] | None,
|
|
188
|
+
exc_val: BaseException | None,
|
|
189
|
+
exc_tb: TracebackType | None,
|
|
190
|
+
) -> bool:
|
|
191
|
+
if not self._ended:
|
|
192
|
+
self._run_cm.__exit__(exc_type, exc_val, exc_tb)
|
|
193
|
+
self._ended = True
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
async def __aenter__(self) -> "Trace":
|
|
197
|
+
return self
|
|
198
|
+
|
|
199
|
+
async def __aexit__(
|
|
200
|
+
self,
|
|
201
|
+
exc_type: type[BaseException] | None,
|
|
202
|
+
exc_val: BaseException | None,
|
|
203
|
+
exc_tb: TracebackType | None,
|
|
204
|
+
) -> bool:
|
|
205
|
+
return self.__exit__(exc_type, exc_val, exc_tb)
|
|
206
|
+
|
|
207
|
+
# ------------------------------------------------------------------
|
|
208
|
+
# Serialisation
|
|
209
|
+
# ------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
def to_json(self, *, indent: int | None = None) -> str:
|
|
212
|
+
"""Serialise the trace's collected spans to a JSON string.
|
|
213
|
+
|
|
214
|
+
Returns a JSON object with ``trace_id``, ``agent_name``, ``start_time``,
|
|
215
|
+
and a ``spans`` array of span payload dicts.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
indent: Optional JSON indentation level (``None`` = compact).
|
|
219
|
+
"""
|
|
220
|
+
return json.dumps(self._to_dict(), indent=indent, sort_keys=True, default=str)
|
|
221
|
+
|
|
222
|
+
def save(self, path: str) -> None:
|
|
223
|
+
"""Write the trace as NDJSON (one span per line) to *path*.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
path: File path to write to. The file is created or overwritten.
|
|
227
|
+
"""
|
|
228
|
+
lines = [
|
|
229
|
+
json.dumps(span.to_span_payload().to_dict(), sort_keys=True, default=str)
|
|
230
|
+
for span in self._spans
|
|
231
|
+
]
|
|
232
|
+
with open(path, "w", encoding="utf-8") as fh:
|
|
233
|
+
fh.write("\n".join(lines))
|
|
234
|
+
if lines:
|
|
235
|
+
fh.write("\n")
|
|
236
|
+
|
|
237
|
+
def _to_dict(self) -> dict[str, Any]:
|
|
238
|
+
d: dict[str, Any] = {
|
|
239
|
+
"trace_id": self.trace_id,
|
|
240
|
+
"agent_name": self.agent_name,
|
|
241
|
+
"start_time": self.start_time,
|
|
242
|
+
"spans": [span.to_span_payload().to_dict() for span in self._spans],
|
|
243
|
+
}
|
|
244
|
+
if self.attributes:
|
|
245
|
+
d["attributes"] = self.attributes
|
|
246
|
+
return d
|
|
247
|
+
|
|
248
|
+
# Placeholder methods for Phase 3 debug utilities.
|
|
249
|
+
def print_tree(self, *, file: Any = None) -> None:
|
|
250
|
+
"""Pretty-print a hierarchical tree of spans to stdout.
|
|
251
|
+
|
|
252
|
+
Delegates to :func:`spanforge.debug.print_tree`.
|
|
253
|
+
Requires the trace to have ended (or have accumulated spans).
|
|
254
|
+
"""
|
|
255
|
+
from spanforge.debug import print_tree # noqa: PLC0415
|
|
256
|
+
print_tree(self._spans, file=file)
|
|
257
|
+
|
|
258
|
+
def summary(self) -> dict[str, Any]:
|
|
259
|
+
"""Return an aggregated statistics dict for the trace's spans.
|
|
260
|
+
|
|
261
|
+
Delegates to :func:`spanforge.debug.summary`.
|
|
262
|
+
"""
|
|
263
|
+
from spanforge.debug import summary # noqa: PLC0415
|
|
264
|
+
return summary(self._spans)
|
|
265
|
+
|
|
266
|
+
def visualize(self, *, output: str = "html", path: str | None = None) -> str:
|
|
267
|
+
"""Generate a self-contained HTML Gantt timeline for this trace.
|
|
268
|
+
|
|
269
|
+
Delegates to :func:`spanforge.debug.visualize`.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
output: Output format — currently only ``"html"``.
|
|
273
|
+
path: Optional file path to write the HTML to.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
HTML string.
|
|
277
|
+
"""
|
|
278
|
+
from spanforge.debug import visualize # noqa: PLC0415
|
|
279
|
+
return visualize(self._spans, output=output, path=path)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# ---------------------------------------------------------------------------
|
|
283
|
+
# start_trace()
|
|
284
|
+
# ---------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def start_trace(agent_name: str, **attributes: Any) -> Trace:
|
|
288
|
+
"""Start a new agent trace and return a :class:`Trace` handle.
|
|
289
|
+
|
|
290
|
+
Opens a root :class:`~spanforge._span.AgentRunContextManager` so all spans
|
|
291
|
+
created within the trace automatically inherit the correct ``trace_id``.
|
|
292
|
+
|
|
293
|
+
Must be closed by calling :meth:`Trace.end` or by using the returned
|
|
294
|
+
object as a context manager::
|
|
295
|
+
|
|
296
|
+
# Imperative style
|
|
297
|
+
trace = spanforge.start_trace("research_agent")
|
|
298
|
+
with trace.llm_call(model="gpt-4o"):
|
|
299
|
+
...
|
|
300
|
+
trace.end()
|
|
301
|
+
|
|
302
|
+
# Context manager style (recommended)
|
|
303
|
+
with spanforge.start_trace("research_agent") as trace:
|
|
304
|
+
with trace.llm_call(model="gpt-4o"):
|
|
305
|
+
...
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
agent_name: Name of the agent being executed.
|
|
309
|
+
**attributes: Optional key-value attributes stored on the
|
|
310
|
+
:attr:`Trace.attributes` dict and included in
|
|
311
|
+
:meth:`Trace.to_json` output.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
A :class:`Trace` object that acts as the root execution context.
|
|
315
|
+
"""
|
|
316
|
+
if not isinstance(agent_name, str) or not agent_name:
|
|
317
|
+
raise ValueError("start_trace: agent_name must be a non-empty string")
|
|
318
|
+
|
|
319
|
+
cm = AgentRunContextManager(agent_name=agent_name)
|
|
320
|
+
run_ctx = cm.__enter__()
|
|
321
|
+
|
|
322
|
+
# Attach a back-reference so _stream can call _record_span on this Trace.
|
|
323
|
+
trace = Trace(
|
|
324
|
+
agent_name=agent_name,
|
|
325
|
+
trace_id=run_ctx.trace_id,
|
|
326
|
+
start_time=time.time(),
|
|
327
|
+
_run_ctx=run_ctx,
|
|
328
|
+
_run_cm=cm,
|
|
329
|
+
attributes=dict(attributes) if attributes else {},
|
|
330
|
+
)
|
|
331
|
+
# Store the Trace on the AgentRunContext so _stream.emit_span can find it.
|
|
332
|
+
run_ctx._trace_collector = trace # type: ignore[attr-defined]
|
|
333
|
+
|
|
334
|
+
return trace
|
spanforge/_tracer.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""spanforge._tracer — :class:`Tracer` class and module-level ``tracer`` singleton.
|
|
2
|
+
|
|
3
|
+
The :class:`Tracer` is the primary entry point for instrumenting code with
|
|
4
|
+
SpanForge. Import the module-level singleton ``tracer`` and use its context
|
|
5
|
+
managers to create spans and agent traces::
|
|
6
|
+
|
|
7
|
+
from spanforge import tracer, configure
|
|
8
|
+
|
|
9
|
+
configure(exporter="console")
|
|
10
|
+
|
|
11
|
+
with tracer.span("chat", model="gpt-4o") as s:
|
|
12
|
+
s.set_attribute("prompt_tokens", 512)
|
|
13
|
+
|
|
14
|
+
with tracer.agent_run("research-agent") as run:
|
|
15
|
+
with tracer.agent_step("web-search") as step:
|
|
16
|
+
step.set_attribute("query", "what is RAG?")
|
|
17
|
+
with tracer.agent_step("summarize"):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
# Imperative trace API (Phase 1)
|
|
21
|
+
trace = tracer.start_trace("research-agent")
|
|
22
|
+
with trace.llm_call(model="gpt-4o"):
|
|
23
|
+
...
|
|
24
|
+
trace.end()
|
|
25
|
+
|
|
26
|
+
All context managers support both ``with`` and ``async with``. Async usage
|
|
27
|
+
is safe because the span stack is backed by :mod:`contextvars` so each asyncio
|
|
28
|
+
task sees its own stack slice.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
from typing import Any
|
|
34
|
+
|
|
35
|
+
from spanforge._span import (
|
|
36
|
+
AgentRunContextManager,
|
|
37
|
+
AgentStepContextManager,
|
|
38
|
+
SpanContextManager,
|
|
39
|
+
)
|
|
40
|
+
from spanforge._trace import Trace, start_trace as _start_trace
|
|
41
|
+
from spanforge.trace import trace as _trace_decorator, _TraceDecorator
|
|
42
|
+
|
|
43
|
+
__all__ = ["Tracer", "tracer"]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Tracer:
|
|
47
|
+
"""The SpanForge tracing façade.
|
|
48
|
+
|
|
49
|
+
A single module-level instance is created as :data:`tracer` and is the
|
|
50
|
+
recommended way to instrument code. Creating additional :class:`Tracer`
|
|
51
|
+
instances is supported but shares the same thread-local context stacks.
|
|
52
|
+
|
|
53
|
+
All ``span``/``agent_run``/``agent_step`` methods return context managers
|
|
54
|
+
that push the new context onto the thread-local stack on ``__enter__`` and
|
|
55
|
+
pop it (and emit the event) on ``__exit__``.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
# ------------------------------------------------------------------
|
|
59
|
+
# Span API (Phase 2)
|
|
60
|
+
# ------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
def span(
|
|
63
|
+
self,
|
|
64
|
+
name: str,
|
|
65
|
+
*,
|
|
66
|
+
model: str | None = None,
|
|
67
|
+
operation: str = "chat",
|
|
68
|
+
temperature: float | None = None,
|
|
69
|
+
top_p: float | None = None,
|
|
70
|
+
max_tokens: int | None = None,
|
|
71
|
+
attributes: dict[str, Any] | None = None,
|
|
72
|
+
) -> SpanContextManager:
|
|
73
|
+
"""Create a new :class:`~spanforge._span.SpanContextManager`.
|
|
74
|
+
|
|
75
|
+
Use as a context manager::
|
|
76
|
+
|
|
77
|
+
with tracer.span("llm-call", model="gpt-4o", temperature=0.7) as s:
|
|
78
|
+
s.set_attribute("prompt_tokens", 512)
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
name: Human-readable span name (non-empty string).
|
|
82
|
+
model: Model name string (e.g. ``"gpt-4o"``). Used to infer
|
|
83
|
+
the provider when no integration has set
|
|
84
|
+
:attr:`~spanforge._span.Span.token_usage`.
|
|
85
|
+
operation: GenAI operation name (default ``"chat"``). Any
|
|
86
|
+
:class:`~spanforge.namespaces.trace.GenAIOperationName`
|
|
87
|
+
value or a custom string.
|
|
88
|
+
temperature: Sampling temperature forwarded to :class:`SpanPayload`.
|
|
89
|
+
top_p: Nucleus sampling ``top_p`` value.
|
|
90
|
+
max_tokens: Maximum token limit for this LLM call.
|
|
91
|
+
attributes: Initial key-value attributes. Additional attributes
|
|
92
|
+
can be added inside the block via
|
|
93
|
+
:meth:`~spanforge._span.Span.set_attribute`.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
A :class:`~spanforge._span.SpanContextManager` that yields a
|
|
97
|
+
:class:`~spanforge._span.Span` on ``__enter__``.
|
|
98
|
+
"""
|
|
99
|
+
return SpanContextManager(
|
|
100
|
+
name=name,
|
|
101
|
+
model=model,
|
|
102
|
+
operation=operation,
|
|
103
|
+
temperature=temperature,
|
|
104
|
+
top_p=top_p,
|
|
105
|
+
max_tokens=max_tokens,
|
|
106
|
+
attributes=attributes,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# ------------------------------------------------------------------
|
|
110
|
+
# Agent API (Phase 4)
|
|
111
|
+
# ------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
def agent_run(self, agent_name: str) -> AgentRunContextManager:
|
|
114
|
+
"""Create a root agent-run context manager.
|
|
115
|
+
|
|
116
|
+
Use as an outer context that wraps one or more ``agent_step`` calls::
|
|
117
|
+
|
|
118
|
+
with tracer.agent_run("my-agent") as run:
|
|
119
|
+
with tracer.agent_step("step-1"):
|
|
120
|
+
...
|
|
121
|
+
|
|
122
|
+
On exit, emits an
|
|
123
|
+
:data:`~spanforge.types.EventType.TRACE_AGENT_COMPLETED` event with
|
|
124
|
+
aggregated totals across all child steps.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
agent_name: Name of the agent (non-empty string).
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
:class:`~spanforge._span.AgentRunContextManager`
|
|
131
|
+
"""
|
|
132
|
+
return AgentRunContextManager(agent_name=agent_name)
|
|
133
|
+
|
|
134
|
+
def agent_step(
|
|
135
|
+
self,
|
|
136
|
+
step_name: str,
|
|
137
|
+
*,
|
|
138
|
+
operation: str = "invoke_agent",
|
|
139
|
+
attributes: dict[str, Any] | None = None,
|
|
140
|
+
) -> AgentStepContextManager:
|
|
141
|
+
"""Create a single agent-step context manager.
|
|
142
|
+
|
|
143
|
+
Must be used inside an ``agent_run`` block::
|
|
144
|
+
|
|
145
|
+
with tracer.agent_run("my-agent"):
|
|
146
|
+
with tracer.agent_step("search") as step:
|
|
147
|
+
step.set_attribute("query", "hello")
|
|
148
|
+
|
|
149
|
+
On exit, emits an
|
|
150
|
+
:data:`~spanforge.types.EventType.TRACE_AGENT_STEP` event.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
step_name: Human-readable step name.
|
|
154
|
+
operation: GenAI operation name (default ``"invoke_agent"``).
|
|
155
|
+
attributes: Initial key-value attributes.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
:class:`~spanforge._span.AgentStepContextManager`
|
|
159
|
+
|
|
160
|
+
Raises:
|
|
161
|
+
RuntimeError: If called outside an ``agent_run`` context.
|
|
162
|
+
"""
|
|
163
|
+
return AgentStepContextManager(
|
|
164
|
+
step_name=step_name,
|
|
165
|
+
operation=operation,
|
|
166
|
+
attributes=attributes,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# ------------------------------------------------------------------
|
|
170
|
+
# Trace API (Phase 1)
|
|
171
|
+
# ------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
def start_trace(self, agent_name: str, **attributes: Any) -> Trace:
|
|
174
|
+
"""Start a new agent trace and return a :class:`~spanforge._trace.Trace` handle.
|
|
175
|
+
|
|
176
|
+
Convenience wrapper around the module-level :func:`~spanforge._trace.start_trace`.
|
|
177
|
+
The returned :class:`Trace` must be closed with :meth:`~spanforge._trace.Trace.end`
|
|
178
|
+
or used as a context manager::
|
|
179
|
+
|
|
180
|
+
with tracer.start_trace("research-agent") as trace:
|
|
181
|
+
with trace.llm_call(model="gpt-4o") as s:
|
|
182
|
+
...
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
agent_name: Name of the agent being executed.
|
|
186
|
+
**attributes: Optional key-value attributes for the root run context.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
:class:`~spanforge._trace.Trace`
|
|
190
|
+
"""
|
|
191
|
+
return _start_trace(agent_name, **attributes)
|
|
192
|
+
|
|
193
|
+
def trace(
|
|
194
|
+
self,
|
|
195
|
+
fn: Any = None,
|
|
196
|
+
*,
|
|
197
|
+
name: str | None = None,
|
|
198
|
+
model: str | None = None,
|
|
199
|
+
operation: str = "chat",
|
|
200
|
+
tool: bool = False,
|
|
201
|
+
capture_args: bool = False,
|
|
202
|
+
capture_return: bool = False,
|
|
203
|
+
attributes: dict[str, Any] | None = None,
|
|
204
|
+
) -> Any:
|
|
205
|
+
"""Decorator that instruments a function as an SpanForge span.
|
|
206
|
+
|
|
207
|
+
Delegates to :func:`~spanforge.trace.trace`. Provided as a convenience
|
|
208
|
+
method so callers who already hold a :class:`Tracer` reference do not
|
|
209
|
+
need a separate import::
|
|
210
|
+
|
|
211
|
+
@tracer.trace(name="my-step", model="gpt-4o")
|
|
212
|
+
def call_llm(prompt: str) -> str: ...
|
|
213
|
+
|
|
214
|
+
@tracer.trace(name="async-step")
|
|
215
|
+
async def async_step(x: int) -> dict: ...
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
fn: Function to wrap when used bare (``@tracer.trace``).
|
|
219
|
+
name: Span name (defaults to ``fn.__qualname__``).
|
|
220
|
+
model: Model identifier string.
|
|
221
|
+
operation: GenAI operation name (default ``"chat"``).
|
|
222
|
+
tool: Mark as tool call; sets operation to ``"execute_tool"``.
|
|
223
|
+
capture_args: Record call arguments as span attributes.
|
|
224
|
+
capture_return: Record return value as a span attribute.
|
|
225
|
+
attributes: Static key-value attributes on every span.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Decorated callable, or a single-argument decorator.
|
|
229
|
+
"""
|
|
230
|
+
return _trace_decorator(
|
|
231
|
+
fn,
|
|
232
|
+
name=name,
|
|
233
|
+
model=model,
|
|
234
|
+
operation=operation,
|
|
235
|
+
tool=tool,
|
|
236
|
+
capture_args=capture_args,
|
|
237
|
+
capture_return=capture_return,
|
|
238
|
+
attributes=attributes,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# ---------------------------------------------------------------------------
|
|
243
|
+
# Module-level singleton — ``from spanforge import tracer``
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
#: The default :class:`Tracer` singleton.
|
|
247
|
+
#:
|
|
248
|
+
#: Import this directly for convenience::
|
|
249
|
+
#:
|
|
250
|
+
#: from spanforge import tracer
|
|
251
|
+
#: with tracer.span("my-span"):
|
|
252
|
+
#: ...
|
|
253
|
+
tracer: Tracer = Tracer()
|