diff --git a/go/go.mod b/go/go.mod index 80861ddb..1e063791 100644 --- a/go/go.mod +++ b/go/go.mod @@ -11,10 +11,14 @@ require ( ) require ( + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/tidwall/gjson v1.14.4 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/text v0.21.0 // indirect ) diff --git a/go/go.sum b/go/go.sum index 2e79888c..66db2f13 100644 --- a/go/go.sum +++ b/go/go.sum @@ -1,3 +1,11 @@ +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -5,16 +13,22 @@ github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/openai/openai-go/v3 v3.6.1 h1:f8J6jhT9wkYnNvHTKR7bxHXSZrSvvcfpHGkmBra04tI= github.com/openai/openai-go/v3 v3.6.1/go.mod h1:UOpNxkqC9OdNXNUfpNByKOtB4jAL0EssQXq5p8gO0Xs= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -31,5 +45,13 @@ go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGX go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go/pkg/openai/openai.go b/go/pkg/openai/openai.go index 1dc52499..34c74584 100644 --- a/go/pkg/openai/openai.go +++ b/go/pkg/openai/openai.go @@ -9,6 +9,7 @@ import ( "github.com/microsoft/agent-framework/go/pkg/agent" "github.com/microsoft/agent-framework/go/pkg/agent/chat" "github.com/openai/openai-go/v3" + "github.com/openai/openai-go/v3/azure" "github.com/openai/openai-go/v3/option" "github.com/openai/openai-go/v3/packages/param" "github.com/openai/openai-go/v3/shared" @@ -76,7 +77,12 @@ func (c *ChatClient) CompleteStream(ctx context.Context, options *chat.Options, return func(yield func(*chat.ResponseUpdate, error) bool) { defer stream.Close() for stream.Next() { - choice := stream.Current().Choices[0] + current := stream.Current() + // Skip if no choices are available (common in streaming responses) + if len(current.Choices) == 0 { + continue + } + choice := current.Choices[0] resp := &chat.ResponseUpdate{ Delta: chat.NewMessage(agent.Role(choice.Delta.Role), choice.Delta.Content), FinishReason: agent.FinishReason(choice.FinishReason), @@ -139,42 +145,140 @@ func (c *ChatClient) buildCompletionParams(options *chat.Options, messages ...*c // AzureOpenAIChatClient is a ChatClient implementation for Azure OpenAI. type AzureOpenAIChatClient struct { *chat.BaseChatClient - apiKey string - endpoint string - deploymentName string + client *openai.Client } // AzureOpenAIChatClientConfig contains configuration for AzureOpenAIChatClient. type AzureOpenAIChatClientConfig struct { - APIKey string - Endpoint string - DeploymentName string - Model string + APIKey string // Optional, if not set will use Azure authentication + Endpoint string // Azure OpenAI endpoint (e.g., https://your-resource.openai.azure.com/) + DeploymentName string // Deployment name for the model + APIVersion string // Optional, defaults to latest API version } // NewAzureOpenAIChatClient creates a new AzureOpenAIChatClient. -func NewAzureOpenAIChatClient(config AzureOpenAIChatClientConfig) (*AzureOpenAIChatClient, error) { +func NewAzureOpenAIChatClient(config AzureOpenAIChatClientConfig) *AzureOpenAIChatClient { + ops := make([]option.RequestOption, 0, 3) + + // Set API version for Azure OpenAI + apiVersion := config.APIVersion + if apiVersion == "" { + // The latest API versions, including previews, can be found here: + // https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#rest-api-versioning + apiVersion = "2025-01-01-preview" // Default to latest stable version + } + + // Configure Azure OpenAI specific settings + ops = append(ops, azure.WithEndpoint(config.Endpoint, apiVersion)) + + // Configure API key if provided + if config.APIKey != "" { + ops = append(ops, azure.WithAPIKey(config.APIKey)) + } + + // Create Azure OpenAI client + client := openai.NewClient(ops...) return &AzureOpenAIChatClient{ - BaseChatClient: chat.NewBaseChatClient(config.Model), - apiKey: config.APIKey, - endpoint: config.Endpoint, - deploymentName: config.DeploymentName, - }, nil + BaseChatClient: chat.NewBaseChatClient(config.DeploymentName), + client: &client, + } +} + +// NewAgent creates a new agent that uses this Azure OpenAI chat client. +func (c *AzureOpenAIChatClient) NewAgent(instructions string) agent.Agent[*chat.Message] { + return chat.New(chat.Config{ + Name: "Azure OpenAI Chat Agent", + Instructions: instructions, + Client: c, + }) } // Complete generates a single response for the given messages. func (c *AzureOpenAIChatClient) Complete(ctx context.Context, options *chat.Options, messages ...*chat.Message) (*chat.Response, error) { - // TODO: Implement Azure OpenAI API call + resp, err := c.client.Chat.Completions.New(ctx, c.buildCompletionParams(options, messages...)) + if err != nil { + return nil, err + } + choice := resp.Choices[0] + if len(choice.Message.ToolCalls) > 0 { + // Handle tool calls + + } return &chat.Response{ - Message: chat.NewMessage("assistant", "Not implemented"), - FinishReason: "stop", - ModelID: c.ModelID, + Message: chat.NewMessage(agent.Role(choice.Message.Role), choice.Message.Content), + FinishReason: agent.FinishReason(choice.FinishReason), + ModelID: resp.Model, }, nil } // CompleteStream generates a streaming response for the given messages. func (c *AzureOpenAIChatClient) CompleteStream(ctx context.Context, options *chat.Options, messages ...*chat.Message) iter.Seq2[*chat.ResponseUpdate, error] { + stream := c.client.Chat.Completions.NewStreaming(ctx, c.buildCompletionParams(options, messages...)) return func(yield func(*chat.ResponseUpdate, error) bool) { - // TODO: Implement Azure OpenAI streaming API call + defer stream.Close() + for stream.Next() { + current := stream.Current() + // Skip if no choices are available (common in streaming responses) + if len(current.Choices) == 0 { + continue + } + choice := current.Choices[0] + resp := &chat.ResponseUpdate{ + Delta: chat.NewMessage(agent.Role(choice.Delta.Role), choice.Delta.Content), + FinishReason: agent.FinishReason(choice.FinishReason), + } + if !yield(resp, nil) { + return + } + } + if stream.Err() != nil { + yield(nil, stream.Err()) + } + } +} + +// buildCompletionParams constructs the parameters for the Azure OpenAI chat completion API. +// This method is similar to the regular OpenAI client but uses the deployment name as the model. +func (c *AzureOpenAIChatClient) buildCompletionParams(options *chat.Options, messages ...*chat.Message) openai.ChatCompletionNewParams { + params := openai.ChatCompletionNewParams{ + Model: c.ModelID, // For Azure OpenAI, this is the deployment name + N: openai.Int(1), + Messages: make([]openai.ChatCompletionMessageParamUnion, 0, len(messages)), + } + for _, msg := range messages { + // TODO: support roles, content types, and multiple messages + params.Messages = append(params.Messages, openai.ChatCompletionMessageParamUnion{ + OfUser: &openai.ChatCompletionUserMessageParam{ + Content: openai.ChatCompletionUserMessageParamContentUnion{ + OfString: param.NewOpt(msg.Text()), + }, + }, + }) + } + if options != nil { + if options.Temperature != nil { + params.Temperature = openai.Float(*options.Temperature) + } + if options.TopP != nil { + params.TopP = openai.Float(*options.TopP) + } + if options.MaxTokens != nil { + params.MaxTokens = openai.Int(int64(*options.MaxTokens)) + } + for _, tool := range options.Tools { + params.Tools = append(params.Tools, openai.ChatCompletionToolUnionParam{ + OfFunction: &openai.ChatCompletionFunctionToolParam{ + Function: shared.FunctionDefinitionParam{ + Name: tool.Name, + Description: param.NewOpt(tool.Description), + Parameters: tool.Schema, + }, + }, + }) + } + params.ToolChoice = openai.ChatCompletionToolChoiceOptionUnionParam{ + OfAuto: openai.String(string(options.ToolMode)), + } } + return params } diff --git a/go/samples/getting_started/azure_openai/azure_openai_chat_client_basic.go b/go/samples/getting_started/azure_openai/azure_openai_chat_client_basic.go new file mode 100644 index 00000000..8ba64cbe --- /dev/null +++ b/go/samples/getting_started/azure_openai/azure_openai_chat_client_basic.go @@ -0,0 +1,55 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/microsoft/agent-framework/go/pkg/agent" + "github.com/microsoft/agent-framework/go/pkg/agent/chat" + "github.com/microsoft/agent-framework/go/pkg/openai" +) + +func main() { + // Azure OpenAI configuration + // You can also set these via environment variables: + // - AZURE_OPENAI_API_KEY + // - AZURE_OPENAI_ENDPOINT + // - AZURE_OPENAI_DEPLOYMENT_NAME + client := openai.NewAzureOpenAIChatClient(openai.AzureOpenAIChatClientConfig{ + APIKey: os.Getenv("AZURE_OPENAI_API_KEY"), // or set directly + Endpoint: os.Getenv("AZURE_OPENAI_ENDPOINT"), // e.g., "https://your-resource.openai.azure.com/" + DeploymentName: os.Getenv("AZURE_OPENAI_DEPLOYMENT_NAME"), // e.g., "gpt-4o" + APIVersion: "2025-01-01-preview", // optional, uses default if not specified + }) + + ag := client.NewAgent("You are a helpful weather agent.") + + nonStreamingExample(ag, "What's the weather like in Seattle?") + streamingExample(ag, "What's the weather like in Portland?") +} + +func nonStreamingExample(ag agent.Agent[*chat.Message], query string) { + ctx := context.Background() + log.Printf("=== Non-streaming Response Example ===\n") + log.Printf("User: %s\n", query) + resp, err := ag.Run(ctx, nil, nil, chat.NewMessage(agent.RoleUser, query)) + if err != nil { + log.Fatal(err) + } + log.Printf("Result: %s\n", resp.Message.Text()) +} + +func streamingExample(ag agent.Agent[*chat.Message], query string) { + ctx := context.Background() + log.Printf("=== Streaming Response Example ===\n") + log.Printf("User: %s\n", query) + stream := agent.RunStream(ctx, ag, nil, nil, chat.NewMessage(agent.RoleUser, query)) + for update := range stream { + if update.Delta != nil { + fmt.Print(update.Delta.Text()) + } + } + fmt.Print("\n") +} diff --git a/go/samples/getting_started/openai/openai_chat_client_basic.go b/go/samples/getting_started/openai/openai_chat_client_basic.go index 5dd10166..f74334b7 100644 --- a/go/samples/getting_started/openai/openai_chat_client_basic.go +++ b/go/samples/getting_started/openai/openai_chat_client_basic.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "os" "github.com/microsoft/agent-framework/go/pkg/agent" "github.com/microsoft/agent-framework/go/pkg/agent/chat" @@ -11,8 +12,17 @@ import ( ) func main() { + // OpenAI configuration + // Set your API key via environment variable: export OPENAI_API_KEY=your-key-here + // Or get one from: https://platform.openai.com/account/api-keys + apiKey := os.Getenv("OPENAI_API_KEY") + if apiKey == "" { + log.Fatal("OPENAI_API_KEY environment variable is required. Get your key from https://platform.openai.com/account/api-keys") + } + client := openai.NewChatClient(openai.ChatClientConfig{ - Model: "gpt-5-nano", + Model: "gpt-5-nano", + APIKey: apiKey, }) ag := client.NewAgent("You are a helpful weather agent.")