From e07b110b47a68317fa2424fc4640e9811bc01590 Mon Sep 17 00:00:00 2001 From: andrewmjc Date: Tue, 2 Jul 2024 16:57:13 -0600 Subject: [PATCH 1/2] matching openai tool result spec --- litellm/llms/prompt_templates/factory.py | 16 +++++++++++++--- litellm/llms/vertex_ai.py | 5 ++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/litellm/llms/prompt_templates/factory.py b/litellm/llms/prompt_templates/factory.py index 87af2a6bdc..f8affa8876 100644 --- a/litellm/llms/prompt_templates/factory.py +++ b/litellm/llms/prompt_templates/factory.py @@ -1022,16 +1022,17 @@ def convert_to_gemini_tool_call_invoke( def convert_to_gemini_tool_call_result( message: dict, + last_message_with_tool_calls: dict|None, ) -> litellm.types.llms.vertex_ai.PartType: """ OpenAI message with a tool result looks like: { "tool_call_id": "tool_1", - "role": "tool", - "name": "get_current_weather", + "role": "tool", "content": "function result goes here", }, + # NOTE: Function messages have been deprecated OpenAI message with a function call result looks like: { "role": "function", @@ -1040,7 +1041,16 @@ def convert_to_gemini_tool_call_result( } """ content = message.get("content", "") - name = message.get("name", "") + name = "" + + # Recover name from last message with tool calls + if last_message_with_tool_calls: + tools = last_message_with_tool_calls.get("tool_calls", []) + msg_tool_call_id = message.get("tool_call_id", None) + for tool in tools: + prev_tool_call_id = tool.get("id", None) + if msg_tool_call_id and prev_tool_call_id and msg_tool_call_id == prev_tool_call_id: + name = tool.get("function", {}).get("name", "") # We can't determine from openai message format whether it's a successful or # error call result so default to the successful result template diff --git a/litellm/llms/vertex_ai.py b/litellm/llms/vertex_ai.py index c1e628d175..d121288a2c 100644 --- a/litellm/llms/vertex_ai.py +++ b/litellm/llms/vertex_ai.py @@ -328,6 +328,8 @@ def _gemini_convert_messages_with_history(messages: list) -> List[ContentType]: user_message_types = {"user", "system"} contents: List[ContentType] = [] + last_message_with_tool_calls = None + msg_i = 0 try: while msg_i < len(messages): @@ -383,6 +385,7 @@ def _gemini_convert_messages_with_history(messages: list) -> List[ContentType]: messages[msg_i]["tool_calls"] ) ) + last_message_with_tool_calls = messages[msg_i] else: assistant_text = ( messages[msg_i].get("content") or "" @@ -397,7 +400,7 @@ def _gemini_convert_messages_with_history(messages: list) -> List[ContentType]: ## APPEND TOOL CALL MESSAGES ## if msg_i < len(messages) and messages[msg_i]["role"] == "tool": - _part = convert_to_gemini_tool_call_result(messages[msg_i]) + _part = convert_to_gemini_tool_call_result(messages[msg_i], last_message_with_tool_calls) contents.append(ContentType(parts=[_part])) # type: ignore msg_i += 1 if msg_i == init_msg_i: # prevent infinite loops From 95dc480204eb22cce2698387012823a405afaf91 Mon Sep 17 00:00:00 2001 From: andrewmjc Date: Wed, 3 Jul 2024 12:23:26 -0600 Subject: [PATCH 2/2] unit test --- litellm/llms/prompt_templates/factory.py | 3 ++ .../tests/test_amazing_vertex_completion.py | 53 +++++++++++++++++-- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/litellm/llms/prompt_templates/factory.py b/litellm/llms/prompt_templates/factory.py index f8affa8876..30acaa0340 100644 --- a/litellm/llms/prompt_templates/factory.py +++ b/litellm/llms/prompt_templates/factory.py @@ -1052,6 +1052,9 @@ def convert_to_gemini_tool_call_result( if msg_tool_call_id and prev_tool_call_id and msg_tool_call_id == prev_tool_call_id: name = tool.get("function", {}).get("name", "") + if not name: + raise Exception("Missing corresponding tool call for tool response message") + # We can't determine from openai message format whether it's a successful or # error call result so default to the successful result template inferred_content_value = infer_protocol_value(value=content) diff --git a/litellm/tests/test_amazing_vertex_completion.py b/litellm/tests/test_amazing_vertex_completion.py index c4705325b9..5d494ac135 100644 --- a/litellm/tests/test_amazing_vertex_completion.py +++ b/litellm/tests/test_amazing_vertex_completion.py @@ -1154,8 +1154,7 @@ async def test_gemini_pro_function_calling(provider, sync_mode): # The result of the tool call is added to the history { "role": "tool", - "tool_call_id": "call_123", - "name": "get_weather", + "tool_call_id": "call_123", "content": "27 degrees celsius and clear in San Francisco, CA", }, # Now the assistant can reply with the result of the tool call. @@ -1377,6 +1376,52 @@ async def test_vertexai_aembedding(): except Exception as e: pytest.fail(f"Error occurred: {e}") +@pytest.mark.asyncio +def test_tool_name_conversion(): + messages = [ + { + "role": "system", + "content": "Your name is Litellm Bot, you are a helpful assistant", + }, + # User asks for their name and weather in San Francisco + { + "role": "user", + "content": "Hello, what is your name and can you tell me the weather?", + }, + # Assistant replies with a tool call + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": "call_123", + "type": "function", + "index": 0, + "function": { + "name": "get_weather", + "arguments": '{"location":"San Francisco, CA"}', + }, + } + ], + }, + # The result of the tool call is added to the history + { + "role": "tool", + "tool_call_id": "call_123", + "content": "27 degrees celsius and clear in San Francisco, CA", + }, + # Now the assistant can reply with the result of the tool call. + ] + + translated_messages = _gemini_convert_messages_with_history(messages=messages) + + print(f"\n\ntranslated_messages: {translated_messages}\ntranslated_messages") + + # assert that the last tool response has the corresponding tool name + assert ( + translated_messages[-1]["parts"][0]["function_response"]["name"] == "get_weather" + ) + # Extra gemini Vision tests for completion + stream, async, async + stream # if we run into issues with gemini, we will also add these to our ci/cd pipeline @@ -1526,7 +1571,6 @@ def test_prompt_factory(): { "role": "tool", "tool_call_id": "call_123", - "name": "get_weather", "content": "27 degrees celsius and clear in San Francisco, CA", }, # Now the assistant can reply with the result of the tool call. @@ -1536,7 +1580,6 @@ def test_prompt_factory(): print(f"\n\ntranslated_messages: {translated_messages}\ntranslated_messages") - def test_prompt_factory_nested(): messages = [ {"role": "user", "content": [{"type": "text", "text": "hi"}]}, @@ -1558,4 +1601,4 @@ def test_prompt_factory_nested(): assert "text" in message["parts"][0], "Missing 'text' from 'parts'" assert isinstance( message["parts"][0]["text"], str - ), "'text' value not a string." + ), "'text' value not a string." \ No newline at end of file