Skip to content

Commit 5fd75c7

Browse files
committed
feat(llma): add more tests
1 parent 0c61c79 commit 5fd75c7

File tree

5 files changed

+599
-1
lines changed

5 files changed

+599
-1
lines changed

posthog/ai/anthropic/anthropic_converter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ def extract_anthropic_web_search_count(response: Any) -> int:
184184
server_tool_use = response.usage.server_tool_use
185185

186186
if hasattr(server_tool_use, "web_search_requests"):
187-
return int(getattr(server_tool_use, "web_search_requests", 0))
187+
return max(0, int(getattr(server_tool_use, "web_search_requests", 0)))
188188

189189
return 0
190190

posthog/ai/openai/openai_converter.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,8 @@ def extract_openai_web_search_count(response: Any) -> int:
279279
if hasattr(item, "type") and item.type == "web_search_call":
280280
web_search_count += 1
281281

282+
web_search_count = max(0, web_search_count)
283+
282284
if web_search_count > 0:
283285
return web_search_count
284286

posthog/test/ai/anthropic/test_anthropic.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,3 +1077,189 @@ def __init__(self):
10771077
assert props["$ai_web_search_count"] == 3
10781078
assert props["$ai_input_tokens"] == 100
10791079
assert props["$ai_output_tokens"] == 50
1080+
1081+
1082+
@pytest.fixture
1083+
def mock_anthropic_stream_with_web_search():
1084+
"""Mock stream events for web search."""
1085+
1086+
class MockServerToolUse:
1087+
def __init__(self):
1088+
self.web_search_requests = 2
1089+
1090+
class MockMessage:
1091+
def __init__(self):
1092+
self.usage = MockUsage(
1093+
input_tokens=50,
1094+
cache_creation_input_tokens=0,
1095+
cache_read_input_tokens=5,
1096+
)
1097+
1098+
def stream_generator():
1099+
# Message start with usage
1100+
event = MockStreamEvent("message_start")
1101+
event.message = MockMessage()
1102+
yield event
1103+
1104+
# Text block start
1105+
event = MockStreamEvent("content_block_start")
1106+
event.content_block = MockContentBlock("text")
1107+
event.index = 0
1108+
yield event
1109+
1110+
# Text delta
1111+
event = MockStreamEvent("content_block_delta")
1112+
event.delta = MockDelta(text="Here are the search results...")
1113+
event.index = 0
1114+
yield event
1115+
1116+
# Text block stop
1117+
event = MockStreamEvent("content_block_stop")
1118+
event.index = 0
1119+
yield event
1120+
1121+
# Message delta with final usage including web search
1122+
event = MockStreamEvent("message_delta")
1123+
usage = MockUsage(output_tokens=25)
1124+
usage.server_tool_use = MockServerToolUse()
1125+
event.usage = usage
1126+
yield event
1127+
1128+
# Message stop
1129+
event = MockStreamEvent("message_stop")
1130+
yield event
1131+
1132+
return stream_generator()
1133+
1134+
1135+
def test_streaming_with_web_search(mock_client, mock_anthropic_stream_with_web_search):
1136+
"""Test that web search count is properly captured in streaming mode."""
1137+
with patch(
1138+
"anthropic.resources.Messages.create",
1139+
return_value=mock_anthropic_stream_with_web_search,
1140+
):
1141+
client = Anthropic(api_key="test-key", posthog_client=mock_client)
1142+
response = client.messages.create(
1143+
model="claude-3-opus-20240229",
1144+
messages=[{"role": "user", "content": "Search for recent news"}],
1145+
stream=True,
1146+
posthog_distinct_id="test-id",
1147+
)
1148+
1149+
# Consume the stream - this triggers the finally block synchronously
1150+
list(response)
1151+
1152+
# Capture happens synchronously when generator is exhausted
1153+
assert mock_client.capture.call_count == 1
1154+
1155+
call_args = mock_client.capture.call_args[1]
1156+
props = call_args["properties"]
1157+
1158+
# Verify web search count is captured
1159+
assert props["$ai_web_search_count"] == 2
1160+
assert props["$ai_input_tokens"] == 50
1161+
assert props["$ai_output_tokens"] == 25
1162+
1163+
1164+
def test_async_with_web_search(mock_client):
1165+
"""Test that web search count is properly tracked in async non-streaming mode."""
1166+
import asyncio
1167+
1168+
# Create a mock usage with web search
1169+
class MockServerToolUse:
1170+
def __init__(self):
1171+
self.web_search_requests = 3
1172+
1173+
class MockUsageWithWebSearch:
1174+
def __init__(self):
1175+
self.input_tokens = 100
1176+
self.output_tokens = 50
1177+
self.cache_read_input_tokens = 0
1178+
self.cache_creation_input_tokens = 0
1179+
self.server_tool_use = MockServerToolUse()
1180+
1181+
class MockResponseWithWebSearch:
1182+
def __init__(self):
1183+
self.content = [MockContent(text="Search results show...")]
1184+
self.model = "claude-3-opus-20240229"
1185+
self.usage = MockUsageWithWebSearch()
1186+
1187+
mock_response = MockResponseWithWebSearch()
1188+
1189+
async def mock_async_create(**kwargs):
1190+
return mock_response
1191+
1192+
with patch(
1193+
"anthropic.resources.AsyncMessages.create",
1194+
side_effect=mock_async_create,
1195+
):
1196+
async_client = AsyncAnthropic(api_key="test-key", posthog_client=mock_client)
1197+
1198+
async def run_test():
1199+
response = await async_client.messages.create(
1200+
model="claude-3-opus-20240229",
1201+
messages=[{"role": "user", "content": "Search for recent news"}],
1202+
posthog_distinct_id="test-id",
1203+
)
1204+
return response
1205+
1206+
# asyncio.run() waits for all async operations to complete
1207+
response = asyncio.run(run_test())
1208+
1209+
assert response == mock_response
1210+
assert mock_client.capture.call_count == 1
1211+
1212+
call_args = mock_client.capture.call_args[1]
1213+
props = call_args["properties"]
1214+
1215+
# Verify web search count is captured
1216+
assert props["$ai_web_search_count"] == 3
1217+
assert props["$ai_input_tokens"] == 100
1218+
assert props["$ai_output_tokens"] == 50
1219+
1220+
1221+
def test_async_streaming_with_web_search(
1222+
mock_client, mock_anthropic_stream_with_web_search
1223+
):
1224+
"""Test that web search count is properly captured in async streaming mode."""
1225+
import asyncio
1226+
1227+
async def mock_async_generator():
1228+
# Convert regular generator to async generator
1229+
for event in mock_anthropic_stream_with_web_search:
1230+
yield event
1231+
1232+
async def mock_async_create(**kwargs):
1233+
# Return the async generator (to be awaited by the implementation)
1234+
return mock_async_generator()
1235+
1236+
with patch(
1237+
"anthropic.resources.AsyncMessages.create",
1238+
side_effect=mock_async_create,
1239+
):
1240+
async_client = AsyncAnthropic(api_key="test-key", posthog_client=mock_client)
1241+
1242+
async def run_test():
1243+
response = await async_client.messages.create(
1244+
model="claude-3-opus-20240229",
1245+
messages=[{"role": "user", "content": "Search for recent news"}],
1246+
stream=True,
1247+
posthog_distinct_id="test-id",
1248+
)
1249+
1250+
# Consume the async stream
1251+
[event async for event in response]
1252+
1253+
# asyncio.run() waits for all async operations to complete
1254+
asyncio.run(run_test())
1255+
1256+
# Capture completes before asyncio.run() returns
1257+
assert mock_client.capture.call_count == 1
1258+
1259+
call_args = mock_client.capture.call_args[1]
1260+
props = call_args["properties"]
1261+
1262+
# Verify web search count is captured
1263+
assert props["$ai_web_search_count"] == 2
1264+
assert props["$ai_input_tokens"] == 50
1265+
assert props["$ai_output_tokens"] == 25

0 commit comments

Comments
 (0)