Merge branch 'main' into filinto/fix-tool-call-echo
This commit is contained in:
commit
c92a64d804
|
@ -62,7 +62,7 @@ func (a *AzureServiceBusQueues) Init(ctx context.Context, metadata bindings.Meta
|
|||
return err
|
||||
}
|
||||
|
||||
a.client, err = impl.NewClient(a.metadata, metadata.Properties)
|
||||
a.client, err = impl.NewClient(a.metadata, metadata.Properties, a.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -110,13 +110,7 @@ func (g *GCPStorage) Init(ctx context.Context, metadata bindings.Metadata) error
|
|||
return err
|
||||
}
|
||||
|
||||
b, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
clientOptions := option.WithCredentialsJSON(b)
|
||||
client, err := storage.NewClient(ctx, clientOptions)
|
||||
client, err := g.getClient(ctx, m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -127,6 +121,41 @@ func (g *GCPStorage) Init(ctx context.Context, metadata bindings.Metadata) error
|
|||
return nil
|
||||
}
|
||||
|
||||
func (g *GCPStorage) getClient(ctx context.Context, m *gcpMetadata) (*storage.Client, error) {
|
||||
var client *storage.Client
|
||||
var err error
|
||||
|
||||
if m.Bucket == "" {
|
||||
return nil, errors.New("missing property `bucket` in metadata")
|
||||
}
|
||||
if m.ProjectID == "" {
|
||||
return nil, errors.New("missing property `project_id` in metadata")
|
||||
}
|
||||
|
||||
// Explicit authentication
|
||||
if m.PrivateKeyID != "" {
|
||||
var b []byte
|
||||
b, err = json.Marshal(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clientOptions := option.WithCredentialsJSON(b)
|
||||
client, err = storage.NewClient(ctx, clientOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// Implicit authentication, using GCP Application Default Credentials (ADC)
|
||||
// Credentials search order: https://cloud.google.com/docs/authentication/application-default-credentials#order
|
||||
client, err = storage.NewClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (g *GCPStorage) parseMetadata(meta bindings.Metadata) (*gcpMetadata, error) {
|
||||
m := gcpMetadata{}
|
||||
err := kitmd.DecodeMetadata(meta.Properties, &m)
|
||||
|
|
|
@ -15,6 +15,7 @@ package bucket
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -234,6 +235,30 @@ func TestMergeWithRequestMetadata(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestInit(t *testing.T) {
|
||||
t.Run("Init missing bucket from metadata", func(t *testing.T) {
|
||||
m := bindings.Metadata{}
|
||||
m.Properties = map[string]string{
|
||||
"projectID": "my_project_id",
|
||||
}
|
||||
gs := GCPStorage{logger: logger.NewLogger("test")}
|
||||
err := gs.Init(t.Context(), m)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, err, errors.New("missing property `bucket` in metadata"))
|
||||
})
|
||||
|
||||
t.Run("Init missing projectID from metadata", func(t *testing.T) {
|
||||
m := bindings.Metadata{}
|
||||
m.Properties = map[string]string{
|
||||
"bucket": "my_bucket",
|
||||
}
|
||||
gs := GCPStorage{logger: logger.NewLogger("test")}
|
||||
err := gs.Init(t.Context(), m)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, err, errors.New("missing property `project_id` in metadata"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetOption(t *testing.T) {
|
||||
gs := GCPStorage{logger: logger.NewLogger("test")}
|
||||
gs.metadata = &gcpMetadata{}
|
||||
|
|
|
@ -42,6 +42,30 @@ authenticationProfiles:
|
|||
secret reference
|
||||
example: "KeFg23!"
|
||||
default: ""
|
||||
- name: sentinelUsername
|
||||
type: string
|
||||
required: false
|
||||
description: |
|
||||
Username for Redis Sentinel. Applicable only when "failover" is true, and
|
||||
Redis Sentinel has authentication enabled. Defaults to empty.
|
||||
example: "my-sentinel-username"
|
||||
default: ""
|
||||
url:
|
||||
title: "Redis Sentinel authentication documentation"
|
||||
url: "https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/#configuring-sentinel-instances-with-authentication"
|
||||
- name: sentinelPassword
|
||||
type: string
|
||||
required: false
|
||||
sensitive: true
|
||||
description: |
|
||||
Password for Redis Sentinel. Applicable only when "failover" is true, and
|
||||
Redis Sentinel has authentication enabled. Use secretKeyRef for
|
||||
secret reference. Defaults to empty.
|
||||
example: "KeFg23!"
|
||||
default: ""
|
||||
url:
|
||||
title: "Redis Sentinel authentication documentation"
|
||||
url: "https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/#configuring-sentinel-instances-with-authentication"
|
||||
metadata:
|
||||
- name: redisHost
|
||||
required: true
|
||||
|
|
|
@ -16,6 +16,7 @@ package servicebus
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
@ -40,7 +41,7 @@ type Client struct {
|
|||
}
|
||||
|
||||
// NewClient creates a new Client object.
|
||||
func NewClient(metadata *Metadata, rawMetadata map[string]string) (*Client, error) {
|
||||
func NewClient(metadata *Metadata, rawMetadata map[string]string, log logger.Logger) (*Client, error) {
|
||||
client := &Client{
|
||||
metadata: metadata,
|
||||
lock: &sync.RWMutex{},
|
||||
|
@ -86,9 +87,17 @@ func NewClient(metadata *Metadata, rawMetadata map[string]string) (*Client, erro
|
|||
}
|
||||
|
||||
if !metadata.DisableEntityManagement {
|
||||
client.adminClient, err = sbadmin.NewClient(metadata.NamespaceName, token, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if isAzureEmulator(metadata.ConnectionString) {
|
||||
log.Warn(
|
||||
"UseDevelopmentEmulator=true detected in connection string. " +
|
||||
"Azure emulator does not support topic management APIs. " +
|
||||
"Dapr will skip admin operations. " +
|
||||
"To suppress this warning, explicitly set disableEntityManagement: true.")
|
||||
} else {
|
||||
client.adminClient, err = sbadmin.NewClient(metadata.NamespaceName, token, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -394,3 +403,7 @@ func notEqual(a, b *bool) bool {
|
|||
}
|
||||
return *a != *b
|
||||
}
|
||||
|
||||
func isAzureEmulator(connectionString string) bool {
|
||||
return strings.Contains(strings.ToLower(connectionString), "usedevelopmentemulator=true")
|
||||
}
|
||||
|
|
|
@ -25,6 +25,8 @@ const (
|
|||
host = "redisHost"
|
||||
password = "redisPassword"
|
||||
username = "redisUsername"
|
||||
sentinelUsername = "sentinelUsername"
|
||||
sentinelPassword = "sentinelPassword"
|
||||
db = "redisDB"
|
||||
redisType = "redisType"
|
||||
redisMaxRetries = "redisMaxRetries"
|
||||
|
@ -51,6 +53,8 @@ func getFakeProperties() map[string]string {
|
|||
host: "fake.redis.com",
|
||||
password: "fakePassword",
|
||||
username: "fakeUsername",
|
||||
sentinelUsername: "fakeSentinelUsername",
|
||||
sentinelPassword: "fakeSentinelPassword",
|
||||
redisType: "node",
|
||||
enableTLS: "true",
|
||||
clientCert: "fakeCert",
|
||||
|
@ -86,6 +90,8 @@ func TestParseRedisMetadata(t *testing.T) {
|
|||
assert.Equal(t, fakeProperties[host], m.Host)
|
||||
assert.Equal(t, fakeProperties[password], m.Password)
|
||||
assert.Equal(t, fakeProperties[username], m.Username)
|
||||
assert.Equal(t, fakeProperties[sentinelUsername], m.SentinelUsername)
|
||||
assert.Equal(t, fakeProperties[sentinelPassword], m.SentinelPassword)
|
||||
assert.Equal(t, fakeProperties[redisType], m.RedisType)
|
||||
assert.True(t, m.EnableTLS)
|
||||
assert.Equal(t, fakeProperties[clientCert], m.ClientCert)
|
||||
|
|
|
@ -29,6 +29,10 @@ type Settings struct {
|
|||
Password string `mapstructure:"redisPassword"`
|
||||
// The Redis username
|
||||
Username string `mapstructure:"redisUsername"`
|
||||
// The Redis Sentinel password
|
||||
SentinelPassword string `mapstructure:"sentinelPassword"`
|
||||
// The Redis Sentinel username
|
||||
SentinelUsername string `mapstructure:"sentinelUsername"`
|
||||
// Database to be selected after connecting to the server.
|
||||
DB int `mapstructure:"redisDB"`
|
||||
// The redis type node or cluster
|
||||
|
|
|
@ -330,6 +330,8 @@ func newV8FailoverClient(s *Settings) (RedisClient, error) {
|
|||
DB: s.DB,
|
||||
MasterName: s.SentinelMasterName,
|
||||
SentinelAddrs: []string{s.Host},
|
||||
SentinelUsername: s.SentinelUsername,
|
||||
SentinelPassword: s.SentinelPassword,
|
||||
Password: s.Password,
|
||||
Username: s.Username,
|
||||
MaxRetries: s.RedisMaxRetries,
|
||||
|
|
|
@ -330,6 +330,8 @@ func newV9FailoverClient(s *Settings) (RedisClient, error) {
|
|||
DB: s.DB,
|
||||
MasterName: s.SentinelMasterName,
|
||||
SentinelAddrs: []string{s.Host},
|
||||
SentinelUsername: s.SentinelUsername,
|
||||
SentinelPassword: s.SentinelPassword,
|
||||
Password: s.Password,
|
||||
Username: s.Username,
|
||||
MaxRetries: s.RedisMaxRetries,
|
||||
|
|
|
@ -30,6 +30,30 @@ authenticationProfiles:
|
|||
secret reference
|
||||
example: "KeFg23!"
|
||||
default: ""
|
||||
- name: sentinelUsername
|
||||
type: string
|
||||
required: false
|
||||
description: |
|
||||
Username for Redis Sentinel. Applicable only when "failover" is true, and
|
||||
Redis Sentinel has authentication enabled. Defaults to empty.
|
||||
example: "my-sentinel-username"
|
||||
default: ""
|
||||
url:
|
||||
title: "Redis Sentinel authentication documentation"
|
||||
url: "https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/#configuring-sentinel-instances-with-authentication"
|
||||
- name: sentinelPassword
|
||||
type: string
|
||||
required: false
|
||||
sensitive: true
|
||||
description: |
|
||||
Password for Redis Sentinel. Applicable only when "failover" is true, and
|
||||
Redis Sentinel has authentication enabled. Use secretKeyRef for
|
||||
secret reference. Defaults to empty.
|
||||
example: "KeFg23!"
|
||||
default: ""
|
||||
url:
|
||||
title: "Redis Sentinel authentication documentation"
|
||||
url: "https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/#configuring-sentinel-instances-with-authentication"
|
||||
metadata:
|
||||
- name: redisHost
|
||||
required: true
|
||||
|
|
|
@ -244,6 +244,8 @@ func Test_parseRedisMetadata(t *testing.T) {
|
|||
testProperties := make(map[string]string)
|
||||
testProperties["redisHost"] = "testHost"
|
||||
testProperties["redisPassword"] = "testPassword"
|
||||
testProperties["sentinelUsername"] = "testSentinelUsername"
|
||||
testProperties["sentinelPassword"] = "testSentinelPassword"
|
||||
testProperties["enableTLS"] = "true"
|
||||
testProperties["redisMaxRetries"] = "10"
|
||||
testProperties["redisMaxRetryInterval"] = "100ms"
|
||||
|
@ -254,6 +256,8 @@ func Test_parseRedisMetadata(t *testing.T) {
|
|||
testSettings := redisComponent.Settings{
|
||||
Host: "testHost",
|
||||
Password: "testPassword",
|
||||
SentinelUsername: "testSentinelUsername",
|
||||
SentinelPassword: "testSentinelPassword",
|
||||
EnableTLS: true,
|
||||
RedisMaxRetries: 10,
|
||||
RedisMaxRetryInterval: redisComponent.Duration(100 * time.Millisecond),
|
||||
|
@ -268,6 +272,8 @@ func Test_parseRedisMetadata(t *testing.T) {
|
|||
defaultSettings := redisComponent.Settings{
|
||||
Host: "testHost",
|
||||
Password: "",
|
||||
SentinelUsername: "",
|
||||
SentinelPassword: "",
|
||||
EnableTLS: false,
|
||||
RedisMaxRetries: 3,
|
||||
RedisMaxRetryInterval: redisComponent.Duration(time.Second * 2),
|
||||
|
@ -311,6 +317,8 @@ func Test_parseRedisMetadata(t *testing.T) {
|
|||
}
|
||||
assert.Equal(t, tt.want.Host, got.Host)
|
||||
assert.Equal(t, tt.want.Password, got.Password)
|
||||
assert.Equal(t, tt.want.SentinelUsername, got.SentinelUsername)
|
||||
assert.Equal(t, tt.want.SentinelPassword, got.SentinelPassword)
|
||||
assert.Equal(t, tt.want.EnableTLS, got.EnableTLS)
|
||||
assert.Equal(t, tt.want.RedisMaxRetries, got.RedisMaxRetries)
|
||||
assert.Equal(t, tt.want.RedisMaxRetryInterval, got.RedisMaxRetryInterval)
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
Copyright 2025 The Dapr Authors
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package openai
|
||||
|
||||
import "github.com/dapr/components-contrib/conversation"
|
||||
|
||||
// OpenAILangchainMetadata extends LangchainMetadata with OpenAI-specific properties.
|
||||
type OpenAILangchainMetadata struct {
|
||||
conversation.LangchainMetadata `json:",inline" mapstructure:",squash"`
|
||||
APIType string `json:"apiType" mapstructure:"apiType"`
|
||||
APIVersion string `json:"apiVersion" mapstructure:"apiVersion"`
|
||||
}
|
|
@ -40,3 +40,17 @@ metadata:
|
|||
A time-to-live value for a prompt cache to expire. Uses Golang durations
|
||||
type: string
|
||||
example: '10m'
|
||||
- name: apiVersion
|
||||
required: false
|
||||
description: |
|
||||
The API version to use for the Azure OpenAI service. This is required when using Azure OpenAI.
|
||||
type: string
|
||||
example: '2025-01-01-preview'
|
||||
default: ''
|
||||
- name: apiType
|
||||
required: false
|
||||
description: |
|
||||
The type of API to use for the OpenAI service. This is required when using Azure OpenAI.
|
||||
type: string
|
||||
example: 'azure'
|
||||
default: ''
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
Copyright 2025 The Dapr Authors
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package openai
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/dapr/components-contrib/conversation"
|
||||
)
|
||||
|
||||
func TestOpenaiLangchainMetadata(t *testing.T) {
|
||||
t.Run("json marshaling with endpoint", func(t *testing.T) {
|
||||
metadata := OpenAILangchainMetadata{
|
||||
LangchainMetadata: conversation.LangchainMetadata{
|
||||
Key: "test-key",
|
||||
Model: "gpt-4",
|
||||
CacheTTL: "10m",
|
||||
Endpoint: "https://custom-endpoint.openai.azure.com/",
|
||||
},
|
||||
APIType: "azure",
|
||||
APIVersion: "2025-01-01-preview",
|
||||
}
|
||||
|
||||
bytes, err := json.Marshal(metadata)
|
||||
require.NoError(t, err)
|
||||
|
||||
var unmarshaled OpenAILangchainMetadata
|
||||
err = json.Unmarshal(bytes, &unmarshaled)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, metadata.Key, unmarshaled.Key)
|
||||
assert.Equal(t, metadata.Model, unmarshaled.Model)
|
||||
assert.Equal(t, metadata.CacheTTL, unmarshaled.CacheTTL)
|
||||
assert.Equal(t, metadata.Endpoint, unmarshaled.Endpoint)
|
||||
assert.Equal(t, metadata.APIType, unmarshaled.APIType)
|
||||
assert.Equal(t, metadata.APIVersion, unmarshaled.APIVersion)
|
||||
})
|
||||
|
||||
t.Run("json unmarshaling with endpoint", func(t *testing.T) {
|
||||
jsonStr := `{"key": "test-key", "model": "gpt-4", "endpoint": "https://custom-endpoint.openai.azure.com/", "apiType": "azure", "apiVersion": "2025-01-01-preview"}`
|
||||
|
||||
var metadata OpenAILangchainMetadata
|
||||
err := json.Unmarshal([]byte(jsonStr), &metadata)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "test-key", metadata.Key)
|
||||
assert.Equal(t, "gpt-4", metadata.Model)
|
||||
assert.Equal(t, "https://custom-endpoint.openai.azure.com/", metadata.Endpoint)
|
||||
assert.Equal(t, "azure", metadata.APIType)
|
||||
assert.Equal(t, "2025-01-01-preview", metadata.APIVersion)
|
||||
})
|
||||
}
|
|
@ -16,6 +16,7 @@ package openai
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
|
||||
"github.com/dapr/components-contrib/conversation"
|
||||
|
@ -44,7 +45,7 @@ func NewOpenAI(logger logger.Logger) conversation.Conversation {
|
|||
const defaultModel = "gpt-4o"
|
||||
|
||||
func (o *OpenAI) Init(ctx context.Context, meta conversation.Metadata) error {
|
||||
md := conversation.LangchainMetadata{}
|
||||
md := OpenAILangchainMetadata{}
|
||||
err := kmeta.DecodeMetadata(meta.Properties, &md)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -65,6 +66,14 @@ func (o *OpenAI) Init(ctx context.Context, meta conversation.Metadata) error {
|
|||
options = append(options, openai.WithBaseURL(md.Endpoint))
|
||||
}
|
||||
|
||||
if md.APIType == "azure" {
|
||||
if md.Endpoint == "" || md.APIVersion == "" {
|
||||
return errors.New("endpoint and apiVersion must be provided when apiType is set to 'azure'")
|
||||
}
|
||||
|
||||
options = append(options, openai.WithAPIType(openai.APITypeAzure), openai.WithAPIVersion(md.APIVersion))
|
||||
}
|
||||
|
||||
llm, err := openai.New(options...)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -84,7 +93,7 @@ func (o *OpenAI) Init(ctx context.Context, meta conversation.Metadata) error {
|
|||
}
|
||||
|
||||
func (o *OpenAI) GetComponentMetadata() (metadataInfo metadata.MetadataMap) {
|
||||
metadataStruct := conversation.LangchainMetadata{}
|
||||
metadataStruct := OpenAILangchainMetadata{}
|
||||
metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.ConversationType)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -55,8 +55,47 @@ func TestInit(t *testing.T) {
|
|||
// we're mainly testing that initialization succeeds
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with apiType azure and missing apiVersion",
|
||||
metadata: map[string]string{
|
||||
"key": "test-key",
|
||||
"model": "gpt-4",
|
||||
"apiType": "azure",
|
||||
"endpoint": "https://custom-endpoint.openai.azure.com/",
|
||||
},
|
||||
testFn: func(t *testing.T, o *OpenAI, err error) {
|
||||
require.Error(t, err)
|
||||
assert.EqualError(t, err, "endpoint and apiVersion must be provided when apiType is set to 'azure'")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with apiType azure and custom apiVersion",
|
||||
metadata: map[string]string{
|
||||
"key": "test-key",
|
||||
"model": "gpt-4",
|
||||
"apiType": "azure",
|
||||
"endpoint": "https://custom-endpoint.openai.azure.com/",
|
||||
"apiVersion": "2025-01-01-preview",
|
||||
},
|
||||
testFn: func(t *testing.T, o *OpenAI, err error) {
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, o.LLM)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with apiType azure but missing endpoint",
|
||||
metadata: map[string]string{
|
||||
"key": "test-key",
|
||||
"model": "gpt-4",
|
||||
"apiType": "azure",
|
||||
"apiVersion": "2025-01-01-preview",
|
||||
},
|
||||
testFn: func(t *testing.T, o *OpenAI, err error) {
|
||||
require.Error(t, err)
|
||||
assert.EqualError(t, err, "endpoint and apiVersion must be provided when apiType is set to 'azure'")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
o := NewOpenAI(logger.NewLogger("openai test"))
|
||||
|
|
|
@ -57,7 +57,7 @@ func (a *azureServiceBus) Init(_ context.Context, metadata pubsub.Metadata) (err
|
|||
return err
|
||||
}
|
||||
|
||||
a.client, err = impl.NewClient(a.metadata, metadata.Properties)
|
||||
a.client, err = impl.NewClient(a.metadata, metadata.Properties, a.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ func (a *azureServiceBus) Init(_ context.Context, metadata pubsub.Metadata) (err
|
|||
return err
|
||||
}
|
||||
|
||||
a.client, err = impl.NewClient(a.metadata, metadata.Properties)
|
||||
a.client, err = impl.NewClient(a.metadata, metadata.Properties, a.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -31,6 +31,30 @@ authenticationProfiles:
|
|||
secret reference
|
||||
example: "KeFg23!"
|
||||
default: ""
|
||||
- name: sentinelUsername
|
||||
type: string
|
||||
required: false
|
||||
description: |
|
||||
Username for Redis Sentinel. Applicable only when "failover" is true, and
|
||||
Redis Sentinel has authentication enabled. Defaults to empty.
|
||||
example: "my-sentinel-username"
|
||||
default: ""
|
||||
url:
|
||||
title: "Redis Sentinel authentication documentation"
|
||||
url: "https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/#configuring-sentinel-instances-with-authentication"
|
||||
- name: sentinelPassword
|
||||
type: string
|
||||
required: false
|
||||
sensitive: true
|
||||
description: |
|
||||
Password for Redis Sentinel. Applicable only when "failover" is true, and
|
||||
Redis Sentinel has authentication enabled. Use secretKeyRef for
|
||||
secret reference. Defaults to empty.
|
||||
example: "KeFg23!"
|
||||
default: ""
|
||||
url:
|
||||
title: "Redis Sentinel authentication documentation"
|
||||
url: "https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/#configuring-sentinel-instances-with-authentication"
|
||||
metadata:
|
||||
- name: redisHost
|
||||
required: true
|
||||
|
|
|
@ -88,12 +88,38 @@ func (s *Store) Init(ctx context.Context, metadataRaw secretstores.Metadata) err
|
|||
}
|
||||
|
||||
func (s *Store) getClient(ctx context.Context, metadata *GcpSecretManagerMetadata) (*secretmanager.Client, error) {
|
||||
b, _ := json.Marshal(metadata)
|
||||
clientOptions := option.WithCredentialsJSON(b)
|
||||
var client *secretmanager.Client
|
||||
var err error
|
||||
|
||||
client, err := secretmanager.NewClient(ctx, clientOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if metadata.ProjectID == "" {
|
||||
return nil, errors.New("missing property `project_id` in metadata")
|
||||
}
|
||||
|
||||
// Explicit authentication
|
||||
if metadata.PrivateKeyID != "" {
|
||||
if metadata.Type == "" {
|
||||
return nil, errors.New("missing property `type` in metadata")
|
||||
}
|
||||
if metadata.PrivateKey == "" {
|
||||
return nil, errors.New("missing property `private_key` in metadata")
|
||||
}
|
||||
if metadata.ClientEmail == "" {
|
||||
return nil, errors.New("missing property `client_email` in metadata")
|
||||
}
|
||||
|
||||
b, _ := json.Marshal(metadata)
|
||||
clientOptions := option.WithCredentialsJSON(b)
|
||||
client, err = secretmanager.NewClient(ctx, clientOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// Implicit authentication, using GCP Application Default Credentials (ADC)
|
||||
// Credentials search order: https://cloud.google.com/docs/authentication/application-default-credentials#order
|
||||
client, err = secretmanager.NewClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return client, nil
|
||||
|
@ -183,18 +209,9 @@ func (s *Store) parseSecretManagerMetadata(metadataRaw secretstores.Metadata) (*
|
|||
return nil, fmt.Errorf("failed to decode metadata: %w", err)
|
||||
}
|
||||
|
||||
if meta.Type == "" {
|
||||
return nil, errors.New("missing property `type` in metadata")
|
||||
}
|
||||
if meta.ProjectID == "" {
|
||||
return nil, errors.New("missing property `project_id` in metadata")
|
||||
}
|
||||
if meta.PrivateKey == "" {
|
||||
return nil, errors.New("missing property `private_key` in metadata")
|
||||
}
|
||||
if meta.ClientEmail == "" {
|
||||
return nil, errors.New("missing property `client_email` in metadata")
|
||||
}
|
||||
|
||||
return &meta, nil
|
||||
}
|
||||
|
|
|
@ -76,11 +76,38 @@ func TestInit(t *testing.T) {
|
|||
|
||||
t.Run("Init with missing `type` metadata", func(t *testing.T) {
|
||||
m.Properties = map[string]string{
|
||||
"dummy": "a",
|
||||
"dummy": "a",
|
||||
"private_key_id": "a",
|
||||
"project_id": "a",
|
||||
}
|
||||
err := sm.Init(ctx, m)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, err, errors.New("missing property `type` in metadata"))
|
||||
assert.Equal(t, errors.New("failed to setup secretmanager client: missing property `type` in metadata"), err)
|
||||
})
|
||||
|
||||
t.Run("Init with missing `private_key` metadata", func(t *testing.T) {
|
||||
m.Properties = map[string]string{
|
||||
"dummy": "a",
|
||||
"private_key_id": "a",
|
||||
"type": "a",
|
||||
"project_id": "a",
|
||||
}
|
||||
err := sm.Init(ctx, m)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, errors.New("failed to setup secretmanager client: missing property `private_key` in metadata"), err)
|
||||
})
|
||||
|
||||
t.Run("Init with missing `client_email` metadata", func(t *testing.T) {
|
||||
m.Properties = map[string]string{
|
||||
"dummy": "a",
|
||||
"private_key_id": "a",
|
||||
"private_key": "a",
|
||||
"type": "a",
|
||||
"project_id": "a",
|
||||
}
|
||||
err := sm.Init(ctx, m)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, errors.New("failed to setup secretmanager client: missing property `client_email` in metadata"), err)
|
||||
})
|
||||
|
||||
t.Run("Init with missing `project_id` metadata", func(t *testing.T) {
|
||||
|
@ -91,6 +118,22 @@ func TestInit(t *testing.T) {
|
|||
require.Error(t, err)
|
||||
assert.Equal(t, err, errors.New("missing property `project_id` in metadata"))
|
||||
})
|
||||
|
||||
t.Run("Init with missing `project_id` metadata", func(t *testing.T) {
|
||||
m.Properties = map[string]string{
|
||||
"type": "service_account",
|
||||
}
|
||||
err := sm.Init(ctx, m)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, err, errors.New("missing property `project_id` in metadata"))
|
||||
})
|
||||
|
||||
t.Run("Init with empty metadata", func(t *testing.T) {
|
||||
m.Properties = map[string]string{}
|
||||
err := sm.Init(ctx, m)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, err, errors.New("missing property `project_id` in metadata"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetSecret(t *testing.T) {
|
||||
|
|
|
@ -16,6 +16,7 @@ package tablestore
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"sync"
|
||||
|
||||
"github.com/aliyun/aliyun-tablestore-go-sdk/tablestore"
|
||||
)
|
||||
|
@ -24,6 +25,7 @@ type mockClient struct {
|
|||
tablestore.TableStoreClient
|
||||
|
||||
data map[string][]byte
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (m *mockClient) DeleteRow(request *tablestore.DeleteRowRequest) (*tablestore.DeleteRowResponse, error) {
|
||||
|
@ -36,7 +38,9 @@ func (m *mockClient) DeleteRow(request *tablestore.DeleteRowRequest) (*tablestor
|
|||
}
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
delete(m.data, key)
|
||||
m.mu.Unlock()
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -51,7 +55,9 @@ func (m *mockClient) GetRow(request *tablestore.GetRowRequest) (*tablestore.GetR
|
|||
}
|
||||
}
|
||||
|
||||
m.mu.RLock()
|
||||
val := m.data[key]
|
||||
m.mu.RUnlock()
|
||||
|
||||
resp := &tablestore.GetRowResponse{
|
||||
Columns: []*tablestore.AttributeColumn{{
|
||||
|
@ -87,7 +93,9 @@ func (m *mockClient) UpdateRow(req *tablestore.UpdateRowRequest) (*tablestore.Up
|
|||
}
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
m.data[key] = val
|
||||
m.mu.Unlock()
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -97,6 +105,7 @@ func (m *mockClient) BatchGetRow(request *tablestore.BatchGetRowRequest) (*table
|
|||
TableToRowsResult: map[string][]tablestore.RowResult{},
|
||||
}
|
||||
|
||||
m.mu.RLock()
|
||||
for _, criteria := range request.MultiRowQueryCriteria {
|
||||
tableRes := resp.TableToRowsResult[criteria.TableName]
|
||||
if tableRes == nil {
|
||||
|
@ -136,12 +145,14 @@ func (m *mockClient) BatchGetRow(request *tablestore.BatchGetRowRequest) (*table
|
|||
}
|
||||
}
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (m *mockClient) BatchWriteRow(request *tablestore.BatchWriteRowRequest) (*tablestore.BatchWriteRowResponse, error) {
|
||||
resp := &tablestore.BatchWriteRowResponse{}
|
||||
m.mu.Lock()
|
||||
for _, changes := range request.RowChangesGroupByTable {
|
||||
for _, change := range changes {
|
||||
switch inst := change.(type) {
|
||||
|
@ -174,6 +185,7 @@ func (m *mockClient) BatchWriteRow(request *tablestore.BatchWriteRowRequest) (*t
|
|||
}
|
||||
}
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
|
|
@ -514,8 +514,16 @@ func (o *oracleDatabaseAccess) ensureStateTable(stateTableName string) error {
|
|||
}
|
||||
|
||||
func tableExists(db *sql.DB, tableName string) (bool, error) {
|
||||
var tblCount int32
|
||||
err := db.QueryRow("SELECT count(table_name) tbl_count FROM user_tables WHERE table_name = upper(:tablename)", tableName).Scan(&tblCount)
|
||||
exists := tblCount > 0
|
||||
return exists, err
|
||||
//nolint:gosec
|
||||
query := fmt.Sprintf("SELECT 1 FROM %s WHERE ROWNUM = 1", tableName)
|
||||
|
||||
var dummy int
|
||||
err := db.QueryRow(query).Scan(&dummy)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return true, nil // Table exists but is empty
|
||||
}
|
||||
return false, nil // Likely a table does not exist error
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
|
|
@ -36,6 +36,30 @@ authenticationProfiles:
|
|||
secret reference
|
||||
example: "KeFg23!"
|
||||
default: ""
|
||||
- name: sentinelUsername
|
||||
type: string
|
||||
required: false
|
||||
description: |
|
||||
Username for Redis Sentinel. Applicable only when "failover" is true, and
|
||||
Redis Sentinel has authentication enabled. Defaults to empty.
|
||||
example: "my-sentinel-username"
|
||||
default: ""
|
||||
url:
|
||||
title: "Redis Sentinel authentication documentation"
|
||||
url: "https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/#configuring-sentinel-instances-with-authentication"
|
||||
- name: sentinelPassword
|
||||
type: string
|
||||
required: false
|
||||
sensitive: true
|
||||
description: |
|
||||
Password for Redis Sentinel. Applicable only when "failover" is true, and
|
||||
Redis Sentinel has authentication enabled. Use secretKeyRef for
|
||||
secret reference. Defaults to empty.
|
||||
example: "KeFg23!"
|
||||
default: ""
|
||||
url:
|
||||
title: "Redis Sentinel authentication documentation"
|
||||
url: "https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/#configuring-sentinel-instances-with-authentication"
|
||||
metadata:
|
||||
- name: redisHost
|
||||
required: true
|
||||
|
|
|
@ -5,7 +5,7 @@ This directory contains conformance tests for all conversation components, inclu
|
|||
## Available Components
|
||||
|
||||
- **echo** - Simple echo component for testing (no configuration needed)
|
||||
- **openai** - OpenAI GPT models
|
||||
- **openai** - OpenAI GPT models (also supports Azure OpenAI)
|
||||
- **anthropic** - Anthropic Claude models
|
||||
- **googleai** - Google Gemini models
|
||||
- **mistral** - Mistral AI models
|
||||
|
@ -52,6 +52,14 @@ export OPENAI_API_KEY="your_openai_api_key"
|
|||
```
|
||||
Get your API key from: https://platform.openai.com/api-keys
|
||||
|
||||
### Azure OpenAI
|
||||
```bash
|
||||
export AZURE_OPENAI_API_KEY="your_openai_api_key"
|
||||
export AZURE_OPENAI_ENDPOINT="your_azureopenai_endpoint_here"
|
||||
export AZURE_OPENAI_API_VERSION="your_azreopenai_api_version_here"
|
||||
```
|
||||
Get your configuration values from: https://ai.azure.com/
|
||||
|
||||
### Anthropic
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY="your_anthropic_api_key"
|
||||
|
@ -142,4 +150,5 @@ This approach provides better reliability and compatibility while maintaining ac
|
|||
- Cost-effective models are used by default to minimize API costs
|
||||
- HuggingFace uses the OpenAI compatibility layer as a workaround due to langchaingo API issues
|
||||
- Ollama requires a local server and must be explicitly enabled
|
||||
- OpenAI component is tested for OpenAI and Azure
|
||||
- All tests include proper initialization and basic conversation functionality testing
|
|
@ -4,6 +4,11 @@
|
|||
# OpenAI API Key - Get from https://platform.openai.com/api-keys
|
||||
OPENAI_API_KEY=your_openai_api_key_here
|
||||
|
||||
# Azure OpenAI - Get from https://ai.azure.com/
|
||||
AZURE_OPENAI_API_KEY=your_azureopenai_api_key_here
|
||||
AZURE_OPENAI_ENDPOINT=your_azureopenai_endpoint_here
|
||||
AZURE_OPENAI_API_VERSION=your_azreopenai_api_version_here
|
||||
|
||||
# Anthropic API Key - Get from https://console.anthropic.com/
|
||||
ANTHROPIC_API_KEY=your_anthropic_api_key_here
|
||||
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
apiVersion: dapr.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: openai
|
||||
spec:
|
||||
type: conversation.openai
|
||||
version: v1
|
||||
metadata:
|
||||
- name: key
|
||||
value: "${{AZURE_OPENAI_API_KEY}}"
|
||||
- name: model
|
||||
value: "gpt-4o-mini"
|
||||
- name: endpoint
|
||||
value: "${{AZURE_OPENAI_ENDPOINT}}"
|
||||
- name: apiType
|
||||
value: "azure"
|
||||
- name: apiVersion
|
||||
value: "${{AZURE_OPENAI_API_VERSION}}"
|
|
@ -21,6 +21,9 @@ echo " ./test_conformance.sh"
|
|||
echo ""
|
||||
echo "Option 2: Set environment variables directly:"
|
||||
echo " export OPENAI_API_KEY=\"your_openai_api_key\""
|
||||
echo " export AZURE_OPENAI_API_KEY=\"your_azureopenai_api_key\""
|
||||
echo " export AZURE_OPENAI_ENDPOINT=\"your_azureopenai_endpoint\""
|
||||
echo " export AZURE_OPENAI_API_VERSION=\"your_azureopenai_api_version\""
|
||||
echo " export ANTHROPIC_API_KEY=\"your_anthropic_api_key\""
|
||||
echo " export GOOGLE_AI_API_KEY=\"your_google_ai_api_key\""
|
||||
echo " export MISTRAL_API_KEY=\"your_mistral_api_key\""
|
||||
|
|
|
@ -2,7 +2,9 @@ componentType: conversation
|
|||
components:
|
||||
- component: echo
|
||||
operations: []
|
||||
- component: openai
|
||||
- component: openai.openai
|
||||
operations: []
|
||||
- component: openai.azure
|
||||
operations: []
|
||||
- component: anthropic
|
||||
operations: []
|
||||
|
|
|
@ -73,11 +73,16 @@ func TestConversationConformance(t *testing.T) {
|
|||
// shouldSkipComponent checks if a component test should be skipped due to missing environment variables
|
||||
func shouldSkipComponent(t *testing.T, componentName string) bool {
|
||||
switch componentName {
|
||||
case "openai":
|
||||
case "openai.openai":
|
||||
if os.Getenv("OPENAI_API_KEY") == "" {
|
||||
t.Skipf("Skipping OpenAI conformance test: OPENAI_API_KEY environment variable not set")
|
||||
return true
|
||||
}
|
||||
case "openai.azure":
|
||||
if os.Getenv("AZURE_OPENAI_API_KEY") == "" || os.Getenv("AZURE_OPENAI_ENDPOINT") == "" || os.Getenv("AZURE_OPENAI_API_TYPE") == "" || os.Getenv("AZURE_OPENAI_API_VERSION") == "" {
|
||||
t.Skipf("Skipping Azure OpenAI conformance test: AZURE_OPENAI_API_KEY, AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_API_TYPE, and AZURE_OPENAI_API_VERSION environment variables must be set")
|
||||
return true
|
||||
}
|
||||
case "anthropic":
|
||||
if os.Getenv("ANTHROPIC_API_KEY") == "" {
|
||||
t.Skipf("Skipping Anthropic conformance test: ANTHROPIC_API_KEY environment variable not set")
|
||||
|
@ -117,7 +122,7 @@ func loadConversationComponent(name string) conversation.Conversation {
|
|||
switch name {
|
||||
case "echo":
|
||||
return echo.NewEcho(testLogger)
|
||||
case "openai":
|
||||
case "openai.openai", "openai.azure":
|
||||
return openai.NewOpenAI(testLogger)
|
||||
case "anthropic":
|
||||
return anthropic.NewAnthropic(testLogger)
|
||||
|
|
Loading…
Reference in New Issue