Merge branch 'main' into filinto/fix-tool-call-echo

This commit is contained in:
Filinto Duran 2025-08-01 09:05:38 -05:00 committed by GitHub
commit c92a64d804
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 502 additions and 41 deletions

View File

@ -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
}

View File

@ -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)

View File

@ -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{}

View File

@ -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

View File

@ -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")
}

View File

@ -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)

View File

@ -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

View File

@ -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,

View File

@ -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,

View File

@ -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

View File

@ -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)

View File

@ -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"`
}

View File

@ -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: ''

View File

@ -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)
})
}

View File

@ -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
}

View File

@ -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"))

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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) {

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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}}"

View File

@ -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\""

View File

@ -2,7 +2,9 @@ componentType: conversation
components:
- component: echo
operations: []
- component: openai
- component: openai.openai
operations: []
- component: openai.azure
operations: []
- component: anthropic
operations: []

View File

@ -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)