braintrust 0.5.0__py3-none-any.whl → 0.5.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. braintrust/__init__.py +14 -0
  2. braintrust/_generated_types.py +56 -3
  3. braintrust/auto.py +179 -0
  4. braintrust/conftest.py +23 -4
  5. braintrust/db_fields.py +10 -0
  6. braintrust/framework.py +18 -5
  7. braintrust/generated_types.py +3 -1
  8. braintrust/logger.py +369 -134
  9. braintrust/merge_row_batch.py +49 -109
  10. braintrust/oai.py +51 -0
  11. braintrust/test_bt_json.py +0 -5
  12. braintrust/test_context.py +1264 -0
  13. braintrust/test_framework.py +37 -0
  14. braintrust/test_http.py +444 -0
  15. braintrust/test_logger.py +179 -5
  16. braintrust/test_merge_row_batch.py +160 -0
  17. braintrust/test_util.py +58 -1
  18. braintrust/util.py +20 -0
  19. braintrust/version.py +2 -2
  20. braintrust/wrappers/agno/__init__.py +2 -3
  21. braintrust/wrappers/anthropic.py +64 -0
  22. braintrust/wrappers/claude_agent_sdk/__init__.py +2 -3
  23. braintrust/wrappers/claude_agent_sdk/test_wrapper.py +9 -0
  24. braintrust/wrappers/dspy.py +52 -1
  25. braintrust/wrappers/google_genai/__init__.py +9 -6
  26. braintrust/wrappers/litellm.py +6 -43
  27. braintrust/wrappers/pydantic_ai.py +2 -3
  28. braintrust/wrappers/test_agno.py +9 -0
  29. braintrust/wrappers/test_anthropic.py +156 -0
  30. braintrust/wrappers/test_dspy.py +117 -0
  31. braintrust/wrappers/test_google_genai.py +9 -0
  32. braintrust/wrappers/test_litellm.py +57 -55
  33. braintrust/wrappers/test_openai.py +253 -1
  34. braintrust/wrappers/test_pydantic_ai_integration.py +9 -0
  35. braintrust/wrappers/test_utils.py +79 -0
  36. braintrust/wrappers/threads.py +114 -0
  37. {braintrust-0.5.0.dist-info → braintrust-0.5.3.dist-info}/METADATA +1 -1
  38. {braintrust-0.5.0.dist-info → braintrust-0.5.3.dist-info}/RECORD +41 -37
  39. {braintrust-0.5.0.dist-info → braintrust-0.5.3.dist-info}/WHEEL +1 -1
  40. braintrust/graph_util.py +0 -147
  41. {braintrust-0.5.0.dist-info → braintrust-0.5.3.dist-info}/entry_points.txt +0 -0
  42. {braintrust-0.5.0.dist-info → braintrust-0.5.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,160 @@
1
+ import unittest
2
+
3
+ from braintrust.db_fields import IS_MERGE_FIELD
4
+ from braintrust.merge_row_batch import batch_items, merge_row_batch
5
+
6
+
7
+ class MergeRowBatchTest(unittest.TestCase):
8
+ def test_basic(self):
9
+ rows = [
10
+ # These rows should get merged together, ending up as a merge.
11
+ dict(
12
+ experiment_id="e0",
13
+ id="x",
14
+ inputs=dict(a=12),
15
+ **{IS_MERGE_FIELD: True},
16
+ ),
17
+ dict(
18
+ experiment_id="e0",
19
+ id="x",
20
+ inputs=dict(b=10),
21
+ **{IS_MERGE_FIELD: True},
22
+ ),
23
+ dict(
24
+ experiment_id="e0",
25
+ id="x",
26
+ inputs=dict(c="hello"),
27
+ **{IS_MERGE_FIELD: True},
28
+ ),
29
+ # The first row should be clobbered by the second, but the third
30
+ # merged with the second, ending up as a replacement.
31
+ dict(
32
+ experiment_id="e0",
33
+ id="y",
34
+ inputs=dict(a="hello"),
35
+ ),
36
+ dict(
37
+ experiment_id="e0",
38
+ id="y",
39
+ inputs=dict(b=10),
40
+ ),
41
+ dict(
42
+ experiment_id="e0",
43
+ id="y",
44
+ inputs=dict(c=12),
45
+ **{IS_MERGE_FIELD: True},
46
+ ),
47
+ # These rows should be clobbered separately from the last batch.
48
+ dict(
49
+ dataset_id="d0",
50
+ id="y",
51
+ inputs=dict(a="hello"),
52
+ ),
53
+ dict(
54
+ dataset_id="d0",
55
+ id="y",
56
+ inputs=dict(b=10),
57
+ ),
58
+ dict(
59
+ dataset_id="d0",
60
+ id="y",
61
+ inputs=dict(c=12),
62
+ ),
63
+ ]
64
+
65
+ merged_rows = merge_row_batch(rows)
66
+ key_to_rows = {(row.get("experiment_id"), row.get("dataset_id"), row.get("id")): row for row in merged_rows}
67
+ self.assertEqual(
68
+ {
69
+ ("e0", None, "x"): dict(
70
+ experiment_id="e0",
71
+ id="x",
72
+ inputs=dict(a=12, b=10, c="hello"),
73
+ **{IS_MERGE_FIELD: True},
74
+ ),
75
+ ("e0", None, "y"): dict(
76
+ experiment_id="e0",
77
+ id="y",
78
+ inputs=dict(b=10, c=12),
79
+ ),
80
+ (None, "d0", "y"): dict(
81
+ dataset_id="d0",
82
+ id="y",
83
+ inputs=dict(c=12),
84
+ ),
85
+ },
86
+ key_to_rows,
87
+ )
88
+
89
+ def test_skip_fields(self):
90
+ rows = [
91
+ # These rows should get merged together, ending up as a merge. But
92
+ # the original fields should be retained, regardless of whether we
93
+ # populated them or not.
94
+ dict(
95
+ experiment_id="e0",
96
+ id="x",
97
+ inputs=dict(a=12),
98
+ **{IS_MERGE_FIELD: True},
99
+ created=123,
100
+ root_span_id="abc",
101
+ _parent_id="baz",
102
+ span_parents=["foo", "bar"],
103
+ ),
104
+ dict(
105
+ experiment_id="e0",
106
+ id="x",
107
+ inputs=dict(b=10),
108
+ **{IS_MERGE_FIELD: True},
109
+ created=456,
110
+ span_id="foo",
111
+ root_span_id="bar",
112
+ _parent_id="boop",
113
+ span_parents=[],
114
+ ),
115
+ ]
116
+
117
+ merged_rows = merge_row_batch(rows)
118
+ self.assertEqual(
119
+ merged_rows,
120
+ [
121
+ dict(
122
+ experiment_id="e0",
123
+ id="x",
124
+ inputs=dict(a=12, b=10),
125
+ **{IS_MERGE_FIELD: True},
126
+ created=123,
127
+ root_span_id="abc",
128
+ _parent_id="baz",
129
+ span_parents=["foo", "bar"],
130
+ ),
131
+ ],
132
+ )
133
+
134
+
135
+ class BatchItemsTest(unittest.TestCase):
136
+ def test_basic(self):
137
+ a = "x" * 1
138
+ b = "x" * 2
139
+ c = "x" * 4
140
+ d = "y" * 1
141
+ e = "y" * 2
142
+ f = "y" * 4
143
+
144
+ items = [a, b, c, f, e, d]
145
+
146
+ # No limits.
147
+ output = batch_items(items)
148
+ self.assertEqual(output, [[a, b, c, f, e, d]])
149
+
150
+ # Num items limit.
151
+ output = batch_items(items, batch_max_num_items=2)
152
+ self.assertEqual(output, [[a, b], [c, f], [e, d]])
153
+
154
+ # Num bytes limit.
155
+ output = batch_items(items, batch_max_num_bytes=2)
156
+ self.assertEqual(output, [[a], [b], [c], [f], [e], [d]])
157
+
158
+ # Both items and num bytes limit.
159
+ output = batch_items(items, batch_max_num_items=2, batch_max_num_bytes=5)
160
+ self.assertEqual(output, [[a, b], [c], [f], [e, d]])
braintrust/test_util.py CHANGED
@@ -1,9 +1,66 @@
1
+ import os
1
2
  import unittest
2
3
  from typing import List
3
4
 
4
5
  import pytest
5
6
 
6
- from .util import LazyValue, mask_api_key, merge_dicts_with_paths
7
+ from .util import LazyValue, mask_api_key, merge_dicts_with_paths, parse_env_var_float
8
+
9
+
10
+ class TestParseEnvVarFloat:
11
+ """Tests for parse_env_var_float helper."""
12
+
13
+ def test_returns_default_when_env_not_set(self):
14
+ assert parse_env_var_float("NONEXISTENT_VAR_12345", 42.0) == 42.0
15
+
16
+ def test_parses_valid_float(self):
17
+ os.environ["TEST_FLOAT"] = "123.45"
18
+ try:
19
+ assert parse_env_var_float("TEST_FLOAT", 0.0) == 123.45
20
+ finally:
21
+ del os.environ["TEST_FLOAT"]
22
+
23
+ def test_returns_default_for_nan(self):
24
+ os.environ["TEST_FLOAT"] = "nan"
25
+ try:
26
+ assert parse_env_var_float("TEST_FLOAT", 99.0) == 99.0
27
+ finally:
28
+ del os.environ["TEST_FLOAT"]
29
+
30
+ def test_returns_default_for_inf(self):
31
+ os.environ["TEST_FLOAT"] = "inf"
32
+ try:
33
+ assert parse_env_var_float("TEST_FLOAT", 99.0) == 99.0
34
+ finally:
35
+ del os.environ["TEST_FLOAT"]
36
+
37
+ def test_returns_default_for_negative_inf(self):
38
+ os.environ["TEST_FLOAT"] = "-inf"
39
+ try:
40
+ assert parse_env_var_float("TEST_FLOAT", 99.0) == 99.0
41
+ finally:
42
+ del os.environ["TEST_FLOAT"]
43
+
44
+ def test_returns_default_for_empty_string(self):
45
+ os.environ["TEST_FLOAT"] = ""
46
+ try:
47
+ assert parse_env_var_float("TEST_FLOAT", 99.0) == 99.0
48
+ finally:
49
+ del os.environ["TEST_FLOAT"]
50
+
51
+ def test_returns_default_for_invalid_string(self):
52
+ os.environ["TEST_FLOAT"] = "not_a_number"
53
+ try:
54
+ assert parse_env_var_float("TEST_FLOAT", 99.0) == 99.0
55
+ finally:
56
+ del os.environ["TEST_FLOAT"]
57
+
58
+ def test_allows_negative_values(self):
59
+ os.environ["TEST_FLOAT"] = "-5.5"
60
+ try:
61
+ assert parse_env_var_float("TEST_FLOAT", 0.0) == -5.5
62
+ finally:
63
+ del os.environ["TEST_FLOAT"]
7
64
 
8
65
 
9
66
  class TestLazyValue(unittest.TestCase):
braintrust/util.py CHANGED
@@ -1,5 +1,7 @@
1
1
  import inspect
2
2
  import json
3
+ import math
4
+ import os
3
5
  import sys
4
6
  import threading
5
7
  import urllib.parse
@@ -9,6 +11,24 @@ from typing import Any, Generic, Literal, TypedDict, TypeVar, Union
9
11
 
10
12
  from requests import HTTPError, Response
11
13
 
14
+
15
+ def parse_env_var_float(name: str, default: float) -> float:
16
+ """Parse a float from an environment variable, returning default if invalid.
17
+
18
+ Returns the default value if the env var is missing, empty, not a valid
19
+ float, NaN, or infinity.
20
+ """
21
+ value = os.environ.get(name)
22
+ if value is None:
23
+ return default
24
+ try:
25
+ result = float(value)
26
+ if math.isnan(result) or math.isinf(result):
27
+ return default
28
+ return result
29
+ except (ValueError, TypeError):
30
+ return default
31
+
12
32
  GLOBAL_PROJECT = "Global"
13
33
  BT_IS_ASYNC_ATTRIBUTE = "_BT_IS_ASYNC"
14
34
 
braintrust/version.py CHANGED
@@ -1,4 +1,4 @@
1
- VERSION = "0.5.0"
1
+ VERSION = "0.5.3"
2
2
 
3
3
  # this will be templated during the build
4
- GIT_COMMIT = "617d9b730b37e96b7d05a099b95f5387944d0951"
4
+ GIT_COMMIT = "ce261c13723e8985e61819f6df56298f863155d0"
@@ -62,7 +62,6 @@ def setup_agno(
62
62
  models.base.Model = wrap_model(models.base.Model) # pyright: ignore[reportUnknownMemberType]
63
63
  tools.function.FunctionCall = wrap_function_call(tools.function.FunctionCall) # pyright: ignore[reportUnknownMemberType]
64
64
  return True
65
- except ImportError as e:
66
- logger.error(f"Failed to import Agno: {e}")
67
- logger.error("Agno is not installed. Please install it with: pip install agno")
65
+ except ImportError:
66
+ # Not installed - this is expected when using auto_instrument()
68
67
  return False
@@ -5,6 +5,7 @@ from contextlib import contextmanager
5
5
 
6
6
  from braintrust.logger import NOOP_SPAN, log_exc_info_to_span, start_span
7
7
  from braintrust.wrappers._anthropic_utils import Wrapper, extract_anthropic_usage, finalize_anthropic_tokens
8
+ from wrapt import wrap_function_wrapper
8
9
 
9
10
  log = logging.getLogger(__name__)
10
11
 
@@ -358,3 +359,66 @@ def wrap_anthropic(client):
358
359
 
359
360
  def wrap_anthropic_client(client):
360
361
  return wrap_anthropic(client)
362
+
363
+
364
+ def _apply_anthropic_wrapper(client):
365
+ """Apply tracing wrapper to an Anthropic client instance in-place."""
366
+ wrapped = wrap_anthropic(client)
367
+ client.messages = wrapped.messages
368
+ if hasattr(wrapped, "beta"):
369
+ client.beta = wrapped.beta
370
+
371
+
372
+ def _apply_async_anthropic_wrapper(client):
373
+ """Apply tracing wrapper to an AsyncAnthropic client instance in-place."""
374
+ wrapped = wrap_anthropic(client)
375
+ client.messages = wrapped.messages
376
+ if hasattr(wrapped, "beta"):
377
+ client.beta = wrapped.beta
378
+
379
+
380
+ def _anthropic_init_wrapper(wrapped, instance, args, kwargs):
381
+ """Wrapper for Anthropic.__init__ that applies tracing after initialization."""
382
+ wrapped(*args, **kwargs)
383
+ _apply_anthropic_wrapper(instance)
384
+
385
+
386
+ def _async_anthropic_init_wrapper(wrapped, instance, args, kwargs):
387
+ """Wrapper for AsyncAnthropic.__init__ that applies tracing after initialization."""
388
+ wrapped(*args, **kwargs)
389
+ _apply_async_anthropic_wrapper(instance)
390
+
391
+
392
+ def patch_anthropic() -> bool:
393
+ """
394
+ Patch Anthropic to add Braintrust tracing globally.
395
+
396
+ After calling this, all new Anthropic() and AsyncAnthropic() clients
397
+ will automatically have tracing enabled.
398
+
399
+ Returns:
400
+ True if Anthropic was patched (or already patched), False if Anthropic is not installed.
401
+
402
+ Example:
403
+ ```python
404
+ import braintrust
405
+ braintrust.patch_anthropic()
406
+
407
+ import anthropic
408
+ client = anthropic.Anthropic()
409
+ # All calls are now traced!
410
+ ```
411
+ """
412
+ try:
413
+ import anthropic
414
+
415
+ if getattr(anthropic, "__braintrust_wrapped__", False):
416
+ return True # Already patched
417
+
418
+ wrap_function_wrapper("anthropic", "Anthropic.__init__", _anthropic_init_wrapper)
419
+ wrap_function_wrapper("anthropic", "AsyncAnthropic.__init__", _async_anthropic_init_wrapper)
420
+ anthropic.__braintrust_wrapped__ = True
421
+ return True
422
+
423
+ except ImportError:
424
+ return False
@@ -105,7 +105,6 @@ def setup_claude_agent_sdk(
105
105
  setattr(module, "tool", wrapped_tool_fn)
106
106
 
107
107
  return True
108
- except ImportError as e:
109
- logger.error(f"Failed to import Claude Agent SDK: {e}")
110
- logger.error("claude-agent-sdk is not installed. Please install it with: pip install claude-agent-sdk")
108
+ except ImportError:
109
+ # Not installed - this is expected when using auto_instrument()
111
110
  return False
@@ -23,6 +23,7 @@ from braintrust.wrappers.claude_agent_sdk._wrapper import (
23
23
  _create_client_wrapper_class,
24
24
  _create_tool_wrapper_class,
25
25
  )
26
+ from braintrust.wrappers.test_utils import verify_autoinstrument_script
26
27
 
27
28
  PROJECT_NAME = "test-claude-agent-sdk"
28
29
  TEST_MODEL = "claude-haiku-4-5-20251001"
@@ -283,3 +284,11 @@ async def _multi_message_generator():
283
284
  """Generator yielding multiple messages."""
284
285
  yield _make_message("Part 1")
285
286
  yield _make_message("Part 2")
287
+
288
+
289
+ class TestAutoInstrumentClaudeAgentSDK:
290
+ """Tests for auto_instrument() with Claude Agent SDK."""
291
+
292
+ def test_auto_instrument_claude_agent_sdk(self):
293
+ """Test auto_instrument patches Claude Agent SDK and creates spans."""
294
+ verify_autoinstrument_script("test_auto_claude_agent_sdk.py")
@@ -51,6 +51,7 @@ from typing import Any
51
51
 
52
52
  from braintrust.logger import current_span, start_span
53
53
  from braintrust.span_types import SpanTypeAttribute
54
+ from wrapt import wrap_function_wrapper
54
55
 
55
56
  # Note: For detailed token and cost metrics, use patch_litellm() before importing DSPy.
56
57
  # The DSPy callback focuses on execution flow and span hierarchy.
@@ -60,6 +61,8 @@ try:
60
61
  except ImportError:
61
62
  raise ImportError("DSPy is not installed. Please install it with: pip install dspy")
62
63
 
64
+ __all__ = ["BraintrustDSpyCallback", "patch_dspy"]
65
+
63
66
 
64
67
  class BraintrustDSpyCallback(BaseCallback):
65
68
  """Callback handler that logs DSPy execution traces to Braintrust.
@@ -412,4 +415,52 @@ class BraintrustDSpyCallback(BaseCallback):
412
415
  span.end()
413
416
 
414
417
 
415
- __all__ = ["BraintrustDSpyCallback"]
418
+ def _configure_wrapper(wrapped, instance, args, kwargs):
419
+ """Wrapper for dspy.configure that auto-adds BraintrustDSpyCallback."""
420
+ callbacks = kwargs.get("callbacks")
421
+ if callbacks is None:
422
+ callbacks = []
423
+ else:
424
+ callbacks = list(callbacks)
425
+
426
+ # Check if already has Braintrust callback
427
+ has_bt_callback = any(isinstance(cb, BraintrustDSpyCallback) for cb in callbacks)
428
+ if not has_bt_callback:
429
+ callbacks.append(BraintrustDSpyCallback())
430
+
431
+ kwargs["callbacks"] = callbacks
432
+ return wrapped(*args, **kwargs)
433
+
434
+
435
+ def patch_dspy() -> bool:
436
+ """
437
+ Patch DSPy to automatically add Braintrust tracing callback.
438
+
439
+ After calling this, all calls to dspy.configure() will automatically
440
+ include the BraintrustDSpyCallback.
441
+
442
+ Returns:
443
+ True if DSPy was patched (or already patched), False if DSPy is not installed.
444
+
445
+ Example:
446
+ ```python
447
+ import braintrust
448
+ braintrust.patch_dspy()
449
+
450
+ import dspy
451
+ lm = dspy.LM("openai/gpt-4o-mini")
452
+ dspy.configure(lm=lm) # BraintrustDSpyCallback auto-added!
453
+ ```
454
+ """
455
+ try:
456
+ import dspy
457
+
458
+ if getattr(dspy, "__braintrust_wrapped__", False):
459
+ return True # Already patched
460
+
461
+ wrap_function_wrapper("dspy", "configure", _configure_wrapper)
462
+ dspy.__braintrust_wrapped__ = True
463
+ return True
464
+
465
+ except ImportError:
466
+ return False
@@ -15,7 +15,13 @@ def setup_genai(
15
15
  api_key: str | None = None,
16
16
  project_id: str | None = None,
17
17
  project_name: str | None = None,
18
- ):
18
+ ) -> bool:
19
+ """
20
+ Setup Braintrust integration with Google GenAI.
21
+
22
+ Returns:
23
+ True if setup was successful, False if google-genai is not installed.
24
+ """
19
25
  span = current_span()
20
26
  if span == NOOP_SPAN:
21
27
  init_logger(project=project_name, api_key=api_key, project_id=project_id)
@@ -27,11 +33,8 @@ def setup_genai(
27
33
  genai.Client = wrap_client(genai.Client)
28
34
  models.Models = wrap_models(models.Models)
29
35
  models.AsyncModels = wrap_async_models(models.AsyncModels)
30
- pass
31
- except ImportError as e:
32
- logger.error(
33
- f"Failed to import Google ADK agents: {e}. Google ADK is not installed. Please install it with: pip install google-adk"
34
- )
36
+ return True
37
+ except ImportError:
35
38
  return False
36
39
 
37
40
 
@@ -631,13 +631,16 @@ def serialize_response_format(response_format: Any) -> Any:
631
631
  return response_format
632
632
 
633
633
 
634
- def patch_litellm():
634
+ def patch_litellm() -> bool:
635
635
  """
636
636
  Patch LiteLLM to add Braintrust tracing.
637
637
 
638
638
  This wraps litellm.completion and litellm.acompletion to automatically
639
639
  create Braintrust spans with detailed token metrics, timing, and costs.
640
640
 
641
+ Returns:
642
+ True if LiteLLM was patched (or already patched), False if LiteLLM is not installed.
643
+
641
644
  Example:
642
645
  ```python
643
646
  import braintrust
@@ -657,52 +660,12 @@ def patch_litellm():
657
660
  import litellm
658
661
 
659
662
  if not hasattr(litellm, "_braintrust_wrapped"):
660
- # Store originals for unpatch_litellm()
661
- litellm._braintrust_original_completion = litellm.completion
662
- litellm._braintrust_original_acompletion = litellm.acompletion
663
- litellm._braintrust_original_responses = litellm.responses
664
- litellm._braintrust_original_aresponses = litellm.aresponses
665
-
666
663
  wrapped = wrap_litellm(litellm)
667
664
  litellm.completion = wrapped.completion
668
665
  litellm.acompletion = wrapped.acompletion
669
666
  litellm.responses = wrapped.responses
670
667
  litellm.aresponses = wrapped.aresponses
671
668
  litellm._braintrust_wrapped = True
669
+ return True
672
670
  except ImportError:
673
- pass # litellm not available
674
-
675
-
676
- def unpatch_litellm():
677
- """
678
- Restore LiteLLM to its original state, removing Braintrust tracing.
679
-
680
- This undoes the patching done by patch_litellm(), restoring the original
681
- completion, acompletion, responses, and aresponses functions.
682
-
683
- Example:
684
- ```python
685
- import braintrust
686
- braintrust.patch_litellm()
687
-
688
- # ... use litellm with tracing ...
689
-
690
- braintrust.unpatch_litellm() # restore original behavior
691
- ```
692
- """
693
- try:
694
- import litellm
695
-
696
- if hasattr(litellm, "_braintrust_wrapped"):
697
- litellm.completion = litellm._braintrust_original_completion
698
- litellm.acompletion = litellm._braintrust_original_acompletion
699
- litellm.responses = litellm._braintrust_original_responses
700
- litellm.aresponses = litellm._braintrust_original_aresponses
701
-
702
- delattr(litellm, "_braintrust_wrapped")
703
- delattr(litellm, "_braintrust_original_completion")
704
- delattr(litellm, "_braintrust_original_acompletion")
705
- delattr(litellm, "_braintrust_original_responses")
706
- delattr(litellm, "_braintrust_original_aresponses")
707
- except ImportError:
708
- pass # litellm not available
671
+ return False
@@ -51,9 +51,8 @@ def setup_pydantic_ai(
51
51
  wrap_model_classes()
52
52
 
53
53
  return True
54
- except ImportError as e:
55
- logger.error(f"Failed to import Pydantic AI: {e}")
56
- logger.error("Pydantic AI is not installed. Please install it with: pip install pydantic-ai-slim")
54
+ except ImportError:
55
+ # Not installed - this is expected when using auto_instrument()
57
56
  return False
58
57
 
59
58
 
@@ -8,6 +8,7 @@ import pytest
8
8
  from braintrust import logger
9
9
  from braintrust.test_helpers import init_test_logger
10
10
  from braintrust.wrappers.agno import setup_agno
11
+ from braintrust.wrappers.test_utils import verify_autoinstrument_script
11
12
 
12
13
  TEST_ORG_ID = "test-org-123"
13
14
  PROJECT_NAME = "test-agno-app"
@@ -94,3 +95,11 @@ def test_agno_simple_agent_execution(memory_logger):
94
95
  assert llm_span["metrics"]["prompt_tokens"] == 38
95
96
  assert llm_span["metrics"]["completion_tokens"] == 4
96
97
  assert llm_span["metrics"]["tokens"] == 42
98
+
99
+
100
+ class TestAutoInstrumentAgno:
101
+ """Tests for auto_instrument() with Agno."""
102
+
103
+ def test_auto_instrument_agno(self):
104
+ """Test auto_instrument patches Agno and creates spans."""
105
+ verify_autoinstrument_script("test_auto_agno.py")