From 70a773bbf75160711290fccb62ac9ebac6efb6ad Mon Sep 17 00:00:00 2001 From: Loong Date: Fri, 2 Aug 2024 12:03:08 +0800 Subject: [PATCH] init conversation API with openai component Signed-off-by: Loong --- conversation/README.md | 3 + conversation/converse.go | 53 +++++++++++++++++ conversation/metadata.go | 22 +++++++ conversation/openai/openai.go | 108 ++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 + 6 files changed, 189 insertions(+) create mode 100644 conversation/README.md create mode 100644 conversation/converse.go create mode 100644 conversation/metadata.go create mode 100644 conversation/openai/openai.go diff --git a/conversation/README.md b/conversation/README.md new file mode 100644 index 0000000000..f2ab4a3c9c --- /dev/null +++ b/conversation/README.md @@ -0,0 +1,3 @@ +# Conversation + +Conversations provide a common way to converse with different LLM providers. diff --git a/conversation/converse.go b/conversation/converse.go new file mode 100644 index 0000000000..f0940f7656 --- /dev/null +++ b/conversation/converse.go @@ -0,0 +1,53 @@ +/* +Copyright 2024 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 conversation + +import ( + "context" + + "google.golang.org/protobuf/types/known/anypb" + + "github.com/dapr/components-contrib/metadata" +) + +type Conversation interface { + metadata.ComponentWithMetadata + + Init(ctx context.Context, meta Metadata) error + + Converse(ctx context.Context, req *ConversationRequest) (*ConversationResponse, error) +} + +type ConversationRequest struct { + Inputs []string `json:"inputs"` + Parameters map[string]*anypb.Any `json:"parameters"` + ConversationContext string `json:"conversationContext"` + + // from metadata + Key string `json:"key"` + Model string `json:"model"` + Endpoints []string `json:"endpoints"` + Policy string `json:"loadBalancingPolicy"` +} + +type ConversationResult struct { + Result string `json:"result"` + Parameters map[string]*anypb.Any `json:"parameters"` +} + +type ConversationResponse struct { + ConversationContext string `json:"conversationContext"` + Outputs []ConversationResult `json:"outputs"` +} diff --git a/conversation/metadata.go b/conversation/metadata.go new file mode 100644 index 0000000000..33259a1c5c --- /dev/null +++ b/conversation/metadata.go @@ -0,0 +1,22 @@ +/* +Copyright 2024 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 conversation + +import "github.com/dapr/components-contrib/metadata" + +// Metadata represents a set of conversation specific properties. +type Metadata struct { + metadata.Base `json:",inline"` +} diff --git a/conversation/openai/openai.go b/conversation/openai/openai.go new file mode 100644 index 0000000000..9ee8319550 --- /dev/null +++ b/conversation/openai/openai.go @@ -0,0 +1,108 @@ +/* +Copyright 2024 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 ( + "context" + "reflect" + + "github.com/dapr/components-contrib/conversation" + "github.com/dapr/components-contrib/metadata" + "github.com/dapr/kit/logger" + kmeta "github.com/dapr/kit/metadata" + + "github.com/sashabaranov/go-openai" +) + +type OpenAI struct { + conversation.Conversation + + key string + model string + + logger logger.Logger +} + +func NewOpenAI(logger logger.Logger) conversation.Conversation { + o := &OpenAI{ + logger: logger, + } + + return o +} + +func (o *OpenAI) Init(ctx context.Context, meta conversation.Metadata) error { + r := &conversation.ConversationRequest{} + err := kmeta.DecodeMetadata(meta.Properties, &r) + if err != nil { + return err + } + + o.key = r.Key + o.model = r.Model + + return nil +} + +func (o *OpenAI) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { + metadataStruct := conversation.ConversationRequest{} + metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.StateStoreType) + return +} + +func (o *OpenAI) Converse(ctx context.Context, r *conversation.ConversationRequest) (res *conversation.ConversationResponse, err error) { + // Note: OPENAI does not support load balance + client := openai.NewClient(o.key) + + messages := make([]openai.ChatCompletionMessage, 0, len(r.Inputs)) + + for _, input := range r.Inputs { + messages = append(messages, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleUser, + Content: input, + }) + } + + req := openai.ChatCompletionRequest{ + Model: o.model, + Messages: messages, + } + + // TODO: support ConversationContext + + resp, err := client.CreateChatCompletion(ctx, req) + if err != nil { + o.logger.Error(err) + return nil, err + } + + o.logger.Debug(resp) + + outputs := make([]conversation.ConversationResult, 0, len(resp.Choices)) + + for i := range resp.Choices { + outputs = append(outputs, conversation.ConversationResult{ + Result: resp.Choices[i].Message.Content, + Parameters: r.Parameters, + }) + } + + res = &conversation.ConversationResponse{ + ConversationContext: resp.ID, + Outputs: outputs, + } + + return res, nil +} diff --git a/go.mod b/go.mod index 3d63145d08..73fda1afae 100644 --- a/go.mod +++ b/go.mod @@ -101,6 +101,7 @@ require ( github.com/rabbitmq/amqp091-go v1.8.1 github.com/redis/go-redis/v9 v9.2.1 github.com/riferrei/srclient v0.6.0 + github.com/sashabaranov/go-openai v1.27.1 github.com/sendgrid/sendgrid-go v3.13.0+incompatible github.com/sijms/go-ora/v2 v2.7.18 github.com/spf13/cast v1.5.1 diff --git a/go.sum b/go.sum index dc9bc8af63..5a94028008 100644 --- a/go.sum +++ b/go.sum @@ -1424,6 +1424,8 @@ github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIH github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 h1:TToq11gyfNlrMFZiYujSekIsPd9AmsA2Bj/iv+s4JHE= github.com/santhosh-tekuri/jsonschema/v5 v5.0.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= +github.com/sashabaranov/go-openai v1.27.1 h1:7Nx6db5NXbcoutNmAUQulEQZEpHG/SkzfexP2X5RWMk= +github.com/sashabaranov/go-openai v1.27.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=