mantisdk 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.
Potentially problematic release.
This version of mantisdk might be problematic. Click here for more details.
- mantisdk/__init__.py +22 -0
- mantisdk/adapter/__init__.py +15 -0
- mantisdk/adapter/base.py +94 -0
- mantisdk/adapter/messages.py +270 -0
- mantisdk/adapter/triplet.py +1028 -0
- mantisdk/algorithm/__init__.py +39 -0
- mantisdk/algorithm/apo/__init__.py +5 -0
- mantisdk/algorithm/apo/apo.py +889 -0
- mantisdk/algorithm/apo/prompts/apply_edit_variant01.poml +22 -0
- mantisdk/algorithm/apo/prompts/apply_edit_variant02.poml +18 -0
- mantisdk/algorithm/apo/prompts/text_gradient_variant01.poml +18 -0
- mantisdk/algorithm/apo/prompts/text_gradient_variant02.poml +16 -0
- mantisdk/algorithm/apo/prompts/text_gradient_variant03.poml +107 -0
- mantisdk/algorithm/base.py +162 -0
- mantisdk/algorithm/decorator.py +264 -0
- mantisdk/algorithm/fast.py +250 -0
- mantisdk/algorithm/gepa/__init__.py +59 -0
- mantisdk/algorithm/gepa/adapter.py +459 -0
- mantisdk/algorithm/gepa/gepa.py +364 -0
- mantisdk/algorithm/gepa/lib/__init__.py +18 -0
- mantisdk/algorithm/gepa/lib/adapters/README.md +12 -0
- mantisdk/algorithm/gepa/lib/adapters/__init__.py +0 -0
- mantisdk/algorithm/gepa/lib/adapters/anymaths_adapter/README.md +341 -0
- mantisdk/algorithm/gepa/lib/adapters/anymaths_adapter/__init__.py +1 -0
- mantisdk/algorithm/gepa/lib/adapters/anymaths_adapter/anymaths_adapter.py +174 -0
- mantisdk/algorithm/gepa/lib/adapters/anymaths_adapter/requirements.txt +1 -0
- mantisdk/algorithm/gepa/lib/adapters/default_adapter/README.md +0 -0
- mantisdk/algorithm/gepa/lib/adapters/default_adapter/__init__.py +0 -0
- mantisdk/algorithm/gepa/lib/adapters/default_adapter/default_adapter.py +209 -0
- mantisdk/algorithm/gepa/lib/adapters/dspy_adapter/README.md +7 -0
- mantisdk/algorithm/gepa/lib/adapters/dspy_adapter/__init__.py +0 -0
- mantisdk/algorithm/gepa/lib/adapters/dspy_adapter/dspy_adapter.py +307 -0
- mantisdk/algorithm/gepa/lib/adapters/dspy_full_program_adapter/README.md +99 -0
- mantisdk/algorithm/gepa/lib/adapters/dspy_full_program_adapter/dspy_program_proposal_signature.py +137 -0
- mantisdk/algorithm/gepa/lib/adapters/dspy_full_program_adapter/full_program_adapter.py +266 -0
- mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/GEPA_RAG.md +621 -0
- mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/__init__.py +56 -0
- mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/evaluation_metrics.py +226 -0
- mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/generic_rag_adapter.py +496 -0
- mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/rag_pipeline.py +238 -0
- mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_store_interface.py +212 -0
- mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/__init__.py +2 -0
- mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/chroma_store.py +196 -0
- mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/lancedb_store.py +422 -0
- mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/milvus_store.py +409 -0
- mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/qdrant_store.py +368 -0
- mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/weaviate_store.py +418 -0
- mantisdk/algorithm/gepa/lib/adapters/mcp_adapter/README.md +552 -0
- mantisdk/algorithm/gepa/lib/adapters/mcp_adapter/__init__.py +37 -0
- mantisdk/algorithm/gepa/lib/adapters/mcp_adapter/mcp_adapter.py +705 -0
- mantisdk/algorithm/gepa/lib/adapters/mcp_adapter/mcp_client.py +364 -0
- mantisdk/algorithm/gepa/lib/adapters/terminal_bench_adapter/README.md +9 -0
- mantisdk/algorithm/gepa/lib/adapters/terminal_bench_adapter/__init__.py +0 -0
- mantisdk/algorithm/gepa/lib/adapters/terminal_bench_adapter/terminal_bench_adapter.py +217 -0
- mantisdk/algorithm/gepa/lib/api.py +375 -0
- mantisdk/algorithm/gepa/lib/core/__init__.py +0 -0
- mantisdk/algorithm/gepa/lib/core/adapter.py +180 -0
- mantisdk/algorithm/gepa/lib/core/data_loader.py +74 -0
- mantisdk/algorithm/gepa/lib/core/engine.py +356 -0
- mantisdk/algorithm/gepa/lib/core/result.py +233 -0
- mantisdk/algorithm/gepa/lib/core/state.py +636 -0
- mantisdk/algorithm/gepa/lib/examples/__init__.py +0 -0
- mantisdk/algorithm/gepa/lib/examples/aime.py +24 -0
- mantisdk/algorithm/gepa/lib/examples/anymaths-bench/eval_default.py +111 -0
- mantisdk/algorithm/gepa/lib/examples/anymaths-bench/prompt-templates/instruction_prompt.txt +9 -0
- mantisdk/algorithm/gepa/lib/examples/anymaths-bench/prompt-templates/optimal_prompt.txt +24 -0
- mantisdk/algorithm/gepa/lib/examples/anymaths-bench/train_anymaths.py +177 -0
- mantisdk/algorithm/gepa/lib/examples/dspy_full_program_evolution/arc_agi.ipynb +25705 -0
- mantisdk/algorithm/gepa/lib/examples/dspy_full_program_evolution/example.ipynb +348 -0
- mantisdk/algorithm/gepa/lib/examples/mcp_adapter/__init__.py +4 -0
- mantisdk/algorithm/gepa/lib/examples/mcp_adapter/mcp_optimization_example.py +455 -0
- mantisdk/algorithm/gepa/lib/examples/rag_adapter/RAG_GUIDE.md +613 -0
- mantisdk/algorithm/gepa/lib/examples/rag_adapter/__init__.py +9 -0
- mantisdk/algorithm/gepa/lib/examples/rag_adapter/rag_optimization.py +824 -0
- mantisdk/algorithm/gepa/lib/examples/rag_adapter/requirements-rag.txt +29 -0
- mantisdk/algorithm/gepa/lib/examples/terminal-bench/prompt-templates/instruction_prompt.txt +16 -0
- mantisdk/algorithm/gepa/lib/examples/terminal-bench/prompt-templates/terminus.txt +9 -0
- mantisdk/algorithm/gepa/lib/examples/terminal-bench/train_terminus.py +161 -0
- mantisdk/algorithm/gepa/lib/gepa_utils.py +117 -0
- mantisdk/algorithm/gepa/lib/logging/__init__.py +0 -0
- mantisdk/algorithm/gepa/lib/logging/experiment_tracker.py +187 -0
- mantisdk/algorithm/gepa/lib/logging/logger.py +75 -0
- mantisdk/algorithm/gepa/lib/logging/utils.py +103 -0
- mantisdk/algorithm/gepa/lib/proposer/__init__.py +0 -0
- mantisdk/algorithm/gepa/lib/proposer/base.py +31 -0
- mantisdk/algorithm/gepa/lib/proposer/merge.py +357 -0
- mantisdk/algorithm/gepa/lib/proposer/reflective_mutation/__init__.py +0 -0
- mantisdk/algorithm/gepa/lib/proposer/reflective_mutation/base.py +49 -0
- mantisdk/algorithm/gepa/lib/proposer/reflective_mutation/reflective_mutation.py +176 -0
- mantisdk/algorithm/gepa/lib/py.typed +0 -0
- mantisdk/algorithm/gepa/lib/strategies/__init__.py +0 -0
- mantisdk/algorithm/gepa/lib/strategies/batch_sampler.py +77 -0
- mantisdk/algorithm/gepa/lib/strategies/candidate_selector.py +50 -0
- mantisdk/algorithm/gepa/lib/strategies/component_selector.py +36 -0
- mantisdk/algorithm/gepa/lib/strategies/eval_policy.py +64 -0
- mantisdk/algorithm/gepa/lib/strategies/instruction_proposal.py +127 -0
- mantisdk/algorithm/gepa/lib/utils/__init__.py +10 -0
- mantisdk/algorithm/gepa/lib/utils/stop_condition.py +196 -0
- mantisdk/algorithm/gepa/tracing.py +105 -0
- mantisdk/algorithm/utils.py +177 -0
- mantisdk/algorithm/verl/__init__.py +5 -0
- mantisdk/algorithm/verl/interface.py +202 -0
- mantisdk/cli/__init__.py +56 -0
- mantisdk/cli/prometheus.py +115 -0
- mantisdk/cli/store.py +131 -0
- mantisdk/cli/vllm.py +29 -0
- mantisdk/client.py +408 -0
- mantisdk/config.py +348 -0
- mantisdk/emitter/__init__.py +43 -0
- mantisdk/emitter/annotation.py +370 -0
- mantisdk/emitter/exception.py +54 -0
- mantisdk/emitter/message.py +61 -0
- mantisdk/emitter/object.py +117 -0
- mantisdk/emitter/reward.py +320 -0
- mantisdk/env_var.py +156 -0
- mantisdk/execution/__init__.py +15 -0
- mantisdk/execution/base.py +64 -0
- mantisdk/execution/client_server.py +443 -0
- mantisdk/execution/events.py +69 -0
- mantisdk/execution/inter_process.py +16 -0
- mantisdk/execution/shared_memory.py +282 -0
- mantisdk/instrumentation/__init__.py +119 -0
- mantisdk/instrumentation/agentops.py +314 -0
- mantisdk/instrumentation/agentops_langchain.py +45 -0
- mantisdk/instrumentation/litellm.py +83 -0
- mantisdk/instrumentation/vllm.py +81 -0
- mantisdk/instrumentation/weave.py +500 -0
- mantisdk/litagent/__init__.py +11 -0
- mantisdk/litagent/decorator.py +536 -0
- mantisdk/litagent/litagent.py +252 -0
- mantisdk/llm_proxy.py +1890 -0
- mantisdk/logging.py +370 -0
- mantisdk/reward.py +7 -0
- mantisdk/runner/__init__.py +11 -0
- mantisdk/runner/agent.py +845 -0
- mantisdk/runner/base.py +182 -0
- mantisdk/runner/legacy.py +309 -0
- mantisdk/semconv.py +170 -0
- mantisdk/server.py +401 -0
- mantisdk/store/__init__.py +23 -0
- mantisdk/store/base.py +897 -0
- mantisdk/store/client_server.py +2092 -0
- mantisdk/store/collection/__init__.py +30 -0
- mantisdk/store/collection/base.py +587 -0
- mantisdk/store/collection/memory.py +970 -0
- mantisdk/store/collection/mongo.py +1412 -0
- mantisdk/store/collection_based.py +1823 -0
- mantisdk/store/insight.py +648 -0
- mantisdk/store/listener.py +58 -0
- mantisdk/store/memory.py +396 -0
- mantisdk/store/mongo.py +165 -0
- mantisdk/store/sqlite.py +3 -0
- mantisdk/store/threading.py +357 -0
- mantisdk/store/utils.py +142 -0
- mantisdk/tracer/__init__.py +16 -0
- mantisdk/tracer/agentops.py +242 -0
- mantisdk/tracer/base.py +287 -0
- mantisdk/tracer/dummy.py +106 -0
- mantisdk/tracer/otel.py +555 -0
- mantisdk/tracer/weave.py +677 -0
- mantisdk/trainer/__init__.py +6 -0
- mantisdk/trainer/init_utils.py +263 -0
- mantisdk/trainer/legacy.py +367 -0
- mantisdk/trainer/registry.py +12 -0
- mantisdk/trainer/trainer.py +618 -0
- mantisdk/types/__init__.py +6 -0
- mantisdk/types/core.py +553 -0
- mantisdk/types/resources.py +204 -0
- mantisdk/types/tracer.py +515 -0
- mantisdk/types/tracing.py +218 -0
- mantisdk/utils/__init__.py +1 -0
- mantisdk/utils/id.py +18 -0
- mantisdk/utils/metrics.py +1025 -0
- mantisdk/utils/otel.py +578 -0
- mantisdk/utils/otlp.py +536 -0
- mantisdk/utils/server_launcher.py +1045 -0
- mantisdk/utils/system_snapshot.py +81 -0
- mantisdk/verl/__init__.py +8 -0
- mantisdk/verl/__main__.py +6 -0
- mantisdk/verl/async_server.py +46 -0
- mantisdk/verl/config.yaml +27 -0
- mantisdk/verl/daemon.py +1154 -0
- mantisdk/verl/dataset.py +44 -0
- mantisdk/verl/entrypoint.py +248 -0
- mantisdk/verl/trainer.py +549 -0
- mantisdk-0.1.0.dist-info/METADATA +119 -0
- mantisdk-0.1.0.dist-info/RECORD +190 -0
- mantisdk-0.1.0.dist-info/WHEEL +4 -0
- mantisdk-0.1.0.dist-info/entry_points.txt +2 -0
- mantisdk-0.1.0.dist-info/licenses/LICENSE +19 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Helpers for emitting annotation/operation spans."""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import functools
|
|
7
|
+
import inspect
|
|
8
|
+
import logging
|
|
9
|
+
from types import TracebackType
|
|
10
|
+
from typing import (
|
|
11
|
+
Any,
|
|
12
|
+
Callable,
|
|
13
|
+
ContextManager,
|
|
14
|
+
Dict,
|
|
15
|
+
Optional,
|
|
16
|
+
Tuple,
|
|
17
|
+
Type,
|
|
18
|
+
TypeVar,
|
|
19
|
+
Union,
|
|
20
|
+
cast,
|
|
21
|
+
overload,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
from mantisdk.semconv import AGL_ANNOTATION, AGL_OPERATION, LightningSpanAttributes
|
|
25
|
+
from mantisdk.tracer.base import get_active_tracer
|
|
26
|
+
from mantisdk.tracer.dummy import DummyTracer
|
|
27
|
+
from mantisdk.types import SpanCoreFields, SpanRecordingContext, TraceStatus
|
|
28
|
+
from mantisdk.utils.otel import check_attributes_sanity, flatten_attributes, sanitize_attributes
|
|
29
|
+
|
|
30
|
+
_FnType = TypeVar("_FnType", bound=Callable[..., Any])
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def emit_annotation(annotation: Dict[str, Any], propagate: bool = True) -> SpanCoreFields:
|
|
36
|
+
"""Emit a new annotation span.
|
|
37
|
+
|
|
38
|
+
This is the underlying implementation of [`emit_reward`][mantisdk.emit_reward].
|
|
39
|
+
|
|
40
|
+
Annotation spans are used to annotate a specific event or a part of rollout.
|
|
41
|
+
See [semconv][mantisdk.semconv] for conventional annotation keys in Mantisdk.
|
|
42
|
+
|
|
43
|
+
If annotations contain nested dicts, they will be flattened before emitting.
|
|
44
|
+
Complex objects will lead to emitting failures.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
annotation: Dictionary containing annotation key-value pairs.
|
|
48
|
+
Representatives are rewards, tags, and metadata.
|
|
49
|
+
propagate: Whether to propagate the span to tracers automatically.
|
|
50
|
+
"""
|
|
51
|
+
annotation_attributes = flatten_attributes(annotation, expand_leaf_lists=False)
|
|
52
|
+
check_attributes_sanity(annotation_attributes)
|
|
53
|
+
sanitized_attributes = sanitize_attributes(annotation_attributes)
|
|
54
|
+
logger.debug("Emitting annotation span with keys %s", sanitized_attributes.keys())
|
|
55
|
+
|
|
56
|
+
if propagate:
|
|
57
|
+
tracer = get_active_tracer()
|
|
58
|
+
if tracer is None:
|
|
59
|
+
raise RuntimeError("No active tracer found. Cannot emit annotation span.")
|
|
60
|
+
else:
|
|
61
|
+
tracer = DummyTracer()
|
|
62
|
+
|
|
63
|
+
return tracer.create_span(
|
|
64
|
+
name=AGL_ANNOTATION,
|
|
65
|
+
attributes=sanitized_attributes,
|
|
66
|
+
status=TraceStatus(status_code="OK"),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class OperationContext:
|
|
71
|
+
"""Context manager and decorator for tracing operations.
|
|
72
|
+
|
|
73
|
+
This class manages a tracer-backed span for a logical unit of work. It can be
|
|
74
|
+
used either:
|
|
75
|
+
|
|
76
|
+
* As a decorator, in which case inputs and outputs are inferred
|
|
77
|
+
automatically from the wrapped function's signature.
|
|
78
|
+
* As a context manager, in which case inputs and outputs can be recorded
|
|
79
|
+
explicitly via [`set_input`][mantisdk.emitter.annotation.OperationContext.set_input]
|
|
80
|
+
and [`set_output`][mantisdk.emitter.annotation.OperationContext.set_output].
|
|
81
|
+
|
|
82
|
+
Attributes:
|
|
83
|
+
name: Human-readable span name.
|
|
84
|
+
initial_attributes: Attributes applied when the span is created.
|
|
85
|
+
tracer: Tracer implementation used to create spans.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def __init__(self, name: str, attributes: Dict[str, Any], propagate: bool = True) -> None:
|
|
89
|
+
"""Initialize a new operation context.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
name: Human-readable name of the span.
|
|
93
|
+
attributes: Initial attributes attached to the span. Values are
|
|
94
|
+
JSON-serialized where necessary.
|
|
95
|
+
propagate: Whether the span should be sent to active exporters.
|
|
96
|
+
"""
|
|
97
|
+
self.name = name
|
|
98
|
+
self.initial_attributes = flatten_attributes(attributes, expand_leaf_lists=False)
|
|
99
|
+
self.propagate = propagate
|
|
100
|
+
if propagate:
|
|
101
|
+
tracer = get_active_tracer()
|
|
102
|
+
if tracer is None:
|
|
103
|
+
raise RuntimeError("No active tracer found. Cannot trace operation spans.")
|
|
104
|
+
self.tracer = tracer
|
|
105
|
+
else:
|
|
106
|
+
self.tracer = DummyTracer()
|
|
107
|
+
self._ctx_manager: Optional[ContextManager[SpanRecordingContext]] = None
|
|
108
|
+
self._recording_context: Optional[SpanRecordingContext] = None
|
|
109
|
+
self._span: Optional[SpanCoreFields] = None
|
|
110
|
+
|
|
111
|
+
def __enter__(self) -> "OperationContext":
|
|
112
|
+
"""Enter the context manager and start a new span.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
The current :class:`OperationContext` instance with an active span.
|
|
116
|
+
"""
|
|
117
|
+
sanitized_attrs = sanitize_attributes(self.initial_attributes)
|
|
118
|
+
self._ctx_manager = self.tracer.operation_context(self.name, attributes=sanitized_attrs)
|
|
119
|
+
recording_context = self._ctx_manager.__enter__()
|
|
120
|
+
self._recording_context = recording_context
|
|
121
|
+
return self
|
|
122
|
+
|
|
123
|
+
def __exit__(
|
|
124
|
+
self,
|
|
125
|
+
exc_type: Optional[Type[BaseException]],
|
|
126
|
+
exc_val: Optional[BaseException],
|
|
127
|
+
exc_tb: Optional[TracebackType],
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Exit the context manager and finish the span."""
|
|
130
|
+
if self._ctx_manager:
|
|
131
|
+
self._ctx_manager.__exit__(exc_type, exc_val, exc_tb)
|
|
132
|
+
if self._recording_context:
|
|
133
|
+
self._span = self._recording_context.get_recorded_span()
|
|
134
|
+
self._ctx_manager = None
|
|
135
|
+
self._recording_context = None
|
|
136
|
+
|
|
137
|
+
def span(self) -> SpanCoreFields:
|
|
138
|
+
"""Get the span that was created by this context manager."""
|
|
139
|
+
if self._span is None:
|
|
140
|
+
raise RuntimeError("Span is not ready yet.")
|
|
141
|
+
return self._span
|
|
142
|
+
|
|
143
|
+
def set_input(self, *args: Any, **kwargs: Any) -> None:
|
|
144
|
+
"""Record input arguments on the current span.
|
|
145
|
+
|
|
146
|
+
Positional arguments are stored under the `input.args.<index>` attributes,
|
|
147
|
+
and keyword arguments are stored under `input.<name>` attributes.
|
|
148
|
+
|
|
149
|
+
This is intended for use inside a `with operation(...) as op` block.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
*args: Positional arguments to record.
|
|
153
|
+
**kwargs: Keyword arguments to record.
|
|
154
|
+
"""
|
|
155
|
+
if not self._recording_context:
|
|
156
|
+
raise RuntimeError("No recording context found. Cannot set input.")
|
|
157
|
+
|
|
158
|
+
prefix = LightningSpanAttributes.OPERATION_INPUT.value
|
|
159
|
+
attributes: Dict[str, Any] = {}
|
|
160
|
+
if args:
|
|
161
|
+
for idx, value in enumerate(args):
|
|
162
|
+
flattened = flatten_attributes({str(idx): value})
|
|
163
|
+
for nested_key, nested_value in flattened.items():
|
|
164
|
+
attributes[f"{prefix}.args.{nested_key}"] = nested_value
|
|
165
|
+
if kwargs:
|
|
166
|
+
for key, value in kwargs.items():
|
|
167
|
+
flattened = flatten_attributes({key: value})
|
|
168
|
+
for nested_key, nested_value in flattened.items():
|
|
169
|
+
attributes[f"{prefix}.{nested_key}"] = nested_value
|
|
170
|
+
if attributes:
|
|
171
|
+
self._recording_context.record_attributes(sanitize_attributes(attributes))
|
|
172
|
+
|
|
173
|
+
def set_output(self, output: Any) -> None:
|
|
174
|
+
"""Record the output value on the current span.
|
|
175
|
+
|
|
176
|
+
This is intended for use inside a `with operation(...) as op` block.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
output: The output value to record.
|
|
180
|
+
"""
|
|
181
|
+
if not self._recording_context:
|
|
182
|
+
raise RuntimeError("No recording context found. Cannot set output.")
|
|
183
|
+
|
|
184
|
+
flattened = flatten_attributes({LightningSpanAttributes.OPERATION_OUTPUT.value: output})
|
|
185
|
+
self._recording_context.record_attributes(sanitize_attributes(flattened))
|
|
186
|
+
|
|
187
|
+
def __call__(self, fn: _FnType) -> _FnType:
|
|
188
|
+
"""Wrap a callable so its execution is traced in a span.
|
|
189
|
+
|
|
190
|
+
When used as a decorator, a new span is created for each call to
|
|
191
|
+
the wrapped function. The bound arguments are recorded as input
|
|
192
|
+
attributes, the return value is recorded as an output attribute,
|
|
193
|
+
and any exception is recorded and marks the span as an error.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
fn: The function or coroutine function to wrap.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
The wrapped callable.
|
|
200
|
+
"""
|
|
201
|
+
function_name = fn.__name__
|
|
202
|
+
|
|
203
|
+
sig = inspect.signature(fn)
|
|
204
|
+
|
|
205
|
+
sanitized_init_attrs = sanitize_attributes(
|
|
206
|
+
{LightningSpanAttributes.OPERATION_NAME.value: function_name, **self.initial_attributes}
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
def _record_auto_inputs(
|
|
210
|
+
recording_ctx: SpanRecordingContext, args: Tuple[Any, ...], kwargs: Dict[str, Any]
|
|
211
|
+
) -> None:
|
|
212
|
+
"""Bind arguments to signature and log them on the span."""
|
|
213
|
+
attributes: Dict[str, Any] = {}
|
|
214
|
+
try:
|
|
215
|
+
bound = sig.bind(*args, **kwargs)
|
|
216
|
+
bound.apply_defaults()
|
|
217
|
+
for name, value in bound.arguments.items():
|
|
218
|
+
parameter = sig.parameters.get(name)
|
|
219
|
+
if parameter and parameter.kind is inspect.Parameter.VAR_POSITIONAL:
|
|
220
|
+
attr_prefix = f"{LightningSpanAttributes.OPERATION_INPUT.value}.{name}"
|
|
221
|
+
for idx, item in enumerate(value):
|
|
222
|
+
flattened = flatten_attributes({str(idx): item})
|
|
223
|
+
for nested_key, nested_value in flattened.items():
|
|
224
|
+
attributes[f"{attr_prefix}.{nested_key}"] = nested_value
|
|
225
|
+
else:
|
|
226
|
+
flattened = flatten_attributes({name: value})
|
|
227
|
+
for nested_key, nested_value in flattened.items():
|
|
228
|
+
attributes[f"{LightningSpanAttributes.OPERATION_INPUT.value}.{nested_key}"] = nested_value
|
|
229
|
+
except Exception:
|
|
230
|
+
if args:
|
|
231
|
+
for idx, value in enumerate(args):
|
|
232
|
+
flattened = flatten_attributes({str(idx): value})
|
|
233
|
+
for nested_key, nested_value in flattened.items():
|
|
234
|
+
attributes[f"{LightningSpanAttributes.OPERATION_INPUT.value}.args.{nested_key}"] = (
|
|
235
|
+
nested_value
|
|
236
|
+
)
|
|
237
|
+
if kwargs:
|
|
238
|
+
flattened = flatten_attributes({"kwargs": kwargs})
|
|
239
|
+
for nested_key, nested_value in flattened.items():
|
|
240
|
+
attributes[f"{LightningSpanAttributes.OPERATION_INPUT.value}.{nested_key}"] = nested_value
|
|
241
|
+
if attributes:
|
|
242
|
+
recording_ctx.record_attributes(sanitize_attributes(attributes))
|
|
243
|
+
|
|
244
|
+
def _record_auto_outputs(recording_ctx: SpanRecordingContext, result: Any) -> None:
|
|
245
|
+
"""Record the output value on the span."""
|
|
246
|
+
flattened = flatten_attributes({LightningSpanAttributes.OPERATION_OUTPUT.value: result})
|
|
247
|
+
recording_ctx.record_attributes(sanitize_attributes(flattened))
|
|
248
|
+
|
|
249
|
+
if asyncio.iscoroutinefunction(fn) or inspect.iscoroutinefunction(fn):
|
|
250
|
+
|
|
251
|
+
@functools.wraps(fn)
|
|
252
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
253
|
+
"""Async wrapper that traces the wrapped coroutine."""
|
|
254
|
+
with self.tracer.operation_context(self.name, attributes=sanitized_init_attrs) as recording_ctx:
|
|
255
|
+
_record_auto_inputs(recording_ctx, args, kwargs)
|
|
256
|
+
result = await fn(*args, **kwargs)
|
|
257
|
+
_record_auto_outputs(recording_ctx, result)
|
|
258
|
+
return result
|
|
259
|
+
|
|
260
|
+
return cast(_FnType, async_wrapper)
|
|
261
|
+
|
|
262
|
+
else:
|
|
263
|
+
|
|
264
|
+
@functools.wraps(fn)
|
|
265
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
266
|
+
"""Sync wrapper that traces the wrapped callable."""
|
|
267
|
+
with self.tracer.operation_context(self.name, attributes=sanitized_init_attrs) as recording_ctx:
|
|
268
|
+
_record_auto_inputs(recording_ctx, args, kwargs)
|
|
269
|
+
result = fn(*args, **kwargs)
|
|
270
|
+
_record_auto_outputs(recording_ctx, result)
|
|
271
|
+
return result
|
|
272
|
+
|
|
273
|
+
return cast(_FnType, sync_wrapper)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@overload
|
|
277
|
+
def operation(
|
|
278
|
+
fn: _FnType, *, propagate: bool = True, name: Optional[str] = None, **additional_attributes: Any
|
|
279
|
+
) -> _FnType: ...
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@overload
|
|
283
|
+
def operation(
|
|
284
|
+
*, propagate: bool = True, name: Optional[str] = None, **additional_attributes: Any
|
|
285
|
+
) -> OperationContext: ...
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@overload
|
|
289
|
+
def operation(fn: _FnType, *, name: Optional[str] = None, **additional_attributes: Any) -> _FnType: ...
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@overload
|
|
293
|
+
def operation(*, name: Optional[str] = None, **additional_attributes: Any) -> OperationContext: ...
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@overload
|
|
297
|
+
def operation(fn: _FnType, **additional_attributes: Any) -> _FnType: ...
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@overload
|
|
301
|
+
def operation(**additional_attributes: Any) -> OperationContext: ...
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def operation(
|
|
305
|
+
fn: Optional[_FnType] = None,
|
|
306
|
+
*,
|
|
307
|
+
propagate: bool = True,
|
|
308
|
+
name: Optional[str] = None,
|
|
309
|
+
**additional_attributes: Any,
|
|
310
|
+
) -> Union[_FnType, OperationContext]:
|
|
311
|
+
"""Entry point for tracking operations.
|
|
312
|
+
|
|
313
|
+
This helper can be used either as a decorator or as a context manager.
|
|
314
|
+
The span name is fixed to [`AGL_OPERATION`][mantisdk.semconv.AGL_OPERATION];
|
|
315
|
+
custom span names are not supported. Any keyword arguments are recorded as span attributes.
|
|
316
|
+
|
|
317
|
+
Usage as a decorator:
|
|
318
|
+
|
|
319
|
+
```python
|
|
320
|
+
@operation
|
|
321
|
+
def func(...):
|
|
322
|
+
...
|
|
323
|
+
|
|
324
|
+
@operation(category="compute")
|
|
325
|
+
def func(...):
|
|
326
|
+
...
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Usage as a context manager:
|
|
330
|
+
|
|
331
|
+
```python
|
|
332
|
+
with operation(user_id=123) as op:
|
|
333
|
+
op.set_input(data=data)
|
|
334
|
+
# ... do work ...
|
|
335
|
+
op.set_output(result)
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
fn: When used as `@operation`, this is the wrapped function.
|
|
340
|
+
When used as `operation(**attrs)`, this should be omitted (or
|
|
341
|
+
left as `None`) and only keyword attributes are provided.
|
|
342
|
+
propagate: Whether spans should use the active span processor. When False,
|
|
343
|
+
spans will stay local and not be exported.
|
|
344
|
+
name: Optional alias that populates
|
|
345
|
+
[`LightningSpanAttributes.OPERATION_NAME`][mantisdk.semconv.LightningSpanAttributes.OPERATION_NAME]
|
|
346
|
+
when `additional_attributes` does not already define it.
|
|
347
|
+
**additional_attributes: Additional span attributes to attach at
|
|
348
|
+
creation time.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Either a wrapped callable (when used as a decorator) or an
|
|
352
|
+
[`OperationContext`][mantisdk.emitter.annotation.OperationContext]
|
|
353
|
+
(when used as a context manager factory).
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
if name is not None:
|
|
357
|
+
if LightningSpanAttributes.OPERATION_NAME.value in additional_attributes:
|
|
358
|
+
raise ValueError("Cannot specify both `name` and `additional_attributes.operation_name`.")
|
|
359
|
+
additional_attributes[LightningSpanAttributes.OPERATION_NAME.value] = name
|
|
360
|
+
|
|
361
|
+
# Case 1: Used as @operation (bare decorator or with attributes)
|
|
362
|
+
if callable(fn):
|
|
363
|
+
# Create context with fixed name, then immediately wrap the function
|
|
364
|
+
return OperationContext(AGL_OPERATION, additional_attributes, propagate=propagate)(fn)
|
|
365
|
+
|
|
366
|
+
# Case 2: Used as operation(...) / with operation(...)
|
|
367
|
+
# Custom span names are intentionally not supported; use AGL_OPERATION.
|
|
368
|
+
if fn is not None:
|
|
369
|
+
raise ValueError("Custom span names are intentionally not supported when used as a context manager.")
|
|
370
|
+
return OperationContext(AGL_OPERATION, additional_attributes, propagate=propagate)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
from mantisdk.semconv import AGL_EXCEPTION
|
|
7
|
+
from mantisdk.tracer.base import get_active_tracer
|
|
8
|
+
from mantisdk.tracer.dummy import DummyTracer
|
|
9
|
+
from mantisdk.types import TraceStatus
|
|
10
|
+
from mantisdk.utils.otel import flatten_attributes, format_exception_attributes, sanitize_attributes
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def emit_exception(
|
|
16
|
+
exception: BaseException, attributes: Optional[Dict[str, Any]] = None, propagate: bool = True
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Record an exception with OpenTelemetry metadata.
|
|
19
|
+
|
|
20
|
+
Classic OpenTelemetry records exceptions in a dedicated logging service.
|
|
21
|
+
We simplify the model and use trace spans to record exceptions as well.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
exception: Raised exception instance to serialize into telemetry attributes.
|
|
25
|
+
attributes: Additional attributes to attach to the exception span.
|
|
26
|
+
propagate: Whether to propagate the span to exporters automatically.
|
|
27
|
+
|
|
28
|
+
!!! note
|
|
29
|
+
|
|
30
|
+
The helper validates its input. If a non-exception value is provided,
|
|
31
|
+
a TypeError is raised to indicate a programming mistake.
|
|
32
|
+
"""
|
|
33
|
+
if not isinstance(exception, BaseException): # type: ignore
|
|
34
|
+
raise TypeError(f"Expected a BaseException instance, got: {type(exception)}.")
|
|
35
|
+
span_attributes = format_exception_attributes(exception)
|
|
36
|
+
|
|
37
|
+
if attributes:
|
|
38
|
+
flattened = flatten_attributes(attributes, expand_leaf_lists=False)
|
|
39
|
+
span_attributes.update(sanitize_attributes(flattened))
|
|
40
|
+
|
|
41
|
+
logger.debug("Emitting exception span for %s", type(exception).__name__)
|
|
42
|
+
|
|
43
|
+
if propagate:
|
|
44
|
+
tracer = get_active_tracer()
|
|
45
|
+
if tracer is None:
|
|
46
|
+
raise RuntimeError("No active tracer found. Cannot emit exception span.")
|
|
47
|
+
else:
|
|
48
|
+
tracer = DummyTracer()
|
|
49
|
+
tracer.create_span(
|
|
50
|
+
AGL_EXCEPTION,
|
|
51
|
+
attributes=span_attributes,
|
|
52
|
+
# The exception span is successful by itself.
|
|
53
|
+
status=TraceStatus(status_code="OK"),
|
|
54
|
+
)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
from mantisdk.semconv import AGL_MESSAGE, LightningSpanAttributes
|
|
7
|
+
from mantisdk.tracer.base import get_active_tracer
|
|
8
|
+
from mantisdk.tracer.dummy import DummyTracer
|
|
9
|
+
from mantisdk.types import Attributes, SpanLike
|
|
10
|
+
from mantisdk.utils.otel import flatten_attributes, sanitize_attributes
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def emit_message(message: str, attributes: Optional[Dict[str, Any]] = None, propagate: bool = True) -> None:
|
|
16
|
+
"""Emit a textual message as an OpenTelemetry span.
|
|
17
|
+
|
|
18
|
+
Commonly used for sending debugging and logging messages.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
message: Human readable message to attach as a span attribute.
|
|
22
|
+
attributes: Additional attributes to attach to the message span.
|
|
23
|
+
propagate: Whether to propagate the span to exporters automatically.
|
|
24
|
+
|
|
25
|
+
!!! note
|
|
26
|
+
OpenTelemetry distinguishes between logs and spans. Emitting the message as a
|
|
27
|
+
span keeps all Mantisdk telemetry in a single data store for analysis.
|
|
28
|
+
"""
|
|
29
|
+
if not isinstance(message, str): # type: ignore
|
|
30
|
+
raise TypeError(f"Message must be a string or list of strings, got: {type(message)}.")
|
|
31
|
+
|
|
32
|
+
if propagate:
|
|
33
|
+
tracer = get_active_tracer()
|
|
34
|
+
if tracer is None:
|
|
35
|
+
raise RuntimeError("No active tracer found. Cannot emit message span.")
|
|
36
|
+
else:
|
|
37
|
+
tracer = DummyTracer()
|
|
38
|
+
span_attributes: Attributes = {LightningSpanAttributes.MESSAGE_BODY.value: message}
|
|
39
|
+
if attributes:
|
|
40
|
+
flattened = flatten_attributes(attributes, expand_leaf_lists=False)
|
|
41
|
+
span_attributes.update(sanitize_attributes(flattened))
|
|
42
|
+
logger.debug("Emitting message span with message: %s", message)
|
|
43
|
+
tracer.create_span(
|
|
44
|
+
AGL_MESSAGE,
|
|
45
|
+
attributes=span_attributes,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_message_value(span: SpanLike) -> Optional[str]:
|
|
50
|
+
"""Extract the message string from a message span.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
span: Span-like object to extract the message from.
|
|
54
|
+
"""
|
|
55
|
+
span_attributes = span.attributes or {}
|
|
56
|
+
if LightningSpanAttributes.MESSAGE_BODY.value not in span_attributes:
|
|
57
|
+
return None
|
|
58
|
+
message = span_attributes[LightningSpanAttributes.MESSAGE_BODY.value]
|
|
59
|
+
if isinstance(message, str):
|
|
60
|
+
return message
|
|
61
|
+
raise TypeError(f"Message must be a string, got: {type(message)}.")
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
from mantisdk.semconv import AGL_OBJECT, LightningSpanAttributes
|
|
9
|
+
from mantisdk.tracer.base import get_active_tracer
|
|
10
|
+
from mantisdk.tracer.dummy import DummyTracer
|
|
11
|
+
from mantisdk.types import SpanCoreFields, SpanLike, TraceStatus
|
|
12
|
+
from mantisdk.utils.otel import flatten_attributes, full_qualified_name, sanitize_attributes
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def emit_object(object: Any, attributes: Optional[Dict[str, Any]] = None, propagate: bool = True) -> SpanCoreFields:
|
|
18
|
+
"""Emit an object's serialized representation as an OpenTelemetry span.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
object: Data structure to encode as JSON and attach to the span payload.
|
|
22
|
+
attributes: Additional attributes to attach to the object span.
|
|
23
|
+
propagate: Whether to propagate the span to exporters automatically.
|
|
24
|
+
|
|
25
|
+
!!! note
|
|
26
|
+
The payload must be JSON serializable. Non-serializable objects will lead to a RuntimeError.
|
|
27
|
+
"""
|
|
28
|
+
span_attributes = encode_object(object)
|
|
29
|
+
if attributes:
|
|
30
|
+
flattened = flatten_attributes(attributes, expand_leaf_lists=False)
|
|
31
|
+
span_attributes.update(sanitize_attributes(flattened))
|
|
32
|
+
|
|
33
|
+
attr_length = 0
|
|
34
|
+
if LightningSpanAttributes.OBJECT_JSON.value in span_attributes:
|
|
35
|
+
attr_length = len(span_attributes[LightningSpanAttributes.OBJECT_JSON.value])
|
|
36
|
+
elif LightningSpanAttributes.OBJECT_LITERAL.value in span_attributes:
|
|
37
|
+
attr_length = len(span_attributes[LightningSpanAttributes.OBJECT_LITERAL.value])
|
|
38
|
+
logger.debug("Emitting object span with payload size %d characters", attr_length)
|
|
39
|
+
|
|
40
|
+
if propagate:
|
|
41
|
+
tracer = get_active_tracer()
|
|
42
|
+
if tracer is None:
|
|
43
|
+
raise RuntimeError("No active tracer found. Cannot emit object span.")
|
|
44
|
+
else:
|
|
45
|
+
# Do not actually propagate to any store or tracer backend.
|
|
46
|
+
tracer = DummyTracer()
|
|
47
|
+
|
|
48
|
+
return tracer.create_span(
|
|
49
|
+
name=AGL_OBJECT,
|
|
50
|
+
attributes=span_attributes,
|
|
51
|
+
status=TraceStatus(status_code="OK"),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def encode_object(object: Any) -> Dict[str, Any]:
|
|
56
|
+
"""Encode an object as span attributes.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
object: Data structure to encode as JSON.
|
|
60
|
+
"""
|
|
61
|
+
span_attributes = {}
|
|
62
|
+
if isinstance(object, (str, int, float, bool)):
|
|
63
|
+
span_attributes = {
|
|
64
|
+
LightningSpanAttributes.OBJECT_TYPE.value: type(object).__name__,
|
|
65
|
+
LightningSpanAttributes.OBJECT_LITERAL.value: str(object),
|
|
66
|
+
}
|
|
67
|
+
elif isinstance(object, bytes):
|
|
68
|
+
b64_encoded = base64.b64encode(object).decode("utf-8")
|
|
69
|
+
span_attributes = {
|
|
70
|
+
LightningSpanAttributes.OBJECT_TYPE.value: "bytes",
|
|
71
|
+
LightningSpanAttributes.OBJECT_LITERAL.value: b64_encoded,
|
|
72
|
+
}
|
|
73
|
+
else:
|
|
74
|
+
try:
|
|
75
|
+
serialized = json.dumps(object)
|
|
76
|
+
except (TypeError, ValueError) as exc:
|
|
77
|
+
raise RuntimeError(f"Object must be JSON serializable, got: {type(object)}.") from exc
|
|
78
|
+
|
|
79
|
+
span_attributes = {
|
|
80
|
+
LightningSpanAttributes.OBJECT_TYPE.value: full_qualified_name(type(object)), # type: ignore
|
|
81
|
+
LightningSpanAttributes.OBJECT_JSON.value: serialized,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return span_attributes
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_object_value(span: SpanLike) -> Any:
|
|
88
|
+
"""Extract the object payload from an object span.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
span: Span object produced by Mantisdk emitters.
|
|
92
|
+
"""
|
|
93
|
+
attributes = span.attributes or {}
|
|
94
|
+
if LightningSpanAttributes.OBJECT_JSON.value in attributes:
|
|
95
|
+
serialized = attributes[LightningSpanAttributes.OBJECT_JSON.value]
|
|
96
|
+
try:
|
|
97
|
+
return json.loads(serialized) # type: ignore
|
|
98
|
+
except (TypeError, ValueError) as exc:
|
|
99
|
+
raise RuntimeError("Failed to deserialize object JSON from span.") from exc
|
|
100
|
+
elif LightningSpanAttributes.OBJECT_LITERAL.value in attributes:
|
|
101
|
+
literal = attributes[LightningSpanAttributes.OBJECT_LITERAL.value]
|
|
102
|
+
obj_type = attributes.get(LightningSpanAttributes.OBJECT_TYPE.value, "str")
|
|
103
|
+
if obj_type == "str":
|
|
104
|
+
return literal
|
|
105
|
+
elif obj_type == "int":
|
|
106
|
+
# Let it raise errors if there are any
|
|
107
|
+
return int(literal) # type: ignore
|
|
108
|
+
elif obj_type == "float":
|
|
109
|
+
return float(literal) # type: ignore
|
|
110
|
+
elif obj_type == "bool":
|
|
111
|
+
return literal.lower() == "true" # type: ignore
|
|
112
|
+
elif obj_type == "bytes":
|
|
113
|
+
return base64.b64decode(literal.encode("utf-8")) # type: ignore
|
|
114
|
+
else:
|
|
115
|
+
raise RuntimeError(f"Unsupported object type for literal deserialization: {obj_type}")
|
|
116
|
+
else:
|
|
117
|
+
return None
|