mirascope 2.0.0a2__py3-none-any.whl → 2.0.0a3__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.
Files changed (204) hide show
  1. mirascope/__init__.py +2 -2
  2. mirascope/api/__init__.py +6 -0
  3. mirascope/api/_generated/README.md +207 -0
  4. mirascope/api/_generated/__init__.py +85 -0
  5. mirascope/api/_generated/client.py +155 -0
  6. mirascope/api/_generated/core/__init__.py +52 -0
  7. mirascope/api/_generated/core/api_error.py +23 -0
  8. mirascope/api/_generated/core/client_wrapper.py +58 -0
  9. mirascope/api/_generated/core/datetime_utils.py +30 -0
  10. mirascope/api/_generated/core/file.py +70 -0
  11. mirascope/api/_generated/core/force_multipart.py +16 -0
  12. mirascope/api/_generated/core/http_client.py +619 -0
  13. mirascope/api/_generated/core/http_response.py +55 -0
  14. mirascope/api/_generated/core/jsonable_encoder.py +102 -0
  15. mirascope/api/_generated/core/pydantic_utilities.py +310 -0
  16. mirascope/api/_generated/core/query_encoder.py +60 -0
  17. mirascope/api/_generated/core/remove_none_from_dict.py +11 -0
  18. mirascope/api/_generated/core/request_options.py +35 -0
  19. mirascope/api/_generated/core/serialization.py +282 -0
  20. mirascope/api/_generated/docs/__init__.py +4 -0
  21. mirascope/api/_generated/docs/client.py +95 -0
  22. mirascope/api/_generated/docs/raw_client.py +132 -0
  23. mirascope/api/_generated/environment.py +9 -0
  24. mirascope/api/_generated/errors/__init__.py +7 -0
  25. mirascope/api/_generated/errors/bad_request_error.py +15 -0
  26. mirascope/api/_generated/health/__init__.py +7 -0
  27. mirascope/api/_generated/health/client.py +96 -0
  28. mirascope/api/_generated/health/raw_client.py +129 -0
  29. mirascope/api/_generated/health/types/__init__.py +8 -0
  30. mirascope/api/_generated/health/types/health_check_response.py +24 -0
  31. mirascope/api/_generated/health/types/health_check_response_status.py +5 -0
  32. mirascope/api/_generated/reference.md +167 -0
  33. mirascope/api/_generated/traces/__init__.py +55 -0
  34. mirascope/api/_generated/traces/client.py +162 -0
  35. mirascope/api/_generated/traces/raw_client.py +168 -0
  36. mirascope/api/_generated/traces/types/__init__.py +95 -0
  37. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item.py +36 -0
  38. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource.py +31 -0
  39. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item.py +25 -0
  40. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item_value.py +54 -0
  41. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item_value_array_value.py +23 -0
  42. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item_value_kvlist_value.py +28 -0
  43. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_resource_attributes_item_value_kvlist_value_values_item.py +24 -0
  44. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item.py +35 -0
  45. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope.py +35 -0
  46. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item.py +27 -0
  47. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item_value.py +54 -0
  48. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item_value_array_value.py +23 -0
  49. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item_value_kvlist_value.py +28 -0
  50. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_scope_attributes_item_value_kvlist_value_values_item.py +24 -0
  51. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item.py +60 -0
  52. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item.py +29 -0
  53. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item_value.py +54 -0
  54. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item_value_array_value.py +23 -0
  55. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item_value_kvlist_value.py +28 -0
  56. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_attributes_item_value_kvlist_value_values_item.py +24 -0
  57. mirascope/api/_generated/traces/types/traces_create_request_resource_spans_item_scope_spans_item_spans_item_status.py +24 -0
  58. mirascope/api/_generated/traces/types/traces_create_response.py +27 -0
  59. mirascope/api/_generated/traces/types/traces_create_response_partial_success.py +28 -0
  60. mirascope/api/_generated/types/__init__.py +21 -0
  61. mirascope/api/_generated/types/http_api_decode_error.py +31 -0
  62. mirascope/api/_generated/types/http_api_decode_error_tag.py +5 -0
  63. mirascope/api/_generated/types/issue.py +44 -0
  64. mirascope/api/_generated/types/issue_tag.py +17 -0
  65. mirascope/api/_generated/types/property_key.py +7 -0
  66. mirascope/api/_generated/types/property_key_tag.py +29 -0
  67. mirascope/api/_generated/types/property_key_tag_tag.py +5 -0
  68. mirascope/api/client.py +255 -0
  69. mirascope/api/settings.py +81 -0
  70. mirascope/llm/__init__.py +41 -11
  71. mirascope/llm/calls/calls.py +81 -57
  72. mirascope/llm/calls/decorator.py +121 -115
  73. mirascope/llm/content/__init__.py +3 -2
  74. mirascope/llm/context/_utils.py +19 -6
  75. mirascope/llm/exceptions.py +30 -16
  76. mirascope/llm/formatting/_utils.py +9 -5
  77. mirascope/llm/formatting/format.py +2 -2
  78. mirascope/llm/formatting/from_call_args.py +2 -2
  79. mirascope/llm/messages/message.py +13 -5
  80. mirascope/llm/models/__init__.py +2 -2
  81. mirascope/llm/models/models.py +189 -81
  82. mirascope/llm/prompts/__init__.py +13 -12
  83. mirascope/llm/prompts/_utils.py +27 -24
  84. mirascope/llm/prompts/decorator.py +133 -204
  85. mirascope/llm/prompts/prompts.py +424 -0
  86. mirascope/llm/prompts/protocols.py +25 -59
  87. mirascope/llm/providers/__init__.py +38 -0
  88. mirascope/llm/{clients → providers}/_missing_import_stubs.py +8 -6
  89. mirascope/llm/providers/anthropic/__init__.py +24 -0
  90. mirascope/llm/{clients → providers}/anthropic/_utils/decode.py +5 -4
  91. mirascope/llm/{clients → providers}/anthropic/_utils/encode.py +31 -10
  92. mirascope/llm/providers/anthropic/model_id.py +40 -0
  93. mirascope/llm/{clients/anthropic/clients.py → providers/anthropic/provider.py} +33 -418
  94. mirascope/llm/{clients → providers}/base/__init__.py +3 -3
  95. mirascope/llm/{clients → providers}/base/_utils.py +10 -7
  96. mirascope/llm/{clients/base/client.py → providers/base/base_provider.py} +255 -126
  97. mirascope/llm/providers/google/__init__.py +21 -0
  98. mirascope/llm/{clients → providers}/google/_utils/decode.py +6 -4
  99. mirascope/llm/{clients → providers}/google/_utils/encode.py +30 -24
  100. mirascope/llm/providers/google/model_id.py +28 -0
  101. mirascope/llm/providers/google/provider.py +438 -0
  102. mirascope/llm/providers/load_provider.py +48 -0
  103. mirascope/llm/providers/mlx/__init__.py +24 -0
  104. mirascope/llm/providers/mlx/_utils.py +107 -0
  105. mirascope/llm/providers/mlx/encoding/__init__.py +8 -0
  106. mirascope/llm/providers/mlx/encoding/base.py +69 -0
  107. mirascope/llm/providers/mlx/encoding/transformers.py +131 -0
  108. mirascope/llm/providers/mlx/mlx.py +237 -0
  109. mirascope/llm/providers/mlx/model_id.py +17 -0
  110. mirascope/llm/providers/mlx/provider.py +411 -0
  111. mirascope/llm/providers/model_id.py +16 -0
  112. mirascope/llm/providers/openai/__init__.py +6 -0
  113. mirascope/llm/providers/openai/completions/__init__.py +20 -0
  114. mirascope/llm/{clients/openai/responses → providers/openai/completions}/_utils/__init__.py +2 -0
  115. mirascope/llm/{clients → providers}/openai/completions/_utils/decode.py +5 -3
  116. mirascope/llm/{clients → providers}/openai/completions/_utils/encode.py +33 -23
  117. mirascope/llm/providers/openai/completions/provider.py +456 -0
  118. mirascope/llm/providers/openai/model_id.py +31 -0
  119. mirascope/llm/providers/openai/model_info.py +246 -0
  120. mirascope/llm/providers/openai/provider.py +386 -0
  121. mirascope/llm/providers/openai/responses/__init__.py +21 -0
  122. mirascope/llm/{clients → providers}/openai/responses/_utils/decode.py +5 -3
  123. mirascope/llm/{clients → providers}/openai/responses/_utils/encode.py +28 -17
  124. mirascope/llm/providers/openai/responses/provider.py +470 -0
  125. mirascope/llm/{clients → providers}/openai/shared/_utils.py +7 -3
  126. mirascope/llm/providers/provider_id.py +13 -0
  127. mirascope/llm/providers/provider_registry.py +167 -0
  128. mirascope/llm/responses/base_response.py +10 -5
  129. mirascope/llm/responses/base_stream_response.py +10 -5
  130. mirascope/llm/responses/response.py +24 -13
  131. mirascope/llm/responses/root_response.py +7 -12
  132. mirascope/llm/responses/stream_response.py +35 -23
  133. mirascope/llm/tools/__init__.py +9 -2
  134. mirascope/llm/tools/_utils.py +12 -3
  135. mirascope/llm/tools/protocols.py +4 -4
  136. mirascope/llm/tools/tool_schema.py +44 -9
  137. mirascope/llm/tools/tools.py +10 -9
  138. mirascope/ops/__init__.py +156 -0
  139. mirascope/ops/_internal/__init__.py +5 -0
  140. mirascope/ops/_internal/closure.py +1118 -0
  141. mirascope/ops/_internal/configuration.py +126 -0
  142. mirascope/ops/_internal/context.py +76 -0
  143. mirascope/ops/_internal/exporters/__init__.py +26 -0
  144. mirascope/ops/_internal/exporters/exporters.py +342 -0
  145. mirascope/ops/_internal/exporters/processors.py +104 -0
  146. mirascope/ops/_internal/exporters/types.py +165 -0
  147. mirascope/ops/_internal/exporters/utils.py +29 -0
  148. mirascope/ops/_internal/instrumentation/__init__.py +8 -0
  149. mirascope/ops/_internal/instrumentation/llm/__init__.py +8 -0
  150. mirascope/ops/_internal/instrumentation/llm/encode.py +238 -0
  151. mirascope/ops/_internal/instrumentation/llm/gen_ai_types/__init__.py +38 -0
  152. mirascope/ops/_internal/instrumentation/llm/gen_ai_types/gen_ai_input_messages.py +31 -0
  153. mirascope/ops/_internal/instrumentation/llm/gen_ai_types/gen_ai_output_messages.py +38 -0
  154. mirascope/ops/_internal/instrumentation/llm/gen_ai_types/gen_ai_system_instructions.py +18 -0
  155. mirascope/ops/_internal/instrumentation/llm/gen_ai_types/shared.py +100 -0
  156. mirascope/ops/_internal/instrumentation/llm/llm.py +1288 -0
  157. mirascope/ops/_internal/propagation.py +198 -0
  158. mirascope/ops/_internal/protocols.py +51 -0
  159. mirascope/ops/_internal/session.py +139 -0
  160. mirascope/ops/_internal/spans.py +232 -0
  161. mirascope/ops/_internal/traced_calls.py +371 -0
  162. mirascope/ops/_internal/traced_functions.py +394 -0
  163. mirascope/ops/_internal/tracing.py +276 -0
  164. mirascope/ops/_internal/types.py +13 -0
  165. mirascope/ops/_internal/utils.py +75 -0
  166. mirascope/ops/_internal/versioned_calls.py +512 -0
  167. mirascope/ops/_internal/versioned_functions.py +346 -0
  168. mirascope/ops/_internal/versioning.py +303 -0
  169. mirascope/ops/exceptions.py +21 -0
  170. {mirascope-2.0.0a2.dist-info → mirascope-2.0.0a3.dist-info}/METADATA +76 -1
  171. mirascope-2.0.0a3.dist-info/RECORD +206 -0
  172. {mirascope-2.0.0a2.dist-info → mirascope-2.0.0a3.dist-info}/WHEEL +1 -1
  173. mirascope/graphs/__init__.py +0 -22
  174. mirascope/graphs/finite_state_machine.py +0 -625
  175. mirascope/llm/agents/__init__.py +0 -15
  176. mirascope/llm/agents/agent.py +0 -97
  177. mirascope/llm/agents/agent_template.py +0 -45
  178. mirascope/llm/agents/decorator.py +0 -176
  179. mirascope/llm/calls/base_call.py +0 -33
  180. mirascope/llm/clients/__init__.py +0 -34
  181. mirascope/llm/clients/anthropic/__init__.py +0 -25
  182. mirascope/llm/clients/anthropic/model_ids.py +0 -8
  183. mirascope/llm/clients/google/__init__.py +0 -20
  184. mirascope/llm/clients/google/clients.py +0 -853
  185. mirascope/llm/clients/google/model_ids.py +0 -15
  186. mirascope/llm/clients/openai/__init__.py +0 -25
  187. mirascope/llm/clients/openai/completions/__init__.py +0 -28
  188. mirascope/llm/clients/openai/completions/_utils/model_features.py +0 -81
  189. mirascope/llm/clients/openai/completions/clients.py +0 -833
  190. mirascope/llm/clients/openai/completions/model_ids.py +0 -8
  191. mirascope/llm/clients/openai/responses/__init__.py +0 -26
  192. mirascope/llm/clients/openai/responses/_utils/model_features.py +0 -87
  193. mirascope/llm/clients/openai/responses/clients.py +0 -832
  194. mirascope/llm/clients/openai/responses/model_ids.py +0 -8
  195. mirascope/llm/clients/providers.py +0 -175
  196. mirascope-2.0.0a2.dist-info/RECORD +0 -102
  197. /mirascope/llm/{clients → providers}/anthropic/_utils/__init__.py +0 -0
  198. /mirascope/llm/{clients → providers}/base/kwargs.py +0 -0
  199. /mirascope/llm/{clients → providers}/base/params.py +0 -0
  200. /mirascope/llm/{clients → providers}/google/_utils/__init__.py +0 -0
  201. /mirascope/llm/{clients → providers}/google/message.py +0 -0
  202. /mirascope/llm/{clients/openai/completions → providers/openai/responses}/_utils/__init__.py +0 -0
  203. /mirascope/llm/{clients → providers}/openai/shared/__init__.py +0 -0
  204. {mirascope-2.0.0a2.dist-info → mirascope-2.0.0a3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,198 @@
1
+ """Context propagation utilities for distributed tracing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ from collections.abc import Mapping, MutableMapping
8
+ from typing import Literal, TypeAlias
9
+
10
+ from opentelemetry import propagate
11
+ from opentelemetry.context import Context
12
+ from opentelemetry.propagators.b3 import B3MultiFormat, B3SingleFormat
13
+ from opentelemetry.propagators.composite import CompositePropagator
14
+ from opentelemetry.propagators.jaeger import JaegerPropagator
15
+ from opentelemetry.propagators.textmap import Setter, TextMapPropagator
16
+ from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
17
+
18
+ from ..exceptions import ConfigurationError
19
+ from .session import SESSION_HEADER_NAME, current_session
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ PropagatorFormat: TypeAlias = Literal[
24
+ "tracecontext", "b3", "b3multi", "jaeger", "composite"
25
+ ]
26
+ CarrierValue: TypeAlias = str | list[str]
27
+
28
+ # Environment variable names
29
+ ENV_PROPAGATOR_FORMAT = "MIRASCOPE_PROPAGATOR"
30
+ ENV_PROPAGATOR_SET_GLOBAL = "_MIRASCOPE_PROPAGATOR_SET_GLOBAL"
31
+
32
+ _PROPAGATOR: ContextPropagator | None = None
33
+
34
+
35
+ class _StrSetter(Setter[MutableMapping[str, str]]):
36
+ """Setter that writes string header values into the carrier."""
37
+
38
+ def set(self, carrier: MutableMapping[str, str], key: str, value: str) -> None:
39
+ """Set a single header value on the carrier."""
40
+ carrier[key] = value
41
+
42
+
43
+ _STR_SETTER = _StrSetter()
44
+
45
+
46
+ class ContextPropagator:
47
+ """Manages OpenTelemetry context propagation across service boundaries."""
48
+
49
+ _propagator: TextMapPropagator
50
+
51
+ def __init__(self, *, set_global: bool = True) -> None:
52
+ """Initialize propagator and optionally set as global.
53
+
54
+ Propagator format is determined by MIRASCOPE_PROPAGATOR environment variable.
55
+ Defaults to "tracecontext" if not set.
56
+
57
+ Args:
58
+ set_global: Whether to set this propagator as the global textmap propagator.
59
+
60
+ Raises:
61
+ ConfigurationError: If the propagator format is invalid.
62
+ """
63
+ env_format = os.environ.get(ENV_PROPAGATOR_FORMAT, "tracecontext")
64
+ propagator_name: PropagatorFormat
65
+ if env_format in ("tracecontext", "b3", "b3multi", "jaeger", "composite"):
66
+ propagator_name = env_format
67
+ else:
68
+ error_message = (
69
+ f"Invalid propagator format: {env_format}. "
70
+ f"Valid options: tracecontext, b3, b3multi, jaeger, composite"
71
+ )
72
+ logger.error(error_message)
73
+ raise ConfigurationError(error_message)
74
+ logger.debug(f"Initializing ContextPropagator with format: {propagator_name}")
75
+
76
+ match propagator_name:
77
+ case "tracecontext":
78
+ self._propagator = TraceContextTextMapPropagator()
79
+ case "b3":
80
+ self._propagator = B3SingleFormat()
81
+ case "b3multi":
82
+ self._propagator = B3MultiFormat()
83
+ case "jaeger":
84
+ self._propagator = JaegerPropagator()
85
+ case "composite":
86
+ propagators: list[TextMapPropagator] = [
87
+ TraceContextTextMapPropagator(),
88
+ B3SingleFormat(),
89
+ B3MultiFormat(),
90
+ JaegerPropagator(),
91
+ ]
92
+ self._propagator = CompositePropagator(propagators)
93
+
94
+ if set_global:
95
+ should_set_global = os.environ.get(ENV_PROPAGATOR_SET_GLOBAL, "true")
96
+ if should_set_global.lower() == "true":
97
+ propagate.set_global_textmap(self._propagator)
98
+ logger.debug(f"Set {propagator_name} as global textmap propagator")
99
+
100
+ def extract_context(self, carrier: Mapping[str, CarrierValue]) -> Context:
101
+ """Extract OTEL context from carrier headers.
102
+
103
+ Args:
104
+ carrier: Dictionary containing HTTP headers or similar carrier data.
105
+
106
+ Returns:
107
+ Extracted OpenTelemetry context. Returns empty context if extraction fails.
108
+ """
109
+ try:
110
+ context = self._propagator.extract(carrier=carrier)
111
+ logger.debug("Successfully extracted context from carrier")
112
+ return context
113
+ except Exception as exception:
114
+ logger.debug(
115
+ f"Failed to extract context from carrier: {type(exception).__name__}: {exception}"
116
+ )
117
+ return Context()
118
+
119
+ def inject_context(
120
+ self,
121
+ carrier: MutableMapping[str, str],
122
+ context: Context | None = None,
123
+ ) -> None:
124
+ """Inject current OTEL context into carrier headers.
125
+
126
+ This method also injects session context if one is active, adding the
127
+ SESSION_HEADER_NAME header to the carrier.
128
+
129
+ Args:
130
+ carrier: Mutable mapping (e.g., HTTP headers dict) to inject context into.
131
+ context: Optional specific context to inject. If None, uses current context.
132
+ """
133
+ try:
134
+ self._propagator.inject(
135
+ carrier=carrier, context=context, setter=_STR_SETTER
136
+ )
137
+ logger.debug("Successfully injected context into carrier")
138
+ except Exception as exception:
139
+ logger.debug(
140
+ f"Failed to inject context into carrier: {type(exception).__name__}: {exception}"
141
+ )
142
+
143
+ session_ctx = current_session()
144
+ if session_ctx is not None:
145
+ carrier[SESSION_HEADER_NAME] = session_ctx.id
146
+
147
+
148
+ def get_propagator() -> ContextPropagator:
149
+ """Get or create the singleton ContextPropagator instance.
150
+
151
+ Reads propagator format from MIRASCOPE_PROPAGATOR environment variable.
152
+
153
+ Returns:
154
+ The global ContextPropagator instance.
155
+ """
156
+ global _PROPAGATOR
157
+ if _PROPAGATOR is None:
158
+ _PROPAGATOR = ContextPropagator()
159
+ return _PROPAGATOR
160
+
161
+
162
+ def reset_propagator() -> None:
163
+ """Reset the singleton ContextPropagator instance.
164
+
165
+ This is primarily useful for testing to ensure a clean state between tests.
166
+ The next call to get_propagator() will create a new instance.
167
+ """
168
+ global _PROPAGATOR
169
+ _PROPAGATOR = None
170
+
171
+
172
+ def extract_context(carrier: Mapping[str, CarrierValue]) -> Context:
173
+ """Extract OTEL context from carrier headers using the global propagator.
174
+
175
+ Args:
176
+ carrier: Dictionary containing HTTP headers or similar carrier data.
177
+
178
+ Returns:
179
+ Extracted OpenTelemetry context.
180
+ """
181
+ return get_propagator().extract_context(carrier)
182
+
183
+
184
+ def inject_context(
185
+ carrier: MutableMapping[str, str],
186
+ *,
187
+ context: Context | None = None,
188
+ ) -> None:
189
+ """Inject current OTEL context into carrier headers using the global propagator.
190
+
191
+ This function also injects session context if one is active, adding the
192
+ SESSION_HEADER_NAME header to the carrier.
193
+
194
+ Args:
195
+ carrier: Mutable mapping (e.g., HTTP headers dict) to inject context into.
196
+ context: Optional specific context to inject. If None, uses current context.
197
+ """
198
+ get_propagator().inject_context(carrier, context)
@@ -0,0 +1,51 @@
1
+ """Call protocol helpers for ops.tracing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from typing import Protocol
7
+ from typing_extensions import TypeIs
8
+
9
+ from ...llm.context import Context, DepsT
10
+ from .types import P, R
11
+
12
+
13
+ class SyncFunction(Protocol[P, R]):
14
+ """Protocol for synchronous callable functions."""
15
+
16
+ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
17
+ """The function required a synchronous call method."""
18
+ ... # pragma: no cover
19
+
20
+
21
+ class AsyncFunction(Protocol[P, R]):
22
+ """Protocol for asynchronous callable functions."""
23
+
24
+ async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
25
+ """The function's required asynchronous call method."""
26
+ ... # pragma: no cover
27
+
28
+
29
+ class SyncContextFunction(Protocol[P, DepsT, R]):
30
+ """Protocol for synchronous callable functions with Context parameter."""
31
+
32
+ def __call__(self, ctx: Context[DepsT], *args: P.args, **kwargs: P.kwargs) -> R:
33
+ """The function required a synchronous call method with context."""
34
+ ... # pragma: no cover
35
+
36
+
37
+ class AsyncContextFunction(Protocol[P, DepsT, R]):
38
+ """Protocol for asynchronous callable functions with Context parameter."""
39
+
40
+ async def __call__(
41
+ self, ctx: Context[DepsT], *args: P.args, **kwargs: P.kwargs
42
+ ) -> R:
43
+ """The function's required asynchronous call method with context."""
44
+ ... # pragma: no cover
45
+
46
+
47
+ def fn_is_async(
48
+ fn: SyncFunction[P, R] | AsyncFunction[P, R],
49
+ ) -> TypeIs[AsyncFunction[P, R]]:
50
+ """Type check to determine if a given function is asynchronous."""
51
+ return inspect.iscoroutinefunction(fn) or inspect.isasyncgenfunction(fn)
@@ -0,0 +1,139 @@
1
+ """Session context helpers for grouping traces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from collections.abc import Iterator, Mapping
7
+ from contextlib import contextmanager
8
+ from contextvars import ContextVar, Token
9
+ from dataclasses import dataclass, field
10
+
11
+ from mirascope.ops._internal.types import Jsonable
12
+
13
+ SESSION_HEADER_NAME = "Mirascope-Session-Id"
14
+
15
+
16
+ SESSION_CONTEXT: ContextVar[SessionContext | None] = ContextVar(
17
+ "MIRASCOPE_SESSION_CONTEXT", default=None
18
+ )
19
+
20
+
21
+ @dataclass(slots=True)
22
+ class SessionContext:
23
+ """Represents a session context for grouping related spans."""
24
+
25
+ id: str = field(default_factory=lambda: str(uuid.uuid4()))
26
+ """Unique identifier for the session. Auto-generated if not provided."""
27
+
28
+ attributes: Mapping[str, Jsonable] | None = None
29
+ """Optional JSON-serializable metadata associated with the session."""
30
+
31
+
32
+ @contextmanager
33
+ def session(
34
+ *, id: str | None = None, attributes: Mapping[str, Jsonable] | None = None
35
+ ) -> Iterator[SessionContext]:
36
+ """Context manager for setting session context.
37
+
38
+ Sessions are used to group related traces together. The session ID and
39
+ optional attributes are automatically propagated to all spans created
40
+ within the session context and are included in outgoing HTTP requests.
41
+
42
+ Args:
43
+ id: Unique identifier for the session. If not provided, a UUID will be
44
+ automatically generated.
45
+ attributes: Optional dictionary of JSON-serializable attributes to
46
+ attach to the session.
47
+
48
+ Example:
49
+ ```python
50
+ # With explicit ID
51
+ with mirascope.ops.session(id="user-123") as ctx:
52
+ print(ctx.id) # "user-123"
53
+ result = requests.get("https://api.example.com")
54
+
55
+ # With auto-generated ID
56
+ with mirascope.ops.session() as ctx:
57
+ print(ctx.id) # Auto-generated UUID
58
+ result = requests.get("https://api.example.com")
59
+
60
+ # Nested sessions override parent session
61
+ with mirascope.ops.session(id="1"):
62
+ # Session ID: 1
63
+ with mirascope.ops.session(id="2"):
64
+ # Session ID: 2
65
+ # Session ID: 1
66
+ ```
67
+ """
68
+ if id is not None:
69
+ session_ctx = SessionContext(id=id, attributes=attributes)
70
+ else:
71
+ session_ctx = SessionContext(attributes=attributes)
72
+ token: Token[SessionContext | None] = SESSION_CONTEXT.set(session_ctx)
73
+ try:
74
+ yield session_ctx
75
+ finally:
76
+ SESSION_CONTEXT.reset(token)
77
+
78
+
79
+ def current_session() -> SessionContext | None:
80
+ """Get the current session context if one is active.
81
+
82
+ Returns:
83
+ The active SessionContext or None if no session is active.
84
+
85
+ Example:
86
+ ```python
87
+ with mirascope.ops.session(id="user-123"):
88
+ ctx = mirascope.ops.current_session()
89
+ print(ctx.id) # "user-123"
90
+
91
+ ctx = mirascope.ops.current_session()
92
+ print(ctx) # None
93
+ ```
94
+ """
95
+ return SESSION_CONTEXT.get()
96
+
97
+
98
+ def extract_session_id(headers: Mapping[str, str | list[str]]) -> str | None:
99
+ """Extract session ID from carrier headers.
100
+
101
+ Performs case-insensitive header name matching and handles both string
102
+ and list[str] header values.
103
+
104
+ Args:
105
+ headers: Dictionary of HTTP headers or similar carrier data.
106
+
107
+ Returns:
108
+ The extracted session ID or None if not present.
109
+
110
+ Example:
111
+ ```python
112
+ headers = {"mirascope-session-id": "session-123"}
113
+ session_id = extract_session_id(headers)
114
+ print(session_id) # "session-123"
115
+ ```
116
+ """
117
+ normalized_headers = {key.lower(): value for key, value in headers.items()}
118
+ header_name_lower = SESSION_HEADER_NAME.lower()
119
+
120
+ if header_name_lower not in normalized_headers:
121
+ return None
122
+
123
+ value = normalized_headers[header_name_lower]
124
+
125
+ if isinstance(value, list):
126
+ if len(value) > 0:
127
+ return value[0]
128
+ return None
129
+
130
+ return value if value else None
131
+
132
+
133
+ __all__ = [
134
+ "SESSION_HEADER_NAME",
135
+ "SessionContext",
136
+ "current_session",
137
+ "extract_session_id",
138
+ "session",
139
+ ]
@@ -0,0 +1,232 @@
1
+ """Explicit span management utilities for `mirascope.ops`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from contextvars import Token
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from opentelemetry import context as otel_context, trace as otel_trace
10
+ from opentelemetry.trace import (
11
+ SpanContext,
12
+ Status,
13
+ StatusCode,
14
+ format_span_id,
15
+ format_trace_id,
16
+ )
17
+ from opentelemetry.util.types import AttributeValue
18
+
19
+ from .session import current_session
20
+ from .utils import json_dumps
21
+
22
+ if TYPE_CHECKING:
23
+ from opentelemetry.context import Context
24
+
25
+ logger = logging.getLogger("mirascope.ops")
26
+ _warned_noop = False
27
+
28
+
29
+ class Span:
30
+ """Context-managed span for explicit tracing.
31
+
32
+ Creates a child span within the current trace context. Acts as a no-op
33
+ if tracing is not configured.
34
+ """
35
+
36
+ def __init__(self, name: str, **attributes: AttributeValue) -> None:
37
+ """Initialize a new span with the given name.
38
+
39
+ Args:
40
+ name: Name for the span.
41
+ **attributes: Initial attributes to set on the span.
42
+ """
43
+ self._name = name
44
+ self._initial_attributes = attributes
45
+ self._span: otel_trace.Span | None = None
46
+ self._token: Token[Context] | None = None
47
+ self._is_noop = True
48
+ self._finished = False
49
+
50
+ def __enter__(self) -> Span:
51
+ """Enter the span context.
52
+
53
+ Returns:
54
+ This span instance for use within the context.
55
+ """
56
+ tracer = otel_trace.get_tracer("mirascope.ops")
57
+ self._span = tracer.start_span(self._name)
58
+
59
+ if self._span.__class__.__name__ == "NonRecordingSpan":
60
+ self._is_noop = True
61
+ self._span = None
62
+ global _warned_noop
63
+ if not _warned_noop:
64
+ logger.warning(
65
+ f"mirascope tracing is not configured; Span('{self._name}') is a no-op."
66
+ )
67
+ _warned_noop = True
68
+ else:
69
+ self._is_noop = False
70
+ self._span.set_attribute("mirascope.type", "trace")
71
+
72
+ session_ctx = current_session()
73
+ if session_ctx is not None:
74
+ self._span.set_attribute("mirascope.ops.session.id", session_ctx.id)
75
+ if session_ctx.attributes is not None:
76
+ self._span.set_attribute(
77
+ "mirascope.ops.session.attributes",
78
+ json_dumps(session_ctx.attributes),
79
+ )
80
+
81
+ self._token = otel_context.attach(
82
+ otel_trace.set_span_in_context(self._span)
83
+ )
84
+
85
+ if self._initial_attributes:
86
+ self.set(**self._initial_attributes)
87
+
88
+ return self
89
+
90
+ def __exit__(
91
+ self,
92
+ exc_type: type[BaseException] | None,
93
+ exc: BaseException | None,
94
+ tb: Any, # noqa: ANN401
95
+ ) -> None:
96
+ """Exit the span context.
97
+
98
+ Args:
99
+ exc_type: Exception type if an exception was raised.
100
+ exc: Exception instance if an exception was raised.
101
+ tb: Traceback if an exception was raised.
102
+ """
103
+ if self._span and not self._finished:
104
+ if exc is not None:
105
+ self._span.record_exception(exc)
106
+ self._span.set_status(Status(StatusCode.ERROR))
107
+ self.finish()
108
+
109
+ def set(self, **attributes: AttributeValue) -> None:
110
+ """Set attributes on the current span.
111
+
112
+ Args:
113
+ **attributes: Key-value pairs to set as span attributes.
114
+ """
115
+ if self._span and not self._finished:
116
+ for key, value in attributes.items():
117
+ self._span.set_attribute(key, value)
118
+
119
+ def event(self, name: str, **attributes: AttributeValue) -> None:
120
+ """Record an event within the span.
121
+
122
+ Args:
123
+ name: Name of the event.
124
+ **attributes: Event attributes as key-value pairs.
125
+ """
126
+ if self._span and not self._finished:
127
+ self._span.add_event(name, attributes=attributes)
128
+
129
+ def debug(self, message: str, **additional_attributes: AttributeValue) -> None:
130
+ """Log a debug message within the span.
131
+
132
+ Args:
133
+ message: Debug message text.
134
+ **additional_attributes: Additional structured attributes for the log entry.
135
+ """
136
+ self.event("debug", level="debug", message=message, **additional_attributes)
137
+
138
+ def info(self, message: str, **additional_attributes: AttributeValue) -> None:
139
+ """Log an info message within the span.
140
+
141
+ Args:
142
+ message: Info message text.
143
+ **additional_attributes: Additional structured attributes for the log entry.
144
+ """
145
+ self.event("info", level="info", message=message, **additional_attributes)
146
+
147
+ def warning(self, message: str, **additional_attributes: AttributeValue) -> None:
148
+ """Log a warning message within the span.
149
+
150
+ Args:
151
+ message: Warning message text.
152
+ **additional_attributes: Additional structured attributes for the log entry.
153
+ """
154
+ self.event("warning", level="warning", message=message, **additional_attributes)
155
+
156
+ def error(self, message: str, **additional_attributes: AttributeValue) -> None:
157
+ """Log an error message within the span.
158
+
159
+ Args:
160
+ message: Error message text.
161
+ **additional_attributes: Additional structured attributes for the log entry.
162
+ """
163
+ self.event("error", level="error", message=message, **additional_attributes)
164
+ if self._span and not self._finished:
165
+ self._span.set_status(Status(StatusCode.ERROR))
166
+
167
+ def critical(self, message: str, **additional_attributes: AttributeValue) -> None:
168
+ """Log a critical message within the span.
169
+
170
+ Args:
171
+ message: Critical message text.
172
+ **additional_attributes: Additional structured attributes for the log entry.
173
+ """
174
+ self.event("critical", level="error", message=message, **additional_attributes)
175
+ if self._span and not self._finished:
176
+ self._span.set_status(Status(StatusCode.ERROR))
177
+
178
+ def finish(self) -> None:
179
+ """Explicitly finish the span."""
180
+ if not self._finished:
181
+ self._finished = True
182
+ if self._span:
183
+ self._span.end()
184
+ if self._token:
185
+ otel_context.detach(self._token)
186
+ self._token = None
187
+
188
+ @property
189
+ def span_id(self) -> str | None:
190
+ """Get the span ID if available.
191
+
192
+ Returns:
193
+ The span ID or None if not available.
194
+ """
195
+ if span_context := self.span_context:
196
+ return format_span_id(span_context.span_id)
197
+ return None
198
+
199
+ @property
200
+ def trace_id(self) -> str | None:
201
+ if span_context := self.span_context:
202
+ return format_trace_id(span_context.trace_id)
203
+ return None
204
+
205
+ @property
206
+ def is_noop(self) -> bool:
207
+ """Check if this is a no-op span.
208
+
209
+ Returns:
210
+ True if tracing is disabled, False otherwise.
211
+ """
212
+ return self._is_noop
213
+
214
+ @property
215
+ def span_context(self) -> SpanContext | None:
216
+ """Return the span context if available."""
217
+ if otel_span := self._span:
218
+ return otel_span.get_span_context()
219
+ return None
220
+
221
+
222
+ def span(name: str, **attributes: AttributeValue) -> Span:
223
+ """Create a new span context manager.
224
+
225
+ Args:
226
+ name: Name for the new span.
227
+ **attributes: Initial attributes to set on the span.
228
+
229
+ Returns:
230
+ A Span context manager that creates a child span when entered.
231
+ """
232
+ return Span(name, **attributes)