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
@@ -9,6 +9,7 @@ import pytest
9
9
  from braintrust import logger
10
10
  from braintrust.test_helpers import init_test_logger
11
11
  from braintrust.wrappers.anthropic import wrap_anthropic
12
+ from braintrust.wrappers.test_utils import run_in_subprocess, verify_autoinstrument_script
12
13
 
13
14
  TEST_ORG_ID = "test-org-123"
14
15
  PROJECT_NAME = "test-anthropic-app"
@@ -481,3 +482,158 @@ async def test_anthropic_beta_messages_streaming_async(memory_logger):
481
482
  assert metrics["prompt_tokens"] == usage.input_tokens
482
483
  assert metrics["completion_tokens"] == usage.output_tokens
483
484
  assert metrics["tokens"] == usage.input_tokens + usage.output_tokens
485
+
486
+
487
+ class TestPatchAnthropic:
488
+ """Tests for patch_anthropic() / unpatch_anthropic()."""
489
+
490
+ def test_patch_anthropic_sets_wrapped_flag(self):
491
+ """patch_anthropic() should set __braintrust_wrapped__ on anthropic module."""
492
+ result = run_in_subprocess("""
493
+ from braintrust.wrappers.anthropic import patch_anthropic
494
+ import anthropic
495
+
496
+ assert not hasattr(anthropic, "__braintrust_wrapped__")
497
+ patch_anthropic()
498
+ assert hasattr(anthropic, "__braintrust_wrapped__")
499
+ print("SUCCESS")
500
+ """)
501
+ assert result.returncode == 0, f"Failed: {result.stderr}"
502
+ assert "SUCCESS" in result.stdout
503
+
504
+ def test_patch_anthropic_wraps_new_clients(self):
505
+ """After patch_anthropic(), new Anthropic() clients should be wrapped."""
506
+ result = run_in_subprocess("""
507
+ from braintrust.wrappers.anthropic import patch_anthropic
508
+ patch_anthropic()
509
+
510
+ import anthropic
511
+ client = anthropic.Anthropic(api_key="test-key")
512
+
513
+ # Check that messages is wrapped
514
+ messages_type = type(client.messages).__name__
515
+ print(f"messages_type={messages_type}")
516
+ print("SUCCESS")
517
+ """)
518
+ assert result.returncode == 0, f"Failed: {result.stderr}"
519
+ assert "SUCCESS" in result.stdout
520
+
521
+ def test_patch_anthropic_idempotent(self):
522
+ """Multiple patch_anthropic() calls should be safe."""
523
+ result = run_in_subprocess("""
524
+ from braintrust.wrappers.anthropic import patch_anthropic
525
+ import anthropic
526
+
527
+ patch_anthropic()
528
+ first_class = anthropic.Anthropic
529
+
530
+ patch_anthropic() # Second call
531
+ second_class = anthropic.Anthropic
532
+
533
+ assert first_class is second_class
534
+ print("SUCCESS")
535
+ """)
536
+ assert result.returncode == 0, f"Failed: {result.stderr}"
537
+ assert "SUCCESS" in result.stdout
538
+
539
+ def test_patch_anthropic_creates_spans(self):
540
+ """patch_anthropic() should create spans when making API calls."""
541
+ result = run_in_subprocess("""
542
+ from braintrust.wrappers.anthropic import patch_anthropic
543
+ from braintrust.test_helpers import init_test_logger
544
+ from braintrust import logger
545
+
546
+ # Set up memory logger
547
+ init_test_logger("test-auto")
548
+ with logger._internal_with_memory_background_logger() as memory_logger:
549
+ patch_anthropic()
550
+
551
+ import anthropic
552
+ client = anthropic.Anthropic()
553
+
554
+ # Make a call within a span context
555
+ import braintrust
556
+ with braintrust.start_span(name="test") as span:
557
+ try:
558
+ # This will fail without API key, but span should still be created
559
+ client.messages.create(
560
+ model="claude-3-5-haiku-latest",
561
+ max_tokens=100,
562
+ messages=[{"role": "user", "content": "hi"}],
563
+ )
564
+ except Exception:
565
+ pass # Expected without API key
566
+
567
+ # Check that spans were logged
568
+ spans = memory_logger.pop()
569
+ # Should have at least the parent span
570
+ assert len(spans) >= 1, f"Expected spans, got {spans}"
571
+ print("SUCCESS")
572
+ """)
573
+ assert result.returncode == 0, f"Failed: {result.stderr}"
574
+ assert "SUCCESS" in result.stdout
575
+
576
+
577
+ class TestPatchAnthropicSpans:
578
+ """VCR-based tests verifying that patch_anthropic() produces spans."""
579
+
580
+ @pytest.mark.vcr
581
+ def test_patch_anthropic_creates_spans(self, memory_logger):
582
+ """patch_anthropic() should create spans when making API calls."""
583
+ from braintrust.wrappers.anthropic import patch_anthropic
584
+
585
+ assert not memory_logger.pop()
586
+
587
+ patch_anthropic()
588
+ client = anthropic.Anthropic()
589
+ response = client.messages.create(
590
+ model="claude-3-5-haiku-latest",
591
+ max_tokens=100,
592
+ messages=[{"role": "user", "content": "Say hi"}],
593
+ )
594
+ assert response.content[0].text
595
+
596
+ # Verify span was created
597
+ spans = memory_logger.pop()
598
+ assert len(spans) == 1
599
+ span = spans[0]
600
+ assert span["metadata"]["provider"] == "anthropic"
601
+ assert "claude" in span["metadata"]["model"]
602
+ assert span["input"]
603
+
604
+
605
+ class TestPatchAnthropicAsyncSpans:
606
+ """VCR-based tests verifying that patch_anthropic() produces spans for async clients."""
607
+
608
+ @pytest.mark.vcr
609
+ @pytest.mark.asyncio
610
+ async def test_patch_anthropic_async_creates_spans(self, memory_logger):
611
+ """patch_anthropic() should create spans for async API calls."""
612
+ from braintrust.wrappers.anthropic import patch_anthropic
613
+
614
+ assert not memory_logger.pop()
615
+
616
+ patch_anthropic()
617
+ client = anthropic.AsyncAnthropic()
618
+ response = await client.messages.create(
619
+ model="claude-3-5-haiku-latest",
620
+ max_tokens=100,
621
+ messages=[{"role": "user", "content": "Say hi async"}],
622
+ )
623
+ assert response.content[0].text
624
+
625
+ # Verify span was created
626
+ spans = memory_logger.pop()
627
+ assert len(spans) == 1
628
+ span = spans[0]
629
+ assert span["metadata"]["provider"] == "anthropic"
630
+ assert "claude" in span["metadata"]["model"]
631
+ assert span["input"]
632
+
633
+
634
+ class TestAutoInstrumentAnthropic:
635
+ """Tests for auto_instrument() with Anthropic."""
636
+
637
+ def test_auto_instrument_anthropic(self):
638
+ """Test auto_instrument patches Anthropic, creates spans, and uninstrument works."""
639
+ verify_autoinstrument_script("test_auto_anthropic.py")
@@ -7,6 +7,7 @@ import pytest
7
7
  from braintrust import logger
8
8
  from braintrust.test_helpers import init_test_logger
9
9
  from braintrust.wrappers.dspy import BraintrustDSpyCallback
10
+ from braintrust.wrappers.test_utils import run_in_subprocess, verify_autoinstrument_script
10
11
 
11
12
  PROJECT_NAME = "test-dspy-app"
12
13
  MODEL = "openai/gpt-4o-mini"
@@ -58,3 +59,119 @@ def test_dspy_callback(memory_logger):
58
59
 
59
60
  # Verify span parenting (LM span should have parent)
60
61
  assert lm_span.get("span_parents") # LM span should have parent
62
+
63
+
64
+ class TestPatchDSPy:
65
+ """Tests for patch_dspy() / unpatch_dspy()."""
66
+
67
+ def test_patch_dspy_sets_wrapped_flag(self):
68
+ """patch_dspy() should set __braintrust_wrapped__ on dspy module."""
69
+ result = run_in_subprocess("""
70
+ dspy = __import__("dspy")
71
+ from braintrust.wrappers.dspy import patch_dspy
72
+
73
+ assert not hasattr(dspy, "__braintrust_wrapped__")
74
+ patch_dspy()
75
+ assert hasattr(dspy, "__braintrust_wrapped__")
76
+ print("SUCCESS")
77
+ """)
78
+ assert result.returncode == 0, f"Failed: {result.stderr}"
79
+ assert "SUCCESS" in result.stdout
80
+
81
+ def test_patch_dspy_wraps_configure(self):
82
+ """After patch_dspy(), dspy.configure() should auto-add BraintrustDSpyCallback."""
83
+ result = run_in_subprocess("""
84
+ from braintrust.wrappers.dspy import patch_dspy, BraintrustDSpyCallback
85
+ patch_dspy()
86
+
87
+ import dspy
88
+
89
+ # Configure without explicitly adding callback
90
+ dspy.configure(lm=None)
91
+
92
+ # Check that BraintrustDSpyCallback was auto-added
93
+ from dspy.dsp.utils.settings import settings
94
+ callbacks = settings.callbacks
95
+ has_bt_callback = any(isinstance(cb, BraintrustDSpyCallback) for cb in callbacks)
96
+ assert has_bt_callback, f"Expected BraintrustDSpyCallback in {callbacks}"
97
+ print("SUCCESS")
98
+ """)
99
+ assert result.returncode == 0, f"Failed: {result.stderr}"
100
+ assert "SUCCESS" in result.stdout
101
+
102
+ def test_patch_dspy_preserves_existing_callbacks(self):
103
+ """patch_dspy() should preserve user-provided callbacks."""
104
+ result = run_in_subprocess("""
105
+ from braintrust.wrappers.dspy import patch_dspy, BraintrustDSpyCallback
106
+ patch_dspy()
107
+
108
+ import dspy
109
+ from dspy.utils.callback import BaseCallback
110
+
111
+ class MyCallback(BaseCallback):
112
+ pass
113
+
114
+ my_callback = MyCallback()
115
+ dspy.configure(lm=None, callbacks=[my_callback])
116
+
117
+ from dspy.dsp.utils.settings import settings
118
+ callbacks = settings.callbacks
119
+
120
+ # Should have both callbacks
121
+ has_my_callback = any(cb is my_callback for cb in callbacks)
122
+ has_bt_callback = any(isinstance(cb, BraintrustDSpyCallback) for cb in callbacks)
123
+
124
+ assert has_my_callback, "User callback should be preserved"
125
+ assert has_bt_callback, "BraintrustDSpyCallback should be added"
126
+ print("SUCCESS")
127
+ """)
128
+ assert result.returncode == 0, f"Failed: {result.stderr}"
129
+ assert "SUCCESS" in result.stdout
130
+
131
+ def test_patch_dspy_does_not_duplicate_callback(self):
132
+ """patch_dspy() should not add duplicate BraintrustDSpyCallback."""
133
+ result = run_in_subprocess("""
134
+ from braintrust.wrappers.dspy import patch_dspy, BraintrustDSpyCallback
135
+ patch_dspy()
136
+
137
+ import dspy
138
+
139
+ # User explicitly adds BraintrustDSpyCallback
140
+ bt_callback = BraintrustDSpyCallback()
141
+ dspy.configure(lm=None, callbacks=[bt_callback])
142
+
143
+ from dspy.dsp.utils.settings import settings
144
+ callbacks = settings.callbacks
145
+
146
+ # Should only have one BraintrustDSpyCallback
147
+ bt_callbacks = [cb for cb in callbacks if isinstance(cb, BraintrustDSpyCallback)]
148
+ assert len(bt_callbacks) == 1, f"Expected 1 BraintrustDSpyCallback, got {len(bt_callbacks)}"
149
+ print("SUCCESS")
150
+ """)
151
+ assert result.returncode == 0, f"Failed: {result.stderr}"
152
+ assert "SUCCESS" in result.stdout
153
+
154
+ def test_patch_dspy_idempotent(self):
155
+ """Multiple patch_dspy() calls should be safe."""
156
+ result = run_in_subprocess("""
157
+ from braintrust.wrappers.dspy import patch_dspy
158
+ import dspy
159
+
160
+ patch_dspy()
161
+ patch_dspy() # Second call - should be no-op, not double-wrap
162
+
163
+ # Verify configure still works
164
+ lm = dspy.LM("openai/gpt-4o-mini")
165
+ dspy.configure(lm=lm)
166
+ print("SUCCESS")
167
+ """)
168
+ assert result.returncode == 0, f"Failed: {result.stderr}"
169
+ assert "SUCCESS" in result.stdout
170
+
171
+
172
+ class TestAutoInstrumentDSPy:
173
+ """Tests for auto_instrument() with DSPy."""
174
+
175
+ def test_auto_instrument_dspy(self):
176
+ """Test auto_instrument patches DSPy, creates spans, and uninstrument works."""
177
+ verify_autoinstrument_script("test_auto_dspy.py")
@@ -6,6 +6,7 @@ import pytest
6
6
  from braintrust import logger
7
7
  from braintrust.test_helpers import init_test_logger
8
8
  from braintrust.wrappers.google_genai import setup_genai
9
+ from braintrust.wrappers.test_utils import verify_autoinstrument_script
9
10
  from google.genai import types
10
11
  from google.genai.client import Client
11
12
 
@@ -637,3 +638,11 @@ def test_attachment_with_pydantic_model(memory_logger):
637
638
 
638
639
  # Attachment should be preserved
639
640
  assert copied["context_file"] is attachment
641
+
642
+
643
+ class TestAutoInstrumentGoogleGenAI:
644
+ """Tests for auto_instrument() with Google GenAI."""
645
+
646
+ def test_auto_instrument_google_genai(self):
647
+ """Test auto_instrument patches Google GenAI and creates spans."""
648
+ verify_autoinstrument_script("test_auto_google_genai.py")
@@ -6,7 +6,7 @@ import pytest
6
6
  from braintrust import logger
7
7
  from braintrust.test_helpers import assert_dict_matches, init_test_logger
8
8
  from braintrust.wrappers.litellm import wrap_litellm
9
- from braintrust.wrappers.test_utils import assert_metrics_are_valid
9
+ from braintrust.wrappers.test_utils import assert_metrics_are_valid, verify_autoinstrument_script
10
10
 
11
11
  TEST_ORG_ID = "test-org-litellm-py-tracing"
12
12
  PROJECT_NAME = "test-project-litellm-py-tracing"
@@ -697,71 +697,73 @@ async def test_litellm_async_streaming_with_break(memory_logger):
697
697
  @pytest.mark.vcr
698
698
  def test_patch_litellm_responses(memory_logger):
699
699
  """Test that patch_litellm() patches responses."""
700
- from braintrust.wrappers.litellm import patch_litellm, unpatch_litellm
700
+ from braintrust.wrappers.litellm import patch_litellm
701
701
 
702
702
  assert not memory_logger.pop()
703
703
 
704
704
  patch_litellm()
705
- try:
706
- start = time.time()
707
- # Call litellm.responses directly (not wrapped_litellm.responses)
708
- response = litellm.responses(
709
- model=TEST_MODEL,
710
- input=TEST_PROMPT,
711
- instructions="Just the number please",
712
- )
713
- end = time.time()
714
-
715
- assert response
716
- assert response.output
717
- assert len(response.output) > 0
718
- content = response.output[0].content[0].text
719
- assert "24" in content or "twenty-four" in content.lower()
720
-
721
- # Verify span was created
722
- spans = memory_logger.pop()
723
- assert len(spans) == 1
724
- span = spans[0]
725
- assert_metrics_are_valid(span["metrics"], start, end)
726
- assert span["metadata"]["model"] == TEST_MODEL
727
- assert span["metadata"]["provider"] == "litellm"
728
- assert TEST_PROMPT in str(span["input"])
729
- finally:
730
- unpatch_litellm()
705
+ start = time.time()
706
+ # Call litellm.responses directly (not wrapped_litellm.responses)
707
+ response = litellm.responses(
708
+ model=TEST_MODEL,
709
+ input=TEST_PROMPT,
710
+ instructions="Just the number please",
711
+ )
712
+ end = time.time()
713
+
714
+ assert response
715
+ assert response.output
716
+ assert len(response.output) > 0
717
+ content = response.output[0].content[0].text
718
+ assert "24" in content or "twenty-four" in content.lower()
719
+
720
+ # Verify span was created
721
+ spans = memory_logger.pop()
722
+ assert len(spans) == 1
723
+ span = spans[0]
724
+ assert_metrics_are_valid(span["metrics"], start, end)
725
+ assert span["metadata"]["model"] == TEST_MODEL
726
+ assert span["metadata"]["provider"] == "litellm"
727
+ assert TEST_PROMPT in str(span["input"])
731
728
 
732
729
 
733
730
  @pytest.mark.vcr
734
731
  @pytest.mark.asyncio
735
732
  async def test_patch_litellm_aresponses(memory_logger):
736
733
  """Test that patch_litellm() patches aresponses."""
737
- from braintrust.wrappers.litellm import patch_litellm, unpatch_litellm
734
+ from braintrust.wrappers.litellm import patch_litellm
738
735
 
739
736
  assert not memory_logger.pop()
740
737
 
741
738
  patch_litellm()
742
- try:
743
- start = time.time()
744
- # Call litellm.aresponses directly (not wrapped_litellm.aresponses)
745
- response = await litellm.aresponses(
746
- model=TEST_MODEL,
747
- input=TEST_PROMPT,
748
- instructions="Just the number please",
749
- )
750
- end = time.time()
751
-
752
- assert response
753
- assert response.output
754
- assert len(response.output) > 0
755
- content = response.output[0].content[0].text
756
- assert "24" in content or "twenty-four" in content.lower()
757
-
758
- # Verify span was created
759
- spans = memory_logger.pop()
760
- assert len(spans) == 1
761
- span = spans[0]
762
- assert_metrics_are_valid(span["metrics"], start, end)
763
- assert span["metadata"]["model"] == TEST_MODEL
764
- assert span["metadata"]["provider"] == "litellm"
765
- assert TEST_PROMPT in str(span["input"])
766
- finally:
767
- unpatch_litellm()
739
+ start = time.time()
740
+ # Call litellm.aresponses directly (not wrapped_litellm.aresponses)
741
+ response = await litellm.aresponses(
742
+ model=TEST_MODEL,
743
+ input=TEST_PROMPT,
744
+ instructions="Just the number please",
745
+ )
746
+ end = time.time()
747
+
748
+ assert response
749
+ assert response.output
750
+ assert len(response.output) > 0
751
+ content = response.output[0].content[0].text
752
+ assert "24" in content or "twenty-four" in content.lower()
753
+
754
+ # Verify span was created
755
+ spans = memory_logger.pop()
756
+ assert len(spans) == 1
757
+ span = spans[0]
758
+ assert_metrics_are_valid(span["metrics"], start, end)
759
+ assert span["metadata"]["model"] == TEST_MODEL
760
+ assert span["metadata"]["provider"] == "litellm"
761
+ assert TEST_PROMPT in str(span["input"])
762
+
763
+
764
+ class TestAutoInstrumentLiteLLM:
765
+ """Tests for auto_instrument() with LiteLLM."""
766
+
767
+ def test_auto_instrument_litellm(self):
768
+ """Test auto_instrument patches LiteLLM, creates spans, and uninstrument works."""
769
+ verify_autoinstrument_script("test_auto_litellm.py")