Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Anthropic to use tools that are no longer in beta #249

Merged
merged 3 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
6 changes: 5 additions & 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 Expand Up @@ -94,7 +94,11 @@ gitdb==4.0.11
gitpython==3.1.43
googleapis-common-protos==1.63.1
# via opentelemetry-exporter-otlp-proto-http
<<<<<<< HEAD
griffe==0.44.0
=======
griffe==0.45.2
>>>>>>> main
alexmojaki marked this conversation as resolved.
Show resolved Hide resolved
# via mkdocstrings-python
h11==0.14.0
# via httpcore
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
Loading