agentmark-sdk 0.1.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.
@@ -0,0 +1,71 @@
1
+ """AgentMark SDK for Python.
2
+
3
+ Provides OpenTelemetry-based tracing and observability for AI applications.
4
+
5
+ Example:
6
+ from agentmark_sdk import AgentMarkSDK, span, SpanOptions
7
+
8
+ # Initialize the SDK
9
+ sdk = AgentMarkSDK(api_key="sk-...", app_id="app_123")
10
+ sdk.init_tracing()
11
+
12
+ # Create a span around an operation
13
+ result = await span(
14
+ SpanOptions(name="my-operation", user_id="user-1"),
15
+ my_async_function,
16
+ )
17
+
18
+ # Auto-capture IO with decorator
19
+ from agentmark_sdk import observe, SpanKind
20
+
21
+ @observe(kind=SpanKind.TOOL)
22
+ async def my_tool(query: str) -> dict:
23
+ return {"result": "data"}
24
+
25
+ # Submit a score
26
+ await sdk.score(
27
+ resource_id=result.trace_id,
28
+ name="accuracy",
29
+ score=0.95,
30
+ )
31
+ """
32
+
33
+ from .config import (
34
+ AGENTMARK_KEY,
35
+ AGENTMARK_SCORE_ENDPOINT,
36
+ AGENTMARK_TRACE_ENDPOINT,
37
+ DEFAULT_BASE_URL,
38
+ METADATA_KEY,
39
+ )
40
+ from .decorator import SpanKind, observe
41
+ from .sampler import AgentmarkSampler
42
+ from .sdk import AgentMarkSDK
43
+ from .serialize import serialize_value
44
+ from .trace import SpanContext, SpanOptions, SpanResult, span, span_context, span_context_sync
45
+
46
+ __all__ = [
47
+ # SDK
48
+ "AgentMarkSDK",
49
+ # Span utilities
50
+ "span",
51
+ "span_context",
52
+ "span_context_sync",
53
+ "observe",
54
+ "SpanOptions",
55
+ "SpanContext",
56
+ "SpanResult",
57
+ # Span kinds
58
+ "SpanKind",
59
+ # Serialization
60
+ "serialize_value",
61
+ # Sampler
62
+ "AgentmarkSampler",
63
+ # Config
64
+ "AGENTMARK_KEY",
65
+ "METADATA_KEY",
66
+ "AGENTMARK_TRACE_ENDPOINT",
67
+ "AGENTMARK_SCORE_ENDPOINT",
68
+ "DEFAULT_BASE_URL",
69
+ ]
70
+
71
+ __version__ = "0.1.0"
@@ -0,0 +1,12 @@
1
+ """Configuration constants for AgentMark SDK."""
2
+
3
+ # API Endpoints
4
+ AGENTMARK_TRACE_ENDPOINT = "v1/traces"
5
+ AGENTMARK_SCORE_ENDPOINT = "v1/score"
6
+
7
+ # Default base URL
8
+ DEFAULT_BASE_URL = "https://api.agentmark.co"
9
+
10
+ # Span attribute prefixes
11
+ AGENTMARK_KEY = "agentmark"
12
+ METADATA_KEY = "agentmark.metadata"
@@ -0,0 +1,182 @@
1
+ """Decorator-based tracing for automatic IO capture.
2
+
3
+ Provides the @observe decorator that auto-captures function arguments
4
+ as span input and return values as span output.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import functools
10
+ import inspect
11
+ from enum import Enum
12
+ from typing import Any, Callable, TypeVar, overload
13
+
14
+ from opentelemetry import trace as otel_trace
15
+ from opentelemetry.trace import StatusCode
16
+
17
+ from .config import AGENTMARK_KEY
18
+ from .serialize import serialize_value
19
+
20
+ F = TypeVar("F", bound=Callable[..., Any])
21
+
22
+ # Attribute keys matching the gen_ai semantic conventions used by the adapter
23
+ INPUT_KEY = "gen_ai.request.input"
24
+ OUTPUT_KEY = "gen_ai.response.output"
25
+ SPAN_KIND_KEY = f"{AGENTMARK_KEY}.span.kind"
26
+
27
+
28
+ class SpanKind(str, Enum):
29
+ """Span kind for categorizing observed operations."""
30
+
31
+ FUNCTION = "function"
32
+ LLM = "llm"
33
+ TOOL = "tool"
34
+
35
+
36
+ def _capture_inputs(
37
+ fn: Callable[..., Any],
38
+ args: tuple[Any, ...],
39
+ kwargs: dict[str, Any],
40
+ process_inputs: Callable[[dict[str, Any]], dict[str, Any]] | None,
41
+ ) -> str | None:
42
+ """Capture function arguments as a serialized input string."""
43
+ try:
44
+ sig = inspect.signature(fn)
45
+ bound = sig.bind(*args, **kwargs)
46
+ bound.apply_defaults()
47
+ inputs = dict(bound.arguments)
48
+ except (TypeError, ValueError):
49
+ # Fallback if signature binding fails
50
+ inputs = {"args": list(args), "kwargs": kwargs} if kwargs else {"args": list(args)}
51
+
52
+ if process_inputs is not None:
53
+ # Pass raw args (including self) to custom processor
54
+ inputs = process_inputs(inputs)
55
+ # Exclude self/cls for methods (after process_inputs so it can access self)
56
+ inputs.pop("self", None)
57
+ inputs.pop("cls", None)
58
+
59
+ return serialize_value(inputs)
60
+
61
+
62
+ def _capture_output(
63
+ result: Any,
64
+ process_outputs: Callable[[Any], Any] | None,
65
+ ) -> str | None:
66
+ """Capture function return value as a serialized output string."""
67
+ output = result
68
+ if process_outputs is not None:
69
+ output = process_outputs(output)
70
+ return serialize_value(output)
71
+
72
+
73
+ @overload
74
+ def observe(_fn: F) -> F: ...
75
+
76
+
77
+ @overload
78
+ def observe(
79
+ _fn: None = None,
80
+ *,
81
+ name: str | None = None,
82
+ kind: SpanKind = SpanKind.FUNCTION,
83
+ capture_input: bool = True,
84
+ capture_output: bool = True,
85
+ process_inputs: Callable[[dict[str, Any]], dict[str, Any]] | None = None,
86
+ process_outputs: Callable[[Any], Any] | None = None,
87
+ ) -> Callable[[F], F]: ...
88
+
89
+
90
+ def observe(
91
+ _fn: F | None = None,
92
+ *,
93
+ name: str | None = None,
94
+ kind: SpanKind = SpanKind.FUNCTION,
95
+ capture_input: bool = True,
96
+ capture_output: bool = True,
97
+ process_inputs: Callable[[dict[str, Any]], dict[str, Any]] | None = None,
98
+ process_outputs: Callable[[Any], Any] | None = None,
99
+ ) -> F | Callable[[F], F]:
100
+ """Decorator that auto-captures function IO as span attributes.
101
+
102
+ Supports both @observe and @observe() syntax, sync and async functions.
103
+
104
+ Args:
105
+ name: Custom span name. Defaults to the function name.
106
+ kind: Span kind for categorization. Defaults to SpanKind.FUNCTION.
107
+ capture_input: Whether to capture function args as input. Default True.
108
+ capture_output: Whether to capture return value as output. Default True.
109
+ process_inputs: Optional transform applied to inputs before serialization.
110
+ process_outputs: Optional transform applied to output before serialization.
111
+
112
+ Example:
113
+ @observe
114
+ async def my_function(item_type: str) -> dict:
115
+ return {"result": "data"}
116
+
117
+ @observe(name="custom-name", kind=SpanKind.TOOL)
118
+ async def call_api(query: str) -> dict:
119
+ ...
120
+
121
+ @observe(process_inputs=lambda inputs: {k: v for k, v in inputs.items() if k != "api_key"})
122
+ async def call_api(api_key: str, query: str) -> dict:
123
+ ...
124
+ """
125
+
126
+ def decorator(fn: F) -> F:
127
+ span_name = name or fn.__name__
128
+
129
+ @functools.wraps(fn)
130
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
131
+ tracer = otel_trace.get_tracer("agentmark")
132
+ with tracer.start_as_current_span(span_name) as span:
133
+ span.set_attribute(SPAN_KIND_KEY, kind.value)
134
+
135
+ if capture_input:
136
+ input_str = _capture_inputs(fn, args, kwargs, process_inputs)
137
+ if input_str is not None:
138
+ span.set_attribute(INPUT_KEY, input_str)
139
+
140
+ try:
141
+ result = await fn(*args, **kwargs)
142
+ if capture_output:
143
+ output_str = _capture_output(result, process_outputs)
144
+ if output_str is not None:
145
+ span.set_attribute(OUTPUT_KEY, output_str)
146
+ span.set_status(StatusCode.OK)
147
+ return result
148
+ except Exception as e:
149
+ span.set_status(StatusCode.ERROR, str(e))
150
+ raise
151
+
152
+ @functools.wraps(fn)
153
+ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
154
+ tracer = otel_trace.get_tracer("agentmark")
155
+ with tracer.start_as_current_span(span_name) as span:
156
+ span.set_attribute(SPAN_KIND_KEY, kind.value)
157
+
158
+ if capture_input:
159
+ input_str = _capture_inputs(fn, args, kwargs, process_inputs)
160
+ if input_str is not None:
161
+ span.set_attribute(INPUT_KEY, input_str)
162
+
163
+ try:
164
+ result = fn(*args, **kwargs)
165
+ if capture_output:
166
+ output_str = _capture_output(result, process_outputs)
167
+ if output_str is not None:
168
+ span.set_attribute(OUTPUT_KEY, output_str)
169
+ span.set_status(StatusCode.OK)
170
+ return result
171
+ except Exception as e:
172
+ span.set_status(StatusCode.ERROR, str(e))
173
+ raise
174
+
175
+ if inspect.iscoroutinefunction(fn):
176
+ return async_wrapper # type: ignore[return-value]
177
+ return sync_wrapper # type: ignore[return-value]
178
+
179
+ # Support both @observe and @observe() syntax
180
+ if _fn is not None:
181
+ return decorator(_fn)
182
+ return decorator # type: ignore[return-value]
@@ -0,0 +1,60 @@
1
+ """Custom sampler for AgentMark traces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Sequence
6
+
7
+ from opentelemetry.context import Context
8
+ from opentelemetry.sdk.trace.sampling import (
9
+ Decision,
10
+ Sampler,
11
+ SamplingResult,
12
+ )
13
+ from opentelemetry.trace import Link, SpanKind
14
+ from opentelemetry.util.types import Attributes
15
+
16
+ # Span attributes that indicate internal framework spans to filter out
17
+ FILTERED_ATTRIBUTE_KEYS = ["next.span_name", "next.clientComponentLoadCount"]
18
+
19
+
20
+ class AgentmarkSampler(Sampler):
21
+ """Custom sampler that filters out internal framework spans.
22
+
23
+ This sampler drops spans that have attributes indicating they are
24
+ internal framework spans (e.g., Next.js internals) that would add
25
+ noise to traces without providing useful debugging information.
26
+ """
27
+
28
+ def should_sample(
29
+ self,
30
+ parent_context: Context | None,
31
+ trace_id: int,
32
+ name: str,
33
+ kind: SpanKind | None = None,
34
+ attributes: Attributes | None = None,
35
+ links: Sequence[Link] | None = None,
36
+ ) -> SamplingResult:
37
+ """Determine whether to sample this span.
38
+
39
+ Args:
40
+ parent_context: The parent context (if any).
41
+ trace_id: The trace ID.
42
+ name: The span name.
43
+ kind: The span kind.
44
+ attributes: Span attributes.
45
+ links: Span links.
46
+
47
+ Returns:
48
+ SamplingResult indicating whether to record/sample.
49
+ """
50
+ # Check if any filtered attribute keys are present
51
+ if attributes:
52
+ for key in FILTERED_ATTRIBUTE_KEYS:
53
+ if key in attributes:
54
+ return SamplingResult(Decision.DROP)
55
+
56
+ return SamplingResult(Decision.RECORD_AND_SAMPLE)
57
+
58
+ def get_description(self) -> str:
59
+ """Return a description of this sampler."""
60
+ return "AgentmarkSampler"
agentmark_sdk/sdk.py ADDED
@@ -0,0 +1,252 @@
1
+ """AgentMark SDK main class."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import httpx
8
+ from opentelemetry import trace as otel_trace
9
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
10
+ from opentelemetry.sdk.resources import Resource
11
+ from opentelemetry.sdk.trace import TracerProvider
12
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor, SimpleSpanProcessor
13
+
14
+ from .config import (
15
+ AGENTMARK_SCORE_ENDPOINT,
16
+ AGENTMARK_TRACE_ENDPOINT,
17
+ DEFAULT_BASE_URL,
18
+ )
19
+ from .sampler import AgentmarkSampler
20
+
21
+
22
+ class AgentMarkSDK:
23
+ """AgentMark SDK for Python.
24
+
25
+ Provides OpenTelemetry tracing initialization and score submission.
26
+
27
+ Example:
28
+ sdk = AgentMarkSDK(api_key="sk-...", app_id="app_123")
29
+ sdk.init_tracing()
30
+
31
+ # Use span() function from the SDK
32
+ from agentmark_sdk import span, SpanOptions
33
+ result = await span(
34
+ SpanOptions(name="my-operation"),
35
+ my_async_function,
36
+ )
37
+
38
+ # Submit a score
39
+ await sdk.score(
40
+ resource_id=result.trace_id,
41
+ name="accuracy",
42
+ score=0.95,
43
+ label="good",
44
+ reason="Response matched expected output",
45
+ )
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ api_key: str,
51
+ app_id: str,
52
+ base_url: str = DEFAULT_BASE_URL,
53
+ ) -> None:
54
+ """Initialize the SDK.
55
+
56
+ Args:
57
+ api_key: AgentMark API key.
58
+ app_id: AgentMark application ID.
59
+ base_url: Base URL for the AgentMark API.
60
+ """
61
+ self._api_key = api_key
62
+ self._app_id = app_id
63
+ self._base_url = base_url.rstrip("/")
64
+ self._tracer_provider: TracerProvider | None = None
65
+
66
+ @property
67
+ def api_key(self) -> str:
68
+ """Get the API key."""
69
+ return self._api_key
70
+
71
+ @property
72
+ def app_id(self) -> str:
73
+ """Get the application ID."""
74
+ return self._app_id
75
+
76
+ @property
77
+ def base_url(self) -> str:
78
+ """Get the base URL."""
79
+ return self._base_url
80
+
81
+ def init_tracing(self, disable_batch: bool = False) -> TracerProvider:
82
+ """Initialize OpenTelemetry tracing with AgentMark exporter.
83
+
84
+ Call this once at application startup.
85
+
86
+ Args:
87
+ disable_batch: If True, use SimpleSpanProcessor (immediate export).
88
+ If False (default), use BatchSpanProcessor for better performance.
89
+
90
+ Returns:
91
+ The configured TracerProvider.
92
+
93
+ Example:
94
+ sdk = AgentMarkSDK(api_key="sk-...", app_id="app_123")
95
+ provider = sdk.init_tracing()
96
+ """
97
+ exporter_url = f"{self._base_url}/{AGENTMARK_TRACE_ENDPOINT}"
98
+
99
+ exporter = OTLPSpanExporter(
100
+ endpoint=exporter_url,
101
+ headers={
102
+ "Authorization": self._api_key,
103
+ "X-Agentmark-App-Id": self._app_id,
104
+ },
105
+ )
106
+
107
+ processor: SimpleSpanProcessor | BatchSpanProcessor
108
+ if disable_batch:
109
+ processor = SimpleSpanProcessor(exporter)
110
+ else:
111
+ processor = BatchSpanProcessor(exporter)
112
+
113
+ resource = Resource.create(
114
+ {
115
+ "service.name": "agentmark-client",
116
+ "agentmark.app_id": self._app_id,
117
+ }
118
+ )
119
+
120
+ provider = TracerProvider(
121
+ resource=resource,
122
+ sampler=AgentmarkSampler(),
123
+ )
124
+ provider.add_span_processor(processor)
125
+
126
+ otel_trace.set_tracer_provider(provider)
127
+ self._tracer_provider = provider
128
+
129
+ return provider
130
+
131
+ async def score(
132
+ self,
133
+ resource_id: str,
134
+ name: str,
135
+ score: float,
136
+ label: str | None = None,
137
+ reason: str | None = None,
138
+ type: str | None = None,
139
+ ) -> dict[str, Any]:
140
+ """Submit a score for a trace/span.
141
+
142
+ Args:
143
+ resource_id: The trace or span ID to score.
144
+ name: Name of the score metric (e.g., "accuracy", "relevance").
145
+ score: Numeric score value (typically 0.0-1.0).
146
+ label: Optional label (e.g., "good", "bad", "neutral").
147
+ reason: Optional explanation for the score.
148
+ type: Optional score type identifier.
149
+
150
+ Returns:
151
+ Response data from the API.
152
+
153
+ Raises:
154
+ Exception: If the API request fails.
155
+
156
+ Example:
157
+ await sdk.score(
158
+ resource_id="abc123",
159
+ name="accuracy",
160
+ score=0.95,
161
+ label="good",
162
+ reason="Response matched expected",
163
+ )
164
+ """
165
+ url = f"{self._base_url}/{AGENTMARK_SCORE_ENDPOINT}"
166
+
167
+ payload: dict[str, Any] = {
168
+ "resourceId": resource_id,
169
+ "name": name,
170
+ "score": score,
171
+ }
172
+ if label is not None:
173
+ payload["label"] = label
174
+ if reason is not None:
175
+ payload["reason"] = reason
176
+ if type is not None:
177
+ payload["type"] = type
178
+
179
+ async with httpx.AsyncClient() as client:
180
+ response = await client.post(
181
+ url,
182
+ json=payload,
183
+ headers={
184
+ "Content-Type": "application/json",
185
+ "Authorization": self._api_key,
186
+ "X-Agentmark-App-Id": self._app_id,
187
+ },
188
+ )
189
+
190
+ if response.is_success:
191
+ data = response.json()
192
+ return data.get("data", {})
193
+
194
+ error_data = response.json()
195
+ raise Exception(error_data.get("error", "Unknown error"))
196
+
197
+ def score_sync(
198
+ self,
199
+ resource_id: str,
200
+ name: str,
201
+ score: float,
202
+ label: str | None = None,
203
+ reason: str | None = None,
204
+ type: str | None = None,
205
+ ) -> dict[str, Any]:
206
+ """Submit a score for a trace/span (synchronous version).
207
+
208
+ Args:
209
+ resource_id: The trace or span ID to score.
210
+ name: Name of the score metric.
211
+ score: Numeric score value.
212
+ label: Optional label.
213
+ reason: Optional explanation.
214
+ type: Optional score type.
215
+
216
+ Returns:
217
+ Response data from the API.
218
+
219
+ Raises:
220
+ Exception: If the API request fails.
221
+ """
222
+ url = f"{self._base_url}/{AGENTMARK_SCORE_ENDPOINT}"
223
+
224
+ payload: dict[str, Any] = {
225
+ "resourceId": resource_id,
226
+ "name": name,
227
+ "score": score,
228
+ }
229
+ if label is not None:
230
+ payload["label"] = label
231
+ if reason is not None:
232
+ payload["reason"] = reason
233
+ if type is not None:
234
+ payload["type"] = type
235
+
236
+ with httpx.Client() as client:
237
+ response = client.post(
238
+ url,
239
+ json=payload,
240
+ headers={
241
+ "Content-Type": "application/json",
242
+ "Authorization": self._api_key,
243
+ "X-Agentmark-App-Id": self._app_id,
244
+ },
245
+ )
246
+
247
+ if response.is_success:
248
+ data = response.json()
249
+ return data.get("data", {})
250
+
251
+ error_data = response.json()
252
+ raise Exception(error_data.get("error", "Unknown error"))
@@ -0,0 +1,33 @@
1
+ """Serialization utilities for observed function IO capture."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses
6
+ import json
7
+ from typing import Any
8
+
9
+ MAX_SERIALIZE_LENGTH = 1_000_000
10
+
11
+
12
+ def serialize_value(value: Any, max_length: int = MAX_SERIALIZE_LENGTH) -> str:
13
+ """Serialize a value to a JSON string for span attributes.
14
+
15
+ Serialization chain:
16
+ 1. Pydantic model → .model_dump() → JSON
17
+ 2. Dataclass → dataclasses.asdict() → JSON
18
+ 3. Dict/list/primitive → JSON directly
19
+ 4. Fallback → str(obj)
20
+
21
+ Truncated to max_length characters.
22
+ """
23
+ try:
24
+ if hasattr(value, "model_dump"):
25
+ serialized = json.dumps(value.model_dump(), default=str)
26
+ elif dataclasses.is_dataclass(value) and not isinstance(value, type):
27
+ serialized = json.dumps(dataclasses.asdict(value), default=str)
28
+ else:
29
+ serialized = json.dumps(value, default=str)
30
+ except (TypeError, ValueError, OverflowError):
31
+ serialized = str(value)
32
+
33
+ return serialized[:max_length]
agentmark_sdk/trace.py ADDED
@@ -0,0 +1,331 @@
1
+ """Tracing utilities for AgentMark SDK.
2
+
3
+ Provides the trace() function for wrapping operations with OpenTelemetry spans.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from contextlib import asynccontextmanager
9
+ from dataclasses import dataclass, field
10
+ from typing import Any, AsyncIterator, Callable, Generic, TypeVar
11
+
12
+ from opentelemetry import trace as otel_trace
13
+ from opentelemetry.trace import Span, SpanKind, StatusCode, Tracer
14
+ from opentelemetry.util.types import Attributes
15
+
16
+ from .config import AGENTMARK_KEY, METADATA_KEY
17
+
18
+ T = TypeVar("T")
19
+
20
+
21
+ @dataclass
22
+ class TraceOptions:
23
+ """Options for creating a trace.
24
+
25
+ Attributes:
26
+ name: Name of the trace/span.
27
+ metadata: Additional metadata key-value pairs.
28
+ session_id: Session identifier for grouping traces.
29
+ session_name: Human-readable session name.
30
+ user_id: User identifier.
31
+ dataset_run_id: Dataset run identifier (for experiments).
32
+ dataset_run_name: Dataset run name (for experiments).
33
+ dataset_item_name: Dataset item name (for experiments).
34
+ dataset_expected_output: Expected output for dataset item.
35
+ """
36
+
37
+ name: str
38
+ metadata: dict[str, str] | None = None
39
+ session_id: str | None = None
40
+ session_name: str | None = None
41
+ user_id: str | None = None
42
+ prompt_name: str | None = None
43
+ dataset_run_id: str | None = None
44
+ dataset_run_name: str | None = None
45
+ dataset_item_name: str | None = None
46
+ dataset_expected_output: str | None = None
47
+ dataset_input: str | None = None
48
+ dataset_path: str | None = None
49
+
50
+
51
+ @dataclass
52
+ class TraceContext:
53
+ """Context passed to traced functions.
54
+
55
+ Provides access to trace information and methods for adding
56
+ attributes, events, and child spans.
57
+
58
+ Attributes:
59
+ trace_id: The trace ID in hex format.
60
+ span_id: The span ID in hex format.
61
+ """
62
+
63
+ trace_id: str
64
+ span_id: str
65
+ _span: Span = field(repr=False)
66
+ _tracer: Tracer = field(repr=False)
67
+
68
+ def set_attribute(self, key: str, value: str | int | float | bool) -> None:
69
+ """Set an attribute on this span.
70
+
71
+ Args:
72
+ key: Attribute key.
73
+ value: Attribute value.
74
+ """
75
+ self._span.set_attribute(key, value)
76
+
77
+ def set_input(self, data: dict[str, Any]) -> None:
78
+ """Record input data on this span."""
79
+ from .serialize import serialize_value
80
+
81
+ self._span.set_attribute(
82
+ f"{AGENTMARK_KEY}.input", serialize_value(data),
83
+ )
84
+
85
+ def set_output(self, data: dict[str, Any]) -> None:
86
+ """Record output data on this span."""
87
+ from .serialize import serialize_value
88
+
89
+ self._span.set_attribute(
90
+ f"{AGENTMARK_KEY}.output", serialize_value(data),
91
+ )
92
+
93
+ def add_event(
94
+ self, name: str, attributes: dict[str, Any] | None = None
95
+ ) -> None:
96
+ """Add an event to this span.
97
+
98
+ Args:
99
+ name: Event name.
100
+ attributes: Optional event attributes.
101
+ """
102
+ self._span.add_event(name, attributes or {})
103
+
104
+ @asynccontextmanager
105
+ async def span(
106
+ self, name: str, metadata: dict[str, str] | None = None
107
+ ) -> AsyncIterator[TraceContext]:
108
+ """Create a child span within this trace.
109
+
110
+ Args:
111
+ name: Child span name.
112
+ metadata: Optional metadata for the child span.
113
+
114
+ Yields:
115
+ TraceContext for the child span.
116
+ """
117
+ with self._tracer.start_as_current_span(name) as child_span:
118
+ if metadata:
119
+ for key, value in metadata.items():
120
+ child_span.set_attribute(f"{METADATA_KEY}.{key}", value)
121
+
122
+ span_ctx = child_span.get_span_context()
123
+ child_ctx = TraceContext(
124
+ trace_id=self.trace_id, # Same trace ID
125
+ span_id=format(span_ctx.span_id, "016x"),
126
+ _span=child_span,
127
+ _tracer=self._tracer,
128
+ )
129
+ try:
130
+ yield child_ctx
131
+ child_span.set_status(StatusCode.OK)
132
+ except Exception as e:
133
+ child_span.set_status(StatusCode.ERROR, str(e))
134
+ raise
135
+
136
+
137
+ @dataclass
138
+ class TraceResult(Generic[T]):
139
+ """Result from trace execution.
140
+
141
+ Attributes:
142
+ result: The result of the traced function.
143
+ trace_id: The trace ID for correlation.
144
+ """
145
+
146
+ result: T
147
+ trace_id: str
148
+
149
+
150
+ def _set_agentmark_attributes(span: Span, options: TraceOptions) -> None:
151
+ """Set AgentMark-specific attributes on a span.
152
+
153
+ Args:
154
+ span: The span to set attributes on.
155
+ options: Trace options containing attribute values.
156
+ """
157
+ span.set_attribute(f"{AGENTMARK_KEY}.trace_name", options.name)
158
+
159
+ if options.session_id:
160
+ span.set_attribute(f"{AGENTMARK_KEY}.session_id", options.session_id)
161
+ if options.session_name:
162
+ span.set_attribute(f"{AGENTMARK_KEY}.session_name", options.session_name)
163
+ if options.user_id:
164
+ span.set_attribute(f"{AGENTMARK_KEY}.user_id", options.user_id)
165
+ if options.prompt_name:
166
+ span.set_attribute(f"{AGENTMARK_KEY}.prompt_name", options.prompt_name)
167
+ if options.dataset_run_id:
168
+ span.set_attribute(f"{AGENTMARK_KEY}.dataset_run_id", options.dataset_run_id)
169
+ if options.dataset_run_name:
170
+ span.set_attribute(f"{AGENTMARK_KEY}.dataset_run_name", options.dataset_run_name)
171
+ if options.dataset_item_name:
172
+ span.set_attribute(f"{AGENTMARK_KEY}.dataset_item_name", options.dataset_item_name)
173
+ if options.dataset_expected_output:
174
+ span.set_attribute(
175
+ f"{AGENTMARK_KEY}.dataset_expected_output", options.dataset_expected_output
176
+ )
177
+ if options.dataset_input:
178
+ span.set_attribute(f"{AGENTMARK_KEY}.dataset_input", options.dataset_input)
179
+ if options.dataset_path:
180
+ span.set_attribute(f"{AGENTMARK_KEY}.dataset_path", options.dataset_path)
181
+
182
+ if options.metadata:
183
+ for key, value in options.metadata.items():
184
+ span.set_attribute(f"{METADATA_KEY}.{key}", value)
185
+
186
+
187
+ async def trace(
188
+ options: TraceOptions | str,
189
+ fn: Callable[..., Any],
190
+ *args: Any,
191
+ **kwargs: Any,
192
+ ) -> TraceResult[Any]:
193
+ """Start a new trace and execute a function within it.
194
+
195
+ Creates a root span, executes the provided function, and returns
196
+ both the result and the trace ID.
197
+
198
+ Args:
199
+ options: Trace options or just a name string.
200
+ fn: The async function to execute within the trace.
201
+ *args: Positional arguments to pass to the function.
202
+ **kwargs: Keyword arguments to pass to the function.
203
+
204
+ Returns:
205
+ TraceResult containing the function result and trace ID.
206
+
207
+ Example:
208
+ result = await trace(
209
+ TraceOptions(name="my-operation", user_id="123"),
210
+ my_async_function,
211
+ arg1, arg2,
212
+ )
213
+ print(f"Result: {result.result}, Trace ID: {result.trace_id}")
214
+ """
215
+ if isinstance(options, str):
216
+ options = TraceOptions(name=options)
217
+
218
+ tracer = otel_trace.get_tracer("agentmark")
219
+
220
+ with tracer.start_as_current_span(options.name) as span:
221
+ _set_agentmark_attributes(span, options)
222
+
223
+ span_ctx = span.get_span_context()
224
+ ctx = TraceContext(
225
+ trace_id=format(span_ctx.trace_id, "032x"),
226
+ span_id=format(span_ctx.span_id, "016x"),
227
+ _span=span,
228
+ _tracer=tracer,
229
+ )
230
+
231
+ try:
232
+ result = await fn(*args, **kwargs)
233
+ span.set_status(StatusCode.OK)
234
+ return TraceResult(result=result, trace_id=ctx.trace_id)
235
+ except Exception as e:
236
+ span.set_status(StatusCode.ERROR, str(e))
237
+ raise
238
+
239
+
240
+ @asynccontextmanager
241
+ async def trace_context(
242
+ options: TraceOptions | str,
243
+ ) -> AsyncIterator[TraceContext]:
244
+ """Create a trace as an async context manager.
245
+
246
+ Alternative API for when you want to use a context manager instead
247
+ of passing a function.
248
+
249
+ Args:
250
+ options: Trace options or just a name string.
251
+
252
+ Yields:
253
+ TraceContext with trace_id, span_id, and utility methods.
254
+
255
+ Example:
256
+ async with trace_context(TraceOptions(name="my-operation")) as ctx:
257
+ print(f"Trace ID: {ctx.trace_id}")
258
+ ctx.set_attribute("custom_key", "value")
259
+ result = await my_async_function()
260
+ """
261
+ if isinstance(options, str):
262
+ options = TraceOptions(name=options)
263
+
264
+ tracer = otel_trace.get_tracer("agentmark")
265
+
266
+ with tracer.start_as_current_span(options.name) as span:
267
+ _set_agentmark_attributes(span, options)
268
+
269
+ span_ctx = span.get_span_context()
270
+ ctx = TraceContext(
271
+ trace_id=format(span_ctx.trace_id, "032x"),
272
+ span_id=format(span_ctx.span_id, "016x"),
273
+ _span=span,
274
+ _tracer=tracer,
275
+ )
276
+
277
+ try:
278
+ yield ctx
279
+ span.set_status(StatusCode.OK)
280
+ except Exception as e:
281
+ span.set_status(StatusCode.ERROR, str(e))
282
+ raise
283
+
284
+
285
+ # ---------------------------------------------------------------------------
286
+ # Renamed aliases (trace → span rename)
287
+ # ---------------------------------------------------------------------------
288
+ SpanOptions = TraceOptions
289
+ SpanContext = TraceContext
290
+ SpanResult = TraceResult
291
+ span = trace
292
+ span_context = trace_context
293
+
294
+
295
+ # ---------------------------------------------------------------------------
296
+ # Sync wrapper for span_context (used by orchestrator in sync code)
297
+ # ---------------------------------------------------------------------------
298
+ from contextlib import contextmanager # noqa: E402
299
+
300
+
301
+ @contextmanager
302
+ def span_context_sync(options: TraceOptions | str):
303
+ """Synchronous context manager wrapper around span_context.
304
+
305
+ Creates an OTEL span without requiring async. The span is started
306
+ and stopped synchronously; the yielded context has the same API
307
+ as the async version (set_attribute, add_event, etc.) but without
308
+ the async child-span helper.
309
+ """
310
+ if isinstance(options, str):
311
+ options = TraceOptions(name=options)
312
+
313
+ tracer = otel_trace.get_tracer("agentmark")
314
+
315
+ with tracer.start_as_current_span(options.name) as otel_span:
316
+ _set_agentmark_attributes(otel_span, options)
317
+
318
+ span_ctx = otel_span.get_span_context()
319
+ ctx = TraceContext(
320
+ trace_id=format(span_ctx.trace_id, "032x"),
321
+ span_id=format(span_ctx.span_id, "016x"),
322
+ _span=otel_span,
323
+ _tracer=tracer,
324
+ )
325
+
326
+ try:
327
+ yield ctx
328
+ otel_span.set_status(StatusCode.OK)
329
+ except Exception as e:
330
+ otel_span.set_status(StatusCode.ERROR, str(e))
331
+ raise
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentmark-sdk
3
+ Version: 0.1.0
4
+ Summary: AgentMark SDK for Python - Tracing and Observability
5
+ Author-email: AgentMark <support@agentmark.co>
6
+ License: MIT
7
+ Keywords: agentmark,llm,observability,opentelemetry,tracing
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: httpx>=0.25
18
+ Requires-Dist: opentelemetry-api>=1.20
19
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.20
20
+ Requires-Dist: opentelemetry-sdk>=1.20
21
+ Provides-Extra: dev
22
+ Requires-Dist: mypy>=1.0; extra == 'dev'
23
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
24
+ Requires-Dist: pytest>=7.0; extra == 'dev'
@@ -0,0 +1,10 @@
1
+ agentmark_sdk/__init__.py,sha256=6QecsmFhFtg6H6S7OphsWXzGQLSL9ehMbDI4js_W6Jk,1637
2
+ agentmark_sdk/config.py,sha256=M2oZBF4vQPTMtSiYJtLwQmUdrXGqq55Jcceo8uNXeNY,300
3
+ agentmark_sdk/decorator.py,sha256=Xvb74xFqnxtOjlvLwidSe576Fl4u1vNjpUB7JYBY7mw,6330
4
+ agentmark_sdk/sampler.py,sha256=bXqhw7ma3mINh7TkLneAeyQz4bYwiJj4TLZJpOsHx5E,1897
5
+ agentmark_sdk/sdk.py,sha256=G2jzdWaWIwcjC6lagilgyN4FwnEYp-HpR3ouuLvDxjY,7319
6
+ agentmark_sdk/serialize.py,sha256=OzzMmYpllnxtYh7Cs-n-Dx3jpyeQMv0wPucUQhjz6dc,1053
7
+ agentmark_sdk/trace.py,sha256=_Z-BUlHiPCAhVihTBQ19FTNDAFqeCEq4isMJ7Ebtn3A,10682
8
+ agentmark_sdk-0.1.0.dist-info/METADATA,sha256=YDmXZxeN63AQj9VoBXWwX6GPqb3boOx6Ph1zzNA1pQc,997
9
+ agentmark_sdk-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
10
+ agentmark_sdk-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any