braintrust 0.4.0__tar.gz → 0.4.1__tar.gz
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.
- {braintrust-0.4.0 → braintrust-0.4.1}/PKG-INFO +1 -1
- braintrust-0.4.1/src/braintrust/bt_json.py +275 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/framework.py +11 -2
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/logger.py +30 -117
- braintrust-0.4.1/src/braintrust/test_bt_json.py +644 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/test_framework.py +56 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/test_logger.py +211 -107
- braintrust-0.4.1/src/braintrust/version.py +4 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/wrappers/google_genai/__init__.py +2 -15
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/wrappers/pydantic_ai.py +209 -95
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/wrappers/test_google_genai.py +62 -1
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/wrappers/test_pydantic_ai_integration.py +819 -22
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust.egg-info/PKG-INFO +1 -1
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust.egg-info/SOURCES.txt +1 -0
- braintrust-0.4.0/src/braintrust/bt_json.py +0 -116
- braintrust-0.4.0/src/braintrust/version.py +0 -4
- {braintrust-0.4.0 → braintrust-0.4.1}/README.md +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/setup.cfg +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/setup.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/__init__.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/_generated_types.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/audit.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/aws.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/cli/__init__.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/cli/__main__.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/cli/eval.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/cli/install/__init__.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/cli/install/api.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/cli/install/bump_versions.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/cli/install/logs.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/cli/install/redshift.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/cli/install/run_migrations.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/cli/push.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/conftest.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/context.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/contrib/__init__.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/contrib/temporal/__init__.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/contrib/temporal/test_temporal.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/db_fields.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/devserver/__init__.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/devserver/auth.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/devserver/cache.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/devserver/cors.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/devserver/dataset.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/devserver/eval_hooks.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/devserver/schemas.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/devserver/server.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/devserver/test_cached_login.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/devserver/test_lru_cache.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/devserver/test_server_integration.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/framework2.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/functions/__init__.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/functions/constants.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/functions/invoke.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/functions/stream.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/generated_types.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/git_fields.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/gitutil.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/graph_util.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/http_headers.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/id_gen.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/merge_row_batch.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/oai.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/object.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/otel/__init__.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/otel/context.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/otel/test_distributed_tracing.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/otel/test_otel_bt_integration.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/parameters.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/prompt.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/prompt_cache/__init__.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/prompt_cache/disk_cache.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/prompt_cache/lru_cache.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/prompt_cache/prompt_cache.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/prompt_cache/test_disk_cache.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/prompt_cache/test_lru_cache.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/prompt_cache/test_prompt_cache.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/py.typed +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/queue.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/resource_manager.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/score.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/serializable_data_class.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/span_identifier_v1.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/span_identifier_v2.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/span_identifier_v3.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/span_identifier_v4.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/span_types.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/test_framework2.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/test_helpers.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/test_id_gen.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/test_otel.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/test_queue.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/test_score.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/test_serializable_data_class.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/test_span_components.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/test_util.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/test_version.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/util.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/wrappers/__init__.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/wrappers/_anthropic_utils.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/wrappers/agno/__init__.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/wrappers/agno/agent.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/wrappers/agno/function_call.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/wrappers/agno/model.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/wrappers/agno/team.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/wrappers/agno/utils.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/wrappers/anthropic.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/wrappers/claude_agent_sdk/__init__.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/wrappers/claude_agent_sdk/_wrapper.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/wrappers/claude_agent_sdk/test_wrapper.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/wrappers/dspy.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/wrappers/langchain.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/wrappers/litellm.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/wrappers/openai.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/wrappers/test_agno.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/wrappers/test_anthropic.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/wrappers/test_dspy.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/wrappers/test_litellm.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/wrappers/test_oai_attachments.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/wrappers/test_openai.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/wrappers/test_openrouter.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/wrappers/test_pydantic_ai_wrap_openai.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/wrappers/test_utils.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust/xact_ids.py +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust.egg-info/dependency_links.txt +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust.egg-info/entry_points.txt +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust.egg-info/requires.txt +0 -0
- {braintrust-0.4.0 → braintrust-0.4.1}/src/braintrust.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import json
|
|
3
|
+
import math
|
|
4
|
+
from typing import Any, Callable, Mapping, NamedTuple, cast, overload
|
|
5
|
+
|
|
6
|
+
# Try to import orjson for better performance
|
|
7
|
+
# If not available, we'll use standard json
|
|
8
|
+
try:
|
|
9
|
+
import orjson
|
|
10
|
+
|
|
11
|
+
_HAS_ORJSON = True
|
|
12
|
+
except ImportError:
|
|
13
|
+
_HAS_ORJSON = False
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _to_bt_safe(v: Any) -> Any:
|
|
18
|
+
"""
|
|
19
|
+
Converts the object to a Braintrust-safe representation (i.e. Attachment objects are safe (specially handled by background logger)).
|
|
20
|
+
"""
|
|
21
|
+
# avoid circular imports
|
|
22
|
+
from braintrust.logger import BaseAttachment, Dataset, Experiment, Logger, ReadonlyAttachment, Span
|
|
23
|
+
|
|
24
|
+
if isinstance(v, Span):
|
|
25
|
+
return "<span>"
|
|
26
|
+
|
|
27
|
+
if isinstance(v, Experiment):
|
|
28
|
+
return "<experiment>"
|
|
29
|
+
|
|
30
|
+
if isinstance(v, Dataset):
|
|
31
|
+
return "<dataset>"
|
|
32
|
+
|
|
33
|
+
if isinstance(v, Logger):
|
|
34
|
+
return "<logger>"
|
|
35
|
+
|
|
36
|
+
if isinstance(v, BaseAttachment):
|
|
37
|
+
return v
|
|
38
|
+
|
|
39
|
+
if isinstance(v, ReadonlyAttachment):
|
|
40
|
+
return v.reference
|
|
41
|
+
|
|
42
|
+
if dataclasses.is_dataclass(v) and not isinstance(v, type):
|
|
43
|
+
# Use manual field iteration instead of dataclasses.asdict() because
|
|
44
|
+
# asdict() deep-copies values, which breaks objects like Attachment
|
|
45
|
+
# that contain non-copyable items (thread locks, file handles, etc.)
|
|
46
|
+
return {f.name: _to_bt_safe(getattr(v, f.name)) for f in dataclasses.fields(v)}
|
|
47
|
+
|
|
48
|
+
# Pydantic model classes (not instances) with model_json_schema
|
|
49
|
+
if isinstance(v, type) and hasattr(v, "model_json_schema") and callable(cast(Any, v).model_json_schema):
|
|
50
|
+
try:
|
|
51
|
+
return cast(Any, v).model_json_schema()
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
# Attempt to dump a Pydantic v2 `BaseModel`.
|
|
56
|
+
try:
|
|
57
|
+
return cast(Any, v).model_dump(exclude_none=True)
|
|
58
|
+
except (AttributeError, TypeError):
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
# Attempt to dump a Pydantic v1 `BaseModel`.
|
|
62
|
+
try:
|
|
63
|
+
return cast(Any, v).dict(exclude_none=True)
|
|
64
|
+
except (AttributeError, TypeError):
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
if isinstance(v, float):
|
|
68
|
+
# Handle NaN and Infinity for JSON compatibility
|
|
69
|
+
if math.isnan(v):
|
|
70
|
+
return "NaN"
|
|
71
|
+
|
|
72
|
+
if math.isinf(v):
|
|
73
|
+
return "Infinity" if v > 0 else "-Infinity"
|
|
74
|
+
|
|
75
|
+
return v
|
|
76
|
+
|
|
77
|
+
if isinstance(v, (int, str, bool)) or v is None:
|
|
78
|
+
# Skip roundtrip for primitive types.
|
|
79
|
+
return v
|
|
80
|
+
|
|
81
|
+
# Note: we avoid using copy.deepcopy, because it's difficult to
|
|
82
|
+
# guarantee the independence of such copied types from their origin.
|
|
83
|
+
# E.g. the original type could have a `__del__` method that alters
|
|
84
|
+
# some shared internal state, and we need this deep copy to be
|
|
85
|
+
# fully-independent from the original.
|
|
86
|
+
|
|
87
|
+
# We pass `encoder=_str_encoder` since we've already tried converting rich objects to json safe objects.
|
|
88
|
+
return bt_loads(bt_dumps(v, encoder=_str_encoder))
|
|
89
|
+
|
|
90
|
+
@overload
|
|
91
|
+
def bt_safe_deep_copy(
|
|
92
|
+
obj: Mapping[str, Any],
|
|
93
|
+
max_depth: int = ...,
|
|
94
|
+
) -> dict[str, Any]: ...
|
|
95
|
+
|
|
96
|
+
@overload
|
|
97
|
+
def bt_safe_deep_copy(
|
|
98
|
+
obj: list[Any],
|
|
99
|
+
max_depth: int = ...,
|
|
100
|
+
) -> list[Any]: ...
|
|
101
|
+
|
|
102
|
+
@overload
|
|
103
|
+
def bt_safe_deep_copy(
|
|
104
|
+
obj: Any,
|
|
105
|
+
max_depth: int = ...,
|
|
106
|
+
) -> Any: ...
|
|
107
|
+
def bt_safe_deep_copy(obj: Any, max_depth: int=200):
|
|
108
|
+
"""
|
|
109
|
+
Creates a deep copy of the given object and converts rich objects to Braintrust-safe representations. See `_to_bt_safe` for more details.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
obj: Object to deep copy and sanitize.
|
|
113
|
+
to_json_safe: Function to ensure the object is json safe.
|
|
114
|
+
max_depth: Maximum depth to copy.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Deep copy of the object.
|
|
118
|
+
"""
|
|
119
|
+
# Track visited objects to detect circular references
|
|
120
|
+
visited: set[int] = set()
|
|
121
|
+
|
|
122
|
+
def _deep_copy_object(v: Any, depth: int = 0) -> Any:
|
|
123
|
+
# Check depth limit - use >= to stop before exceeding
|
|
124
|
+
if depth >= max_depth:
|
|
125
|
+
return "<max depth exceeded>"
|
|
126
|
+
|
|
127
|
+
# Check for circular references in mutable containers
|
|
128
|
+
# Use id() to track object identity
|
|
129
|
+
if isinstance(v, (Mapping, list, tuple, set)):
|
|
130
|
+
obj_id = id(v)
|
|
131
|
+
if obj_id in visited:
|
|
132
|
+
return "<circular reference>"
|
|
133
|
+
visited.add(obj_id)
|
|
134
|
+
try:
|
|
135
|
+
if isinstance(v, Mapping):
|
|
136
|
+
# Prevent dict keys from holding references to user data. Note that
|
|
137
|
+
# `bt_json` already coerces keys to string, a behavior that comes from
|
|
138
|
+
# `json.dumps`. However, that runs at log upload time, while we want to
|
|
139
|
+
# cut out all the references to user objects synchronously in this
|
|
140
|
+
# function.
|
|
141
|
+
result = {}
|
|
142
|
+
for k in v:
|
|
143
|
+
try:
|
|
144
|
+
key_str = str(k)
|
|
145
|
+
except Exception:
|
|
146
|
+
# If str() fails on the key, use a fallback representation
|
|
147
|
+
key_str = f"<non-stringifiable-key: {type(k).__name__}>"
|
|
148
|
+
result[key_str] = _deep_copy_object(v[k], depth + 1)
|
|
149
|
+
return result
|
|
150
|
+
elif isinstance(v, (list, tuple, set)):
|
|
151
|
+
return [_deep_copy_object(x, depth + 1) for x in v]
|
|
152
|
+
finally:
|
|
153
|
+
# Remove from visited set after processing to allow the same object
|
|
154
|
+
# to appear in different branches of the tree
|
|
155
|
+
visited.discard(obj_id)
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
return _to_bt_safe(v)
|
|
159
|
+
except Exception:
|
|
160
|
+
return f"<non-sanitizable: {type(v).__name__}>"
|
|
161
|
+
|
|
162
|
+
return _deep_copy_object(obj)
|
|
163
|
+
|
|
164
|
+
def _safe_str(obj: Any) -> str:
|
|
165
|
+
try:
|
|
166
|
+
return str(obj)
|
|
167
|
+
except Exception:
|
|
168
|
+
return f"<non-serializable: {type(obj).__name__}>"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _to_json_safe(obj: Any) -> Any:
|
|
172
|
+
"""
|
|
173
|
+
Handler for non-JSON-serializable objects. Returns a string representation of the object.
|
|
174
|
+
"""
|
|
175
|
+
# avoid circular imports
|
|
176
|
+
from braintrust.logger import BaseAttachment
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
v = _to_bt_safe(obj)
|
|
180
|
+
|
|
181
|
+
# JSON-safe representation of Attachment objects are their reference.
|
|
182
|
+
# If we get this object at this point, we have to assume someone has already uploaded the attachment!
|
|
183
|
+
if isinstance(v, BaseAttachment):
|
|
184
|
+
v = v.reference
|
|
185
|
+
|
|
186
|
+
return v
|
|
187
|
+
except Exception:
|
|
188
|
+
pass
|
|
189
|
+
|
|
190
|
+
# When everything fails, try to return the string representation of the object
|
|
191
|
+
return _safe_str(obj)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class BraintrustJSONEncoder(json.JSONEncoder):
|
|
195
|
+
"""
|
|
196
|
+
Custom JSON encoder for standard json library.
|
|
197
|
+
|
|
198
|
+
This is used as a fallback when orjson is not available or fails.
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
def default(self, o: Any):
|
|
202
|
+
return _to_json_safe(o)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class BraintrustStrEncoder(json.JSONEncoder):
|
|
206
|
+
def default(self, o: Any):
|
|
207
|
+
return _safe_str(o)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class Encoder(NamedTuple):
|
|
211
|
+
native: type[json.JSONEncoder]
|
|
212
|
+
orjson: Callable[[Any], Any]
|
|
213
|
+
|
|
214
|
+
_json_encoder = Encoder(native=BraintrustJSONEncoder, orjson=_to_json_safe)
|
|
215
|
+
_str_encoder = Encoder(native=BraintrustStrEncoder, orjson=_safe_str)
|
|
216
|
+
|
|
217
|
+
def bt_dumps(obj: Any, encoder: Encoder | None=_json_encoder, **kwargs: Any) -> str:
|
|
218
|
+
"""
|
|
219
|
+
Serialize obj to a JSON-formatted string.
|
|
220
|
+
|
|
221
|
+
Automatically uses orjson if available for better performance (3-5x faster),
|
|
222
|
+
with fallback to standard json library if orjson is not installed or fails.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
obj: Object to serialize
|
|
226
|
+
encoder: Encoder to use, defaults to `_default_encoder`
|
|
227
|
+
**kwargs: Additional arguments (passed to json.dumps in fallback path)
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
JSON string representation of obj
|
|
231
|
+
"""
|
|
232
|
+
if _HAS_ORJSON:
|
|
233
|
+
# Try orjson first for better performance
|
|
234
|
+
try:
|
|
235
|
+
# pylint: disable=no-member # orjson is a C extension, pylint can't introspect it
|
|
236
|
+
return orjson.dumps( # type: ignore[possibly-unbound]
|
|
237
|
+
obj,
|
|
238
|
+
default=encoder.orjson if encoder else None,
|
|
239
|
+
# options match json.dumps behavior for bc
|
|
240
|
+
option=orjson.OPT_SORT_KEYS | orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_NON_STR_KEYS, # type: ignore[possibly-unbound]
|
|
241
|
+
).decode("utf-8")
|
|
242
|
+
except Exception:
|
|
243
|
+
# If orjson fails, fall back to standard json
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
# Use standard json (either orjson not available or it failed)
|
|
247
|
+
# Use sort_keys=True for deterministic output (matches orjson OPT_SORT_KEYS)
|
|
248
|
+
return json.dumps(obj, cls=encoder.native if encoder else None, allow_nan=False, sort_keys=True, **kwargs)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def bt_loads(s: str, **kwargs) -> Any:
|
|
252
|
+
"""
|
|
253
|
+
Deserialize s (a str containing a JSON document) to a Python object.
|
|
254
|
+
|
|
255
|
+
Automatically uses orjson if available for better performance (2-3x faster),
|
|
256
|
+
with fallback to standard json library if orjson is not installed or fails.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
s: JSON string to deserialize
|
|
260
|
+
**kwargs: Additional arguments (passed to json.loads in fallback path)
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Python object representation of JSON string
|
|
264
|
+
"""
|
|
265
|
+
if _HAS_ORJSON:
|
|
266
|
+
# Try orjson first for better performance
|
|
267
|
+
try:
|
|
268
|
+
# pylint: disable=no-member # orjson is a C extension, pylint can't introspect it
|
|
269
|
+
return orjson.loads(s) # type: ignore[possibly-unbound]
|
|
270
|
+
except Exception:
|
|
271
|
+
# If orjson fails, fall back to standard json
|
|
272
|
+
pass
|
|
273
|
+
|
|
274
|
+
# Use standard json (either orjson not available or it failed)
|
|
275
|
+
return json.loads(s, **kwargs)
|
|
@@ -47,7 +47,7 @@ from .resource_manager import ResourceManager
|
|
|
47
47
|
from .score import Score, is_score, is_scorer
|
|
48
48
|
from .serializable_data_class import SerializableDataClass
|
|
49
49
|
from .span_types import SpanTypeAttribute
|
|
50
|
-
from .util import bt_iscoroutinefunction, eprint
|
|
50
|
+
from .util import bt_iscoroutinefunction, eprint, merge_dicts
|
|
51
51
|
|
|
52
52
|
Input = TypeVar("Input")
|
|
53
53
|
Output = TypeVar("Output")
|
|
@@ -1284,8 +1284,17 @@ async def _run_evaluator_internal(
|
|
|
1284
1284
|
event_loop = asyncio.get_event_loop()
|
|
1285
1285
|
|
|
1286
1286
|
async def await_or_run_scorer(root_span, scorer, name, **kwargs):
|
|
1287
|
+
# Merge purpose into parent's propagated_event rather than replacing it
|
|
1288
|
+
parent_propagated = root_span.propagated_event or {}
|
|
1289
|
+
merged_propagated = merge_dicts(
|
|
1290
|
+
{**parent_propagated},
|
|
1291
|
+
{"span_attributes": {"purpose": "scorer"}},
|
|
1292
|
+
)
|
|
1287
1293
|
with root_span.start_span(
|
|
1288
|
-
name=name,
|
|
1294
|
+
name=name,
|
|
1295
|
+
span_attributes={"type": SpanTypeAttribute.SCORE, "purpose": "scorer"},
|
|
1296
|
+
propagated_event=merged_propagated,
|
|
1297
|
+
input=dict(**kwargs),
|
|
1289
1298
|
) as span:
|
|
1290
1299
|
score = scorer
|
|
1291
1300
|
if hasattr(scorer, "eval_async"):
|
|
@@ -9,7 +9,6 @@ import inspect
|
|
|
9
9
|
import io
|
|
10
10
|
import json
|
|
11
11
|
import logging
|
|
12
|
-
import math
|
|
13
12
|
import os
|
|
14
13
|
import sys
|
|
15
14
|
import textwrap
|
|
@@ -46,7 +45,7 @@ from requests.adapters import HTTPAdapter
|
|
|
46
45
|
from urllib3.util.retry import Retry
|
|
47
46
|
|
|
48
47
|
from . import context, id_gen
|
|
49
|
-
from .bt_json import bt_dumps,
|
|
48
|
+
from .bt_json import bt_dumps, bt_safe_deep_copy
|
|
50
49
|
from .db_fields import (
|
|
51
50
|
ASYNC_SCORING_CONTROL_FIELD,
|
|
52
51
|
AUDIT_METADATA_FIELD,
|
|
@@ -271,6 +270,10 @@ class _NoopSpan(Span):
|
|
|
271
270
|
def id(self):
|
|
272
271
|
return ""
|
|
273
272
|
|
|
273
|
+
@property
|
|
274
|
+
def propagated_event(self):
|
|
275
|
+
return None
|
|
276
|
+
|
|
274
277
|
def log(self, **event: Any):
|
|
275
278
|
pass
|
|
276
279
|
|
|
@@ -739,13 +742,6 @@ def construct_logs3_data(items: Sequence[str]):
|
|
|
739
742
|
return '{"rows": ' + rowsS + ', "api_version": ' + str(DATA_API_VERSION) + "}"
|
|
740
743
|
|
|
741
744
|
|
|
742
|
-
def _check_json_serializable(event):
|
|
743
|
-
try:
|
|
744
|
-
return bt_dumps(event)
|
|
745
|
-
except (TypeError, ValueError) as e:
|
|
746
|
-
raise Exception(f"All logged values must be JSON-serializable: {event}") from e
|
|
747
|
-
|
|
748
|
-
|
|
749
745
|
class _MaskingError:
|
|
750
746
|
"""Internal class to signal masking errors that need special handling."""
|
|
751
747
|
|
|
@@ -795,6 +791,7 @@ class _MemoryBackgroundLogger(_BackgroundLogger):
|
|
|
795
791
|
self.lock = threading.Lock()
|
|
796
792
|
self.logs = []
|
|
797
793
|
self.masking_function: Callable[[Any], Any] | None = None
|
|
794
|
+
self.upload_attempts: list[BaseAttachment] = [] # Track upload attempts
|
|
798
795
|
|
|
799
796
|
def enforce_queue_size_limit(self, enforce: bool) -> None:
|
|
800
797
|
pass
|
|
@@ -808,7 +805,21 @@ class _MemoryBackgroundLogger(_BackgroundLogger):
|
|
|
808
805
|
self.masking_function = masking_function
|
|
809
806
|
|
|
810
807
|
def flush(self, batch_size: int | None = None):
|
|
811
|
-
|
|
808
|
+
"""Flush the memory logger, extracting attachments and tracking upload attempts."""
|
|
809
|
+
with self.lock:
|
|
810
|
+
if not self.logs:
|
|
811
|
+
return
|
|
812
|
+
|
|
813
|
+
# Unwrap lazy values and extract attachments
|
|
814
|
+
logs = [l.get() for l in self.logs]
|
|
815
|
+
|
|
816
|
+
# Extract attachments from all logs
|
|
817
|
+
attachments: list[BaseAttachment] = []
|
|
818
|
+
for log in logs:
|
|
819
|
+
_extract_attachments(log, attachments)
|
|
820
|
+
|
|
821
|
+
# Track upload attempts (don't actually call upload() in tests)
|
|
822
|
+
self.upload_attempts.extend(attachments)
|
|
812
823
|
|
|
813
824
|
def pop(self):
|
|
814
825
|
with self.lock:
|
|
@@ -1959,24 +1970,14 @@ def get_span_parent_object(
|
|
|
1959
1970
|
|
|
1960
1971
|
def _try_log_input(span, f_sig, f_args, f_kwargs):
|
|
1961
1972
|
if f_sig:
|
|
1962
|
-
|
|
1963
|
-
input_serializable = bound_args
|
|
1973
|
+
input_data = f_sig.bind(*f_args, **f_kwargs).arguments
|
|
1964
1974
|
else:
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
_check_json_serializable(input_serializable)
|
|
1968
|
-
except Exception as e:
|
|
1969
|
-
input_serializable = "<input not json-serializable>: " + str(e)
|
|
1970
|
-
span.log(input=input_serializable)
|
|
1975
|
+
input_data = dict(args=f_args, kwargs=f_kwargs)
|
|
1976
|
+
span.log(input=input_data)
|
|
1971
1977
|
|
|
1972
1978
|
|
|
1973
1979
|
def _try_log_output(span, output):
|
|
1974
|
-
|
|
1975
|
-
try:
|
|
1976
|
-
_check_json_serializable(output)
|
|
1977
|
-
except Exception as e:
|
|
1978
|
-
output_serializable = "<output not json-serializable>: " + str(e)
|
|
1979
|
-
span.log(output=output_serializable)
|
|
1980
|
+
span.log(output=output)
|
|
1980
1981
|
|
|
1981
1982
|
|
|
1982
1983
|
F = TypeVar("F", bound=Callable[..., Any])
|
|
@@ -2426,91 +2427,6 @@ def _validate_and_sanitize_experiment_log_full_args(event: Mapping[str, Any], ha
|
|
|
2426
2427
|
return event
|
|
2427
2428
|
|
|
2428
2429
|
|
|
2429
|
-
def _deep_copy_event(event: Mapping[str, Any]) -> dict[str, Any]:
|
|
2430
|
-
"""
|
|
2431
|
-
Creates a deep copy of the given event. Replaces references to user objects
|
|
2432
|
-
with placeholder strings to ensure serializability, except for `Attachment`
|
|
2433
|
-
and `ExternalAttachment` objects, which are preserved and not deep-copied.
|
|
2434
|
-
|
|
2435
|
-
Handles circular references and excessive nesting depth to prevent
|
|
2436
|
-
RecursionError during serialization.
|
|
2437
|
-
"""
|
|
2438
|
-
# Maximum depth to prevent hitting Python's recursion limit
|
|
2439
|
-
# Python's default limit is ~1000, we use a conservative limit
|
|
2440
|
-
# to account for existing call stack usage from pytest, application code, etc.
|
|
2441
|
-
MAX_DEPTH = 200
|
|
2442
|
-
|
|
2443
|
-
# Track visited objects to detect circular references
|
|
2444
|
-
visited: set[int] = set()
|
|
2445
|
-
|
|
2446
|
-
def _deep_copy_object(v: Any, depth: int = 0) -> Any:
|
|
2447
|
-
# Check depth limit - use >= to stop before exceeding
|
|
2448
|
-
if depth >= MAX_DEPTH:
|
|
2449
|
-
return "<max depth exceeded>"
|
|
2450
|
-
|
|
2451
|
-
# Check for circular references in mutable containers
|
|
2452
|
-
# Use id() to track object identity
|
|
2453
|
-
if isinstance(v, (Mapping, list, tuple, set)):
|
|
2454
|
-
obj_id = id(v)
|
|
2455
|
-
if obj_id in visited:
|
|
2456
|
-
return "<circular reference>"
|
|
2457
|
-
visited.add(obj_id)
|
|
2458
|
-
try:
|
|
2459
|
-
if isinstance(v, Mapping):
|
|
2460
|
-
# Prevent dict keys from holding references to user data. Note that
|
|
2461
|
-
# `bt_json` already coerces keys to string, a behavior that comes from
|
|
2462
|
-
# `json.dumps`. However, that runs at log upload time, while we want to
|
|
2463
|
-
# cut out all the references to user objects synchronously in this
|
|
2464
|
-
# function.
|
|
2465
|
-
result = {}
|
|
2466
|
-
for k in v:
|
|
2467
|
-
try:
|
|
2468
|
-
key_str = str(k)
|
|
2469
|
-
except Exception:
|
|
2470
|
-
# If str() fails on the key, use a fallback representation
|
|
2471
|
-
key_str = f"<non-stringifiable-key: {type(k).__name__}>"
|
|
2472
|
-
result[key_str] = _deep_copy_object(v[k], depth + 1)
|
|
2473
|
-
return result
|
|
2474
|
-
elif isinstance(v, (list, tuple, set)):
|
|
2475
|
-
return [_deep_copy_object(x, depth + 1) for x in v]
|
|
2476
|
-
finally:
|
|
2477
|
-
# Remove from visited set after processing to allow the same object
|
|
2478
|
-
# to appear in different branches of the tree
|
|
2479
|
-
visited.discard(obj_id)
|
|
2480
|
-
|
|
2481
|
-
if isinstance(v, Span):
|
|
2482
|
-
return "<span>"
|
|
2483
|
-
elif isinstance(v, Experiment):
|
|
2484
|
-
return "<experiment>"
|
|
2485
|
-
elif isinstance(v, Dataset):
|
|
2486
|
-
return "<dataset>"
|
|
2487
|
-
elif isinstance(v, Logger):
|
|
2488
|
-
return "<logger>"
|
|
2489
|
-
elif isinstance(v, BaseAttachment):
|
|
2490
|
-
return v
|
|
2491
|
-
elif isinstance(v, ReadonlyAttachment):
|
|
2492
|
-
return v.reference
|
|
2493
|
-
elif isinstance(v, float):
|
|
2494
|
-
# Handle NaN and Infinity for JSON compatibility
|
|
2495
|
-
if math.isnan(v):
|
|
2496
|
-
return "NaN"
|
|
2497
|
-
elif math.isinf(v):
|
|
2498
|
-
return "Infinity" if v > 0 else "-Infinity"
|
|
2499
|
-
return v
|
|
2500
|
-
elif isinstance(v, (int, str, bool)) or v is None:
|
|
2501
|
-
# Skip roundtrip for primitive types.
|
|
2502
|
-
return v
|
|
2503
|
-
else:
|
|
2504
|
-
# Note: we avoid using copy.deepcopy, because it's difficult to
|
|
2505
|
-
# guarantee the independence of such copied types from their origin.
|
|
2506
|
-
# E.g. the original type could have a `__del__` method that alters
|
|
2507
|
-
# some shared internal state, and we need this deep copy to be
|
|
2508
|
-
# fully-independent from the original.
|
|
2509
|
-
return bt_loads(bt_dumps(v))
|
|
2510
|
-
|
|
2511
|
-
return _deep_copy_object(event)
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
2430
|
class ObjectIterator(Generic[T]):
|
|
2515
2431
|
def __init__(self, refetch_fn: Callable[[], Sequence[T]]):
|
|
2516
2432
|
self.refetch_fn = refetch_fn
|
|
@@ -3060,7 +2976,7 @@ def _log_feedback_impl(
|
|
|
3060
2976
|
metadata = update_event.pop("metadata")
|
|
3061
2977
|
update_event = {k: v for k, v in update_event.items() if v is not None}
|
|
3062
2978
|
|
|
3063
|
-
update_event =
|
|
2979
|
+
update_event = bt_safe_deep_copy(update_event)
|
|
3064
2980
|
|
|
3065
2981
|
def parent_ids():
|
|
3066
2982
|
exporter = _get_exporter()
|
|
@@ -3116,7 +3032,7 @@ def _update_span_impl(
|
|
|
3116
3032
|
event=event,
|
|
3117
3033
|
)
|
|
3118
3034
|
|
|
3119
|
-
update_event =
|
|
3035
|
+
update_event = bt_safe_deep_copy(update_event)
|
|
3120
3036
|
|
|
3121
3037
|
def parent_ids():
|
|
3122
3038
|
exporter = _get_exporter()
|
|
@@ -3936,8 +3852,7 @@ class SpanImpl(Span):
|
|
|
3936
3852
|
**{IS_MERGE_FIELD: self._is_merge},
|
|
3937
3853
|
)
|
|
3938
3854
|
|
|
3939
|
-
serializable_partial_record =
|
|
3940
|
-
_check_json_serializable(serializable_partial_record)
|
|
3855
|
+
serializable_partial_record = bt_safe_deep_copy(partial_record)
|
|
3941
3856
|
if serializable_partial_record.get("metrics", {}).get("end") is not None:
|
|
3942
3857
|
self._logged_end_time = serializable_partial_record["metrics"]["end"]
|
|
3943
3858
|
|
|
@@ -4304,8 +4219,7 @@ class Dataset(ObjectFetcher[DatasetEvent]):
|
|
|
4304
4219
|
args[IS_MERGE_FIELD] = True
|
|
4305
4220
|
args = _filter_none_args(args) # If merging, then remove None values to prevent null value writes
|
|
4306
4221
|
|
|
4307
|
-
|
|
4308
|
-
args = _deep_copy_event(args)
|
|
4222
|
+
args = bt_safe_deep_copy(args)
|
|
4309
4223
|
|
|
4310
4224
|
def compute_args() -> dict[str, Any]:
|
|
4311
4225
|
return dict(
|
|
@@ -4408,8 +4322,7 @@ class Dataset(ObjectFetcher[DatasetEvent]):
|
|
|
4408
4322
|
"_object_delete": True, # XXX potentially place this in the logging endpoint
|
|
4409
4323
|
},
|
|
4410
4324
|
)
|
|
4411
|
-
|
|
4412
|
-
partial_args = _deep_copy_event(partial_args)
|
|
4325
|
+
partial_args = bt_safe_deep_copy(partial_args)
|
|
4413
4326
|
|
|
4414
4327
|
def compute_args():
|
|
4415
4328
|
return dict(
|