generative-ai icon indicating copy to clipboard operation
generative-ai copied to clipboard

[Bug]: The number of function response parts should be equal to number of function call parts of the function call turn

Open lgbaeza opened this issue 11 months ago • 2 comments

File Name

gemini/function-calling/sql-talk-app/app.py

What happened?

This error appears when the BigQuery dataset contains multiple tables, basically because there is one part response for each table, leading to having more function parts in the response that in the function call definition.

I tested replacing the get_table function for a get_tables that handles several tables. I think I had a different code base, but Im sharing what I did as a sample

get_table_func = FunctionDeclaration(
    name="get_tables",
    description="Obtiene información sobre las tablas, incluida la descripción, el esquema y la cantidad de filas que ayudarán a responder la pregunta del usuario. Utilice siempre el nombre completo del conjunto de datos y de las tablas.",
    parameters={
        "type": "object",
        "properties": {
            "tables": {
                "type": "array",
                "description": "List of tables with informatioin in BQ",
                "items": {
                    "type": "object",
                    "properties": {
                        "table_id": {
                            "type": "string",
                            "description": "ID del Dataset del que se recuperarán las tablas",
                        },
                        "description": {
                            "type": "string",
                            "description": "Description of the table",
                        }
                    },
                    "required": [
                        "table_id",
                    ],
                }
            }
        }
    },
)

And the function implementation

        if response.function_call.name == "get_tables":
            api_responses = []
            processed_tables = None #Handles all tables in an array
            for table in params["tables"]:  # repeat for each table
                table_id = table["table_id"]
                ftable_id = f"{BIGQUERY_PROJECT_ID}.{BIGQUERY_DATASET_ID}.{table_id}"
                api_response = client.get_table(ftable_id)
                api_response = api_response.to_api_repr()
                api_responses.append(api_response)

                if processed_tables is None:
                    processed_tables = [
                        str([
                            str(api_response.get("description", "")),
                            str(
                                [
                                    column["name"] + ":" + column.get("description", "No description")
                                    for column in api_response["schema"]["fields"]
                                    if "description" in column
                                ]
                            )
                        ])
                    ]
                else:
                    try:
                        processed_tables.append(
                            str([
                                str(api_response.get("description", "")),
                                str(
                                    [
                                        column["name"] + ":" + column.get("description", "No description")
                                        for column in api_response["schema"]["fields"]
                                        if "description" in column
                                    ]
                                )
                            ])
                        )
                    except Exception as e:
                        print(e)
            
            # Return all tables in the function response
            api_requests_and_responses.append(
                [
                    response.function_call.name,
                    params,
                    str( processed_tables )
                ]
            )

            api_response = api_responses
                
            api_response = str( api_response)

Relevant log output


Code of Conduct

  • [x] I agree to follow this project's Code of Conduct

lgbaeza avatar Feb 12 '25 17:02 lgbaeza

Thanks for the info and sample code!

Do you have an example of a input prompt that causes the sample app to error out like this with parallel function calls? Since the sample app uses the thelook_ecommerce sample data with multiple tables, I'd like to see if this is something we can fix with function calling modes or simpler logic so that we can keep this tutorial app as simple as possible.

koverholt avatar Feb 12 '25 20:02 koverholt

Still a bug in 1.73.6

following the code in the docs:

      messages.append(
          {
              "tool_call_id": tool_call.id,
              "role": "tool",
              "name": function_name,
              "content": function_response,
          }
      )

Code that will throw the error is below. it should be noted that tool calls is an array of one call in my example.

from base64 import b64encode
from io import BytesIO
import json
from typing import List
import litellm


class TestIterationTool:
    system_prompt = """
The user will provide an attached file. You have been given tools to accomplish the following task.

Always do the following, in order
1) submit the title of the document
2) submit a brief caption summary of the document
3) submit a random dungeons and dragons NPC name that roughly represents the contents of this document
"""

    user_prompt = """
Please do the tasks specified.
"""

    def submit_title(self, title: str):
        """
        Submit the title of the document.
        """
        print(f"The title is '{title}'")
        return "title submitted"

    def submit_caption(self, caption: str):
        """
        Submit a brief caption summary of the document.
        """
        print(f"The caption is '{caption}'")
        return "caption submitted"

    def submit_npc_name(self, name: str):
        """
        Submit a random dungeons and dragons NPC name that roughly represents the contents of this document.
        """
        print(f"A relevant NPC name is '{name}'")
        return "NPC name submitted"

    def execute(
        self,
        buffer: BytesIO,  # a buffer representing PDF file contents
    ) -> str:
        b64 = b64encode(buffer.getvalue()).decode("utf-8")

        messages = [
            {
                "role": "system",
                "content": self.system_prompt,
            },
            {
                "role": "user",
                "content": [
                    {
                        "type": "file",
                        "file": {
                            "file_data": f"data:application/pdf;base64,{b64}",
                        },
                    },
                    {"type": "text", "text": self.user_prompt},
                ],
            },
        ]
        tools = [self.submit_title, self.submit_caption, self.submit_npc_name]
        text = self.iterate_completion(messages=messages, tools=tools)
        print(f"response: '{text}'")

    def iterate_completion(self, messages: list, tools: List[callable]) -> str:
        tool_dicts = [litellm.utils.function_to_dict(tool) for tool in tools]
        tool_map = {tool.__name__: tool for tool in tools}

        while True:
            response = litellm.completion(
                model="gemini/gemini-2.5-flash", messages=messages, tools=tool_dicts
            )
            choice = response.choices[0]
            tool_calls = choice.message.tool_calls

            if not tool_calls:
                return choice.message.content

            for tool_call in tool_calls:
                try:
                    tool_name = tool_call.function.name
                    tool_args = json.loads(tool_call.function.arguments)
                    tool_func = tool_map.get(tool_name)
                    tool_result = tool_func(**tool_args)
                    messages.append(
                        {
                            "tool_call_id": tool_call.id,
                            "role": "tool",
                            "name": tool_name,
                            "content": tool_result,
                        }
                    )

                except TypeError as e:
                    messages.append(
                        {
                            "tool_call_id": tool_call.id,
                            "role": "tool",
                            "name": tool_name,
                            "content": f"Error calling tool {tool_name}: {str(e)}",
                        }
                    )

barrcodes avatar Jul 04 '25 05:07 barrcodes