agentmark-sdk 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.
@@ -0,0 +1,36 @@
1
+ node_modules
2
+ dist
3
+ .specs
4
+ .turbo
5
+ .yarn/install-state.gz
6
+ .yarn/releases/*
7
+ .yalc
8
+ yalc.lock
9
+ .env
10
+ *storybook.log
11
+ storybook-static
12
+ tmp-dev*/
13
+ .claude
14
+
15
+ # Nx
16
+ .nx/cache/
17
+ .nx/workspace-data/
18
+
19
+ # Python
20
+ __pycache__/
21
+ *.py[cod]
22
+ *$py.class
23
+ *.so
24
+ .Python
25
+ .venv/
26
+ *.egg-info/
27
+ .mypy_cache/
28
+ .ruff_cache/
29
+ .pytest_cache/
30
+
31
+ # AgentMark local development config (contains webhook secrets)
32
+ .agentmark/dev-config.json
33
+
34
+ # Speckit
35
+ specs/
36
+ .specify/
@@ -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,15 @@
1
+ {
2
+ "name": "@agentmark-ai/sdk-python",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "postinstall": "node ../../scripts/setup-python-venv.js -e \".[dev]\"",
7
+ "test": "node ../../scripts/run-venv.js pytest",
8
+ "test:watch": "node ../../scripts/run-venv.js pytest --watch",
9
+ "lint": "node ../../scripts/run-venv.js ruff check src tests || echo Skipping lint: venv not set up",
10
+ "lint:fix": "node ../../scripts/run-venv.js ruff check --fix src tests",
11
+ "typecheck": "node ../../scripts/run-venv.js mypy src --strict",
12
+ "format": "node ../../scripts/run-venv.js ruff format src tests",
13
+ "clean": "shx rm -rf .venv dist *.egg-info .pytest_cache .mypy_cache .ruff_cache"
14
+ }
15
+ }
@@ -0,0 +1,50 @@
1
+ [project]
2
+ name = "agentmark-sdk"
3
+ version = "0.1.0"
4
+ description = "AgentMark SDK for Python - Tracing and Observability"
5
+ requires-python = ">=3.10"
6
+ license = { text = "MIT" }
7
+ authors = [{ name = "AgentMark", email = "support@agentmark.co" }]
8
+ keywords = ["agentmark", "llm", "tracing", "observability", "opentelemetry"]
9
+ classifiers = [
10
+ "Development Status :: 3 - Alpha",
11
+ "Intended Audience :: Developers",
12
+ "License :: OSI Approved :: MIT License",
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3.10",
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Topic :: Software Development :: Libraries :: Python Modules",
18
+ ]
19
+ dependencies = [
20
+ "opentelemetry-api>=1.20",
21
+ "opentelemetry-sdk>=1.20",
22
+ "opentelemetry-exporter-otlp-proto-http>=1.20",
23
+ "httpx>=0.25",
24
+ ]
25
+
26
+ [project.optional-dependencies]
27
+ dev = [
28
+ "pytest>=7.0",
29
+ "pytest-asyncio>=0.21",
30
+ "mypy>=1.0",
31
+ ]
32
+
33
+ [build-system]
34
+ requires = ["hatchling"]
35
+ build-backend = "hatchling.build"
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ packages = ["src/agentmark_sdk"]
39
+
40
+ [tool.pytest.ini_options]
41
+ asyncio_mode = "auto"
42
+ asyncio_default_fixture_loop_scope = "function"
43
+ testpaths = ["tests"]
44
+ python_files = ["test_*.py"]
45
+ python_functions = ["test_*"]
46
+ addopts = "-v --tb=short"
47
+
48
+ [tool.mypy]
49
+ strict = true
50
+ python_version = "3.10"
@@ -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"