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
mantisdk/utils/otlp.py
ADDED
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import gzip
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any, Awaitable, Callable, Dict, List, Mapping, Optional, Sequence, Tuple, Type, TypeVar
|
|
8
|
+
|
|
9
|
+
from fastapi import Request, Response
|
|
10
|
+
from google.protobuf import json_format
|
|
11
|
+
from google.rpc.status_pb2 import Status
|
|
12
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
13
|
+
from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import (
|
|
14
|
+
ExportLogsServiceRequest,
|
|
15
|
+
ExportLogsServiceResponse,
|
|
16
|
+
)
|
|
17
|
+
from opentelemetry.proto.collector.metrics.v1.metrics_service_pb2 import (
|
|
18
|
+
ExportMetricsServiceRequest,
|
|
19
|
+
ExportMetricsServiceResponse,
|
|
20
|
+
)
|
|
21
|
+
from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import (
|
|
22
|
+
ExportTraceServiceRequest,
|
|
23
|
+
ExportTraceServiceResponse,
|
|
24
|
+
)
|
|
25
|
+
from opentelemetry.proto.common.v1.common_pb2 import AnyValue, KeyValue
|
|
26
|
+
from opentelemetry.proto.resource.v1.resource_pb2 import Resource as ProtoResource
|
|
27
|
+
from opentelemetry.proto.trace.v1.trace_pb2 import Span as ProtoSpan
|
|
28
|
+
from opentelemetry.proto.trace.v1.trace_pb2 import Status as ProtoStatus
|
|
29
|
+
from opentelemetry.sdk.resources import Resource
|
|
30
|
+
from opentelemetry.sdk.trace import ReadableSpan
|
|
31
|
+
from opentelemetry.sdk.trace.export import SpanExportResult
|
|
32
|
+
from opentelemetry.util.types import AttributeValue
|
|
33
|
+
|
|
34
|
+
from mantisdk.semconv import LightningResourceAttributes
|
|
35
|
+
from mantisdk.types.tracer import (
|
|
36
|
+
Attributes,
|
|
37
|
+
Event,
|
|
38
|
+
Link,
|
|
39
|
+
OtelResource,
|
|
40
|
+
Span,
|
|
41
|
+
SpanContext,
|
|
42
|
+
StatusCode,
|
|
43
|
+
TraceStatus,
|
|
44
|
+
convert_timestamp,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
PROTOBUF_CT = "application/x-protobuf"
|
|
48
|
+
|
|
49
|
+
logger = logging.getLogger(__name__)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
T_request = TypeVar("T_request", ExportLogsServiceRequest, ExportMetricsServiceRequest, ExportTraceServiceRequest)
|
|
53
|
+
T_response = TypeVar("T_response", ExportLogsServiceResponse, ExportMetricsServiceResponse, ExportTraceServiceResponse)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def handle_otlp_export(
|
|
57
|
+
request: Request,
|
|
58
|
+
request_message_cls: Type[T_request],
|
|
59
|
+
response_message_cls: Type[T_response],
|
|
60
|
+
message_callback: Optional[Callable[[T_request], Awaitable[None]]],
|
|
61
|
+
signal_name: str,
|
|
62
|
+
) -> Response:
|
|
63
|
+
"""
|
|
64
|
+
Generic handler for /v1/traces, /v1/metrics, /v1/logs.
|
|
65
|
+
|
|
66
|
+
Convert the OTLP Protobuf request to a JSON-like object.
|
|
67
|
+
"""
|
|
68
|
+
content_type = request.headers.get("Content-Type", "").split(";")[0].strip()
|
|
69
|
+
|
|
70
|
+
if content_type != PROTOBUF_CT:
|
|
71
|
+
# For brevity we only support binary protobuf here.
|
|
72
|
+
return _bad_request_response(
|
|
73
|
+
request,
|
|
74
|
+
f"Unsupported Content-Type '{content_type}', expected '{PROTOBUF_CT}'",
|
|
75
|
+
content_type=PROTOBUF_CT,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
raw_body = await request.body()
|
|
79
|
+
body = _read_body_maybe_gzip(request, raw_body)
|
|
80
|
+
|
|
81
|
+
# Empty request is allowed and should still succeed.
|
|
82
|
+
if not body:
|
|
83
|
+
req_msg = request_message_cls()
|
|
84
|
+
else:
|
|
85
|
+
req_msg = request_message_cls()
|
|
86
|
+
try:
|
|
87
|
+
req_msg.ParseFromString(body)
|
|
88
|
+
except Exception as exc:
|
|
89
|
+
return _bad_request_response(request, f"Unable to parse OTLP {signal_name} payload: {exc}")
|
|
90
|
+
|
|
91
|
+
if message_callback is not None:
|
|
92
|
+
await message_callback(req_msg)
|
|
93
|
+
|
|
94
|
+
# Build success response. Partial success field is left unset.
|
|
95
|
+
resp_msg = response_message_cls()
|
|
96
|
+
|
|
97
|
+
# Encode response in the same Content-Type as request.
|
|
98
|
+
if content_type == PROTOBUF_CT:
|
|
99
|
+
resp_bytes = resp_msg.SerializeToString()
|
|
100
|
+
else:
|
|
101
|
+
resp_bytes = json_format.MessageToJson(resp_msg).encode("utf-8")
|
|
102
|
+
|
|
103
|
+
resp_bytes, headers = _maybe_gzip_response(request, resp_bytes)
|
|
104
|
+
|
|
105
|
+
return Response(
|
|
106
|
+
content=resp_bytes,
|
|
107
|
+
media_type=content_type,
|
|
108
|
+
status_code=200,
|
|
109
|
+
headers=headers,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
async def spans_from_proto(
|
|
114
|
+
request: ExportTraceServiceRequest,
|
|
115
|
+
sequence_id_bulk_issuer: Callable[[Sequence[Tuple[str, str]]], Awaitable[Sequence[int]]],
|
|
116
|
+
) -> List[Span]:
|
|
117
|
+
"""Parse an OTLP proto payload into List[Span].
|
|
118
|
+
|
|
119
|
+
A store is needed here for generating a sequence ID for each span.
|
|
120
|
+
"""
|
|
121
|
+
output_spans: List[Span] = []
|
|
122
|
+
|
|
123
|
+
for resource_spans in request.resource_spans:
|
|
124
|
+
# Resource-level attributes & IDs
|
|
125
|
+
resource_attrs = _kv_list_to_dict(resource_spans.resource.attributes)
|
|
126
|
+
# rollout_id, attempt_id from resource attributes when present.
|
|
127
|
+
rollout_id_resource = resource_attrs.get(LightningResourceAttributes.ROLLOUT_ID.value)
|
|
128
|
+
attempt_id_resource = resource_attrs.get(LightningResourceAttributes.ATTEMPT_ID.value)
|
|
129
|
+
# If sequence id is provided, all the spans will share the same sequence ID.
|
|
130
|
+
# unless otherwise overridden by span-level attributes.
|
|
131
|
+
sequence_id_resource = resource_attrs.get(LightningResourceAttributes.SPAN_SEQUENCE_ID.value)
|
|
132
|
+
|
|
133
|
+
otel_resource = _resource_from_proto(resource_spans.resource, getattr(resource_spans, "schema_url", ""))
|
|
134
|
+
|
|
135
|
+
# Each ScopeSpans contains multiple spans
|
|
136
|
+
for scope_spans in resource_spans.scope_spans:
|
|
137
|
+
for proto_span in scope_spans.spans:
|
|
138
|
+
trace_id_hex = _bytes_to_trace_id_hex(proto_span.trace_id)
|
|
139
|
+
span_id_hex = _bytes_to_span_id_hex(proto_span.span_id)
|
|
140
|
+
parent_id_hex = _bytes_to_span_id_hex(proto_span.parent_span_id) if proto_span.parent_span_id else None
|
|
141
|
+
|
|
142
|
+
# Status
|
|
143
|
+
status_code_str = _STATUS_CODE_MAP.get(proto_span.status.code, "UNSET")
|
|
144
|
+
status = TraceStatus(
|
|
145
|
+
status_code=status_code_str,
|
|
146
|
+
description=proto_span.status.message or None,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Attributes
|
|
150
|
+
span_attrs = _kv_list_to_dict(proto_span.attributes)
|
|
151
|
+
|
|
152
|
+
# Context
|
|
153
|
+
context = SpanContext(
|
|
154
|
+
trace_id=trace_id_hex,
|
|
155
|
+
span_id=span_id_hex,
|
|
156
|
+
is_remote=False,
|
|
157
|
+
trace_state={},
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Try to get if span attributes contain something like rollout_id or attempt_id
|
|
161
|
+
# Override the resource-level attributes with the span-level attributes if present.
|
|
162
|
+
rollout_id_span = span_attrs.get(LightningResourceAttributes.ROLLOUT_ID.value)
|
|
163
|
+
attempt_id_span = span_attrs.get(LightningResourceAttributes.ATTEMPT_ID.value)
|
|
164
|
+
sequence_id_span = span_attrs.get(LightningResourceAttributes.SPAN_SEQUENCE_ID.value)
|
|
165
|
+
|
|
166
|
+
# Normalize to regular strings and ints
|
|
167
|
+
rollout_id_raw = rollout_id_span if rollout_id_span is not None else rollout_id_resource
|
|
168
|
+
attempt_id_raw = attempt_id_span if attempt_id_span is not None else attempt_id_resource
|
|
169
|
+
sequence_id_raw = sequence_id_span if sequence_id_span is not None else sequence_id_resource
|
|
170
|
+
|
|
171
|
+
rollout_id, attempt_id = _normalize_rollout_attempt_id(rollout_id_raw, attempt_id_raw)
|
|
172
|
+
sequence_id = _normalize_sequence_id(sequence_id_raw)
|
|
173
|
+
|
|
174
|
+
if rollout_id is None or attempt_id is None:
|
|
175
|
+
logger.warning(
|
|
176
|
+
"Both rollout_id and attempt_id must be present in resource attributes. "
|
|
177
|
+
"Spans will not be able to log to the store because of missing IDs: rollout_id=%s, attempt_id=%s, sequence_id=%s",
|
|
178
|
+
rollout_id,
|
|
179
|
+
attempt_id,
|
|
180
|
+
sequence_id,
|
|
181
|
+
)
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
# Generate a new sequence ID if not provided
|
|
185
|
+
if sequence_id is None:
|
|
186
|
+
current_sequence_id = -1
|
|
187
|
+
elif sequence_id < 0:
|
|
188
|
+
logger.error(
|
|
189
|
+
"Invalid sequence_id value in resource attributes: %r. Must be a positive integer. Regenerating one.",
|
|
190
|
+
sequence_id,
|
|
191
|
+
)
|
|
192
|
+
current_sequence_id = -1
|
|
193
|
+
else:
|
|
194
|
+
current_sequence_id = sequence_id
|
|
195
|
+
|
|
196
|
+
# Build Span
|
|
197
|
+
span = Span(
|
|
198
|
+
rollout_id=rollout_id,
|
|
199
|
+
attempt_id=attempt_id,
|
|
200
|
+
sequence_id=current_sequence_id,
|
|
201
|
+
trace_id=trace_id_hex,
|
|
202
|
+
span_id=span_id_hex,
|
|
203
|
+
parent_id=parent_id_hex,
|
|
204
|
+
name=proto_span.name,
|
|
205
|
+
status=status,
|
|
206
|
+
attributes=span_attrs,
|
|
207
|
+
events=_events_from_proto(proto_span),
|
|
208
|
+
links=_links_from_proto(proto_span),
|
|
209
|
+
start_time=convert_timestamp(proto_span.start_time_unix_nano),
|
|
210
|
+
end_time=convert_timestamp(proto_span.end_time_unix_nano),
|
|
211
|
+
context=context,
|
|
212
|
+
parent=None, # OTLP only has parent_span_id; we don't have full SpanContext
|
|
213
|
+
resource=otel_resource,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
output_spans.append(span)
|
|
217
|
+
|
|
218
|
+
# Finalize the sequence IDs
|
|
219
|
+
bulk_issue_requests = [(span.rollout_id, span.attempt_id) for span in output_spans if span.sequence_id < 0]
|
|
220
|
+
bulk_sequence_ids = await sequence_id_bulk_issuer(bulk_issue_requests)
|
|
221
|
+
for span, sequence_id in zip(
|
|
222
|
+
[span for span in output_spans if span.sequence_id < 0], bulk_sequence_ids, strict=True
|
|
223
|
+
):
|
|
224
|
+
span.sequence_id = sequence_id
|
|
225
|
+
|
|
226
|
+
return output_spans
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class LightningStoreOTLPExporter(OTLPSpanExporter):
|
|
230
|
+
"""OTLP Exporter that write to a LightningStore-compatible backend.
|
|
231
|
+
|
|
232
|
+
The backend requires two special attributes on each span:
|
|
233
|
+
|
|
234
|
+
- `mantisdk.rollout_id`: The rollout ID to associate the span with.
|
|
235
|
+
- `mantisdk.attempt_id`: The attempt ID to associate the span with.
|
|
236
|
+
|
|
237
|
+
It can optionally use the following attribute to sequence spans:
|
|
238
|
+
|
|
239
|
+
- `mantisdk.span_sequence_id`: A decimal string representing the sequence ID of the span.
|
|
240
|
+
|
|
241
|
+
It also supports Langfuse-compatible tracing metadata:
|
|
242
|
+
|
|
243
|
+
- `langfuse.environment`: The environment for traces (e.g., "mantisdk-gepa").
|
|
244
|
+
- `langfuse.tags`: Tags for traces (e.g., ["gepa", "train"]).
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
_default_endpoint: Optional[str] = None
|
|
248
|
+
_rollout_id: Optional[str] = None
|
|
249
|
+
_attempt_id: Optional[str] = None
|
|
250
|
+
_environment: Optional[str] = None
|
|
251
|
+
_tags: Optional[List[str]] = None
|
|
252
|
+
|
|
253
|
+
def __repr__(self) -> str:
|
|
254
|
+
return (
|
|
255
|
+
f"{self.__class__.__name__}("
|
|
256
|
+
+ f"endpoint={self.endpoint!r}, "
|
|
257
|
+
+ f"rollout_id={self.rollout_id!r}, "
|
|
258
|
+
+ f"attempt_id={self.attempt_id!r}, "
|
|
259
|
+
+ f"should_bypass={self.should_bypass()!r})"
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def endpoint(self) -> Optional[str]:
|
|
264
|
+
"""The endpoint to submit the spans to."""
|
|
265
|
+
if hasattr(self, "_endpoint"):
|
|
266
|
+
return self._endpoint
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def rollout_id(self) -> Optional[str]:
|
|
271
|
+
"""The rollout ID to submit the spans to."""
|
|
272
|
+
if hasattr(self, "_rollout_id"):
|
|
273
|
+
return self._rollout_id
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
@property
|
|
277
|
+
def attempt_id(self) -> Optional[str]:
|
|
278
|
+
"""The attempt ID to submit the spans to."""
|
|
279
|
+
if hasattr(self, "_attempt_id"):
|
|
280
|
+
return self._attempt_id
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
def enable_store_otlp(
|
|
284
|
+
self,
|
|
285
|
+
endpoint: str,
|
|
286
|
+
rollout_id: str,
|
|
287
|
+
attempt_id: str,
|
|
288
|
+
headers: Optional[Dict[str, str]] = None,
|
|
289
|
+
environment: Optional[str] = None,
|
|
290
|
+
tags: Optional[List[str]] = None,
|
|
291
|
+
) -> None:
|
|
292
|
+
"""Enable storing OTLP data to a specific LightningStore rollout/attempt.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
endpoint: The OTLP endpoint URL.
|
|
296
|
+
rollout_id: The rollout ID to associate spans with.
|
|
297
|
+
attempt_id: The attempt ID to associate spans with.
|
|
298
|
+
headers: Optional authentication headers for the endpoint.
|
|
299
|
+
environment: Optional Langfuse environment for traces.
|
|
300
|
+
tags: Optional Langfuse tags for traces.
|
|
301
|
+
"""
|
|
302
|
+
self._rollout_id = rollout_id
|
|
303
|
+
self._attempt_id = attempt_id
|
|
304
|
+
self._environment = environment
|
|
305
|
+
self._tags = tags
|
|
306
|
+
|
|
307
|
+
self._default_endpoint = self._endpoint
|
|
308
|
+
self._endpoint = endpoint
|
|
309
|
+
|
|
310
|
+
# Store and set auth headers if provided
|
|
311
|
+
if headers:
|
|
312
|
+
if not hasattr(self, "_default_headers"):
|
|
313
|
+
self._default_headers = getattr(self, "_headers", {}) or {}
|
|
314
|
+
self._headers = {**self._default_headers, **headers}
|
|
315
|
+
# Also update the session headers directly (OTLPSpanExporter creates session in __init__)
|
|
316
|
+
# Modifying self._headers alone doesn't affect the already-created session
|
|
317
|
+
if hasattr(self, "_session") and self._session is not None:
|
|
318
|
+
self._session.headers.update(headers)
|
|
319
|
+
|
|
320
|
+
def disable_store_otlp(self) -> None:
|
|
321
|
+
"""Disable storing OTLP data to LightningStore."""
|
|
322
|
+
self._rollout_id = None
|
|
323
|
+
self._attempt_id = None
|
|
324
|
+
self._environment = None
|
|
325
|
+
self._tags = None
|
|
326
|
+
if self._default_endpoint is not None:
|
|
327
|
+
self._endpoint = self._default_endpoint
|
|
328
|
+
|
|
329
|
+
def should_bypass(self) -> bool:
|
|
330
|
+
"""Check if the exporter should bypass the default export if rollout_id and attempt_id are not set."""
|
|
331
|
+
return True
|
|
332
|
+
|
|
333
|
+
def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
|
|
334
|
+
if self._rollout_id is not None and self._attempt_id is not None:
|
|
335
|
+
# rollout_id and attempt_id are present in resource attributes
|
|
336
|
+
# It means that the server supports OTLP endpoint.
|
|
337
|
+
|
|
338
|
+
# Build resource attributes with rollout context
|
|
339
|
+
resource_attrs: Dict[str, Any] = {
|
|
340
|
+
LightningResourceAttributes.ROLLOUT_ID.value: self._rollout_id,
|
|
341
|
+
LightningResourceAttributes.ATTEMPT_ID.value: self._attempt_id,
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
# Add Langfuse-compatible tracing metadata if available
|
|
345
|
+
# These are added as resource attributes which Insight's OTEL processor checks
|
|
346
|
+
# Note: For resource attributes, Insight expects "langfuse.trace.tags" not "langfuse.tags"
|
|
347
|
+
if self._environment:
|
|
348
|
+
resource_attrs["langfuse.environment"] = self._environment
|
|
349
|
+
|
|
350
|
+
# Build tags list from rollout metadata + call_type from span attributes
|
|
351
|
+
tags_list = list(self._tags) if self._tags else []
|
|
352
|
+
|
|
353
|
+
# Extract call_type from span attributes (set by CallTypeSpanProcessor)
|
|
354
|
+
# and add it to tags if present
|
|
355
|
+
for span in spans:
|
|
356
|
+
if span.attributes and "mantis.call_type" in span.attributes:
|
|
357
|
+
call_type = str(span.attributes["mantis.call_type"])
|
|
358
|
+
if call_type and call_type not in tags_list:
|
|
359
|
+
tags_list.append(call_type)
|
|
360
|
+
break # Only need to check first span with call_type
|
|
361
|
+
|
|
362
|
+
if tags_list:
|
|
363
|
+
resource_attrs["langfuse.trace.tags"] = tags_list
|
|
364
|
+
|
|
365
|
+
for span in spans:
|
|
366
|
+
# Override the resources so that the server knows where the request comes from.
|
|
367
|
+
span._resource = span._resource.merge( # pyright: ignore[reportPrivateUsage]
|
|
368
|
+
Resource.create(resource_attrs)
|
|
369
|
+
)
|
|
370
|
+
return super().export(spans)
|
|
371
|
+
elif not self.should_bypass():
|
|
372
|
+
logger.debug("Rollout ID and Attempt ID not set; using default OTLP exporter behavior.")
|
|
373
|
+
return super().export(spans)
|
|
374
|
+
else:
|
|
375
|
+
logger.debug("Rollout ID and Attempt ID not set; bypassing export.")
|
|
376
|
+
return SpanExportResult.SUCCESS
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _read_body_maybe_gzip(request: Request, raw_body: bytes) -> bytes:
|
|
380
|
+
"""
|
|
381
|
+
Decompress body if Content-Encoding: gzip; otherwise return as is.
|
|
382
|
+
"""
|
|
383
|
+
encoding = request.headers.get("Content-Encoding", "").lower()
|
|
384
|
+
if encoding == "gzip":
|
|
385
|
+
return gzip.decompress(raw_body)
|
|
386
|
+
return raw_body
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _maybe_gzip_response(request: Request, payload: bytes) -> Tuple[bytes, Dict[str, str]]:
|
|
390
|
+
"""
|
|
391
|
+
If Accept-Encoding includes gzip, gzip the payload and set Content-Encoding header.
|
|
392
|
+
"""
|
|
393
|
+
ae = request.headers.get("Accept-Encoding", "")
|
|
394
|
+
tokens = [token.split(";")[0].strip().lower() for token in ae.split(",") if token.strip()]
|
|
395
|
+
headers: Dict[str, str] = {}
|
|
396
|
+
if "gzip" in tokens:
|
|
397
|
+
payload = gzip.compress(payload)
|
|
398
|
+
headers["Content-Encoding"] = "gzip"
|
|
399
|
+
return payload, headers
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _bad_request_response(request: Request, message: str, content_type: str = PROTOBUF_CT) -> Response:
|
|
403
|
+
"""
|
|
404
|
+
Build a 400 response whose body is a protobuf Status message, encoded
|
|
405
|
+
in the same Content-Type as the request (OTLP/HTTP requirement).
|
|
406
|
+
"""
|
|
407
|
+
status_msg = Status(message=message)
|
|
408
|
+
|
|
409
|
+
if content_type == PROTOBUF_CT:
|
|
410
|
+
body = status_msg.SerializeToString()
|
|
411
|
+
else:
|
|
412
|
+
# Fallback: JSON representation of Status.
|
|
413
|
+
body = json_format.MessageToJson(status_msg).encode("utf-8")
|
|
414
|
+
|
|
415
|
+
body, headers = _maybe_gzip_response(request, body)
|
|
416
|
+
|
|
417
|
+
return Response(
|
|
418
|
+
content=body,
|
|
419
|
+
status_code=400,
|
|
420
|
+
media_type=content_type,
|
|
421
|
+
headers=headers,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _normalize_rollout_attempt_id(
|
|
426
|
+
rollout_id: Optional[AttributeValue], attempt_id: Optional[AttributeValue]
|
|
427
|
+
) -> Tuple[Optional[str], Optional[str]]:
|
|
428
|
+
"""Normalize a rollout or attempt ID to a string."""
|
|
429
|
+
rollout_id_str = str(rollout_id) if rollout_id is not None else None
|
|
430
|
+
attempt_id_str = str(attempt_id) if attempt_id is not None else None
|
|
431
|
+
return rollout_id_str, attempt_id_str
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _normalize_sequence_id(sequence_id: Optional[AttributeValue]) -> Optional[int]:
|
|
435
|
+
"""Normalize a sequence ID to an integer."""
|
|
436
|
+
if sequence_id is None:
|
|
437
|
+
return None
|
|
438
|
+
try:
|
|
439
|
+
sequence_id_int = int(str(sequence_id))
|
|
440
|
+
except (ValueError, TypeError):
|
|
441
|
+
logger.warning(
|
|
442
|
+
"Invalid sequence_id value in resource attributes: %r. Must be an integer or string representing an integer. Assuming None.",
|
|
443
|
+
sequence_id,
|
|
444
|
+
)
|
|
445
|
+
sequence_id_int = None
|
|
446
|
+
return sequence_id_int
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _any_value_to_python(value: AnyValue) -> Any:
|
|
450
|
+
"""Convert OTLP AnyValue -> plain Python value."""
|
|
451
|
+
kind = value.WhichOneof("value")
|
|
452
|
+
if kind is None:
|
|
453
|
+
return None
|
|
454
|
+
if kind == "string_value":
|
|
455
|
+
return value.string_value
|
|
456
|
+
if kind == "bool_value":
|
|
457
|
+
return value.bool_value
|
|
458
|
+
if kind == "int_value":
|
|
459
|
+
return int(value.int_value)
|
|
460
|
+
if kind == "double_value":
|
|
461
|
+
return float(value.double_value)
|
|
462
|
+
if kind == "array_value":
|
|
463
|
+
return [_any_value_to_python(v) for v in value.array_value.values]
|
|
464
|
+
if kind == "kvlist_value":
|
|
465
|
+
# Map<string, AnyValue> -> dict
|
|
466
|
+
return {kv.key: _any_value_to_python(kv.value) for kv in value.kvlist_value.values}
|
|
467
|
+
if kind == "bytes_value":
|
|
468
|
+
# Serialize bytes as hex string to stay JSON-friendly
|
|
469
|
+
return value.bytes_value.hex()
|
|
470
|
+
return None
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _kv_list_to_dict(kvs: Sequence[KeyValue]) -> Attributes:
|
|
474
|
+
"""Convert repeated KeyValue -> Attributes dict."""
|
|
475
|
+
return {kv.key: _any_value_to_python(kv.value) for kv in kvs}
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
_STATUS_CODE_MAP: Mapping[ProtoStatus.StatusCode.ValueType, StatusCode] = {
|
|
479
|
+
ProtoStatus.STATUS_CODE_UNSET: "UNSET",
|
|
480
|
+
ProtoStatus.STATUS_CODE_OK: "OK",
|
|
481
|
+
ProtoStatus.STATUS_CODE_ERROR: "ERROR",
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _bytes_to_trace_id_hex(b: bytes) -> str:
|
|
486
|
+
# OTLP uses 16-byte trace IDs; format as 32-char hex
|
|
487
|
+
if not b:
|
|
488
|
+
return "0" * 32
|
|
489
|
+
return b.hex().rjust(32, "0")
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _bytes_to_span_id_hex(b: bytes) -> str:
|
|
493
|
+
# OTLP uses 8-byte span IDs; format as 16-char hex
|
|
494
|
+
if not b:
|
|
495
|
+
return "0" * 16
|
|
496
|
+
return b.hex().rjust(16, "0")
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _events_from_proto(span: ProtoSpan) -> List[Event]:
|
|
500
|
+
"""Event converter from OTLP ProtoSpan to List[Event]."""
|
|
501
|
+
return [
|
|
502
|
+
Event(
|
|
503
|
+
name=e.name,
|
|
504
|
+
attributes=_kv_list_to_dict(e.attributes),
|
|
505
|
+
timestamp=convert_timestamp(e.time_unix_nano),
|
|
506
|
+
)
|
|
507
|
+
for e in span.events
|
|
508
|
+
]
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _links_from_proto(span: ProtoSpan) -> List[Link]:
|
|
512
|
+
"""Link converter from OTLP ProtoSpan to List[Link]."""
|
|
513
|
+
links: List[Link] = []
|
|
514
|
+
for link in span.links:
|
|
515
|
+
trace_id_hex = _bytes_to_trace_id_hex(link.trace_id)
|
|
516
|
+
span_id_hex = _bytes_to_span_id_hex(link.span_id)
|
|
517
|
+
ctx = SpanContext(
|
|
518
|
+
trace_id=trace_id_hex,
|
|
519
|
+
span_id=span_id_hex,
|
|
520
|
+
is_remote=False,
|
|
521
|
+
trace_state={}, # OTLP trace_state is currently a string; you can parse if needed
|
|
522
|
+
)
|
|
523
|
+
links.append(
|
|
524
|
+
Link(
|
|
525
|
+
context=ctx,
|
|
526
|
+
attributes=_kv_list_to_dict(link.attributes) or None,
|
|
527
|
+
)
|
|
528
|
+
)
|
|
529
|
+
return links
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _resource_from_proto(resource: ProtoResource, schema_url: str = "") -> OtelResource:
|
|
533
|
+
return OtelResource(
|
|
534
|
+
attributes=_kv_list_to_dict(resource.attributes),
|
|
535
|
+
schema_url=schema_url or "",
|
|
536
|
+
)
|