mirascope 2.0.0a6__py3-none-any.whl → 2.0.1__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/api/_generated/__init__.py +186 -5
- mirascope/api/_generated/annotations/client.py +38 -6
- mirascope/api/_generated/annotations/raw_client.py +366 -47
- mirascope/api/_generated/annotations/types/annotations_create_response.py +19 -6
- mirascope/api/_generated/annotations/types/annotations_get_response.py +19 -6
- mirascope/api/_generated/annotations/types/annotations_list_response_annotations_item.py +22 -7
- mirascope/api/_generated/annotations/types/annotations_update_response.py +19 -6
- mirascope/api/_generated/api_keys/__init__.py +12 -2
- mirascope/api/_generated/api_keys/client.py +107 -6
- mirascope/api/_generated/api_keys/raw_client.py +486 -38
- mirascope/api/_generated/api_keys/types/__init__.py +7 -1
- mirascope/api/_generated/api_keys/types/api_keys_list_all_for_org_response_item.py +40 -0
- mirascope/api/_generated/client.py +36 -0
- mirascope/api/_generated/docs/raw_client.py +71 -9
- mirascope/api/_generated/environment.py +3 -3
- mirascope/api/_generated/environments/__init__.py +6 -0
- mirascope/api/_generated/environments/client.py +158 -9
- mirascope/api/_generated/environments/raw_client.py +620 -52
- mirascope/api/_generated/environments/types/__init__.py +10 -0
- mirascope/api/_generated/environments/types/environments_get_analytics_response.py +60 -0
- mirascope/api/_generated/environments/types/environments_get_analytics_response_top_functions_item.py +24 -0
- mirascope/api/_generated/{organizations/types/organizations_credits_response.py → environments/types/environments_get_analytics_response_top_models_item.py} +6 -3
- mirascope/api/_generated/errors/__init__.py +6 -0
- mirascope/api/_generated/errors/bad_request_error.py +5 -2
- mirascope/api/_generated/errors/conflict_error.py +5 -2
- mirascope/api/_generated/errors/payment_required_error.py +15 -0
- mirascope/api/_generated/errors/service_unavailable_error.py +14 -0
- mirascope/api/_generated/errors/too_many_requests_error.py +15 -0
- mirascope/api/_generated/functions/__init__.py +10 -0
- mirascope/api/_generated/functions/client.py +222 -8
- mirascope/api/_generated/functions/raw_client.py +975 -134
- mirascope/api/_generated/functions/types/__init__.py +28 -4
- mirascope/api/_generated/functions/types/functions_get_by_env_response.py +53 -0
- mirascope/api/_generated/functions/types/functions_get_by_env_response_dependencies_value.py +22 -0
- mirascope/api/_generated/functions/types/functions_list_by_env_response.py +25 -0
- mirascope/api/_generated/functions/types/functions_list_by_env_response_functions_item.py +56 -0
- mirascope/api/_generated/functions/types/functions_list_by_env_response_functions_item_dependencies_value.py +22 -0
- mirascope/api/_generated/health/raw_client.py +74 -10
- mirascope/api/_generated/organization_invitations/__init__.py +33 -0
- mirascope/api/_generated/organization_invitations/client.py +546 -0
- mirascope/api/_generated/organization_invitations/raw_client.py +1519 -0
- mirascope/api/_generated/organization_invitations/types/__init__.py +53 -0
- mirascope/api/_generated/organization_invitations/types/organization_invitations_accept_response.py +34 -0
- mirascope/api/_generated/organization_invitations/types/organization_invitations_accept_response_role.py +7 -0
- mirascope/api/_generated/organization_invitations/types/organization_invitations_create_request_role.py +7 -0
- mirascope/api/_generated/organization_invitations/types/organization_invitations_create_response.py +48 -0
- mirascope/api/_generated/organization_invitations/types/organization_invitations_create_response_role.py +7 -0
- mirascope/api/_generated/organization_invitations/types/organization_invitations_create_response_status.py +7 -0
- mirascope/api/_generated/organization_invitations/types/organization_invitations_get_response.py +48 -0
- mirascope/api/_generated/organization_invitations/types/organization_invitations_get_response_role.py +7 -0
- mirascope/api/_generated/organization_invitations/types/organization_invitations_get_response_status.py +7 -0
- mirascope/api/_generated/organization_invitations/types/organization_invitations_list_response_item.py +48 -0
- mirascope/api/_generated/organization_invitations/types/organization_invitations_list_response_item_role.py +7 -0
- mirascope/api/_generated/organization_invitations/types/organization_invitations_list_response_item_status.py +7 -0
- mirascope/api/_generated/organization_memberships/__init__.py +19 -0
- mirascope/api/_generated/organization_memberships/client.py +302 -0
- mirascope/api/_generated/organization_memberships/raw_client.py +736 -0
- mirascope/api/_generated/organization_memberships/types/__init__.py +27 -0
- mirascope/api/_generated/organization_memberships/types/organization_memberships_list_response_item.py +33 -0
- mirascope/api/_generated/organization_memberships/types/organization_memberships_list_response_item_role.py +7 -0
- mirascope/api/_generated/organization_memberships/types/organization_memberships_update_request_role.py +7 -0
- mirascope/api/_generated/organization_memberships/types/organization_memberships_update_response.py +31 -0
- mirascope/api/_generated/organization_memberships/types/organization_memberships_update_response_role.py +7 -0
- mirascope/api/_generated/organizations/__init__.py +26 -2
- mirascope/api/_generated/organizations/client.py +442 -20
- mirascope/api/_generated/organizations/raw_client.py +1763 -164
- mirascope/api/_generated/organizations/types/__init__.py +48 -2
- mirascope/api/_generated/organizations/types/organizations_create_payment_intent_response.py +24 -0
- mirascope/api/_generated/organizations/types/organizations_preview_subscription_change_request_target_plan.py +7 -0
- mirascope/api/_generated/organizations/types/organizations_preview_subscription_change_response.py +47 -0
- mirascope/api/_generated/organizations/types/organizations_preview_subscription_change_response_validation_errors_item.py +33 -0
- mirascope/api/_generated/organizations/types/organizations_preview_subscription_change_response_validation_errors_item_resource.py +7 -0
- mirascope/api/_generated/organizations/types/organizations_router_balance_response.py +24 -0
- mirascope/api/_generated/organizations/types/organizations_subscription_response.py +53 -0
- mirascope/api/_generated/organizations/types/organizations_subscription_response_current_plan.py +7 -0
- mirascope/api/_generated/organizations/types/organizations_subscription_response_payment_method.py +26 -0
- mirascope/api/_generated/organizations/types/organizations_subscription_response_scheduled_change.py +34 -0
- mirascope/api/_generated/organizations/types/organizations_subscription_response_scheduled_change_target_plan.py +7 -0
- mirascope/api/_generated/organizations/types/organizations_update_subscription_request_target_plan.py +7 -0
- mirascope/api/_generated/organizations/types/organizations_update_subscription_response.py +35 -0
- mirascope/api/_generated/project_memberships/__init__.py +25 -0
- mirascope/api/_generated/project_memberships/client.py +437 -0
- mirascope/api/_generated/project_memberships/raw_client.py +1039 -0
- mirascope/api/_generated/project_memberships/types/__init__.py +29 -0
- mirascope/api/_generated/project_memberships/types/project_memberships_create_request_role.py +7 -0
- mirascope/api/_generated/project_memberships/types/project_memberships_create_response.py +35 -0
- mirascope/api/_generated/project_memberships/types/project_memberships_create_response_role.py +7 -0
- mirascope/api/_generated/project_memberships/types/project_memberships_list_response_item.py +33 -0
- mirascope/api/_generated/project_memberships/types/project_memberships_list_response_item_role.py +7 -0
- mirascope/api/_generated/project_memberships/types/project_memberships_update_request_role.py +7 -0
- mirascope/api/_generated/project_memberships/types/project_memberships_update_response.py +35 -0
- mirascope/api/_generated/project_memberships/types/project_memberships_update_response_role.py +7 -0
- mirascope/api/_generated/projects/raw_client.py +415 -58
- mirascope/api/_generated/reference.md +2767 -397
- mirascope/api/_generated/tags/__init__.py +19 -0
- mirascope/api/_generated/tags/client.py +504 -0
- mirascope/api/_generated/tags/raw_client.py +1288 -0
- mirascope/api/_generated/tags/types/__init__.py +17 -0
- mirascope/api/_generated/tags/types/tags_create_response.py +41 -0
- mirascope/api/_generated/tags/types/tags_get_response.py +41 -0
- mirascope/api/_generated/tags/types/tags_list_response.py +23 -0
- mirascope/api/_generated/tags/types/tags_list_response_tags_item.py +41 -0
- mirascope/api/_generated/tags/types/tags_update_response.py +41 -0
- mirascope/api/_generated/token_cost/__init__.py +7 -0
- mirascope/api/_generated/token_cost/client.py +160 -0
- mirascope/api/_generated/token_cost/raw_client.py +264 -0
- mirascope/api/_generated/token_cost/types/__init__.py +8 -0
- mirascope/api/_generated/token_cost/types/token_cost_calculate_request_usage.py +54 -0
- mirascope/api/_generated/token_cost/types/token_cost_calculate_response.py +52 -0
- mirascope/api/_generated/traces/__init__.py +20 -0
- mirascope/api/_generated/traces/client.py +543 -0
- mirascope/api/_generated/traces/raw_client.py +1366 -96
- mirascope/api/_generated/traces/types/__init__.py +28 -0
- mirascope/api/_generated/traces/types/traces_get_analytics_summary_response.py +6 -0
- mirascope/api/_generated/traces/types/traces_get_trace_detail_by_env_response.py +33 -0
- mirascope/api/_generated/traces/types/traces_get_trace_detail_by_env_response_spans_item.py +88 -0
- mirascope/api/_generated/traces/types/traces_get_trace_detail_response_spans_item.py +0 -2
- mirascope/api/_generated/traces/types/traces_list_by_function_hash_response.py +25 -0
- mirascope/api/_generated/traces/types/traces_list_by_function_hash_response_traces_item.py +44 -0
- mirascope/api/_generated/traces/types/traces_search_by_env_request_attribute_filters_item.py +26 -0
- mirascope/api/_generated/traces/types/traces_search_by_env_request_attribute_filters_item_operator.py +7 -0
- mirascope/api/_generated/traces/types/traces_search_by_env_request_sort_by.py +7 -0
- mirascope/api/_generated/traces/types/traces_search_by_env_request_sort_order.py +7 -0
- mirascope/api/_generated/traces/types/traces_search_by_env_response.py +26 -0
- mirascope/api/_generated/traces/types/traces_search_by_env_response_spans_item.py +50 -0
- mirascope/api/_generated/traces/types/traces_search_response_spans_item.py +10 -1
- mirascope/api/_generated/types/__init__.py +32 -2
- mirascope/api/_generated/types/bad_request_error_body.py +50 -0
- mirascope/api/_generated/types/date.py +3 -0
- mirascope/api/_generated/types/immutable_resource_error.py +22 -0
- mirascope/api/_generated/types/internal_server_error_body.py +3 -3
- mirascope/api/_generated/types/plan_limit_exceeded_error.py +32 -0
- mirascope/api/_generated/types/plan_limit_exceeded_error_tag.py +7 -0
- mirascope/api/_generated/types/pricing_unavailable_error.py +23 -0
- mirascope/api/_generated/types/rate_limit_error.py +31 -0
- mirascope/api/_generated/types/rate_limit_error_tag.py +5 -0
- mirascope/api/_generated/types/service_unavailable_error_body.py +24 -0
- mirascope/api/_generated/types/service_unavailable_error_tag.py +7 -0
- mirascope/api/_generated/types/subscription_past_due_error.py +31 -0
- mirascope/api/_generated/types/subscription_past_due_error_tag.py +7 -0
- mirascope/api/settings.py +19 -1
- mirascope/llm/__init__.py +53 -10
- mirascope/llm/calls/__init__.py +2 -1
- mirascope/llm/calls/calls.py +3 -1
- mirascope/llm/calls/decorator.py +21 -7
- mirascope/llm/content/tool_output.py +22 -5
- mirascope/llm/exceptions.py +284 -71
- mirascope/llm/formatting/__init__.py +17 -0
- mirascope/llm/formatting/format.py +112 -35
- mirascope/llm/formatting/output_parser.py +178 -0
- mirascope/llm/formatting/partial.py +80 -7
- mirascope/llm/formatting/primitives.py +192 -0
- mirascope/llm/formatting/types.py +20 -8
- mirascope/llm/messages/__init__.py +3 -0
- mirascope/llm/messages/_utils.py +34 -0
- mirascope/llm/models/__init__.py +5 -0
- mirascope/llm/models/models.py +137 -69
- mirascope/llm/{providers/base → models}/params.py +7 -57
- mirascope/llm/models/thinking_config.py +61 -0
- mirascope/llm/prompts/_utils.py +0 -32
- mirascope/llm/prompts/decorator.py +16 -5
- mirascope/llm/prompts/prompts.py +131 -68
- mirascope/llm/providers/__init__.py +1 -4
- mirascope/llm/providers/anthropic/_utils/__init__.py +2 -0
- mirascope/llm/providers/anthropic/_utils/beta_decode.py +18 -9
- mirascope/llm/providers/anthropic/_utils/beta_encode.py +62 -13
- mirascope/llm/providers/anthropic/_utils/decode.py +18 -9
- mirascope/llm/providers/anthropic/_utils/encode.py +26 -7
- mirascope/llm/providers/anthropic/_utils/errors.py +2 -2
- mirascope/llm/providers/anthropic/beta_provider.py +64 -18
- mirascope/llm/providers/anthropic/provider.py +91 -33
- mirascope/llm/providers/base/__init__.py +0 -4
- mirascope/llm/providers/base/_utils.py +55 -6
- mirascope/llm/providers/base/base_provider.py +116 -37
- mirascope/llm/providers/google/_utils/__init__.py +2 -0
- mirascope/llm/providers/google/_utils/decode.py +20 -7
- mirascope/llm/providers/google/_utils/encode.py +26 -7
- mirascope/llm/providers/google/_utils/errors.py +3 -2
- mirascope/llm/providers/google/provider.py +64 -18
- mirascope/llm/providers/mirascope/_utils.py +13 -17
- mirascope/llm/providers/mirascope/provider.py +49 -18
- mirascope/llm/providers/mlx/_utils.py +7 -2
- mirascope/llm/providers/mlx/encoding/base.py +5 -2
- mirascope/llm/providers/mlx/encoding/transformers.py +5 -2
- mirascope/llm/providers/mlx/mlx.py +23 -6
- mirascope/llm/providers/mlx/provider.py +42 -13
- mirascope/llm/providers/openai/_utils/errors.py +2 -2
- mirascope/llm/providers/openai/completions/_utils/encode.py +20 -16
- mirascope/llm/providers/openai/completions/base_provider.py +40 -11
- mirascope/llm/providers/openai/provider.py +40 -10
- mirascope/llm/providers/openai/responses/_utils/__init__.py +2 -0
- mirascope/llm/providers/openai/responses/_utils/decode.py +19 -6
- mirascope/llm/providers/openai/responses/_utils/encode.py +22 -10
- mirascope/llm/providers/openai/responses/provider.py +56 -18
- mirascope/llm/providers/provider_registry.py +93 -19
- mirascope/llm/responses/__init__.py +6 -1
- mirascope/llm/responses/_utils.py +102 -12
- mirascope/llm/responses/base_response.py +5 -2
- mirascope/llm/responses/base_stream_response.py +115 -25
- mirascope/llm/responses/response.py +2 -1
- mirascope/llm/responses/root_response.py +89 -17
- mirascope/llm/responses/stream_response.py +6 -9
- mirascope/llm/tools/decorator.py +9 -4
- mirascope/llm/tools/tool_schema.py +12 -6
- mirascope/llm/tools/toolkit.py +35 -27
- mirascope/llm/tools/tools.py +45 -20
- mirascope/ops/__init__.py +4 -0
- mirascope/ops/_internal/configuration.py +82 -31
- mirascope/ops/_internal/exporters/exporters.py +64 -11
- mirascope/ops/_internal/instrumentation/llm/common.py +530 -0
- mirascope/ops/_internal/instrumentation/llm/cost.py +190 -0
- mirascope/ops/_internal/instrumentation/llm/encode.py +1 -1
- mirascope/ops/_internal/instrumentation/llm/llm.py +116 -1242
- mirascope/ops/_internal/instrumentation/llm/model.py +1798 -0
- mirascope/ops/_internal/instrumentation/llm/response.py +521 -0
- mirascope/ops/_internal/instrumentation/llm/serialize.py +300 -0
- mirascope/ops/_internal/protocols.py +83 -1
- mirascope/ops/_internal/traced_calls.py +4 -0
- mirascope/ops/_internal/traced_functions.py +118 -8
- mirascope/ops/_internal/tracing.py +78 -1
- mirascope/ops/_internal/utils.py +52 -4
- {mirascope-2.0.0a6.dist-info → mirascope-2.0.1.dist-info}/METADATA +12 -11
- mirascope-2.0.1.dist-info/RECORD +423 -0
- {mirascope-2.0.0a6.dist-info → mirascope-2.0.1.dist-info}/licenses/LICENSE +1 -1
- mirascope-2.0.0a6.dist-info/RECORD +0 -316
- {mirascope-2.0.0a6.dist-info → mirascope-2.0.1.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""Mirascope-specific serialization for span attributes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Protocol
|
|
8
|
+
|
|
9
|
+
from .....llm.content import (
|
|
10
|
+
Audio,
|
|
11
|
+
Base64ImageSource,
|
|
12
|
+
Document,
|
|
13
|
+
Image,
|
|
14
|
+
Text,
|
|
15
|
+
Thought,
|
|
16
|
+
ToolCall,
|
|
17
|
+
ToolOutput,
|
|
18
|
+
)
|
|
19
|
+
from .....llm.content.document import Base64DocumentSource, TextDocumentSource
|
|
20
|
+
from .....llm.messages import AssistantMessage, Message, SystemMessage, UserMessage
|
|
21
|
+
from .....llm.responses.usage import Usage
|
|
22
|
+
from ...utils import json_dumps
|
|
23
|
+
from .cost import calculate_cost_async, calculate_cost_sync
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from opentelemetry.util.types import AttributeValue
|
|
27
|
+
|
|
28
|
+
from .....llm.responses.root_response import RootResponse
|
|
29
|
+
from .....llm.types import Jsonable
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SpanProtocol(Protocol):
|
|
35
|
+
"""Protocol for span objects that support setting attributes."""
|
|
36
|
+
|
|
37
|
+
def set(self, **attributes: AttributeValue) -> None:
|
|
38
|
+
"""Set attributes on the span."""
|
|
39
|
+
...
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _serialize_content_part(
|
|
43
|
+
part: Text | ToolCall | Thought | Image | Audio | Document | ToolOutput[Jsonable],
|
|
44
|
+
) -> dict[str, Any]:
|
|
45
|
+
"""Serialize a single content part to a dict matching the Mirascope dataclass structure."""
|
|
46
|
+
if isinstance(part, Text):
|
|
47
|
+
return {"type": "text", "text": part.text}
|
|
48
|
+
elif isinstance(part, ToolCall):
|
|
49
|
+
return {
|
|
50
|
+
"type": "tool_call",
|
|
51
|
+
"id": part.id,
|
|
52
|
+
"name": part.name,
|
|
53
|
+
"args": part.args,
|
|
54
|
+
}
|
|
55
|
+
elif isinstance(part, Thought):
|
|
56
|
+
return {"type": "thought", "thought": part.thought}
|
|
57
|
+
elif isinstance(part, ToolOutput):
|
|
58
|
+
return {
|
|
59
|
+
"type": "tool_output",
|
|
60
|
+
"id": part.id,
|
|
61
|
+
"name": part.name,
|
|
62
|
+
"result": part.result,
|
|
63
|
+
}
|
|
64
|
+
elif isinstance(part, Image):
|
|
65
|
+
if isinstance(part.source, Base64ImageSource):
|
|
66
|
+
return {
|
|
67
|
+
"type": "image",
|
|
68
|
+
"source": {
|
|
69
|
+
"type": "base64_image_source",
|
|
70
|
+
"mime_type": part.source.mime_type,
|
|
71
|
+
"data": part.source.data,
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
else: # URLImageSource
|
|
75
|
+
return {
|
|
76
|
+
"type": "image",
|
|
77
|
+
"source": {"type": "url_image_source", "url": part.source.url},
|
|
78
|
+
}
|
|
79
|
+
elif isinstance(part, Audio):
|
|
80
|
+
return {
|
|
81
|
+
"type": "audio",
|
|
82
|
+
"source": {
|
|
83
|
+
"type": "base64_audio_source",
|
|
84
|
+
"mime_type": part.source.mime_type,
|
|
85
|
+
"data": part.source.data,
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
elif isinstance(part, Document):
|
|
89
|
+
# Document has multiple source types - serialize based on actual type
|
|
90
|
+
if isinstance(part.source, Base64DocumentSource):
|
|
91
|
+
return {
|
|
92
|
+
"type": "document",
|
|
93
|
+
"source": {
|
|
94
|
+
"type": "base64_document_source",
|
|
95
|
+
"data": part.source.data,
|
|
96
|
+
"media_type": part.source.media_type,
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
elif isinstance(part.source, TextDocumentSource):
|
|
100
|
+
return {
|
|
101
|
+
"type": "document",
|
|
102
|
+
"source": {
|
|
103
|
+
"type": "text_document_source",
|
|
104
|
+
"data": part.source.data,
|
|
105
|
+
"media_type": part.source.media_type,
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
else: # URLDocumentSource
|
|
109
|
+
return {
|
|
110
|
+
"type": "document",
|
|
111
|
+
"source": {
|
|
112
|
+
"type": "url_document_source",
|
|
113
|
+
"url": part.source.url,
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
return {"type": "unknown"} # pragma: no cover
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _serialize_message(message: Message) -> dict[str, Any]:
|
|
120
|
+
"""Serialize a Message to a dict matching the Mirascope dataclass structure."""
|
|
121
|
+
if isinstance(message, SystemMessage):
|
|
122
|
+
return {
|
|
123
|
+
"role": "system",
|
|
124
|
+
"content": _serialize_content_part(message.content),
|
|
125
|
+
}
|
|
126
|
+
elif isinstance(message, UserMessage):
|
|
127
|
+
return {
|
|
128
|
+
"role": "user",
|
|
129
|
+
"content": [_serialize_content_part(p) for p in message.content],
|
|
130
|
+
"name": message.name,
|
|
131
|
+
}
|
|
132
|
+
elif isinstance(message, AssistantMessage):
|
|
133
|
+
return {
|
|
134
|
+
"role": "assistant",
|
|
135
|
+
"content": [_serialize_content_part(p) for p in message.content],
|
|
136
|
+
"name": message.name,
|
|
137
|
+
}
|
|
138
|
+
return {"role": "unknown"} # pragma: no cover
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def serialize_mirascope_messages(messages: Sequence[Message]) -> str:
|
|
142
|
+
"""Serialize input messages to JSON for span attributes."""
|
|
143
|
+
return json_dumps([_serialize_message(m) for m in messages])
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def serialize_mirascope_content(
|
|
147
|
+
content: Sequence[Text | ToolCall | Thought],
|
|
148
|
+
) -> str:
|
|
149
|
+
"""Serialize response content to JSON for span attributes."""
|
|
150
|
+
return json_dumps([_serialize_content_part(p) for p in content])
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def serialize_mirascope_usage(usage: Usage | None) -> AttributeValue | None:
|
|
154
|
+
"""Serialize response usage to JSON for span attributes. Returns None if usage is None."""
|
|
155
|
+
if usage is None:
|
|
156
|
+
return None
|
|
157
|
+
return json_dumps(
|
|
158
|
+
{
|
|
159
|
+
"input_tokens": usage.input_tokens,
|
|
160
|
+
"output_tokens": usage.output_tokens,
|
|
161
|
+
"cache_read_tokens": usage.cache_read_tokens,
|
|
162
|
+
"cache_write_tokens": usage.cache_write_tokens,
|
|
163
|
+
"reasoning_tokens": usage.reasoning_tokens,
|
|
164
|
+
"total_tokens": usage.total_tokens,
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def serialize_mirascope_cost(
|
|
170
|
+
input_cost: float,
|
|
171
|
+
output_cost: float,
|
|
172
|
+
total_cost: float,
|
|
173
|
+
cache_read_cost: float | None = None,
|
|
174
|
+
cache_write_cost: float | None = None,
|
|
175
|
+
) -> str:
|
|
176
|
+
"""Serialize cost to JSON for span attributes.
|
|
177
|
+
|
|
178
|
+
All costs are in centicents (1 centicent = $0.0001).
|
|
179
|
+
Consumers can divide by 10,000 to get dollar amounts.
|
|
180
|
+
"""
|
|
181
|
+
return json_dumps(
|
|
182
|
+
{
|
|
183
|
+
"input_cost": input_cost,
|
|
184
|
+
"output_cost": output_cost,
|
|
185
|
+
"cache_read_cost": cache_read_cost,
|
|
186
|
+
"cache_write_cost": cache_write_cost,
|
|
187
|
+
"total_cost": total_cost,
|
|
188
|
+
}
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def attach_mirascope_response(
|
|
193
|
+
span: SpanProtocol, response: RootResponse[Any, Any]
|
|
194
|
+
) -> None:
|
|
195
|
+
"""Attach Mirascope-specific response attributes to a span.
|
|
196
|
+
|
|
197
|
+
Sets the following attributes:
|
|
198
|
+
- mirascope.trace.output: Pretty-printed response
|
|
199
|
+
- mirascope.response.messages: Serialized input messages (excluding final assistant message)
|
|
200
|
+
- mirascope.response.content: Serialized response content
|
|
201
|
+
- mirascope.response.usage: Serialized usage (if available)
|
|
202
|
+
- mirascope.response.cost: Serialized cost (if available)
|
|
203
|
+
"""
|
|
204
|
+
span.set(
|
|
205
|
+
**{
|
|
206
|
+
"mirascope.response.provider_id": response.provider_id,
|
|
207
|
+
"mirascope.response.model_id": response.model_id,
|
|
208
|
+
"mirascope.trace.output": response.pretty(),
|
|
209
|
+
"mirascope.response.messages": serialize_mirascope_messages(
|
|
210
|
+
response.messages[:-1]
|
|
211
|
+
),
|
|
212
|
+
"mirascope.response.content": serialize_mirascope_content(response.content),
|
|
213
|
+
}
|
|
214
|
+
)
|
|
215
|
+
if (usage_json := serialize_mirascope_usage(response.usage)) is not None:
|
|
216
|
+
span.set(**{"mirascope.response.usage": usage_json})
|
|
217
|
+
logger.debug("Attached usage to span")
|
|
218
|
+
else:
|
|
219
|
+
logger.debug("No usage available, skipping cost calculation")
|
|
220
|
+
|
|
221
|
+
# Calculate and attach cost if usage is available
|
|
222
|
+
if response.usage is not None:
|
|
223
|
+
logger.debug("Attempting cost calculation (sync)")
|
|
224
|
+
cost = calculate_cost_sync(
|
|
225
|
+
response.provider_id, response.model_id, response.usage
|
|
226
|
+
)
|
|
227
|
+
if cost is not None:
|
|
228
|
+
span.set(
|
|
229
|
+
**{
|
|
230
|
+
"mirascope.response.cost": serialize_mirascope_cost(
|
|
231
|
+
input_cost=cost.input_cost_centicents,
|
|
232
|
+
output_cost=cost.output_cost_centicents,
|
|
233
|
+
total_cost=cost.total_cost_centicents,
|
|
234
|
+
cache_read_cost=cost.cache_read_cost_centicents,
|
|
235
|
+
cache_write_cost=cost.cache_write_cost_centicents,
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
)
|
|
239
|
+
logger.debug(
|
|
240
|
+
"Attached cost to span: total=%s centicents", cost.total_cost_centicents
|
|
241
|
+
)
|
|
242
|
+
else:
|
|
243
|
+
logger.debug("Cost calculation returned None, not attaching cost to span")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
async def attach_mirascope_response_async(
|
|
247
|
+
span: SpanProtocol, response: RootResponse[Any, Any]
|
|
248
|
+
) -> None:
|
|
249
|
+
"""Attach Mirascope-specific response attributes to a span (async version).
|
|
250
|
+
|
|
251
|
+
Sets the following attributes:
|
|
252
|
+
- mirascope.trace.output: Pretty-printed response
|
|
253
|
+
- mirascope.response.messages: Serialized input messages (excluding final assistant message)
|
|
254
|
+
- mirascope.response.content: Serialized response content
|
|
255
|
+
- mirascope.response.usage: Serialized usage (if available)
|
|
256
|
+
- mirascope.response.cost: Serialized cost (if available)
|
|
257
|
+
"""
|
|
258
|
+
span.set(
|
|
259
|
+
**{
|
|
260
|
+
"mirascope.response.provider_id": response.provider_id,
|
|
261
|
+
"mirascope.response.model_id": response.model_id,
|
|
262
|
+
"mirascope.trace.output": response.pretty(),
|
|
263
|
+
"mirascope.response.messages": serialize_mirascope_messages(
|
|
264
|
+
response.messages[:-1]
|
|
265
|
+
),
|
|
266
|
+
"mirascope.response.content": serialize_mirascope_content(response.content),
|
|
267
|
+
}
|
|
268
|
+
)
|
|
269
|
+
if (usage_json := serialize_mirascope_usage(response.usage)) is not None:
|
|
270
|
+
span.set(**{"mirascope.response.usage": usage_json})
|
|
271
|
+
logger.debug("Attached usage to span (async)")
|
|
272
|
+
else:
|
|
273
|
+
logger.debug("No usage available, skipping cost calculation (async)")
|
|
274
|
+
|
|
275
|
+
# Calculate and attach cost if usage is available (async)
|
|
276
|
+
if response.usage is not None:
|
|
277
|
+
logger.debug("Attempting cost calculation (async)")
|
|
278
|
+
cost = await calculate_cost_async(
|
|
279
|
+
response.provider_id, response.model_id, response.usage
|
|
280
|
+
)
|
|
281
|
+
if cost is not None:
|
|
282
|
+
span.set(
|
|
283
|
+
**{
|
|
284
|
+
"mirascope.response.cost": serialize_mirascope_cost(
|
|
285
|
+
input_cost=cost.input_cost_centicents,
|
|
286
|
+
output_cost=cost.output_cost_centicents,
|
|
287
|
+
total_cost=cost.total_cost_centicents,
|
|
288
|
+
cache_read_cost=cost.cache_read_cost_centicents,
|
|
289
|
+
cache_write_cost=cost.cache_write_cost_centicents,
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
)
|
|
293
|
+
logger.debug(
|
|
294
|
+
"Attached cost to span (async): total=%s centicents",
|
|
295
|
+
cost.total_cost_centicents,
|
|
296
|
+
)
|
|
297
|
+
else:
|
|
298
|
+
logger.debug(
|
|
299
|
+
"Cost calculation returned None, not attaching cost to span (async)"
|
|
300
|
+
)
|
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import inspect
|
|
6
|
-
from typing import Protocol
|
|
6
|
+
from typing import TYPE_CHECKING, Protocol
|
|
7
7
|
from typing_extensions import TypeIs
|
|
8
8
|
|
|
9
9
|
from ...llm.context import Context, DepsT
|
|
10
10
|
from .types import P, R
|
|
11
11
|
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .spans import Span
|
|
14
|
+
|
|
12
15
|
|
|
13
16
|
class SyncFunction(Protocol[P, R]):
|
|
14
17
|
"""Protocol for synchronous callable functions."""
|
|
@@ -26,6 +29,30 @@ class AsyncFunction(Protocol[P, R]):
|
|
|
26
29
|
... # pragma: no cover
|
|
27
30
|
|
|
28
31
|
|
|
32
|
+
class SyncSpanFunction(Protocol[P, R]):
|
|
33
|
+
"""Protocol for synchronous functions that receive injected Span.
|
|
34
|
+
|
|
35
|
+
Functions matching this protocol have `trace_ctx: Span` as their first
|
|
36
|
+
parameter. The trace decorator will inject the span automatically.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __call__(self, trace_ctx: Span, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
40
|
+
"""The function receives a Span as first parameter."""
|
|
41
|
+
... # pragma: no cover
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AsyncSpanFunction(Protocol[P, R]):
|
|
45
|
+
"""Protocol for asynchronous functions that receive injected Span.
|
|
46
|
+
|
|
47
|
+
Functions matching this protocol have `trace_ctx: Span` as their first
|
|
48
|
+
parameter. The trace decorator will inject the span automatically.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
async def __call__(self, trace_ctx: Span, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
52
|
+
"""The function receives a Span as first parameter."""
|
|
53
|
+
... # pragma: no cover
|
|
54
|
+
|
|
55
|
+
|
|
29
56
|
class SyncContextFunction(Protocol[P, DepsT, R]):
|
|
30
57
|
"""Protocol for synchronous callable functions with Context parameter."""
|
|
31
58
|
|
|
@@ -49,3 +76,58 @@ def fn_is_async(
|
|
|
49
76
|
) -> TypeIs[AsyncFunction[P, R]]:
|
|
50
77
|
"""Type check to determine if a given function is asynchronous."""
|
|
51
78
|
return inspect.iscoroutinefunction(fn) or inspect.isasyncgenfunction(fn)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def fn_wants_span(
|
|
82
|
+
fn: (
|
|
83
|
+
SyncFunction[P, R]
|
|
84
|
+
| AsyncFunction[P, R]
|
|
85
|
+
| SyncSpanFunction[P, R]
|
|
86
|
+
| AsyncSpanFunction[P, R]
|
|
87
|
+
),
|
|
88
|
+
) -> TypeIs[SyncSpanFunction[P, R] | AsyncSpanFunction[P, R]]:
|
|
89
|
+
"""Check if function wants Span injection as first parameter.
|
|
90
|
+
|
|
91
|
+
Returns True if the function has a first parameter named `trace_ctx`
|
|
92
|
+
with type annotation `Span`.
|
|
93
|
+
"""
|
|
94
|
+
# Import here to avoid circular imports
|
|
95
|
+
from .spans import Span
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
sig = inspect.signature(fn)
|
|
99
|
+
except (ValueError, TypeError):
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
params = list(sig.parameters.values())
|
|
103
|
+
if not params:
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
first_param = params[0]
|
|
107
|
+
if first_param.name != "trace_ctx":
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
# Check annotation
|
|
111
|
+
annotation = first_param.annotation
|
|
112
|
+
if annotation is inspect.Parameter.empty:
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
# Handle string annotations (forward references)
|
|
116
|
+
# The annotation could be "Span", "ops.Span", "mirascope.ops.Span", etc.
|
|
117
|
+
if isinstance(annotation, str):
|
|
118
|
+
return annotation == "Span" or annotation.endswith(".Span")
|
|
119
|
+
|
|
120
|
+
# Check by identity first
|
|
121
|
+
if annotation is Span:
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
# Fallback: check by class name and module for robustness
|
|
125
|
+
# This handles cases where the same class might have different identities
|
|
126
|
+
# due to module reloading or import issues in test environments
|
|
127
|
+
if isinstance(annotation, type):
|
|
128
|
+
return (
|
|
129
|
+
annotation.__name__ == "Span"
|
|
130
|
+
and annotation.__module__ == "mirascope.ops._internal.spans"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return False
|
|
@@ -22,8 +22,10 @@ from ...llm.responses import (
|
|
|
22
22
|
from ...llm.types import P
|
|
23
23
|
from .protocols import (
|
|
24
24
|
AsyncFunction,
|
|
25
|
+
AsyncSpanFunction,
|
|
25
26
|
R,
|
|
26
27
|
SyncFunction,
|
|
28
|
+
SyncSpanFunction,
|
|
27
29
|
)
|
|
28
30
|
from .traced_functions import (
|
|
29
31
|
AsyncTrace,
|
|
@@ -47,6 +49,8 @@ def is_call_type(
|
|
|
47
49
|
fn: (
|
|
48
50
|
SyncFunction[P, R]
|
|
49
51
|
| AsyncFunction[P, R]
|
|
52
|
+
| SyncSpanFunction[P, R]
|
|
53
|
+
| AsyncSpanFunction[P, R]
|
|
50
54
|
| ContextCall[P, DepsT, FormattableT]
|
|
51
55
|
| AsyncContextCall[P, DepsT, FormattableT]
|
|
52
56
|
| Call[P, FormattableT]
|
|
@@ -6,27 +6,31 @@ from abc import ABC, abstractmethod
|
|
|
6
6
|
from collections.abc import Generator
|
|
7
7
|
from contextlib import contextmanager
|
|
8
8
|
from dataclasses import dataclass, field
|
|
9
|
-
from typing import
|
|
10
|
-
Any,
|
|
11
|
-
Generic,
|
|
12
|
-
Literal,
|
|
13
|
-
TypeVar,
|
|
14
|
-
)
|
|
9
|
+
from typing import Any, Generic, Literal, TypeVar
|
|
15
10
|
|
|
16
11
|
from opentelemetry.util.types import AttributeValue
|
|
17
12
|
|
|
18
13
|
from ...api.client import get_async_client, get_sync_client
|
|
19
14
|
from ...llm.context import Context, DepsT
|
|
20
15
|
from ...llm.responses.root_response import RootResponse
|
|
16
|
+
from .instrumentation.llm.serialize import attach_mirascope_response
|
|
21
17
|
from .protocols import (
|
|
22
18
|
AsyncContextFunction,
|
|
23
19
|
AsyncFunction,
|
|
20
|
+
AsyncSpanFunction,
|
|
24
21
|
SyncContextFunction,
|
|
25
22
|
SyncFunction,
|
|
23
|
+
SyncSpanFunction,
|
|
26
24
|
)
|
|
27
25
|
from .spans import Span
|
|
28
26
|
from .types import Jsonable, P, R
|
|
29
|
-
from .utils import
|
|
27
|
+
from .utils import (
|
|
28
|
+
PrimitiveType,
|
|
29
|
+
extract_arguments,
|
|
30
|
+
get_original_fn,
|
|
31
|
+
get_qualified_name,
|
|
32
|
+
json_dumps,
|
|
33
|
+
)
|
|
30
34
|
|
|
31
35
|
FunctionT = TypeVar(
|
|
32
36
|
"FunctionT",
|
|
@@ -47,6 +51,7 @@ def record_result_to_span(span: Span, result: object) -> None:
|
|
|
47
51
|
output: str | int | float | bool = result
|
|
48
52
|
elif isinstance(result, RootResponse):
|
|
49
53
|
output = result.pretty()
|
|
54
|
+
attach_mirascope_response(span, result)
|
|
50
55
|
else:
|
|
51
56
|
try:
|
|
52
57
|
output = json_dumps(result)
|
|
@@ -173,7 +178,8 @@ class _BaseFunction(Generic[P, R, FunctionT], ABC):
|
|
|
173
178
|
def __post_init__(self) -> None:
|
|
174
179
|
"""Initialize additional attributes after dataclass init."""
|
|
175
180
|
self._qualified_name = get_qualified_name(self.fn)
|
|
176
|
-
|
|
181
|
+
original_fn = get_original_fn(self.fn)
|
|
182
|
+
self._module_name = getattr(original_fn, "__module__", "")
|
|
177
183
|
|
|
178
184
|
|
|
179
185
|
@dataclass(kw_only=True)
|
|
@@ -411,3 +417,107 @@ class AsyncTracedContextFunction(BaseTracedAsyncContextFunction[P, DepsT, R]):
|
|
|
411
417
|
result = await self.fn(ctx, *args, **kwargs)
|
|
412
418
|
record_result_to_span(span, result)
|
|
413
419
|
return AsyncTrace(result=result, span=span)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
@dataclass(kw_only=True)
|
|
423
|
+
class BaseSyncTracedSpanFunction(_BaseTracedFunction[P, R, SyncSpanFunction[P, R]]):
|
|
424
|
+
"""Abstract base class for synchronous traced span function wrappers."""
|
|
425
|
+
|
|
426
|
+
_is_async: bool = field(default=False, init=False)
|
|
427
|
+
"""Whether the wrapped function is asynchronous."""
|
|
428
|
+
|
|
429
|
+
@abstractmethod
|
|
430
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
431
|
+
"""Returns the result of the traced function directly."""
|
|
432
|
+
...
|
|
433
|
+
|
|
434
|
+
@abstractmethod
|
|
435
|
+
def wrapped(self, *args: P.args, **kwargs: P.kwargs) -> Trace[R]:
|
|
436
|
+
"""Returns the trace containing the function result and span info."""
|
|
437
|
+
...
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
@dataclass(kw_only=True)
|
|
441
|
+
class TracedSpanFunction(BaseSyncTracedSpanFunction[P, R]):
|
|
442
|
+
"""Wrapper for synchronous functions that receive Span as first parameter.
|
|
443
|
+
|
|
444
|
+
The external interface does NOT include `trace_ctx` - it is injected
|
|
445
|
+
automatically by the decorator when calling the inner function.
|
|
446
|
+
|
|
447
|
+
Example:
|
|
448
|
+
```python
|
|
449
|
+
@ops.trace
|
|
450
|
+
def my_fn(trace_ctx: Span, arg: str) -> str:
|
|
451
|
+
trace_ctx.info(f"Processing: {arg}")
|
|
452
|
+
return arg.upper()
|
|
453
|
+
|
|
454
|
+
# Call without trace_ctx - it's injected
|
|
455
|
+
result = my_fn("hello") # Returns "HELLO"
|
|
456
|
+
```
|
|
457
|
+
"""
|
|
458
|
+
|
|
459
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
460
|
+
"""Returns the result of the traced function directly."""
|
|
461
|
+
with self._span(*args, **kwargs) as span:
|
|
462
|
+
result = self.fn(span, *args, **kwargs)
|
|
463
|
+
record_result_to_span(span, result)
|
|
464
|
+
return result
|
|
465
|
+
|
|
466
|
+
def wrapped(self, *args: P.args, **kwargs: P.kwargs) -> Trace[R]:
|
|
467
|
+
"""Returns the trace containing the function result and span info."""
|
|
468
|
+
with self._span(*args, **kwargs) as span:
|
|
469
|
+
result = self.fn(span, *args, **kwargs)
|
|
470
|
+
record_result_to_span(span, result)
|
|
471
|
+
return Trace(result=result, span=span)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
@dataclass(kw_only=True)
|
|
475
|
+
class BaseAsyncTracedSpanFunction(_BaseTracedFunction[P, R, AsyncSpanFunction[P, R]]):
|
|
476
|
+
"""Abstract base class for asynchronous traced span function wrappers."""
|
|
477
|
+
|
|
478
|
+
_is_async: bool = field(default=True, init=False)
|
|
479
|
+
"""Whether the wrapped function is asynchronous."""
|
|
480
|
+
|
|
481
|
+
@abstractmethod
|
|
482
|
+
async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
483
|
+
"""Returns the result of the traced function directly."""
|
|
484
|
+
...
|
|
485
|
+
|
|
486
|
+
@abstractmethod
|
|
487
|
+
async def wrapped(self, *args: P.args, **kwargs: P.kwargs) -> AsyncTrace[R]:
|
|
488
|
+
"""Returns the trace containing the function result and span info."""
|
|
489
|
+
...
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
@dataclass(kw_only=True)
|
|
493
|
+
class AsyncTracedSpanFunction(BaseAsyncTracedSpanFunction[P, R]):
|
|
494
|
+
"""Wrapper for asynchronous functions that receive Span as first parameter.
|
|
495
|
+
|
|
496
|
+
The external interface does NOT include `trace_ctx` - it is injected
|
|
497
|
+
automatically by the decorator when calling the inner function.
|
|
498
|
+
|
|
499
|
+
Example:
|
|
500
|
+
```python
|
|
501
|
+
@ops.trace
|
|
502
|
+
async def my_fn(trace_ctx: Span, arg: str) -> str:
|
|
503
|
+
trace_ctx.info(f"Processing: {arg}")
|
|
504
|
+
return arg.upper()
|
|
505
|
+
|
|
506
|
+
# Call without trace_ctx - it's injected
|
|
507
|
+
result = await my_fn("hello") # Returns "HELLO"
|
|
508
|
+
```
|
|
509
|
+
"""
|
|
510
|
+
|
|
511
|
+
async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
512
|
+
"""Returns the result of the traced function directly."""
|
|
513
|
+
with self._span(*args, **kwargs) as span:
|
|
514
|
+
result = await self.fn(span, *args, **kwargs)
|
|
515
|
+
record_result_to_span(span, result)
|
|
516
|
+
return result
|
|
517
|
+
|
|
518
|
+
async def wrapped(self, *args: P.args, **kwargs: P.kwargs) -> AsyncTrace[R]:
|
|
519
|
+
"""Returns the trace containing the function result and span info."""
|
|
520
|
+
with self._span(*args, **kwargs) as span:
|
|
521
|
+
result = await self.fn(span, *args, **kwargs)
|
|
522
|
+
record_result_to_span(span, result)
|
|
523
|
+
return AsyncTrace(result=result, span=span)
|