-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathtest_braintrust_adapter.py
More file actions
373 lines (297 loc) · 13.7 KB
/
test_braintrust_adapter.py
File metadata and controls
373 lines (297 loc) · 13.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
import os
from types import SimpleNamespace
from typing import Any, Dict, List
from unittest.mock import Mock
import pytest
import requests
from eval_protocol.adapters.braintrust import BraintrustAdapter
from eval_protocol.models import Message
class MockResponse:
"""Mock response object for requests.post"""
def __init__(self, json_data: Dict[str, Any], status_code: int = 200):
self.json_data = json_data
self.status_code = status_code
def json(self) -> Dict[str, Any]:
return self.json_data
def raise_for_status(self) -> None:
if self.status_code >= 400:
raise requests.HTTPError(f"HTTP {self.status_code}")
@pytest.fixture
def mock_requests_post(monkeypatch):
"""Mock requests.post to return sample data"""
def fake_post(url: str, headers=None, json=None):
# Return a simplified response for basic tests
return MockResponse(
{
"data": [
{
"id": "trace1",
"input": [{"role": "user", "content": "Hello"}],
"output": [{"message": {"role": "assistant", "content": "Hi there!"}}],
}
]
}
)
monkeypatch.setattr("requests.post", fake_post)
return fake_post
def test_basic_btql_query_returns_evaluation_rows(mock_requests_post):
"""Test basic BTQL query execution and conversion to evaluation rows"""
adapter = BraintrustAdapter(api_key="test_key", project_id="test_project")
btql_query = "select: * from: project_logs('test_project') traces limit: 1"
rows = adapter.get_evaluation_rows(btql_query)
assert len(rows) == 1
assert len(rows[0].messages) == 2
assert rows[0].messages[0].role == "user"
assert rows[0].messages[0].content == "Hello"
assert rows[0].messages[1].role == "assistant"
assert rows[0].messages[1].content == "Hi there!"
def test_trace_with_tool_calls_preserved(monkeypatch):
"""Test that tool calls are properly preserved in converted messages"""
def mock_post(url: str, headers=None, json=None):
return MockResponse(
{
"data": [
{
"id": "trace_with_tools",
"input": [{"role": "user", "content": "Get reservation details for 7KJ2PL"}],
"output": [
{
"message": {
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_123",
"type": "function",
"function": {
"name": "get_reservation_details",
"arguments": '{"reservation_id": "7KJ2PL"}',
},
}
],
}
}
],
}
]
}
)
monkeypatch.setattr("requests.post", mock_post)
adapter = BraintrustAdapter(api_key="test_key", project_id="test_project")
rows = adapter.get_evaluation_rows("test query")
assert len(rows) == 1
msgs = rows[0].messages
# Find assistant message with tool calls
assistant_msgs = [m for m in msgs if m.role == "assistant" and m.tool_calls]
assert len(assistant_msgs) == 1
assert assistant_msgs[0].tool_calls is not None
tool_call = assistant_msgs[0].tool_calls[0]
assert tool_call.id == "call_123"
assert tool_call.function.name == "get_reservation_details"
assert '{"reservation_id": "7KJ2PL"}' in tool_call.function.arguments
def test_trace_with_tool_response_messages(monkeypatch):
"""Test that tool response messages are properly handled"""
def mock_post(url: str, headers=None, json=None):
return MockResponse(
{
"data": [
{
"id": "trace_with_tool_response",
"input": [
{"role": "user", "content": "Check reservation"},
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_456",
"type": "function",
"function": {
"name": "get_reservation_details",
"arguments": '{"reservation_id": "ABC123"}',
},
}
],
},
{
"role": "tool",
"tool_call_id": "call_456",
"content": '{"reservation_id": "ABC123", "status": "confirmed"}',
},
],
"output": [
{"message": {"role": "assistant", "content": "Your reservation ABC123 is confirmed."}}
],
}
]
}
)
monkeypatch.setattr("requests.post", mock_post)
adapter = BraintrustAdapter(api_key="test_key", project_id="test_project")
rows = adapter.get_evaluation_rows("test query")
assert len(rows) == 1
msgs = rows[0].messages
# Should have user, assistant with tool_calls, tool response, and final assistant
roles = [m.role for m in msgs]
assert "user" in roles
assert "tool" in roles
assert roles.count("assistant") == 2 # One with tool_calls, one final response
# Check tool message
tool_msgs = [m for m in msgs if m.role == "tool"]
assert len(tool_msgs) == 1
assert tool_msgs[0].tool_call_id == "call_456"
assert tool_msgs[0].content is not None
assert "ABC123" in tool_msgs[0].content
def test_tools_extracted_from_metadata_variants(monkeypatch):
"""Test that tools are extracted from different metadata locations"""
def mock_post_with_tools_in_metadata(url: str, headers=None, json=None):
return MockResponse(
{
"data": [
{
"id": "trace_with_metadata_tools",
"input": [{"role": "user", "content": "Test"}],
"output": [{"message": {"role": "assistant", "content": "Response"}}],
"metadata": {
"tools": [
{
"type": "function",
"function": {"name": "get_weather", "description": "Get weather info"},
}
]
},
}
]
}
)
monkeypatch.setattr("requests.post", mock_post_with_tools_in_metadata)
adapter = BraintrustAdapter(api_key="test_key", project_id="test_project")
rows = adapter.get_evaluation_rows("test query")
assert len(rows) == 1
assert rows[0].tools is not None
assert len(rows[0].tools) == 1
assert rows[0].tools[0]["function"]["name"] == "get_weather"
def test_tools_extracted_from_hidden_params(monkeypatch):
"""Test that tools are extracted from nested hidden_params location"""
def mock_post_with_hidden_tools(url: str, headers=None, json=None):
return MockResponse(
{
"data": [
{
"id": "trace_with_hidden_tools",
"input": [{"role": "user", "content": "Test"}],
"output": [{"message": {"role": "assistant", "content": "Response"}}],
"metadata": {
"hidden_params": {
"optional_params": {
"tools": [
{
"type": "function",
"function": {
"name": "transfer_to_human_agents",
"description": "Transfer to human",
},
}
]
}
}
},
}
]
}
)
monkeypatch.setattr("requests.post", mock_post_with_hidden_tools)
adapter = BraintrustAdapter(api_key="test_key", project_id="test_project")
rows = adapter.get_evaluation_rows("test query")
assert len(rows) == 1
assert rows[0].tools is not None
assert len(rows[0].tools) == 1
assert rows[0].tools[0]["function"]["name"] == "transfer_to_human_agents"
def test_empty_btql_response_returns_empty_list(monkeypatch):
"""Test that empty BTQL response returns empty list"""
def mock_empty_post(url: str, headers=None, json=None):
return MockResponse({"data": []})
monkeypatch.setattr("requests.post", mock_empty_post)
adapter = BraintrustAdapter(api_key="test_key", project_id="test_project")
rows = adapter.get_evaluation_rows("test query")
assert len(rows) == 0
def test_trace_without_meaningful_conversation_skipped(monkeypatch):
"""Test that traces without input or output are skipped"""
def mock_post_incomplete_trace(url: str, headers=None, json=None):
return MockResponse(
{
"data": [
{"id": "incomplete_trace", "input": None, "output": []},
{
"id": "valid_trace",
"input": [{"role": "user", "content": "Hello"}],
"output": [{"message": {"role": "assistant", "content": "Hi"}}],
},
]
}
)
monkeypatch.setattr("requests.post", mock_post_incomplete_trace)
adapter = BraintrustAdapter(api_key="test_key", project_id="test_project")
rows = adapter.get_evaluation_rows("test query")
# Should only get the valid trace
assert len(rows) == 1
assert rows[0].input_metadata is not None
assert rows[0].input_metadata.session_data is not None
assert rows[0].input_metadata.session_data["braintrust_trace_id"] == "valid_trace"
def test_custom_converter_used_when_provided(monkeypatch):
"""Test that custom converter is used when provided"""
def mock_post(url: str, headers=None, json=None):
return MockResponse(
{
"data": [
{
"id": "custom_trace",
"input": [{"role": "user", "content": "Test"}],
"output": [{"message": {"role": "assistant", "content": "Response"}}],
}
]
}
)
monkeypatch.setattr("requests.post", mock_post)
def custom_converter(trace: Dict[str, Any], include_tool_calls: bool):
# Custom converter that adds a special message
from eval_protocol.models import EvaluationRow, InputMetadata
return EvaluationRow(
messages=[Message(role="system", content="Custom converted message")],
input_metadata=InputMetadata(session_data={"custom": True}),
)
adapter = BraintrustAdapter(api_key="test_key", project_id="test_project")
rows = adapter.get_evaluation_rows("test query", converter=custom_converter)
assert len(rows) == 1
assert rows[0].messages[0].role == "system"
assert rows[0].messages[0].content == "Custom converted message"
assert rows[0].input_metadata is not None
assert rows[0].input_metadata.session_data is not None
assert rows[0].input_metadata.session_data["custom"] is True
def test_api_authentication_error_handling(monkeypatch):
"""Test that API authentication errors are handled properly"""
def mock_auth_error(url: str, headers=None, json=None):
return MockResponse({}, status_code=401)
monkeypatch.setattr("requests.post", mock_auth_error)
adapter = BraintrustAdapter(api_key="invalid_key", project_id="test_project")
with pytest.raises(requests.HTTPError):
adapter.get_evaluation_rows("test query")
def test_session_data_includes_trace_id(mock_requests_post):
"""Test that session_data includes the Braintrust trace ID"""
adapter = BraintrustAdapter(api_key="test_key", project_id="test_project")
rows = adapter.get_evaluation_rows("test query")
assert len(rows) == 1
assert rows[0].input_metadata is not None
assert rows[0].input_metadata.session_data is not None
assert rows[0].input_metadata.session_data["braintrust_trace_id"] == "trace1"
def test_missing_required_env_vars(monkeypatch):
"""Test that missing required environment variables raise errors"""
# Mock environment variables to be None
monkeypatch.setenv("BRAINTRUST_API_KEY", "")
monkeypatch.setenv("BRAINTRUST_PROJECT_ID", "")
# Test missing API key
with pytest.raises(ValueError, match="BRAINTRUST_API_KEY"):
BraintrustAdapter(api_key=None, project_id="test_project")
# Test missing project ID
with pytest.raises(ValueError, match="BRAINTRUST_PROJECT_ID"):
BraintrustAdapter(api_key="test_key", project_id=None)