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.

Files changed (190) hide show
  1. mantisdk/__init__.py +22 -0
  2. mantisdk/adapter/__init__.py +15 -0
  3. mantisdk/adapter/base.py +94 -0
  4. mantisdk/adapter/messages.py +270 -0
  5. mantisdk/adapter/triplet.py +1028 -0
  6. mantisdk/algorithm/__init__.py +39 -0
  7. mantisdk/algorithm/apo/__init__.py +5 -0
  8. mantisdk/algorithm/apo/apo.py +889 -0
  9. mantisdk/algorithm/apo/prompts/apply_edit_variant01.poml +22 -0
  10. mantisdk/algorithm/apo/prompts/apply_edit_variant02.poml +18 -0
  11. mantisdk/algorithm/apo/prompts/text_gradient_variant01.poml +18 -0
  12. mantisdk/algorithm/apo/prompts/text_gradient_variant02.poml +16 -0
  13. mantisdk/algorithm/apo/prompts/text_gradient_variant03.poml +107 -0
  14. mantisdk/algorithm/base.py +162 -0
  15. mantisdk/algorithm/decorator.py +264 -0
  16. mantisdk/algorithm/fast.py +250 -0
  17. mantisdk/algorithm/gepa/__init__.py +59 -0
  18. mantisdk/algorithm/gepa/adapter.py +459 -0
  19. mantisdk/algorithm/gepa/gepa.py +364 -0
  20. mantisdk/algorithm/gepa/lib/__init__.py +18 -0
  21. mantisdk/algorithm/gepa/lib/adapters/README.md +12 -0
  22. mantisdk/algorithm/gepa/lib/adapters/__init__.py +0 -0
  23. mantisdk/algorithm/gepa/lib/adapters/anymaths_adapter/README.md +341 -0
  24. mantisdk/algorithm/gepa/lib/adapters/anymaths_adapter/__init__.py +1 -0
  25. mantisdk/algorithm/gepa/lib/adapters/anymaths_adapter/anymaths_adapter.py +174 -0
  26. mantisdk/algorithm/gepa/lib/adapters/anymaths_adapter/requirements.txt +1 -0
  27. mantisdk/algorithm/gepa/lib/adapters/default_adapter/README.md +0 -0
  28. mantisdk/algorithm/gepa/lib/adapters/default_adapter/__init__.py +0 -0
  29. mantisdk/algorithm/gepa/lib/adapters/default_adapter/default_adapter.py +209 -0
  30. mantisdk/algorithm/gepa/lib/adapters/dspy_adapter/README.md +7 -0
  31. mantisdk/algorithm/gepa/lib/adapters/dspy_adapter/__init__.py +0 -0
  32. mantisdk/algorithm/gepa/lib/adapters/dspy_adapter/dspy_adapter.py +307 -0
  33. mantisdk/algorithm/gepa/lib/adapters/dspy_full_program_adapter/README.md +99 -0
  34. mantisdk/algorithm/gepa/lib/adapters/dspy_full_program_adapter/dspy_program_proposal_signature.py +137 -0
  35. mantisdk/algorithm/gepa/lib/adapters/dspy_full_program_adapter/full_program_adapter.py +266 -0
  36. mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/GEPA_RAG.md +621 -0
  37. mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/__init__.py +56 -0
  38. mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/evaluation_metrics.py +226 -0
  39. mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/generic_rag_adapter.py +496 -0
  40. mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/rag_pipeline.py +238 -0
  41. mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_store_interface.py +212 -0
  42. mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/__init__.py +2 -0
  43. mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/chroma_store.py +196 -0
  44. mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/lancedb_store.py +422 -0
  45. mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/milvus_store.py +409 -0
  46. mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/qdrant_store.py +368 -0
  47. mantisdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/weaviate_store.py +418 -0
  48. mantisdk/algorithm/gepa/lib/adapters/mcp_adapter/README.md +552 -0
  49. mantisdk/algorithm/gepa/lib/adapters/mcp_adapter/__init__.py +37 -0
  50. mantisdk/algorithm/gepa/lib/adapters/mcp_adapter/mcp_adapter.py +705 -0
  51. mantisdk/algorithm/gepa/lib/adapters/mcp_adapter/mcp_client.py +364 -0
  52. mantisdk/algorithm/gepa/lib/adapters/terminal_bench_adapter/README.md +9 -0
  53. mantisdk/algorithm/gepa/lib/adapters/terminal_bench_adapter/__init__.py +0 -0
  54. mantisdk/algorithm/gepa/lib/adapters/terminal_bench_adapter/terminal_bench_adapter.py +217 -0
  55. mantisdk/algorithm/gepa/lib/api.py +375 -0
  56. mantisdk/algorithm/gepa/lib/core/__init__.py +0 -0
  57. mantisdk/algorithm/gepa/lib/core/adapter.py +180 -0
  58. mantisdk/algorithm/gepa/lib/core/data_loader.py +74 -0
  59. mantisdk/algorithm/gepa/lib/core/engine.py +356 -0
  60. mantisdk/algorithm/gepa/lib/core/result.py +233 -0
  61. mantisdk/algorithm/gepa/lib/core/state.py +636 -0
  62. mantisdk/algorithm/gepa/lib/examples/__init__.py +0 -0
  63. mantisdk/algorithm/gepa/lib/examples/aime.py +24 -0
  64. mantisdk/algorithm/gepa/lib/examples/anymaths-bench/eval_default.py +111 -0
  65. mantisdk/algorithm/gepa/lib/examples/anymaths-bench/prompt-templates/instruction_prompt.txt +9 -0
  66. mantisdk/algorithm/gepa/lib/examples/anymaths-bench/prompt-templates/optimal_prompt.txt +24 -0
  67. mantisdk/algorithm/gepa/lib/examples/anymaths-bench/train_anymaths.py +177 -0
  68. mantisdk/algorithm/gepa/lib/examples/dspy_full_program_evolution/arc_agi.ipynb +25705 -0
  69. mantisdk/algorithm/gepa/lib/examples/dspy_full_program_evolution/example.ipynb +348 -0
  70. mantisdk/algorithm/gepa/lib/examples/mcp_adapter/__init__.py +4 -0
  71. mantisdk/algorithm/gepa/lib/examples/mcp_adapter/mcp_optimization_example.py +455 -0
  72. mantisdk/algorithm/gepa/lib/examples/rag_adapter/RAG_GUIDE.md +613 -0
  73. mantisdk/algorithm/gepa/lib/examples/rag_adapter/__init__.py +9 -0
  74. mantisdk/algorithm/gepa/lib/examples/rag_adapter/rag_optimization.py +824 -0
  75. mantisdk/algorithm/gepa/lib/examples/rag_adapter/requirements-rag.txt +29 -0
  76. mantisdk/algorithm/gepa/lib/examples/terminal-bench/prompt-templates/instruction_prompt.txt +16 -0
  77. mantisdk/algorithm/gepa/lib/examples/terminal-bench/prompt-templates/terminus.txt +9 -0
  78. mantisdk/algorithm/gepa/lib/examples/terminal-bench/train_terminus.py +161 -0
  79. mantisdk/algorithm/gepa/lib/gepa_utils.py +117 -0
  80. mantisdk/algorithm/gepa/lib/logging/__init__.py +0 -0
  81. mantisdk/algorithm/gepa/lib/logging/experiment_tracker.py +187 -0
  82. mantisdk/algorithm/gepa/lib/logging/logger.py +75 -0
  83. mantisdk/algorithm/gepa/lib/logging/utils.py +103 -0
  84. mantisdk/algorithm/gepa/lib/proposer/__init__.py +0 -0
  85. mantisdk/algorithm/gepa/lib/proposer/base.py +31 -0
  86. mantisdk/algorithm/gepa/lib/proposer/merge.py +357 -0
  87. mantisdk/algorithm/gepa/lib/proposer/reflective_mutation/__init__.py +0 -0
  88. mantisdk/algorithm/gepa/lib/proposer/reflective_mutation/base.py +49 -0
  89. mantisdk/algorithm/gepa/lib/proposer/reflective_mutation/reflective_mutation.py +176 -0
  90. mantisdk/algorithm/gepa/lib/py.typed +0 -0
  91. mantisdk/algorithm/gepa/lib/strategies/__init__.py +0 -0
  92. mantisdk/algorithm/gepa/lib/strategies/batch_sampler.py +77 -0
  93. mantisdk/algorithm/gepa/lib/strategies/candidate_selector.py +50 -0
  94. mantisdk/algorithm/gepa/lib/strategies/component_selector.py +36 -0
  95. mantisdk/algorithm/gepa/lib/strategies/eval_policy.py +64 -0
  96. mantisdk/algorithm/gepa/lib/strategies/instruction_proposal.py +127 -0
  97. mantisdk/algorithm/gepa/lib/utils/__init__.py +10 -0
  98. mantisdk/algorithm/gepa/lib/utils/stop_condition.py +196 -0
  99. mantisdk/algorithm/gepa/tracing.py +105 -0
  100. mantisdk/algorithm/utils.py +177 -0
  101. mantisdk/algorithm/verl/__init__.py +5 -0
  102. mantisdk/algorithm/verl/interface.py +202 -0
  103. mantisdk/cli/__init__.py +56 -0
  104. mantisdk/cli/prometheus.py +115 -0
  105. mantisdk/cli/store.py +131 -0
  106. mantisdk/cli/vllm.py +29 -0
  107. mantisdk/client.py +408 -0
  108. mantisdk/config.py +348 -0
  109. mantisdk/emitter/__init__.py +43 -0
  110. mantisdk/emitter/annotation.py +370 -0
  111. mantisdk/emitter/exception.py +54 -0
  112. mantisdk/emitter/message.py +61 -0
  113. mantisdk/emitter/object.py +117 -0
  114. mantisdk/emitter/reward.py +320 -0
  115. mantisdk/env_var.py +156 -0
  116. mantisdk/execution/__init__.py +15 -0
  117. mantisdk/execution/base.py +64 -0
  118. mantisdk/execution/client_server.py +443 -0
  119. mantisdk/execution/events.py +69 -0
  120. mantisdk/execution/inter_process.py +16 -0
  121. mantisdk/execution/shared_memory.py +282 -0
  122. mantisdk/instrumentation/__init__.py +119 -0
  123. mantisdk/instrumentation/agentops.py +314 -0
  124. mantisdk/instrumentation/agentops_langchain.py +45 -0
  125. mantisdk/instrumentation/litellm.py +83 -0
  126. mantisdk/instrumentation/vllm.py +81 -0
  127. mantisdk/instrumentation/weave.py +500 -0
  128. mantisdk/litagent/__init__.py +11 -0
  129. mantisdk/litagent/decorator.py +536 -0
  130. mantisdk/litagent/litagent.py +252 -0
  131. mantisdk/llm_proxy.py +1890 -0
  132. mantisdk/logging.py +370 -0
  133. mantisdk/reward.py +7 -0
  134. mantisdk/runner/__init__.py +11 -0
  135. mantisdk/runner/agent.py +845 -0
  136. mantisdk/runner/base.py +182 -0
  137. mantisdk/runner/legacy.py +309 -0
  138. mantisdk/semconv.py +170 -0
  139. mantisdk/server.py +401 -0
  140. mantisdk/store/__init__.py +23 -0
  141. mantisdk/store/base.py +897 -0
  142. mantisdk/store/client_server.py +2092 -0
  143. mantisdk/store/collection/__init__.py +30 -0
  144. mantisdk/store/collection/base.py +587 -0
  145. mantisdk/store/collection/memory.py +970 -0
  146. mantisdk/store/collection/mongo.py +1412 -0
  147. mantisdk/store/collection_based.py +1823 -0
  148. mantisdk/store/insight.py +648 -0
  149. mantisdk/store/listener.py +58 -0
  150. mantisdk/store/memory.py +396 -0
  151. mantisdk/store/mongo.py +165 -0
  152. mantisdk/store/sqlite.py +3 -0
  153. mantisdk/store/threading.py +357 -0
  154. mantisdk/store/utils.py +142 -0
  155. mantisdk/tracer/__init__.py +16 -0
  156. mantisdk/tracer/agentops.py +242 -0
  157. mantisdk/tracer/base.py +287 -0
  158. mantisdk/tracer/dummy.py +106 -0
  159. mantisdk/tracer/otel.py +555 -0
  160. mantisdk/tracer/weave.py +677 -0
  161. mantisdk/trainer/__init__.py +6 -0
  162. mantisdk/trainer/init_utils.py +263 -0
  163. mantisdk/trainer/legacy.py +367 -0
  164. mantisdk/trainer/registry.py +12 -0
  165. mantisdk/trainer/trainer.py +618 -0
  166. mantisdk/types/__init__.py +6 -0
  167. mantisdk/types/core.py +553 -0
  168. mantisdk/types/resources.py +204 -0
  169. mantisdk/types/tracer.py +515 -0
  170. mantisdk/types/tracing.py +218 -0
  171. mantisdk/utils/__init__.py +1 -0
  172. mantisdk/utils/id.py +18 -0
  173. mantisdk/utils/metrics.py +1025 -0
  174. mantisdk/utils/otel.py +578 -0
  175. mantisdk/utils/otlp.py +536 -0
  176. mantisdk/utils/server_launcher.py +1045 -0
  177. mantisdk/utils/system_snapshot.py +81 -0
  178. mantisdk/verl/__init__.py +8 -0
  179. mantisdk/verl/__main__.py +6 -0
  180. mantisdk/verl/async_server.py +46 -0
  181. mantisdk/verl/config.yaml +27 -0
  182. mantisdk/verl/daemon.py +1154 -0
  183. mantisdk/verl/dataset.py +44 -0
  184. mantisdk/verl/entrypoint.py +248 -0
  185. mantisdk/verl/trainer.py +549 -0
  186. mantisdk-0.1.0.dist-info/METADATA +119 -0
  187. mantisdk-0.1.0.dist-info/RECORD +190 -0
  188. mantisdk-0.1.0.dist-info/WHEEL +4 -0
  189. mantisdk-0.1.0.dist-info/entry_points.txt +2 -0
  190. 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
+ )