firebase / firebase-ios-sdk

Firebase SDK for Apple App Development
https://firebase.google.com
Apache License 2.0
5.56k stars 1.45k forks source link

Call to function fails if there are an odd number of items in chat history #13685

Open TiVoShane opened 10 hours ago

TiVoShane commented 10 hours ago

Description

When starting a chat (model.startChat) you can include prior history. When there is no history, it would be nice to be able to add some history, for example a greeting from the chatbot. This is easily done by creating a ModelContent and assigning that to the chat's history. However, when calling a function, the process will fail after the function response step with an error if there are an odd number of items in history. I've included a TestAgent class below. Simply create the class and watch the output. The only time it'll fail is when history contains an odd number of entries. I can include 10 model entries, or 10 user entries or a combination of each, as long as it's an even number. However, once I create history with an odd number, it fails.

Reproducing the issue

//
//  TestAgent.swift
//  Expense Tracker LLM
//
//  Created by Shane Miller on 9/20/24.
//

import Foundation
import FirebaseVertexAI

class TestAgent {
    var systemInstructions = """
Users will ask you information about exchange rates.  Use your tools to answer them."
"""
    private var modelName = "gemini-1.5-flash" // gemini-1.5-pro-exp-0801", //gemini-1.5-pro", //gemini-1.5-flash",

    // Initialize the Vertex AI service
    let vertex = VertexAI.vertexAI()
    let config = GenerationConfig(
        temperature: 0,
        topP: 0.95,
        topK: 40,
        maxOutputTokens: 8192,
        responseMIMEType: "text/plain"
    )

    init() {
        Task {
            await success1()
            await success2()
            await success3()
            await failure1()
        }
    }

    let getExchangeRate = FunctionDeclaration(
        name: "getExchangeRate",
        description: "Get the exchange rate for currencies between countries",
        parameters: [
            "currencyFrom": Schema(
                type: .string,
                description: "The currency to convert from."
            ),
            "currencyTo": Schema(
                type: .string,
                description: "The currency to convert to."
            ),
        ],
        requiredParameters: ["currencyFrom", "currencyTo"]
    )

    func makeAPIRequest(currencyFrom: String,
                        currencyTo: String) -> JSONObject {
        // This hypothetical API returns a JSON such as:
        // {"base":"USD","rates":{"SEK": 10.99}}
        return [
            "base": .string(currencyFrom),
            "rates": .object([currencyTo: .number(10.99)]),
        ]
    }

        // succeeds because there are an even number of ModelContents  user, model, user, model
    func success1() async {
        print("Success 1")
        // create some fake history.
        let mc1 = try! ModelContent(role: "user", "Hello, how are you?")
        let mc2 = try! ModelContent(role: "model", "I'm great!  How are you?")
        let mc3 = try! ModelContent(role: "user", "I'm good.  Thanks for asking.")
        let mc4 = try! ModelContent(role: "model", "My pleasure.  What can I do for you today?")

        let history = [mc1, mc2, mc3, mc4]
        let response = await test(history: history)
        print("Success 1 \(response)")

    }

    // succeeds because there are an even number of ModelContents, even though they are both model
    func success2() async {
        print("Success 2")
        // create some fake history.
        let mc1 = try! ModelContent(role: "model", "Hello, how are you?")
        let mc2 = try! ModelContent(role: "model", "I'm great!  How are you?")

        let history = [mc1, mc2]
        let response = await test(history: history)
        print("Success 2 \(response)")

    }
    // succeeds because there are an even number of ModelContents, even though they are both user
    func success3() async {
        print("Success 3")
        // create some fake history.
        let mc1 = try! ModelContent(role: "user", "What can I do for you today?")
        let mc2 = try! ModelContent(role: "user", "What can I do for you today?")

        let history = [mc1, mc2]
        let response = await test(history: history)
        print("Success 3 \(response)")

    }

    // fails because there are is an ODD number of ModelContents
    func failure1() async {
        print("failure1")
        // create some fake history.
        let mc1 = try! ModelContent(role: "model", "What can I do for you today?")

        let history = [mc1]
        let response = await test(history: history)
        print("failure1 \(response)")

    }

    func test(history : [ModelContent]) async -> String  {
        let model = vertex.generativeModel(
            modelName: modelName,
            generationConfig: config,
            tools: [Tool(functionDeclarations: [getExchangeRate])],
            systemInstruction: ModelContent(role: "system", parts: self.systemInstructions)
        )

        let prompt = "How much is 50 US dollars worth in Swedish krona?"
        let chat = await model.startChat(history: history)
        do {
            let response1 = try await chat.sendMessage(prompt)

            // Check if the model responded with a function call
            guard let functionCall = response1.functionCalls.first else {
                fatalError("Model did not respond with a function call.")
            }
            // Print an error if the returned function was not declared
            guard functionCall.name == "getExchangeRate" else {
                fatalError("Unexpected function called: \(functionCall.name)")
            }
            // Verify that the names and types of the parameters match the declaration
            guard case let .string(currencyFrom) = functionCall.args["currencyFrom"] else {
                fatalError("Missing argument: currencyFrom")
            }
            guard case let .string(currencyTo) = functionCall.args["currencyTo"] else {
                fatalError("Missing argument: currencyTo")
            }

            // Call the hypothetical API
            let apiResponse = makeAPIRequest(currencyFrom: currencyFrom, currencyTo: currencyTo)

            // Send the API response back to the model so it can generate a text response that can be
            // displayed to the user.
            let response = try await chat.sendMessage([ModelContent(
                role: "function",
                parts: [.functionResponse(FunctionResponse(
                    name: functionCall.name,
                    response: apiResponse
                ))]
            )])

            // Log the text response.
            guard let modelResponse = response.text else {
                fatalError("Model did not respond with text.")
            }
            return modelResponse
        } catch {
            print("Error sending message: \(error.localizedDescription)")
        }
        return ""
    }
}

// let testAgent = TestAgent()

Firebase SDK Version

11.3

Xcode Version

16

Installation Method

Swift Package Manager

Firebase Product(s)

App Check, Authentication, Firestore, VertexAI

Targeted Platforms

iOS

Relevant Log Output

Success 1

Success 1 50 US dollars is worth 549.5 Swedish krona. 

Success 2

Success 2 50 US dollars is worth 549.5 Swedish krona. 

Success 3

Success 3 50 US dollars is worth 549.5 Swedish krona. 

failure1
[FirebaseVertexAI] Response payload: {
  "error": {
    "code": 400,
    "message": "Please ensure that function call turn comes immediately after a user turn or after a function response turn.",
    "status": "INVALID_ARGUMENT"
  }
}
Error sending message: The operation couldn’t be completed. (FirebaseVertexAI.GenerateContentError error 1.)
failure1

If using Swift Package Manager, the project's Package.resolved

Expand Package.resolved snippet
```json Replace this line with the contents of your Package.resolved. ```

If using CocoaPods, the project's Podfile.lock

Expand Podfile.lock snippet
```yml Replace this line with the contents of your Podfile.lock! ```
google-oss-bot commented 10 hours ago

I couldn't figure out how to label this issue, so I've labeled it for a human to triage. Hang tight.

TiVoShane commented 10 hours ago

I'm actually using Firebase 11.2, not 11.3