mirascope 2.0.2__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/api/_generated/__init__.py +4 -0
- 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 +72 -0
- mirascope/llm/__init__.py +19 -0
- 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 +5 -13
- 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 +4 -2
- 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/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-2.0.2.dist-info → mirascope-2.1.0.dist-info}/METADATA +96 -68
- {mirascope-2.0.2.dist-info → mirascope-2.1.0.dist-info}/RECORD +64 -54
- {mirascope-2.0.2.dist-info → mirascope-2.1.0.dist-info}/WHEEL +0 -0
- {mirascope-2.0.2.dist-info → mirascope-2.1.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mirascope
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Every frontier LLM. One unified interface.
|
|
5
5
|
Project-URL: Homepage, https://mirascope.com
|
|
6
6
|
Project-URL: Documentation, https://mirascope.com/docs/mirascope/v2
|
|
@@ -60,6 +60,9 @@ Requires-Dist: mlx-lm<1,>=0.28.4; extra == 'all'
|
|
|
60
60
|
Requires-Dist: openai<3,>=2.15.0; extra == 'all'
|
|
61
61
|
Requires-Dist: opentelemetry-api<2,>=1.38.0; extra == 'all'
|
|
62
62
|
Requires-Dist: opentelemetry-exporter-otlp<2,>=1.38.0; extra == 'all'
|
|
63
|
+
Requires-Dist: opentelemetry-instrumentation-anthropic<1,>=0.50.0; extra == 'all'
|
|
64
|
+
Requires-Dist: opentelemetry-instrumentation-google-genai<1,>=0.3b0; extra == 'all'
|
|
65
|
+
Requires-Dist: opentelemetry-instrumentation-openai-v2<3,>=2.0b0; extra == 'all'
|
|
63
66
|
Requires-Dist: opentelemetry-instrumentation<1,>=0.59b0; extra == 'all'
|
|
64
67
|
Requires-Dist: opentelemetry-propagator-b3<2,>=1.38.0; extra == 'all'
|
|
65
68
|
Requires-Dist: opentelemetry-propagator-b3>=1.38.0; extra == 'all'
|
|
@@ -88,6 +91,9 @@ Provides-Extra: ops
|
|
|
88
91
|
Requires-Dist: libcst>=1.8.6; extra == 'ops'
|
|
89
92
|
Requires-Dist: opentelemetry-api<2,>=1.38.0; extra == 'ops'
|
|
90
93
|
Requires-Dist: opentelemetry-exporter-otlp<2,>=1.38.0; extra == 'ops'
|
|
94
|
+
Requires-Dist: opentelemetry-instrumentation-anthropic<1,>=0.50.0; extra == 'ops'
|
|
95
|
+
Requires-Dist: opentelemetry-instrumentation-google-genai<1,>=0.3b0; extra == 'ops'
|
|
96
|
+
Requires-Dist: opentelemetry-instrumentation-openai-v2<3,>=2.0b0; extra == 'ops'
|
|
91
97
|
Requires-Dist: opentelemetry-instrumentation<1,>=0.59b0; extra == 'ops'
|
|
92
98
|
Requires-Dist: opentelemetry-propagator-b3<2,>=1.38.0; extra == 'ops'
|
|
93
99
|
Requires-Dist: opentelemetry-propagator-b3>=1.38.0; extra == 'ops'
|
|
@@ -97,9 +103,66 @@ Requires-Dist: orjson>=3.11.4; extra == 'ops'
|
|
|
97
103
|
Requires-Dist: packaging>=25.0; extra == 'ops'
|
|
98
104
|
Description-Content-Type: text/markdown
|
|
99
105
|
|
|
100
|
-
|
|
106
|
+
# Mirascope Python
|
|
101
107
|
|
|
102
|
-
This directory contains the Python implementation of Mirascope.
|
|
108
|
+
This directory contains the Python implementation of Mirascope: The LLM Anti-Framework. It's intended as a "Goldilocks API" that affords the fine-grained control you'd get from using raw provider APIs, as well as the type safety and easy ergonomics that are offered by higher-level agent frameworks. Think of Mirascope as the "React" of LLM development, where provider native APIs are HTML/CSS, and the agent frameworks are Angular.
|
|
109
|
+
|
|
110
|
+
## Documentation
|
|
111
|
+
|
|
112
|
+
- [Why use Mirascope?](https://mirascope.com/docs/why)
|
|
113
|
+
- [Mirascope Quickstart](https://mirascope.com/docs/quickstart)
|
|
114
|
+
- [Mirascope Concepts](https://mirascope.com/docs/learn/llm)
|
|
115
|
+
|
|
116
|
+
## Installation
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
# using uv, with all provider deps
|
|
120
|
+
uv add "mirascope[all]"
|
|
121
|
+
|
|
122
|
+
# using uv, with just Anthropic
|
|
123
|
+
uv add "mirascope[anthropic]"
|
|
124
|
+
|
|
125
|
+
# using pip, with all deps
|
|
126
|
+
pip install "mirascope[all]"
|
|
127
|
+
|
|
128
|
+
# using pip, just OpenAI
|
|
129
|
+
pip install "mirascope[openai]"
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Usage
|
|
133
|
+
|
|
134
|
+
Here's an example of creating a simple agent, with tool-calling and streaming, using Mirascope. For many more examples, [read the docs](https://mirascope.com/docs).
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from mirascope import llm
|
|
138
|
+
|
|
139
|
+
@llm.tool
|
|
140
|
+
def exp(a: float, b: float) -> float:
|
|
141
|
+
"""Compute an exponent"""
|
|
142
|
+
return a ** b
|
|
143
|
+
|
|
144
|
+
@llm.tool
|
|
145
|
+
def add(a: float, b: float) -> float:
|
|
146
|
+
"""Add two numbers"""
|
|
147
|
+
return a + b
|
|
148
|
+
|
|
149
|
+
model = llm.Model("anthropic/claude-haiku-4-5")
|
|
150
|
+
response = model.stream("What is 42 ** 4 + 37 ** 3?", tools=[exp, add])
|
|
151
|
+
|
|
152
|
+
while True:
|
|
153
|
+
for stream in response.streams():
|
|
154
|
+
if stream.content_type == "text":
|
|
155
|
+
for delta in stream:
|
|
156
|
+
print(delta, end="", flush=True)
|
|
157
|
+
elif stream.content_type == "tool_call":
|
|
158
|
+
stream.collect() # consume the stream
|
|
159
|
+
print(f"\n> Calling {stream.tool_name}({stream.partial_args})")
|
|
160
|
+
print()
|
|
161
|
+
if response.tool_calls:
|
|
162
|
+
response = response.resume(response.execute_tools())
|
|
163
|
+
else:
|
|
164
|
+
break
|
|
165
|
+
```
|
|
103
166
|
|
|
104
167
|
## Development Setup
|
|
105
168
|
|
|
@@ -107,97 +170,62 @@ This directory contains the Python implementation of Mirascope.
|
|
|
107
170
|
```bash
|
|
108
171
|
cp .env.example .env
|
|
109
172
|
# Edit .env with your API keys
|
|
173
|
+
# Necessary for updating e2e snapshot tests
|
|
110
174
|
```
|
|
111
175
|
|
|
112
176
|
2. **Install Dependencies**:
|
|
113
177
|
|
|
114
178
|
```bash
|
|
179
|
+
cd python
|
|
115
180
|
uv sync --all-extras --dev
|
|
116
181
|
```
|
|
117
182
|
|
|
118
|
-
|
|
119
|
-
```bash
|
|
120
|
-
uv run pytest
|
|
121
|
-
```
|
|
183
|
+
## Helpful Commands:
|
|
122
184
|
|
|
123
|
-
|
|
185
|
+
Here are helpful development commands. All must be run from within the `python` directory.
|
|
124
186
|
|
|
125
|
-
`
|
|
126
|
-
`llm.Model` calls.
|
|
187
|
+
- `uv run pyright .`
|
|
127
188
|
|
|
128
|
-
|
|
129
|
-
`ops.span` automatically reuses the active provider, so spans from manual
|
|
130
|
-
instrumentation and GenAI instrumentation end up in the same trace tree.
|
|
131
|
-
2. Use `ops.session` to group related spans and attach metadata:
|
|
132
|
-
```python
|
|
133
|
-
from mirascope import ops
|
|
189
|
+
Run typechecking.
|
|
134
190
|
|
|
135
|
-
|
|
136
|
-
with ops.span("load-data") as span:
|
|
137
|
-
span.set(stage="ingest")
|
|
138
|
-
# expensive work here
|
|
139
|
-
```
|
|
140
|
-
3. The span exposes `span_id`/`trace_id`, logging helpers, and graceful no-op
|
|
141
|
-
behavior when OTEL is not configured. When OTEL is active, session metadata is
|
|
142
|
-
attached to every span, and additional tools like `ops.trace`/`ops.version`
|
|
143
|
-
(planned) can build on the same context.
|
|
191
|
+
- `uv run ruff check --fix .`
|
|
144
192
|
|
|
145
|
-
|
|
193
|
+
Check ruff linter (fixing issues where possible).
|
|
146
194
|
|
|
147
|
-
|
|
148
|
-
`llm.Model` helpers) so you can capture argument/return metadata alongside the
|
|
149
|
-
GenAI spans emitted by `llm.instrument_opentelemetry`.
|
|
195
|
+
- `uv run ruff format .`
|
|
150
196
|
|
|
151
|
-
|
|
152
|
-
from mirascope import ops
|
|
197
|
+
Run ruff formatter.
|
|
153
198
|
|
|
154
|
-
|
|
155
|
-
def normalize(record: dict[str, str]) -> dict[str, str]:
|
|
156
|
-
return {k: v.strip() for k, v in record.items()}
|
|
199
|
+
- `uv run pytest`
|
|
157
200
|
|
|
158
|
-
|
|
159
|
-
wrapped = normalize.wrapped({"foo": " bar "})
|
|
160
|
-
print(wrapped.span_id, wrapped.trace_id)
|
|
161
|
-
```
|
|
201
|
+
Run all Python unit tests.
|
|
162
202
|
|
|
163
|
-
-
|
|
164
|
-
serialization logic for arguments/results.
|
|
165
|
-
- Combine with `ops.session` to tag spans with contextual metadata, and with
|
|
166
|
-
`ops.instrument_opentelemetry` to obtain both model-level GenAI spans
|
|
167
|
-
and method-level spans like `recommend_book.__call__`.
|
|
168
|
-
- For now we focus on Mirascope-layer entry points (e.g., decorated functions or
|
|
169
|
-
`llm.Model` wrappers) and do not auto-instrument underlying provider SDK calls.
|
|
203
|
+
- `uv run pytest --cov --cov-config=.coverargc --cov-report=term-missing`
|
|
170
204
|
|
|
171
|
-
|
|
205
|
+
Run all Python unit tests, and report code coverage. (We require 100% coverage in CI.)
|
|
172
206
|
|
|
173
|
-
|
|
207
|
+
- `uv run pytest --fix`
|
|
174
208
|
|
|
175
|
-
|
|
209
|
+
Run all Python unit tests, and update any changed snapshots.
|
|
176
210
|
|
|
177
|
-
-
|
|
178
|
-
- **Recording Mode**: Tests use `"once"` mode - records new interactions if no cassette exists, replays existing cassettes otherwise
|
|
179
|
-
- **CI/CD**: In CI environments, tests use existing cassettes and never make real API calls
|
|
211
|
+
- `uvx codespell --config ../.codespellrc`
|
|
180
212
|
|
|
181
|
-
|
|
213
|
+
Run codespell, identifying many common spelling mistakes.
|
|
182
214
|
|
|
183
|
-
|
|
215
|
+
## Typechecking
|
|
184
216
|
|
|
185
|
-
|
|
186
|
-
- **Review Changes**: Run `uv run pytest --inline-snapshot=review` to preview what would change
|
|
187
|
-
- **Formatted Output**: Snapshots are automatically formatted with `ruff` for consistency
|
|
217
|
+
We prize type safety, both on the API surface and internally. You can run typechecking via `uv run pyright .` within this `python/` directory. (We're looking into supporting `ty` so as to speed up our typechecking.)
|
|
188
218
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
219
|
+
## Testing
|
|
220
|
+
|
|
221
|
+
This project makes extensive use of e2e tests, which replay real interactions with various LLM providers to ensure that Mirascope truly works on the providers' APIs. We use [VCR.py](https://vcrpy.readthedocs.io/) and [inline-snapshot](https://15r10nk.github.io/inline-snapshot/) to maintain these e2e tests. For more details, read [tests/e2e/README.md](./tests/e2e/README.md).
|
|
222
|
+
|
|
223
|
+
If you make changes to Mirascope that require new snapshots, you should manually delete the outdated cassette files, and then run `uv run pytest python/tests/e2e --fix`. Note that doing so requires real API keys in your `.env` file.
|
|
224
|
+
|
|
225
|
+
We require 100% code coverage in CI. You can get a code coverage report via:
|
|
195
226
|
|
|
196
|
-
|
|
227
|
+
`uv run pytest --cov --cov-config=.coverargc --cov-report=term-missing`
|
|
197
228
|
|
|
198
|
-
|
|
229
|
+
## Read More
|
|
199
230
|
|
|
200
|
-
|
|
201
|
-
2. Delete the relevant cassette file (if updating an existing test)
|
|
202
|
-
3. Run the specific test: `uv run pytest tests/path/to/test.py::test_name`
|
|
203
|
-
4. The cassette will be automatically created/updated
|
|
231
|
+
For more info on contributing, read [the contributing page in our docs](https://mirascope.com/docs/contributing).
|