Skip to content

Commit

Permalink
Update Anthropic to use tools that are no longer in beta (#249)
Browse files Browse the repository at this point in the history
Co-authored-by: Alex Hall <[email protected]>
  • Loading branch information
willbakst and alexmojaki authored Jun 6, 2024
1 parent 02d344d commit a8c4627
Show file tree
Hide file tree
Showing 4 changed files with 41 additions and 35 deletions.
17 changes: 8 additions & 9 deletions logfire/_internal/integrations/llm_providers/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
from typing import TYPE_CHECKING, Any

import anthropic
from anthropic.types import ContentBlockDeltaEvent, ContentBlockStartEvent, Message
from anthropic.types.beta.tools import ToolsBetaMessage
from anthropic.types import Message, RawContentBlockDeltaEvent, RawContentBlockStartEvent, TextBlock, TextDelta

from .types import EndpointConfig

Expand All @@ -29,7 +28,7 @@ def get_endpoint_config(options: FinalRequestOptions) -> EndpointConfig:
if not isinstance(json_data, dict):
raise ValueError('Expected `options.json_data` to be a dictionary')

if url == '/v1/messages' or url == '/v1/messages?beta=tools':
if url == '/v1/messages':
return EndpointConfig(
message_template='Message with {request_data[model]!r}',
span_data={'request_data': json_data},
Expand All @@ -40,19 +39,19 @@ def get_endpoint_config(options: FinalRequestOptions) -> EndpointConfig:


def content_from_messages(chunk: anthropic.types.MessageStreamEvent) -> str | None:
if isinstance(chunk, ContentBlockStartEvent):
return chunk.content_block.text
if isinstance(chunk, ContentBlockDeltaEvent):
return chunk.delta.text
if isinstance(chunk, RawContentBlockStartEvent):
return chunk.content_block.text if isinstance(chunk.content_block, TextBlock) else ''
if isinstance(chunk, RawContentBlockDeltaEvent):
return chunk.delta.text if isinstance(chunk.delta, TextDelta) else ''
return None


def on_response(response: ResponseT, span: LogfireSpan) -> ResponseT:
"""Updates the span based on the type of response."""
if isinstance(response, (Message, ToolsBetaMessage)): # pragma: no branch
if isinstance(response, Message): # pragma: no branch
block = response.content[0]
message: dict[str, Any] = {'role': 'assistant'}
if block.type == 'text':
if isinstance(block, TextBlock):
message['content'] = block.text
else:
message['tool_calls'] = [
Expand Down
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,7 @@ dev-dependencies = [
"psycopg2-binary",
"asyncpg",
"cloudpickle>=3.0.0",
# https://github.com/pydantic/logfire/issues/227
"anthropic<0.27.0",
"anthropic>=0.27.0",
# Can remove this when https://github.com/python/typing_extensions/commit/53bcdded534494674f893112f71d3be344d65363 is released
"typing-extensions<4.12",
]
Expand Down
2 changes: 1 addition & 1 deletion requirements-dev.lock
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ aiosignal==1.3.1
# via aiohttp
annotated-types==0.7.0
# via pydantic
anthropic==0.26.1
anthropic==0.28.0
anyio==4.3.0
# via anthropic
# via httpx
Expand Down
54 changes: 31 additions & 23 deletions tests/otel_integrations/test_anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,20 @@
from anthropic._models import FinalRequestOptions
from anthropic.types import (
Completion,
ContentBlockDeltaEvent,
ContentBlockStartEvent,
ContentBlockStopEvent,
Message,
MessageDeltaEvent,
MessageDeltaUsage,
MessageStartEvent,
MessageStopEvent,
RawContentBlockDeltaEvent,
RawContentBlockStartEvent,
RawContentBlockStopEvent,
RawMessageDeltaEvent,
TextBlock,
TextDelta,
ToolUseBlock,
Usage,
)
from anthropic.types.beta.tools import ToolsBetaMessage, ToolUseBlock
from anthropic.types.message_delta_event import Delta
from anthropic.types.raw_message_delta_event import Delta
from dirty_equals import IsJson
from dirty_equals._strings import IsStr
from httpx._transports.mock import MockTransport
Expand All @@ -46,10 +46,7 @@ def request_handler(request: httpx.Request) -> httpx.Response:
mode='json'
),
)
assert request.url in [
'https://api.anthropic.com/v1/messages',
'https://api.anthropic.com/v1/messages?beta=tools',
], f'Unexpected URL: {request.url}'
assert request.url in ['https://api.anthropic.com/v1/messages'], f'Unexpected URL: {request.url}'
json_body = json.loads(request.content)
if json_body.get('stream'):
if json_body['system'] == 'empty response chunk':
Expand All @@ -69,17 +66,17 @@ def request_handler(request: httpx.Request) -> httpx.Response:
),
type='message_start',
),
ContentBlockStartEvent(
RawContentBlockStartEvent(
content_block=TextBlock(text='', type='text'), index=0, type='content_block_start'
),
ContentBlockDeltaEvent(
RawContentBlockDeltaEvent(
delta=TextDelta(text='The answer', type='text_delta'), index=0, type='content_block_delta'
),
ContentBlockDeltaEvent(
RawContentBlockDeltaEvent(
delta=TextDelta(text=' is secret', type='text_delta'), index=0, type='content_block_delta'
),
ContentBlockStopEvent(index=0, type='content_block_stop'),
MessageDeltaEvent(
RawContentBlockStopEvent(index=0, type='content_block_stop'),
RawMessageDeltaEvent(
delta=Delta(stop_reason='end_turn', stop_sequence=None),
type='message_delta',
usage=MessageDeltaUsage(output_tokens=55),
Expand All @@ -92,7 +89,7 @@ def request_handler(request: httpx.Request) -> httpx.Response:
elif json_body['system'] == 'tool response':
return httpx.Response(
200,
json=ToolsBetaMessage(
json=Message(
id='test_id',
content=[ToolUseBlock(id='id', input={'param': 'param'}, name='tool', type='tool_use')],
model='claude-3-haiku-20240307',
Expand Down Expand Up @@ -148,6 +145,7 @@ def test_sync_messages(instrumented_client: anthropic.Anthropic, exporter: TestE
system='You are a helpful assistant.',
messages=[{'role': 'user', 'content': 'What is four plus five?'}],
)
assert isinstance(response.content[0], TextBlock)
assert response.content[0].text == 'Nine'
assert exporter.exported_spans_as_dict() == snapshot(
[
Expand Down Expand Up @@ -215,6 +213,7 @@ async def test_async_messages(instrumented_async_client: anthropic.AsyncAnthropi
system='You are a helpful assistant.',
messages=[{'role': 'user', 'content': 'What is four plus five?'}],
)
assert isinstance(response.content[0], TextBlock)
assert response.content[0].text == 'Nine'
assert exporter.exported_spans_as_dict() == snapshot(
[
Expand Down Expand Up @@ -333,14 +332,19 @@ def test_sync_message_empty_response_chunk(instrumented_client: anthropic.Anthro


def test_sync_messages_stream(instrumented_client: anthropic.Anthropic, exporter: TestExporter) -> None:
response = instrumented_client.messages.stream(
response = instrumented_client.messages.create(
max_tokens=1000,
model='claude-3-haiku-20240307',
system='You are a helpful assistant.',
messages=[{'role': 'user', 'content': 'What is four plus five?'}],
stream=True,
)
with response as stream:
combined = ''.join(chunk.delta.text for chunk in stream if isinstance(chunk, ContentBlockDeltaEvent))
combined = ''.join(
chunk.delta.text
for chunk in stream
if isinstance(chunk, RawContentBlockDeltaEvent) and isinstance(chunk.delta, TextDelta)
)
assert combined == 'The answer is secret'
assert exporter.exported_spans_as_dict() == snapshot(
[
Expand All @@ -354,7 +358,7 @@ def test_sync_messages_stream(instrumented_client: anthropic.Anthropic, exporter
'code.filepath': 'test_anthropic.py',
'code.function': 'test_sync_messages_stream',
'code.lineno': 123,
'request_data': '{"max_tokens":1000,"messages":[{"role":"user","content":"What is four plus five?"}],"model":"claude-3-haiku-20240307","system":"You are a helpful assistant.","stream":true}',
'request_data': '{"max_tokens":1000,"messages":[{"role":"user","content":"What is four plus five?"}],"model":"claude-3-haiku-20240307","stream":true,"system":"You are a helpful assistant."}',
'async': False,
'logfire.msg_template': 'Message with {request_data[model]!r}',
'logfire.msg': "Message with 'claude-3-haiku-20240307'",
Expand All @@ -371,7 +375,7 @@ def test_sync_messages_stream(instrumented_client: anthropic.Anthropic, exporter
'end_time': 5000000000,
'attributes': {
'logfire.level_num': 9,
'request_data': '{"max_tokens":1000,"messages":[{"role":"user","content":"What is four plus five?"}],"model":"claude-3-haiku-20240307","system":"You are a helpful assistant.","stream":true}',
'request_data': '{"max_tokens":1000,"messages":[{"role":"user","content":"What is four plus five?"}],"model":"claude-3-haiku-20240307","stream":true,"system":"You are a helpful assistant."}',
'async': False,
'logfire.msg_template': 'streaming response from {request_data[model]!r} took {duration:.2f}s',
'code.filepath': 'test_anthropic.py',
Expand Down Expand Up @@ -400,7 +404,11 @@ async def test_async_messages_stream(
stream=True,
)
async with response as stream:
chunk_content = [chunk.delta.text async for chunk in stream if isinstance(chunk, ContentBlockDeltaEvent)]
chunk_content = [
chunk.delta.text
async for chunk in stream
if isinstance(chunk, RawContentBlockDeltaEvent) and isinstance(chunk.delta, TextDelta)
]
combined = ''.join(chunk_content)
assert combined == 'The answer is secret'
assert exporter.exported_spans_as_dict() == snapshot(
Expand Down Expand Up @@ -451,13 +459,13 @@ async def test_async_messages_stream(


def test_tool_messages(instrumented_client: anthropic.Anthropic, exporter: TestExporter):
response = instrumented_client.beta.tools.messages.create(
response = instrumented_client.messages.create(
max_tokens=1000,
model='claude-3-haiku-20240307',
system='tool response',
messages=[],
)
assert isinstance(response, ToolsBetaMessage)
assert isinstance(response.content[0], ToolUseBlock)
content = response.content[0]
assert isinstance(content, ToolUseBlock)
assert content.input == {'param': 'param'}
Expand Down

0 comments on commit a8c4627

Please sign in to comment.