mirascope 2.0.1__py3-none-any.whl → 2.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.
- mirascope/_stubs.py +39 -18
- mirascope/_utils.py +34 -0
- mirascope/api/_generated/__init__.py +4 -0
- mirascope/api/_generated/organization_invitations/client.py +2 -2
- mirascope/api/_generated/organization_invitations/raw_client.py +2 -2
- mirascope/api/_generated/project_memberships/__init__.py +4 -0
- mirascope/api/_generated/project_memberships/client.py +91 -0
- mirascope/api/_generated/project_memberships/raw_client.py +239 -0
- mirascope/api/_generated/project_memberships/types/__init__.py +4 -0
- mirascope/api/_generated/project_memberships/types/project_memberships_get_response.py +33 -0
- mirascope/api/_generated/project_memberships/types/project_memberships_get_response_role.py +7 -0
- mirascope/api/_generated/reference.md +73 -1
- mirascope/llm/__init__.py +19 -0
- mirascope/llm/calls/calls.py +28 -21
- mirascope/llm/calls/decorator.py +17 -24
- mirascope/llm/formatting/__init__.py +2 -2
- mirascope/llm/formatting/format.py +2 -4
- mirascope/llm/formatting/types.py +19 -2
- mirascope/llm/models/models.py +66 -146
- mirascope/llm/prompts/decorator.py +5 -16
- mirascope/llm/prompts/prompts.py +35 -38
- mirascope/llm/providers/anthropic/_utils/beta_decode.py +22 -7
- mirascope/llm/providers/anthropic/_utils/beta_encode.py +22 -16
- mirascope/llm/providers/anthropic/_utils/decode.py +45 -7
- mirascope/llm/providers/anthropic/_utils/encode.py +28 -15
- mirascope/llm/providers/anthropic/beta_provider.py +33 -69
- mirascope/llm/providers/anthropic/provider.py +52 -91
- mirascope/llm/providers/base/_utils.py +4 -9
- mirascope/llm/providers/base/base_provider.py +89 -205
- mirascope/llm/providers/google/_utils/decode.py +51 -1
- mirascope/llm/providers/google/_utils/encode.py +38 -21
- mirascope/llm/providers/google/provider.py +33 -69
- mirascope/llm/providers/mirascope/provider.py +25 -61
- mirascope/llm/providers/mlx/encoding/base.py +3 -6
- mirascope/llm/providers/mlx/encoding/transformers.py +4 -8
- mirascope/llm/providers/mlx/mlx.py +9 -21
- mirascope/llm/providers/mlx/provider.py +33 -69
- mirascope/llm/providers/openai/completions/_utils/encode.py +39 -20
- mirascope/llm/providers/openai/completions/base_provider.py +34 -75
- mirascope/llm/providers/openai/provider.py +25 -61
- mirascope/llm/providers/openai/responses/_utils/decode.py +31 -2
- mirascope/llm/providers/openai/responses/_utils/encode.py +32 -17
- mirascope/llm/providers/openai/responses/provider.py +34 -75
- mirascope/llm/responses/__init__.py +2 -1
- mirascope/llm/responses/base_stream_response.py +4 -0
- mirascope/llm/responses/response.py +8 -12
- mirascope/llm/responses/stream_response.py +8 -12
- mirascope/llm/responses/usage.py +44 -0
- mirascope/llm/tools/__init__.py +24 -0
- mirascope/llm/tools/provider_tools.py +18 -0
- mirascope/llm/tools/tool_schema.py +11 -4
- mirascope/llm/tools/toolkit.py +24 -6
- mirascope/llm/tools/types.py +112 -0
- mirascope/llm/tools/web_search_tool.py +32 -0
- mirascope/ops/__init__.py +19 -1
- mirascope/ops/_internal/closure.py +4 -1
- mirascope/ops/_internal/exporters/exporters.py +13 -46
- mirascope/ops/_internal/exporters/utils.py +37 -0
- mirascope/ops/_internal/instrumentation/__init__.py +20 -0
- mirascope/ops/_internal/instrumentation/llm/common.py +19 -49
- mirascope/ops/_internal/instrumentation/llm/model.py +61 -82
- mirascope/ops/_internal/instrumentation/llm/serialize.py +36 -12
- mirascope/ops/_internal/instrumentation/providers/__init__.py +29 -0
- mirascope/ops/_internal/instrumentation/providers/anthropic.py +78 -0
- mirascope/ops/_internal/instrumentation/providers/base.py +179 -0
- mirascope/ops/_internal/instrumentation/providers/google_genai.py +85 -0
- mirascope/ops/_internal/instrumentation/providers/openai.py +82 -0
- mirascope/ops/_internal/traced_calls.py +14 -0
- mirascope/ops/_internal/traced_functions.py +7 -2
- mirascope/ops/_internal/utils.py +12 -4
- mirascope/ops/_internal/versioned_functions.py +1 -1
- {mirascope-2.0.1.dist-info → mirascope-2.1.0.dist-info}/METADATA +96 -68
- {mirascope-2.0.1.dist-info → mirascope-2.1.0.dist-info}/RECORD +75 -64
- {mirascope-2.0.1.dist-info → mirascope-2.1.0.dist-info}/WHEEL +0 -0
- {mirascope-2.0.1.dist-info → mirascope-2.1.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""OpenTelemetry instrumentation for Anthropic SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor
|
|
8
|
+
|
|
9
|
+
from .base import BaseInstrumentation, ContentCaptureMode
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from opentelemetry.trace import TracerProvider
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AnthropicInstrumentation(BaseInstrumentation[AnthropicInstrumentor]):
|
|
16
|
+
"""Manages OpenTelemetry instrumentation lifecycle for the Anthropic SDK."""
|
|
17
|
+
|
|
18
|
+
def _create_instrumentor(self) -> AnthropicInstrumentor:
|
|
19
|
+
"""Create a new Anthropic instrumentor instance."""
|
|
20
|
+
return AnthropicInstrumentor()
|
|
21
|
+
|
|
22
|
+
def _configure_capture_content(self, capture_content: ContentCaptureMode) -> None:
|
|
23
|
+
"""Configure environment variables for Anthropic content capture."""
|
|
24
|
+
if capture_content == "enabled":
|
|
25
|
+
self._set_env_var("TRACELOOP_TRACE_CONTENT", "true")
|
|
26
|
+
elif capture_content == "disabled":
|
|
27
|
+
self._set_env_var("TRACELOOP_TRACE_CONTENT", "false")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def instrument_anthropic(
|
|
31
|
+
*,
|
|
32
|
+
tracer_provider: TracerProvider | None = None,
|
|
33
|
+
capture_content: ContentCaptureMode = "default",
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Enable OpenTelemetry instrumentation for the Anthropic Python SDK.
|
|
36
|
+
|
|
37
|
+
Uses the provided tracer_provider or the global OpenTelemetry tracer provider.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
tracer_provider: Optional tracer provider to use. If not provided,
|
|
41
|
+
uses the global OpenTelemetry tracer provider.
|
|
42
|
+
capture_content: Controls whether to capture message content in spans.
|
|
43
|
+
- "enabled": Capture message content
|
|
44
|
+
- "disabled": Do not capture message content
|
|
45
|
+
- "default": Use the library's default behavior
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
|
|
49
|
+
Enable instrumentation with Mirascope Cloud:
|
|
50
|
+
```python
|
|
51
|
+
from mirascope import ops
|
|
52
|
+
|
|
53
|
+
ops.configure()
|
|
54
|
+
ops.instrument_anthropic()
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Enable instrumentation with content capture:
|
|
58
|
+
```python
|
|
59
|
+
from mirascope import ops
|
|
60
|
+
|
|
61
|
+
ops.configure()
|
|
62
|
+
ops.instrument_anthropic(capture_content="enabled")
|
|
63
|
+
```
|
|
64
|
+
"""
|
|
65
|
+
AnthropicInstrumentation().instrument(
|
|
66
|
+
tracer_provider=tracer_provider,
|
|
67
|
+
capture_content=capture_content,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def uninstrument_anthropic() -> None:
|
|
72
|
+
"""Disable previously configured Anthropic instrumentation."""
|
|
73
|
+
AnthropicInstrumentation().uninstrument()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def is_anthropic_instrumented() -> bool:
|
|
77
|
+
"""Return whether Anthropic instrumentation is currently active."""
|
|
78
|
+
return AnthropicInstrumentation().is_instrumented
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Base class for OpenTelemetry SDK instrumentation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import threading
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from typing import TYPE_CHECKING, Generic, Literal, Protocol, TypeVar
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from opentelemetry.trace import TracerProvider
|
|
12
|
+
|
|
13
|
+
ContentCaptureMode = Literal["enabled", "disabled", "default"]
|
|
14
|
+
|
|
15
|
+
# OpenTelemetry semantic conventions environment variable
|
|
16
|
+
OTEL_SEMCONV_STABILITY_OPT_IN = "OTEL_SEMCONV_STABILITY_OPT_IN"
|
|
17
|
+
OTEL_SEMCONV_STABILITY_VALUE = "gen_ai_latest_experimental"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Instrumentor(Protocol):
|
|
21
|
+
"""Protocol for OpenTelemetry instrumentors."""
|
|
22
|
+
|
|
23
|
+
def instrument(self, *, tracer_provider: TracerProvider | None = None) -> None:
|
|
24
|
+
"""Enable instrumentation."""
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
def uninstrument(self) -> None:
|
|
28
|
+
"""Disable instrumentation."""
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
InstrumentorT = TypeVar("InstrumentorT", bound=Instrumentor)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BaseInstrumentation(ABC, Generic[InstrumentorT]):
|
|
36
|
+
"""Base class for managing OpenTelemetry instrumentation lifecycle.
|
|
37
|
+
|
|
38
|
+
This class provides a thread-safe singleton pattern for SDK instrumentation.
|
|
39
|
+
Subclasses must implement `_create_instrumentor()` and `_configure_capture_content()`.
|
|
40
|
+
|
|
41
|
+
Each subclass gets its own `_instance` and `_instance_lock` via `__init_subclass__`,
|
|
42
|
+
ensuring that different providers can be initialized independently without blocking.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
_instance: BaseInstrumentation[InstrumentorT] | None = None
|
|
46
|
+
_instance_lock: threading.Lock
|
|
47
|
+
_lock: threading.Lock
|
|
48
|
+
_instrumentor: InstrumentorT | None
|
|
49
|
+
_original_env: dict[str, str | None]
|
|
50
|
+
|
|
51
|
+
def __init_subclass__(cls, **kwargs: object) -> None:
|
|
52
|
+
"""Initialize subclass with its own singleton instance and lock."""
|
|
53
|
+
super().__init_subclass__(**kwargs)
|
|
54
|
+
cls._instance = None
|
|
55
|
+
cls._instance_lock = threading.Lock()
|
|
56
|
+
|
|
57
|
+
def __new__(cls) -> BaseInstrumentation[InstrumentorT]:
|
|
58
|
+
"""Create or return the singleton instance."""
|
|
59
|
+
if cls._instance is None:
|
|
60
|
+
with cls._instance_lock:
|
|
61
|
+
if cls._instance is None:
|
|
62
|
+
instance = super().__new__(cls)
|
|
63
|
+
instance._lock = threading.Lock()
|
|
64
|
+
instance._instrumentor = None
|
|
65
|
+
instance._original_env = {}
|
|
66
|
+
cls._instance = instance
|
|
67
|
+
return cls._instance
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def is_instrumented(self) -> bool:
|
|
71
|
+
"""Return whether instrumentation is currently active."""
|
|
72
|
+
return self._instrumentor is not None
|
|
73
|
+
|
|
74
|
+
@abstractmethod
|
|
75
|
+
def _create_instrumentor(self) -> InstrumentorT:
|
|
76
|
+
"""Create and return a new instrumentor instance.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
A new instance of the SDK-specific instrumentor.
|
|
80
|
+
"""
|
|
81
|
+
...
|
|
82
|
+
|
|
83
|
+
@abstractmethod
|
|
84
|
+
def _configure_capture_content(self, capture_content: ContentCaptureMode) -> None:
|
|
85
|
+
"""Configure environment variables for content capture.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
capture_content: The capture content mode to configure.
|
|
89
|
+
"""
|
|
90
|
+
...
|
|
91
|
+
|
|
92
|
+
def instrument(
|
|
93
|
+
self,
|
|
94
|
+
*,
|
|
95
|
+
tracer_provider: TracerProvider | None = None,
|
|
96
|
+
capture_content: ContentCaptureMode = "default",
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Enable OpenTelemetry instrumentation for the SDK.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
tracer_provider: Optional tracer provider to use. If not provided,
|
|
102
|
+
uses the global OpenTelemetry tracer provider.
|
|
103
|
+
capture_content: Controls whether to capture message content in spans.
|
|
104
|
+
"""
|
|
105
|
+
with self._lock:
|
|
106
|
+
if self.is_instrumented:
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
self._set_env_var(
|
|
110
|
+
OTEL_SEMCONV_STABILITY_OPT_IN,
|
|
111
|
+
OTEL_SEMCONV_STABILITY_VALUE,
|
|
112
|
+
use_setdefault=True,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
self._configure_capture_content(capture_content)
|
|
116
|
+
|
|
117
|
+
instrumentor = self._create_instrumentor()
|
|
118
|
+
try:
|
|
119
|
+
if tracer_provider is not None:
|
|
120
|
+
instrumentor.instrument(tracer_provider=tracer_provider)
|
|
121
|
+
else:
|
|
122
|
+
instrumentor.instrument()
|
|
123
|
+
except Exception:
|
|
124
|
+
self._restore_env_vars()
|
|
125
|
+
raise
|
|
126
|
+
|
|
127
|
+
self._instrumentor = instrumentor
|
|
128
|
+
|
|
129
|
+
def uninstrument(self) -> None:
|
|
130
|
+
"""Disable previously configured instrumentation."""
|
|
131
|
+
with self._lock:
|
|
132
|
+
if self._instrumentor is None:
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
self._instrumentor.uninstrument()
|
|
136
|
+
self._instrumentor = None
|
|
137
|
+
self._restore_env_vars()
|
|
138
|
+
|
|
139
|
+
def _set_env_var(
|
|
140
|
+
self, key: str, value: str, *, use_setdefault: bool = False
|
|
141
|
+
) -> None:
|
|
142
|
+
"""Set an environment variable and track the original value.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
key: The environment variable name.
|
|
146
|
+
value: The value to set.
|
|
147
|
+
use_setdefault: If True, only set if not already present.
|
|
148
|
+
"""
|
|
149
|
+
if key not in self._original_env:
|
|
150
|
+
self._original_env[key] = os.environ.get(key)
|
|
151
|
+
|
|
152
|
+
if use_setdefault:
|
|
153
|
+
os.environ.setdefault(key, value)
|
|
154
|
+
else:
|
|
155
|
+
os.environ[key] = value
|
|
156
|
+
|
|
157
|
+
def _restore_env_vars(self) -> None:
|
|
158
|
+
"""Restore all environment variables to their original values."""
|
|
159
|
+
for key, value in self._original_env.items():
|
|
160
|
+
if value is None:
|
|
161
|
+
os.environ.pop(key, None)
|
|
162
|
+
else:
|
|
163
|
+
os.environ[key] = value
|
|
164
|
+
self._original_env.clear()
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def reset_for_testing(cls) -> None:
|
|
168
|
+
"""Reset singleton instance for testing.
|
|
169
|
+
|
|
170
|
+
This properly cleans up the instrumentor and restores environment variables
|
|
171
|
+
before resetting the instance.
|
|
172
|
+
"""
|
|
173
|
+
with cls._instance_lock:
|
|
174
|
+
if cls._instance is not None:
|
|
175
|
+
if cls._instance._instrumentor is not None:
|
|
176
|
+
cls._instance._instrumentor.uninstrument()
|
|
177
|
+
cls._instance._instrumentor = None
|
|
178
|
+
cls._instance._restore_env_vars()
|
|
179
|
+
cls._instance = None
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""OpenTelemetry instrumentation for Google GenAI SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from opentelemetry.instrumentation.google_genai import GoogleGenAiSdkInstrumentor
|
|
8
|
+
|
|
9
|
+
from .base import BaseInstrumentation, ContentCaptureMode
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from opentelemetry.trace import TracerProvider
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GoogleGenAIInstrumentation(BaseInstrumentation[GoogleGenAiSdkInstrumentor]):
|
|
16
|
+
"""Manages OpenTelemetry instrumentation lifecycle for the Google GenAI SDK."""
|
|
17
|
+
|
|
18
|
+
def _create_instrumentor(self) -> GoogleGenAiSdkInstrumentor:
|
|
19
|
+
"""Create a new Google GenAI instrumentor instance."""
|
|
20
|
+
return GoogleGenAiSdkInstrumentor()
|
|
21
|
+
|
|
22
|
+
def _configure_capture_content(self, capture_content: ContentCaptureMode) -> None:
|
|
23
|
+
"""Configure environment variables for Google GenAI content capture."""
|
|
24
|
+
# Google GenAI uses ContentCapturingMode enum instead of true/false.
|
|
25
|
+
# Valid values: NO_CONTENT, SPAN_ONLY, EVENT_ONLY, SPAN_AND_EVENT
|
|
26
|
+
if capture_content == "enabled":
|
|
27
|
+
self._set_env_var(
|
|
28
|
+
"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT",
|
|
29
|
+
"SPAN_AND_EVENT",
|
|
30
|
+
)
|
|
31
|
+
elif capture_content == "disabled":
|
|
32
|
+
self._set_env_var(
|
|
33
|
+
"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", "NO_CONTENT"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def instrument_google_genai(
|
|
38
|
+
*,
|
|
39
|
+
tracer_provider: TracerProvider | None = None,
|
|
40
|
+
capture_content: ContentCaptureMode = "default",
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Enable OpenTelemetry instrumentation for the Google GenAI Python SDK.
|
|
43
|
+
|
|
44
|
+
Uses the provided tracer_provider or the global OpenTelemetry tracer provider.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
tracer_provider: Optional tracer provider to use. If not provided,
|
|
48
|
+
uses the global OpenTelemetry tracer provider.
|
|
49
|
+
capture_content: Controls whether to capture message content in spans.
|
|
50
|
+
- "enabled": Capture message content
|
|
51
|
+
- "disabled": Do not capture message content
|
|
52
|
+
- "default": Use the library's default behavior
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
|
|
56
|
+
Enable instrumentation with Mirascope Cloud:
|
|
57
|
+
```python
|
|
58
|
+
from mirascope import ops
|
|
59
|
+
|
|
60
|
+
ops.configure()
|
|
61
|
+
ops.instrument_google_genai()
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Enable instrumentation with content capture:
|
|
65
|
+
```python
|
|
66
|
+
from mirascope import ops
|
|
67
|
+
|
|
68
|
+
ops.configure()
|
|
69
|
+
ops.instrument_google_genai(capture_content="enabled")
|
|
70
|
+
```
|
|
71
|
+
"""
|
|
72
|
+
GoogleGenAIInstrumentation().instrument(
|
|
73
|
+
tracer_provider=tracer_provider,
|
|
74
|
+
capture_content=capture_content,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def uninstrument_google_genai() -> None:
|
|
79
|
+
"""Disable previously configured Google GenAI instrumentation."""
|
|
80
|
+
GoogleGenAIInstrumentation().uninstrument()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def is_google_genai_instrumented() -> bool:
|
|
84
|
+
"""Return whether Google GenAI instrumentation is currently active."""
|
|
85
|
+
return GoogleGenAIInstrumentation().is_instrumented
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""OpenTelemetry instrumentation for OpenAI SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor
|
|
8
|
+
|
|
9
|
+
from .base import BaseInstrumentation, ContentCaptureMode
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from opentelemetry.trace import TracerProvider
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class OpenAIInstrumentation(BaseInstrumentation[OpenAIInstrumentor]):
|
|
16
|
+
"""Manages OpenTelemetry instrumentation lifecycle for the OpenAI SDK."""
|
|
17
|
+
|
|
18
|
+
def _create_instrumentor(self) -> OpenAIInstrumentor:
|
|
19
|
+
"""Create a new OpenAI instrumentor instance."""
|
|
20
|
+
return OpenAIInstrumentor()
|
|
21
|
+
|
|
22
|
+
def _configure_capture_content(self, capture_content: ContentCaptureMode) -> None:
|
|
23
|
+
"""Configure environment variables for OpenAI content capture."""
|
|
24
|
+
if capture_content == "enabled":
|
|
25
|
+
self._set_env_var(
|
|
26
|
+
"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", "true"
|
|
27
|
+
)
|
|
28
|
+
elif capture_content == "disabled":
|
|
29
|
+
self._set_env_var(
|
|
30
|
+
"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", "false"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def instrument_openai(
|
|
35
|
+
*,
|
|
36
|
+
tracer_provider: TracerProvider | None = None,
|
|
37
|
+
capture_content: ContentCaptureMode = "default",
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Enable OpenTelemetry instrumentation for the OpenAI Python SDK.
|
|
40
|
+
|
|
41
|
+
Uses the provided tracer_provider or the global OpenTelemetry tracer provider.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
tracer_provider: Optional tracer provider to use. If not provided,
|
|
45
|
+
uses the global OpenTelemetry tracer provider.
|
|
46
|
+
capture_content: Controls whether to capture message content in spans.
|
|
47
|
+
- "enabled": Capture message content
|
|
48
|
+
- "disabled": Do not capture message content
|
|
49
|
+
- "default": Use the library's default behavior
|
|
50
|
+
|
|
51
|
+
Example:
|
|
52
|
+
|
|
53
|
+
Enable instrumentation with Mirascope Cloud:
|
|
54
|
+
```python
|
|
55
|
+
from mirascope import ops
|
|
56
|
+
|
|
57
|
+
ops.configure()
|
|
58
|
+
ops.instrument_openai()
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Enable instrumentation with content capture:
|
|
62
|
+
```python
|
|
63
|
+
from mirascope import ops
|
|
64
|
+
|
|
65
|
+
ops.configure()
|
|
66
|
+
ops.instrument_openai(capture_content="enabled")
|
|
67
|
+
```
|
|
68
|
+
"""
|
|
69
|
+
OpenAIInstrumentation().instrument(
|
|
70
|
+
tracer_provider=tracer_provider,
|
|
71
|
+
capture_content=capture_content,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def uninstrument_openai() -> None:
|
|
76
|
+
"""Disable previously configured OpenAI instrumentation."""
|
|
77
|
+
OpenAIInstrumentation().uninstrument()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def is_openai_instrumented() -> bool:
|
|
81
|
+
"""Return whether OpenAI instrumentation is currently active."""
|
|
82
|
+
return OpenAIInstrumentation().is_instrumented
|
|
@@ -6,6 +6,7 @@ from dataclasses import dataclass, field
|
|
|
6
6
|
from typing import Any, Generic, TypeVar
|
|
7
7
|
from typing_extensions import TypeIs
|
|
8
8
|
|
|
9
|
+
from ..._utils import copy_function_metadata
|
|
9
10
|
from ...llm.calls import AsyncCall, AsyncContextCall, Call, ContextCall
|
|
10
11
|
from ...llm.context import Context, DepsT
|
|
11
12
|
from ...llm.formatting import FormattableT
|
|
@@ -35,6 +36,7 @@ from .traced_functions import (
|
|
|
35
36
|
TracedContextFunction,
|
|
36
37
|
TracedFunction,
|
|
37
38
|
)
|
|
39
|
+
from .utils import get_original_fn
|
|
38
40
|
|
|
39
41
|
CallT = TypeVar(
|
|
40
42
|
"CallT",
|
|
@@ -106,6 +108,14 @@ class _BaseTracedCall(Generic[CallT]):
|
|
|
106
108
|
metadata: dict[str, str] = field(default_factory=dict)
|
|
107
109
|
"""Arbitrary key-value pairs for additional metadata."""
|
|
108
110
|
|
|
111
|
+
__name__: str = field(init=False, repr=False, default="")
|
|
112
|
+
"""The name of the underlying function (preserved for decorator stacking)."""
|
|
113
|
+
|
|
114
|
+
def __post_init__(self) -> None:
|
|
115
|
+
"""Preserve standard function attributes for decorator stacking."""
|
|
116
|
+
original_fn = get_original_fn(self._call.prompt.fn)
|
|
117
|
+
copy_function_metadata(self, original_fn)
|
|
118
|
+
|
|
109
119
|
|
|
110
120
|
@dataclass(kw_only=True)
|
|
111
121
|
class TracedCall(_BaseTracedCall[Call[P, FormattableT]]):
|
|
@@ -153,6 +163,7 @@ class TracedCall(_BaseTracedCall[Call[P, FormattableT]]):
|
|
|
153
163
|
|
|
154
164
|
def __post_init__(self) -> None:
|
|
155
165
|
"""Initialize TracedFunction wrappers for call and stream methods."""
|
|
166
|
+
super().__post_init__()
|
|
156
167
|
self.call = TracedFunction(
|
|
157
168
|
fn=self._call.call, tags=self.tags, metadata=self.metadata
|
|
158
169
|
)
|
|
@@ -213,6 +224,7 @@ class TracedAsyncCall(_BaseTracedCall[AsyncCall[P, FormattableT]]):
|
|
|
213
224
|
|
|
214
225
|
def __post_init__(self) -> None:
|
|
215
226
|
"""Initialize AsyncTracedFunction wrappers for call and stream methods."""
|
|
227
|
+
super().__post_init__()
|
|
216
228
|
self.call = AsyncTracedFunction(
|
|
217
229
|
fn=self._call.call, tags=self.tags, metadata=self.metadata
|
|
218
230
|
)
|
|
@@ -276,6 +288,7 @@ class TracedContextCall(_BaseTracedCall[ContextCall[P, DepsT, FormattableT]]):
|
|
|
276
288
|
|
|
277
289
|
def __post_init__(self) -> None:
|
|
278
290
|
"""Initialize TracedContextFunction wrappers for call and stream methods."""
|
|
291
|
+
super().__post_init__()
|
|
279
292
|
self.call = TracedContextFunction(
|
|
280
293
|
fn=self._call.call, tags=self.tags, metadata=self.metadata
|
|
281
294
|
)
|
|
@@ -344,6 +357,7 @@ class TracedAsyncContextCall(_BaseTracedCall[AsyncContextCall[P, DepsT, Formatta
|
|
|
344
357
|
|
|
345
358
|
def __post_init__(self) -> None:
|
|
346
359
|
"""Initialize AsyncTracedContextFunction wrappers for call and stream methods."""
|
|
360
|
+
super().__post_init__()
|
|
347
361
|
self.call = AsyncTracedContextFunction(
|
|
348
362
|
fn=self._call.call, tags=self.tags, metadata=self.metadata
|
|
349
363
|
)
|
|
@@ -10,6 +10,7 @@ from typing import Any, Generic, Literal, TypeVar
|
|
|
10
10
|
|
|
11
11
|
from opentelemetry.util.types import AttributeValue
|
|
12
12
|
|
|
13
|
+
from ..._utils import copy_function_metadata
|
|
13
14
|
from ...api.client import get_async_client, get_sync_client
|
|
14
15
|
from ...llm.context import Context, DepsT
|
|
15
16
|
from ...llm.responses.root_response import RootResponse
|
|
@@ -175,11 +176,15 @@ class _BaseFunction(Generic[P, R, FunctionT], ABC):
|
|
|
175
176
|
_is_async: bool = field(init=False)
|
|
176
177
|
"""Whether the wrapped function is asynchronous."""
|
|
177
178
|
|
|
179
|
+
__name__: str = field(init=False, repr=False, default="")
|
|
180
|
+
"""The name of the underlying function (preserved for decorator stacking)."""
|
|
181
|
+
|
|
178
182
|
def __post_init__(self) -> None:
|
|
179
183
|
"""Initialize additional attributes after dataclass init."""
|
|
180
184
|
self._qualified_name = get_qualified_name(self.fn)
|
|
181
185
|
original_fn = get_original_fn(self.fn)
|
|
182
186
|
self._module_name = getattr(original_fn, "__module__", "")
|
|
187
|
+
copy_function_metadata(self, original_fn)
|
|
183
188
|
|
|
184
189
|
|
|
185
190
|
@dataclass(kw_only=True)
|
|
@@ -200,7 +205,7 @@ class _BaseTracedFunction(_BaseFunction[P, R, FunctionT]):
|
|
|
200
205
|
"mirascope.trace.arg_values": json_dumps(arg_values),
|
|
201
206
|
}
|
|
202
207
|
if self.tags:
|
|
203
|
-
attributes["mirascope.trace.tags"] = self.tags
|
|
208
|
+
attributes["mirascope.trace.tags"] = list(self.tags)
|
|
204
209
|
if self.metadata:
|
|
205
210
|
attributes["mirascope.trace.metadata"] = json_dumps(self.metadata)
|
|
206
211
|
span.set(**attributes)
|
|
@@ -314,7 +319,7 @@ class _BaseTracedContextFunction(
|
|
|
314
319
|
"mirascope.trace.arg_values": json_dumps(arg_values),
|
|
315
320
|
}
|
|
316
321
|
if self.tags:
|
|
317
|
-
attributes["mirascope.trace.tags"] = self.tags
|
|
322
|
+
attributes["mirascope.trace.tags"] = list(self.tags)
|
|
318
323
|
if self.metadata:
|
|
319
324
|
attributes["mirascope.trace.metadata"] = json_dumps(self.metadata)
|
|
320
325
|
span.set(**attributes)
|
mirascope/ops/_internal/utils.py
CHANGED
|
@@ -33,15 +33,23 @@ def _is_call_method(fn: Callable[..., Any]) -> bool:
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
def get_original_fn(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
36
|
-
"""Get the original function
|
|
36
|
+
"""Get the original unwrapped function.
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
Follows the __wrapped__ chain set by copy_function_metadata() to find the
|
|
39
|
+
original function. Falls back to checking for Call methods for bound methods.
|
|
40
40
|
"""
|
|
41
|
+
# Follow __wrapped__ chain if available (set by copy_function_metadata)
|
|
42
|
+
# In practice, get_original_fn is called with original functions or bound methods,
|
|
43
|
+
# so this loop is defensive code for edge cases like @functools.wraps chains.
|
|
44
|
+
while hasattr(fn, "__wrapped__"):
|
|
45
|
+
fn = fn.__wrapped__ # pyright: ignore[reportFunctionMemberAccess] # pragma: no cover
|
|
46
|
+
|
|
47
|
+
# Handle bound methods of Call objects (e.g., Call.call or Call.stream)
|
|
41
48
|
if _is_call_method(fn):
|
|
42
49
|
prompt = fn.__self__.prompt # pyright: ignore[reportFunctionMemberAccess]
|
|
43
50
|
if hasattr(prompt, "fn"):
|
|
44
|
-
return prompt.fn
|
|
51
|
+
return get_original_fn(prompt.fn)
|
|
52
|
+
|
|
45
53
|
return fn
|
|
46
54
|
|
|
47
55
|
|
|
@@ -196,7 +196,7 @@ class _BaseVersionedFunction(_BaseTracedFunction[P, R, Any]):
|
|
|
196
196
|
if self.name:
|
|
197
197
|
span.set(**{"mirascope.version.name": self.name})
|
|
198
198
|
if self.tags:
|
|
199
|
-
span.set(**{"mirascope.version.tags": self.tags})
|
|
199
|
+
span.set(**{"mirascope.version.tags": list(self.tags)})
|
|
200
200
|
if self.metadata:
|
|
201
201
|
for key, value in self.metadata.items():
|
|
202
202
|
span.set(**{f"mirascope.version.meta.{key}": value})
|