From 84f3ee81914f4330dd48a3296b42177d4605d692 Mon Sep 17 00:00:00 2001 From: xiaoxiong Date: Fri, 1 Nov 2024 15:01:45 +0800 Subject: [PATCH 1/2] use tools function call --- examples/agenthandoff.go | 3 +- examples/basic.go | 2 +- examples/contextvariables.go | 2 +- examples/functioncall.go | 7 +- examples/main.go | 15 +++ examples/triageagent.go | 2 +- examples/weatheragent.go | 2 +- swarm.go | 240 +++++++++++++++++++---------------- util.go | 4 +- 9 files changed, 157 insertions(+), 120 deletions(-) create mode 100644 examples/main.go diff --git a/examples/agenthandoff.go b/examples/agenthandoff.go index dc9729e..f8e8c2d 100644 --- a/examples/agenthandoff.go +++ b/examples/agenthandoff.go @@ -22,10 +22,11 @@ func transferToSpanishAgent(args map[string]interface{}, contextVariables map[st Value: "Transferring to Spanish Agent.", } } -func main() { +func agentHandoffExample() { dotenv.Load() client := swarmgo.NewSwarm(os.Getenv("OPENAI_API_KEY")) + fmt.Println("Agent Handoff Example", os.Getenv("OPENAI_API_KEY"), os.Getenv("OPENAI_PROXY")) englishAgent := &swarmgo.Agent{ Name: "EnglishAgent", diff --git a/examples/basic.go b/examples/basic.go index 0ceac36..acad001 100644 --- a/examples/basic.go +++ b/examples/basic.go @@ -10,7 +10,7 @@ import ( openai "github.com/sashabaranov/go-openai" ) -func main() { +func basicExample() { dotenv.Load() client := swarmgo.NewSwarm(os.Getenv("OPENAI_API_KEY")) diff --git a/examples/contextvariables.go b/examples/contextvariables.go index be860bc..e71068d 100644 --- a/examples/contextvariables.go +++ b/examples/contextvariables.go @@ -26,7 +26,7 @@ func printAccountDetails(args map[string]interface{}, contextVariables map[strin Value: fmt.Sprintf("Account Details: %v %v", name, userID), } } -func main() { +func contextVariablesExample() { dotenv.Load() client := swarmgo.NewSwarm(os.Getenv("OPENAI_API_KEY")) diff --git a/examples/functioncall.go b/examples/functioncall.go index a6f5ab9..4f02ea1 100644 --- a/examples/functioncall.go +++ b/examples/functioncall.go @@ -10,15 +10,14 @@ import ( openai "github.com/sashabaranov/go-openai" ) -func getWeather(args map[string]interface{}, contextVariables map[string]interface{}) swarmgo.Result { +func getWeather1(args map[string]interface{}, contextVariables map[string]interface{}) swarmgo.Result { location := args["location"].(string) return swarmgo.Result{ Value: fmt.Sprintf("{'temp':67, 'unit':'F', 'location':'%s'}", location), } } -func main() { +func functionCallExample() { dotenv.Load() - client := swarmgo.NewSwarm(os.Getenv("OPENAI_API_KEY")) agent := &swarmgo.Agent{ @@ -38,7 +37,7 @@ func main() { }, "required": []string{"location"}, }, - Function: getWeather, + Function: getWeather1, }, }, Model: "gpt-4", diff --git a/examples/main.go b/examples/main.go new file mode 100644 index 0000000..528460c --- /dev/null +++ b/examples/main.go @@ -0,0 +1,15 @@ +package main + +func main() { + //basicExample() + + //functionCallExample() + + //weatherAgentExample() + + //triageAgentExample() + + //contextVariablesExample() + + agentHandoffExample() +} diff --git a/examples/triageagent.go b/examples/triageagent.go index e47137c..e9c3a7d 100644 --- a/examples/triageagent.go +++ b/examples/triageagent.go @@ -146,7 +146,7 @@ func initAgents() { }) } -func main() { +func triageAgentExample() { dotenv.Load() client := swarmgo.NewSwarm(os.Getenv("OPENAI_API_KEY")) diff --git a/examples/weatheragent.go b/examples/weatheragent.go index 846beb0..cd64133 100644 --- a/examples/weatheragent.go +++ b/examples/weatheragent.go @@ -30,7 +30,7 @@ func sendEmail(args map[string]interface{}, contextVariables map[string]interfac } } -func main() { +func weatherAgentExample() { dotenv.Load() client := swarmgo.NewSwarm(os.Getenv("OPENAI_API_KEY")) diff --git a/swarm.go b/swarm.go index c4054f6..e3f3927 100644 --- a/swarm.go +++ b/swarm.go @@ -5,18 +5,19 @@ import ( "encoding/json" "fmt" "log" + "sync" openai "github.com/sashabaranov/go-openai" ) // OpenAIClient defines the methods used from the OpenAI client type OpenAIClient interface { - CreateChatCompletion(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) + CreateChatCompletion(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error) } // Swarm represents the main structure type Swarm struct { - client OpenAIClient + client OpenAIClient } // NewSwarm initializes a new Swarm instance with an OpenAI client @@ -27,6 +28,18 @@ func NewSwarm(apiKey string) *Swarm { } } +// NewSwarmWithProxy initializes a new Swarm instance with the given API key and proxy URL. +func NewSwarmWithProxy(apiKey, proxyURL string) *Swarm { + config := openai.DefaultConfig(apiKey) + if proxyURL != "" { + config.BaseURL = proxyURL + } + client := openai.NewClientWithConfig(config) + return &Swarm{ + client: client, + } +} + // getChatCompletion requests a chat completion from the OpenAI API func (s *Swarm) getChatCompletion( ctx context.Context, @@ -41,7 +54,7 @@ func (s *Swarm) getChatCompletion( // Prepare the initial system message with agent instructions instructions := agent.Instructions if agent.InstructionsFunc != nil { - instructions = agent.InstructionsFunc(contextVariables) + instructions += agent.InstructionsFunc(contextVariables) } messages := append([]openai.ChatCompletionMessage{ { @@ -50,11 +63,14 @@ func (s *Swarm) getChatCompletion( }, }, history...) - // Build function definitions from agent's functions - var functionDefs []openai.FunctionDefinition + // Build function tools definitions from agent's functions + var tools []openai.Tool for _, af := range agent.Functions { def := FunctionToDefinition(af) - functionDefs = append(functionDefs, def) + tools = append(tools, openai.Tool{ + Type: openai.ToolTypeFunction, + Function: def, + }) } // Prepare the chat completion request @@ -63,9 +79,9 @@ func (s *Swarm) getChatCompletion( model = modelOverride } req := openai.ChatCompletionRequest{ - Model: model, - Messages: messages, - Functions: functionDefs, + Model: model, + Messages: messages, + Tools: tools, } if debug { @@ -84,68 +100,80 @@ func (s *Swarm) getChatCompletion( // handleFunctionCall processes a function call from the chat completion func (s *Swarm) handleFunctionCall( ctx context.Context, - functionCall *openai.FunctionCall, + ToolCalls []openai.ToolCall, agent *Agent, contextVariables map[string]interface{}, debug bool, ) (Response, error) { - functionName := functionCall.Name - argsJSON := functionCall.Arguments - // Parse the function call arguments - var args map[string]interface{} - if err := json.Unmarshal([]byte(argsJSON), &args); err != nil { - return Response{}, err - } + var functionResultMessage []openai.ChatCompletionMessage + var wg sync.WaitGroup - if debug { - log.Printf("Processing function call: %s with arguments %v\n", functionName, args) - } + for _, call := range ToolCalls { + functionCall := call.Function - // Find the corresponding function in the agent's functions - var functionFound *AgentFunction - for _, af := range agent.Functions { - if af.Name == functionName { - functionFound = &af - break + functionName := functionCall.Name + argsJSON := functionCall.Arguments + + // Parse the function call arguments + var args map[string]interface{} + if err := json.Unmarshal([]byte(argsJSON), &args); err != nil { + return Response{}, err } - } - // Handle case where function is not found - if functionFound == nil { - errorMessage := fmt.Sprintf("Error: Tool %s not found.", functionName) if debug { - log.Println(errorMessage) + log.Printf("Processing function call: %s with arguments %v\n", functionName, args) } - return Response{ - Messages: []openai.ChatCompletionMessage{ - { - Role: "tool", - Name: functionName, - Content: errorMessage, - }, - }, - }, nil - } - // Execute the function and update context variables - result := functionFound.Function(args, contextVariables) - for k, v := range result.ContextVariables { - contextVariables[k] = v - } + // Find the corresponding function in the agent's functions + var functionFound *AgentFunction + for _, af := range agent.Functions { + if af.Name == functionName { + functionFound = &af + break + } + } - // Create a message with the function result - functionResultMessage := openai.ChatCompletionMessage{ - Role: openai.ChatMessageRoleFunction, - Name: functionName, - Content: result.Value, - } + // Handle case where function is not found + if functionFound == nil { + errorMessage := fmt.Sprintf("Error: Tool %s not found.", functionName) + if debug { + log.Println(errorMessage) + } + return Response{ + Messages: []openai.ChatCompletionMessage{ + { + Role: "tool", + Name: functionName, + Content: errorMessage, + }, + }, + }, nil + } + // Execute the function and update context variables using a goroutine for asynchronous execution + wg.Add(1) + go func(args map[string]interface{}, contextVariables map[string]interface{}, call openai.ToolCall) { + defer wg.Done() + result := functionFound.Function(args, contextVariables) + for k, v := range result.ContextVariables { + contextVariables[k] = v + } + // Create a message with the function result + functionResultMessage = append(functionResultMessage, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleTool, + Name: functionName, + Content: result.Value, + ToolCallID: call.ID, + }) + }(args, contextVariables, call) + } + wg.Wait() // Return the partial response with the function result partialResponse := Response{ - Messages: []openai.ChatCompletionMessage{functionResultMessage}, - Agent: result.Agent, - ContextVariables: result.ContextVariables, + Messages: functionResultMessage, + Agent: agent, + ContextVariables: contextVariables, } return partialResponse, nil @@ -209,71 +237,65 @@ func (s *Swarm) Run( history = append(history, message) // Handle function calls if any - for { - if message.FunctionCall != nil && executeTools { - // Process the function call - partialResponse, err := s.handleFunctionCall( - ctx, - message.FunctionCall, - activeAgent, - contextVariables, - debug, - ) - if err != nil { - return Response{}, err - } + if message.ToolCalls != nil && executeTools { + // Process the function call + partialResponse, err := s.handleFunctionCall( + ctx, + message.ToolCalls, + activeAgent, + contextVariables, + debug, + ) + if err != nil { + return Response{}, err + } - history = append(history, partialResponse.Messages...) - for k, v := range partialResponse.ContextVariables { - contextVariables[k] = v - } - if partialResponse.Agent != nil { - activeAgent = partialResponse.Agent - } + history = append(history, partialResponse.Messages...) + for k, v := range partialResponse.ContextVariables { + contextVariables[k] = v + } + if partialResponse.Agent != nil { + activeAgent = partialResponse.Agent + } - // Get the assistant's response after function result - resp, err := s.getChatCompletion( - ctx, - activeAgent, - history, - contextVariables, - modelOverride, - stream, - debug, - ) - if err != nil { - return Response{}, err - } + // Get the assistant's response after function result + resp, err := s.getChatCompletion( + ctx, + activeAgent, + history, + contextVariables, + modelOverride, + stream, + debug, + ) + if err != nil { + return Response{}, err + } - if len(resp.Choices) == 0 { - return Response{}, fmt.Errorf("no choices in response") - } + if len(resp.Choices) == 0 { + return Response{}, fmt.Errorf("no choices in response") + } - choice = resp.Choices[0] - message = choice.Message + choice = resp.Choices[0] + message = choice.Message - if debug { - log.Printf("Received completion: %+v\n", message) - } + if debug { + log.Printf("Received completion: %+v\n", message) + } - message.Role = openai.ChatMessageRoleAssistant - message.Name = activeAgent.Name + message.Role = openai.ChatMessageRoleAssistant + message.Name = activeAgent.Name - history = append(history, message) + history = append(history, message) - } else { - // Exit the loop if no more function calls + // Break the outer loop if the assistant didn't make a function call + if message.FunctionCall == nil || !executeTools { + if debug { + log.Println("Ending turn.") + } break } } - - // Break the outer loop if the assistant didn't make a function call - if message.FunctionCall == nil || !executeTools { - if debug { - log.Println("Ending turn.") - } - break - } } // Return the final response diff --git a/util.go b/util.go index ce97f8f..2988103 100644 --- a/util.go +++ b/util.go @@ -8,8 +8,8 @@ import ( // FunctionToDefinition converts an AgentFunction to an openai.FunctionDefinition. // It directly assigns the Name, Description, and Parameters from the AgentFunction. -func FunctionToDefinition(af AgentFunction) openai.FunctionDefinition { - return openai.FunctionDefinition{ +func FunctionToDefinition(af AgentFunction) *openai.FunctionDefinition { + return &openai.FunctionDefinition{ Name: af.Name, Description: af.Description, Parameters: af.Parameters, // Assign directly without marshaling From 9a51e9d517effe95f2a7bd8fba6df2e21b92a2b5 Mon Sep 17 00:00:00 2001 From: xiaoxiong Date: Fri, 1 Nov 2024 16:03:30 +0800 Subject: [PATCH 2/2] use tools function call --- swarm.go | 5 ++++- types.go | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/swarm.go b/swarm.go index e3f3927..3fd166e 100644 --- a/swarm.go +++ b/swarm.go @@ -289,18 +289,21 @@ func (s *Swarm) Run( history = append(history, message) // Break the outer loop if the assistant didn't make a function call - if message.FunctionCall == nil || !executeTools { + if message.ToolCalls == nil || !executeTools { if debug { log.Println("Ending turn.") } break } + } else { + break } } // Return the final response return Response{ Messages: history[initLen:], + History: history, Agent: activeAgent, ContextVariables: contextVariables, }, nil diff --git a/types.go b/types.go index 91b73ec..7ec0e65 100644 --- a/types.go +++ b/types.go @@ -7,6 +7,7 @@ import ( // Response represents a response from an agent, including messages and context type Response struct { Messages []openai.ChatCompletionMessage // List of chat messages in the response + History []openai.ChatCompletionMessage // List of chat messages in the response Agent *Agent // Reference to the agent that generated the response ContextVariables map[string]interface{} // Additional context variables associated with the response }