Ironclad / rivet

The open-source visual AI programming environment and TypeScript library
https://rivet.ironcladapp.com
MIT License
2.69k stars 235 forks source link

[Bug]: Tool/Function calling not handling mutliple requested calls #431

Open castortech opened 2 months ago

castortech commented 2 months ago

What happened?

Working on a ChatNode plugin that directly extends ChatNode.ts and realized that there is an error.

Looking at this code:

if (functionCalls.length > 0) {
  if (isMultiResponse) {
    output['function-call' as PortId] = {
      type: 'object[]',
      value: functionCalls.map((functionCalls) => ({
        name: functionCalls[0]?.name,
        arguments: functionCalls[0]?.lastParsedArguments,
        id: functionCalls[0]?.id,
      })),
    };
  } else {
    if (this.data.parallelFunctionCalling) {
      console.dir({ functionCalls });
      output['function-calls' as PortId] = {
        type: 'object[]',
        value: functionCalls[0]!.map((functionCall) => ({
          name: functionCall.name,
          arguments: functionCall.lastParsedArguments,
          id: functionCall.id,
        })),
      };
    } else {
      output['function-call' as PortId] = {
        type: 'object',
        value: {
          name: functionCalls[0]![0]?.name,
          arguments: functionCalls[0]![0]?.lastParsedArguments,
          id: functionCalls[0]![0]?.id,
        } as Record<string, unknown>,
      };
    }
  }
}

In the case of multiResponse and single response not parallel. if OpenAI is requesting to run multiple tools, those are not being passed any further and are being dropped, as can be seen. In the first case we take the first function call for the choice (inner array) and in the other case we create a record of the first case in first choice (ok since single choice).

I believe the code should be adjusted as follow:

if (functionCalls.length > 0) {
  if (isMultiResponse) {
    output['function-call' as PortId] = {
      type: 'object[]',
      value: functionCalls.flat().map((functionCall) => ({
        name: functionCall.name,
        arguments: functionCall.lastParsedArguments,
        id: functionCall.id,
      })),
    }
  } else {
    output['function-call' as PortId] = {
      type: 'object[]',
      value: functionCalls[0]!.map((functionCall) => ({
        name: functionCall.name,
        arguments: functionCall.lastParsedArguments,
        id: functionCall.id,
      })),
    }
  }
}

This would always send all tool call requests as intended and do it in a standard way. The parallel argument is more to the receiver to decide how to handle the results IMHO.

What I can see here is that if the input is a single gptFunction and a single choice, then maybe there could have been expectation set that this would not be returned as an array and that case should be singled out to return a single object as it does currently. But again I am not sure that this is even correct, since even with a single tool, open AI could as to call it multiple times with different argument. Imagine a weather tool and I ask for the weather in NY and LA. Obviously 2 calls will more than likely be requested by OpenAI, even if we have a single tool declared.

I can make a PR if there is agreement.

What was the expected functionality?

Rivet did not work as expected!

Describe your environment

Not applicable here

Relevant log output

No response

Relevant screenshots

No response

Code of Conduct

abrenneke commented 2 months ago

Seems reasonable to me! Can you make a PR?