mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
Merge pull request #2527 from lazyhope/support_anthropic_function_result
Add function call result submission support for Claude 3 models
This commit is contained in:
commit
c1495c0d1c
2 changed files with 185 additions and 71 deletions
|
|
@ -551,6 +551,81 @@ def convert_to_anthropic_image_obj(openai_image_url: str):
|
|||
)
|
||||
|
||||
|
||||
def convert_to_anthropic_tool_result(message: dict) -> str:
|
||||
"""
|
||||
OpenAI message with a tool result looks like:
|
||||
{
|
||||
"tool_call_id": "tool_1",
|
||||
"role": "tool",
|
||||
"name": "get_current_weather",
|
||||
"content": "function result goes here",
|
||||
},
|
||||
"""
|
||||
|
||||
"""
|
||||
Anthropic tool_results look like:
|
||||
|
||||
[Successful results]
|
||||
<function_results>
|
||||
<result>
|
||||
<tool_name>get_current_weather</tool_name>
|
||||
<stdout>
|
||||
function result goes here
|
||||
</stdout>
|
||||
</result>
|
||||
</function_results>
|
||||
|
||||
[Error results]
|
||||
<function_results>
|
||||
<error>
|
||||
error message goes here
|
||||
</error>
|
||||
</function_results>
|
||||
"""
|
||||
name = message.get("name")
|
||||
content = message.get("content")
|
||||
|
||||
# We can't determine from openai message format whether it's a successful or
|
||||
# error call result so default to the successful result template
|
||||
anthropic_tool_result = (
|
||||
"<function_results>\n"
|
||||
"<result>\n"
|
||||
f"<tool_name>{name}</tool_name>\n"
|
||||
"<stdout>\n"
|
||||
f"{content}\n"
|
||||
"</stdout>\n"
|
||||
"</result>\n"
|
||||
"</function_results>"
|
||||
)
|
||||
|
||||
return anthropic_tool_result
|
||||
|
||||
|
||||
def convert_to_anthropic_tool_invoke(tool_calls: list) -> str:
|
||||
invokes = ""
|
||||
for tool in tool_calls:
|
||||
if tool["type"] != "function":
|
||||
continue
|
||||
|
||||
tool_name = tool["function"]["name"]
|
||||
parameters = "".join(
|
||||
f"<{param}>{val}</{param}>\n"
|
||||
for param, val in json.loads(tool["function"]["arguments"]).items()
|
||||
)
|
||||
invokes += (
|
||||
"<invoke>\n"
|
||||
f"<tool_name>{tool_name}</tool_name>\n"
|
||||
"<parameters>\n"
|
||||
f"{parameters}"
|
||||
"</parameters>\n"
|
||||
"</invoke>\n"
|
||||
)
|
||||
|
||||
anthropic_tool_invoke = f"<function_calls>\n{invokes}</function_calls>"
|
||||
|
||||
return anthropic_tool_invoke
|
||||
|
||||
|
||||
def anthropic_messages_pt(messages: list):
|
||||
"""
|
||||
format messages for anthropic
|
||||
|
|
@ -561,82 +636,75 @@ def anthropic_messages_pt(messages: list):
|
|||
5. System messages are a separate param to the Messages API (used for tool calling)
|
||||
6. Ensure we only accept role, content. (message.name is not supported)
|
||||
"""
|
||||
## Ensure final assistant message has no trailing whitespace
|
||||
last_assistant_message_idx: Optional[int] = None
|
||||
# add role=tool support to allow function call result/error submission
|
||||
user_message_types = {"user", "tool"}
|
||||
# reformat messages to ensure user/assistant are alternating, if there's either 2 consecutive 'user' messages or 2 consecutive 'assistant' message, add a blank 'user' or 'assistant' message to ensure compatibility
|
||||
new_messages = []
|
||||
if len(messages) == 1:
|
||||
# check if the message is a user message
|
||||
if messages[0]["role"] == "assistant":
|
||||
new_messages.append({"role": "user", "content": ""})
|
||||
|
||||
# check if content is a list (vision)
|
||||
if isinstance(messages[0]["content"], list): # vision input
|
||||
new_content = []
|
||||
for m in messages[0]["content"]:
|
||||
if m.get("type", "") == "image_url":
|
||||
new_content.append(
|
||||
{
|
||||
"type": "image",
|
||||
"source": convert_to_anthropic_image_obj(
|
||||
m["image_url"]["url"]
|
||||
),
|
||||
}
|
||||
)
|
||||
elif m.get("type", "") == "text":
|
||||
new_content.append({"type": "text", "text": m["text"]})
|
||||
new_messages.append({"role": messages[0]["role"], "content": new_content}) # type: ignore
|
||||
else:
|
||||
new_messages.append(
|
||||
{"role": messages[0]["role"], "content": messages[0]["content"]}
|
||||
)
|
||||
|
||||
return new_messages
|
||||
|
||||
for i in range(len(messages) - 1): # type: ignore
|
||||
if i == 0 and messages[i]["role"] == "assistant":
|
||||
new_messages.append({"role": "user", "content": ""})
|
||||
if isinstance(messages[i]["content"], list): # vision input
|
||||
new_content = []
|
||||
for m in messages[i]["content"]:
|
||||
if m.get("type", "") == "image_url":
|
||||
new_content.append(
|
||||
{
|
||||
"type": "image",
|
||||
"source": convert_to_anthropic_image_obj(
|
||||
m["image_url"]["url"]
|
||||
),
|
||||
}
|
||||
)
|
||||
elif m.get("type", "") == "text":
|
||||
new_content.append({"type": "text", "content": m["text"]})
|
||||
new_messages.append({"role": messages[i]["role"], "content": new_content}) # type: ignore
|
||||
else:
|
||||
new_messages.append(
|
||||
{"role": messages[i]["role"], "content": messages[i]["content"]}
|
||||
)
|
||||
|
||||
if messages[i]["role"] == messages[i + 1]["role"]:
|
||||
if messages[i]["role"] == "user":
|
||||
new_messages.append({"role": "assistant", "content": ""})
|
||||
msg_i = 0
|
||||
while msg_i < len(messages):
|
||||
user_content = []
|
||||
while msg_i < len(messages) and messages[msg_i]["role"] in user_message_types:
|
||||
if isinstance(messages[msg_i]["content"], list):
|
||||
for m in messages[msg_i]["content"]:
|
||||
if m.get("type", "") == "image_url":
|
||||
user_content.append(
|
||||
{
|
||||
"type": "image",
|
||||
"source": convert_to_anthropic_image_obj(
|
||||
m["image_url"]["url"]
|
||||
),
|
||||
}
|
||||
)
|
||||
elif m.get("type", "") == "text":
|
||||
user_content.append({"type": "text", "text": m["text"]})
|
||||
else:
|
||||
new_messages.append({"role": "user", "content": ""})
|
||||
# Tool message content will always be a string
|
||||
user_content.append(
|
||||
{
|
||||
"type": "text",
|
||||
"text": (
|
||||
convert_to_anthropic_tool_result(messages[msg_i])
|
||||
if messages[msg_i]["role"] == "tool"
|
||||
else messages[msg_i]["content"]
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
if messages[i]["role"] == "assistant":
|
||||
last_assistant_message_idx = i
|
||||
msg_i += 1
|
||||
|
||||
if user_content:
|
||||
new_messages.append({"role": "user", "content": user_content})
|
||||
|
||||
assistant_content = []
|
||||
while msg_i < len(messages) and messages[msg_i]["role"] == "assistant":
|
||||
assistant_text = (
|
||||
messages[msg_i].get("content") or ""
|
||||
) # either string or none
|
||||
if messages[msg_i].get(
|
||||
"tool_calls", []
|
||||
): # support assistant tool invoke convertion
|
||||
assistant_text += convert_to_anthropic_tool_invoke(
|
||||
messages[msg_i]["tool_calls"]
|
||||
)
|
||||
|
||||
assistant_content.append({"type": "text", "text": assistant_text})
|
||||
msg_i += 1
|
||||
|
||||
if assistant_content:
|
||||
new_messages.append({"role": "assistant", "content": assistant_content})
|
||||
|
||||
if new_messages[0]["role"] != "user":
|
||||
new_messages.insert(
|
||||
0, {"role": "user", "content": [{"type": "text", "text": "."}]}
|
||||
)
|
||||
|
||||
if new_messages[-1]["role"] == "assistant":
|
||||
for content in new_messages[-1]["content"]:
|
||||
if content["type"] == "text":
|
||||
content["text"] = content[
|
||||
"text"
|
||||
].rstrip() # no trailing whitespace for final assistant message
|
||||
|
||||
new_messages.append(messages[-1])
|
||||
if last_assistant_message_idx is not None:
|
||||
try:
|
||||
new_messages[last_assistant_message_idx]["content"] = new_messages[
|
||||
last_assistant_message_idx
|
||||
][
|
||||
"content"
|
||||
].strip() # no trailing whitespace for final assistant message
|
||||
except Exception as e:
|
||||
raise ValueError(
|
||||
f"LiteLLMException: Invalid final assistant message passed in. Messages={messages}"
|
||||
)
|
||||
|
||||
return new_messages
|
||||
|
||||
|
|
|
|||
|
|
@ -152,6 +152,52 @@ def test_completion_claude_3_function_call():
|
|||
assert isinstance(
|
||||
response.choices[0].message.tool_calls[0].function.arguments, str
|
||||
)
|
||||
|
||||
messages.append(
|
||||
response.choices[0].message.model_dump()
|
||||
) # Add assistant tool invokes
|
||||
tool_result = (
|
||||
'{"location": "Boston", "temperature": "72", "unit": "fahrenheit"}'
|
||||
)
|
||||
# Add user submitted tool results in OpenAI format
|
||||
messages.append(
|
||||
{
|
||||
"tool_call_id": response.choices[0].message.tool_calls[0].id,
|
||||
"role": "tool",
|
||||
"name": response.choices[0].message.tool_calls[0].function.name,
|
||||
"content": tool_result,
|
||||
}
|
||||
)
|
||||
# In the second response, Claude should deduce answer from tool results
|
||||
second_response = completion(
|
||||
model="anthropic/claude-3-opus-20240229",
|
||||
messages=messages,
|
||||
tools=tools,
|
||||
tool_choice="auto",
|
||||
)
|
||||
print(second_response)
|
||||
except Exception as e:
|
||||
pytest.fail(f"Error occurred: {e}")
|
||||
|
||||
|
||||
def test_completion_claude_3_multi_turn_conversations():
|
||||
litellm.set_verbose = True
|
||||
messages = [
|
||||
{"role": "assistant", "content": "?"}, # test first user message auto injection
|
||||
{"role": "user", "content": "Hi!"},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [{"type": "text", "text": "What is the weather like today?"}],
|
||||
},
|
||||
{"role": "assistant", "content": "Hi! I am Claude. "},
|
||||
{"role": "assistant", "content": "Today is a sunny "},
|
||||
]
|
||||
try:
|
||||
response = completion(
|
||||
model="anthropic/claude-3-opus-20240229",
|
||||
messages=messages,
|
||||
)
|
||||
print(response)
|
||||
except Exception as e:
|
||||
pytest.fail(f"Error occurred: {e}")
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue