yao/openai/openai.go
Max 17c18e7d56 Refactor Neo API assistant to enhance vision support and message handling
- Updated the Assistant struct to include a vision capability flag and an init hook indicator, allowing for better management of vision-enabled functionalities.
- Refactored message handling throughout the assistant methods to utilize the chatMessage package, improving consistency and type safety.
- Enhanced the handleVision method to support dynamic vision processing options, including improved handling of image descriptions and uploads.
- Streamlined the requestMessages and withAttachments methods to better accommodate vision capabilities, ensuring proper integration with image handling.
- Improved the initialize method to check for vision support based on the model, enhancing the assistant's adaptability.

These changes improve the robustness and maintainability of the Neo API, paving the way for enhanced assistant functionalities and better integration of vision capabilities.
2025-01-17 15:16:26 +08:00

476 lines
13 KiB
Go

package openai
import (
"context"
"encoding/base64"
"fmt"
"strings"
"github.com/pkoukk/tiktoken-go"
"github.com/yaoapp/gou/connector"
"github.com/yaoapp/gou/http"
"github.com/yaoapp/kun/exception"
"github.com/yaoapp/yao/share"
)
// Tiktoken get number of tokens
func Tiktoken(model string, input string) (int, error) {
tkm, err := tiktoken.EncodingForModel(model)
if err != nil {
return 0, err
}
token := tkm.Encode(input, nil, nil)
return len(token), nil
}
// OpenAI struct
type OpenAI struct {
key string
model string
host string
organization string
maxToken int
}
// New create a new OpenAI instance by connector id
func New(id string) (*OpenAI, error) {
// Moapi integration
if id == "" || strings.HasPrefix(id, "moapi") {
model := "gpt-3.5-turbo"
if strings.HasPrefix(id, "moapi:") {
model = strings.TrimPrefix(id, "moapi:")
}
return NewMoapi(model)
}
c, err := connector.Select(id)
if err != nil {
return nil, err
}
if !c.Is(connector.OPENAI) {
return nil, fmt.Errorf("The connector %s is not a OpenAI connector", id)
}
setting := c.Setting()
return NewOpenAI(setting)
}
// NewOpenAI create a new OpenAI instance by setting
func NewOpenAI(setting map[string]interface{}) (*OpenAI, error) {
key := ""
if v, ok := setting["key"].(string); ok {
key = v
}
model := "gpt-3.5-turbo"
if v, ok := setting["model"].(string); ok {
model = v
}
host := "https://api.openai.com"
if v, ok := setting["host"].(string); ok {
host = v
}
organization := ""
if v, ok := setting["organization"].(string); ok {
organization = v
}
maxToken := 2048
if v, ok := setting["max_token"].(int); ok {
maxToken = v
}
return &OpenAI{
key: key,
model: model,
host: host,
organization: organization,
maxToken: maxToken,
}, nil
}
// NewMoapi create a new OpenAI instance by model
// Temporarily: change after the moapi is open source
func NewMoapi(model string) (*OpenAI, error) {
if model == "" {
model = "gpt-3.5-turbo"
}
url := share.MoapiHosts[0]
if share.App.Moapi.Mirrors != nil {
url = share.App.Moapi.Mirrors[0]
}
key := share.App.Moapi.Secret
organization := share.App.Moapi.Organization
if !strings.HasPrefix(url, "http") {
url = "https://" + url
}
if key == "" {
return nil, fmt.Errorf("The moapi secret is empty")
}
return &OpenAI{
key: key,
model: model,
host: url,
organization: organization,
maxToken: 16384,
}, nil
}
// Model get the model
func (openai OpenAI) Model() string {
return openai.model
}
// Completions Creates a completion for the provided prompt and parameters.
// https://platform.openai.com/docs/api-reference/completions/create
func (openai OpenAI) Completions(prompt interface{}, option map[string]interface{}, cb func(data []byte) int) (interface{}, *exception.Exception) {
if option == nil {
option = map[string]interface{}{}
}
option["prompt"] = prompt
if cb != nil {
option["stream"] = true
return nil, openai.stream(context.Background(), "/v1/completions", option, cb)
}
option["stream"] = false
return openai.post("/v1/completions", option)
}
// CompletionsWith Creates a completion for the provided prompt and parameters.
// https://platform.openai.com/docs/api-reference/completions/create
func (openai OpenAI) CompletionsWith(ctx context.Context, prompt interface{}, option map[string]interface{}, cb func(data []byte) int) (interface{}, *exception.Exception) {
if option == nil {
option = map[string]interface{}{}
}
option["prompt"] = prompt
if cb != nil {
option["stream"] = true
return nil, openai.stream(ctx, "/v1/completions", option, cb)
}
option["stream"] = false
return openai.post("/v1/completions", option)
}
// ChatCompletions Creates a model response for the given chat conversation.
// https://platform.openai.com/docs/api-reference/chat/create
func (openai OpenAI) ChatCompletions(messages []map[string]interface{}, option map[string]interface{}, cb func(data []byte) int) (interface{}, *exception.Exception) {
if option == nil {
option = map[string]interface{}{}
}
option["messages"] = messages
if cb != nil {
option["stream"] = true
return nil, openai.stream(context.Background(), "/v1/chat/completions", option, cb)
}
option["stream"] = false
return openai.post("/v1/chat/completions", option)
}
// ChatCompletionsWith Creates a model response for the given chat conversation.
// https://platform.openai.com/docs/api-reference/chat/create
func (openai OpenAI) ChatCompletionsWith(ctx context.Context, messages []map[string]interface{}, option map[string]interface{}, cb func(data []byte) int) (interface{}, *exception.Exception) {
if option == nil {
option = map[string]interface{}{}
}
option["messages"] = messages
if cb != nil {
option["stream"] = true
return nil, openai.stream(ctx, "/v1/chat/completions", option, cb)
}
option["stream"] = false
return openai.post("/v1/chat/completions", option)
}
// Edits Creates a new edit for the provided input, instruction, and parameters.
// https://platform.openai.com/docs/api-reference/edits/create
func (openai OpenAI) Edits(instruction string, option map[string]interface{}) (interface{}, *exception.Exception) {
if option == nil {
option = map[string]interface{}{}
}
option["instruction"] = instruction
return openai.post("/v1/edits", option)
}
// Embeddings Creates an embedding vector representing the input text.
// https://platform.openai.com/docs/api-reference/embeddings/create
func (openai OpenAI) Embeddings(input interface{}, user string) (interface{}, *exception.Exception) {
payload := map[string]interface{}{"input": input}
if user != "" {
payload["user"] = user
}
return openai.post("/v1/embeddings", payload)
}
// AudioTranscriptions Transcribes audio into the input language.
// https://platform.openai.com/docs/api-reference/audio/create
func (openai OpenAI) AudioTranscriptions(dataBase64 string, option map[string]interface{}) (interface{}, *exception.Exception) {
data, err := base64.StdEncoding.DecodeString(dataBase64)
if err != nil {
return nil, exception.New("Base64 error :%s", 400, err.Error())
}
if option == nil {
option = map[string]interface{}{}
}
return openai.postFile("/v1/audio/transcriptions", map[string][]byte{"file": data}, option)
}
// ImagesGenerations Creates an image given a prompt.
// https://platform.openai.com/docs/api-reference/images
func (openai OpenAI) ImagesGenerations(prompt string, option map[string]interface{}) (interface{}, *exception.Exception) {
if option == nil {
option = map[string]interface{}{}
}
if option["response_format"] == nil {
option["response_format"] = "b64_json"
}
option["prompt"] = prompt
return openai.postWithoutModel("/v1/images/generations", option)
}
// ImagesEdits Creates an edited or extended image given an original image and a prompt.
// https://platform.openai.com/docs/api-reference/images/create-edit
func (openai OpenAI) ImagesEdits(imageBase64 string, prompt string, option map[string]interface{}) (interface{}, *exception.Exception) {
image, err := base64.StdEncoding.DecodeString(imageBase64)
if err != nil {
return nil, exception.New("Base64 error :%s", 400, err.Error())
}
files := map[string][]byte{"image": image}
if option == nil {
option = map[string]interface{}{}
}
if maskBase64, ok := option["mask"].(string); ok {
mask, err := base64.StdEncoding.DecodeString(maskBase64)
if err != nil {
return nil, exception.New("Base64 error :%s", 400, err.Error())
}
files["mask"] = mask
}
if option["response_format"] == nil {
option["response_format"] = "b64_json"
}
option["prompt"] = prompt
return openai.postFileWithoutModel("/v1/images/edits", files, option)
}
// ImagesVariations Creates a variation of a given image.
// https://platform.openai.com/docs/api-reference/images/create-variation
func (openai OpenAI) ImagesVariations(imageBase64 string, option map[string]interface{}) (interface{}, *exception.Exception) {
image, err := base64.StdEncoding.DecodeString(imageBase64)
if err != nil {
return nil, exception.New("Base64 error :%s", 400, err.Error())
}
files := map[string][]byte{"image": image}
if option == nil {
option = map[string]interface{}{}
}
if option["response_format"] == nil {
option["response_format"] = "b64_json"
}
return openai.postFileWithoutModel("/v1/images/variations", files, option)
}
// Tiktoken get number of tokens
func (openai OpenAI) Tiktoken(input string) (int, error) {
tkm, err := tiktoken.EncodingForModel(openai.model)
if err != nil {
return 0, err
}
token := tkm.Encode(input, nil, nil)
return len(token), nil
}
// MaxToken get max number of tokens
func (openai OpenAI) MaxToken() int {
return openai.maxToken
}
// GetContent get the content of chat completions
func (openai OpenAI) GetContent(response interface{}) (string, *exception.Exception) {
if response == nil {
return "", exception.New("response is nil", 500)
}
if data, ok := response.(map[string]interface{}); ok {
if choices, ok := data["choices"].([]interface{}); ok {
if len(choices) == 0 {
return "", exception.New("choices is null, %v", 500, response)
}
if choice, ok := choices[0].(map[string]interface{}); ok {
if message, ok := choice["message"].(map[string]interface{}); ok {
if content, ok := message["content"].(string); ok {
return content, nil
}
}
}
}
}
return "", exception.New("response format error, %#v", 500, response)
}
// Post post request
func (openai OpenAI) Post(path string, payload map[string]interface{}) (interface{}, *exception.Exception) {
return openai.post(path, payload)
}
// Stream post request
func (openai OpenAI) Stream(ctx context.Context, path string, payload map[string]interface{}, cb func(data []byte) int) *exception.Exception {
return openai.stream(ctx, path, payload, cb)
}
// post post request
func (openai OpenAI) post(path string, payload map[string]interface{}) (interface{}, *exception.Exception) {
url := fmt.Sprintf("%s%s", openai.host, path)
key := fmt.Sprintf("Bearer %s", openai.key)
payload["model"] = openai.model
req := http.New(url).
WithHeader(map[string][]string{"Authorization": {key}})
res := req.Post(payload)
if err := openai.isError(res); err != nil {
return nil, err
}
return res.Data, nil
}
// post post request without model
func (openai OpenAI) postWithoutModel(path string, payload map[string]interface{}) (interface{}, *exception.Exception) {
url := fmt.Sprintf("%s%s", openai.host, path)
key := fmt.Sprintf("Bearer %s", openai.key)
req := http.New(url).
WithHeader(map[string][]string{"Authorization": {key}})
res := req.Post(payload)
if err := openai.isError(res); err != nil {
return nil, err
}
return res.Data, nil
}
// post post request with file
func (openai OpenAI) postFile(path string, files map[string][]byte, option map[string]interface{}) (interface{}, *exception.Exception) {
url := fmt.Sprintf("%s%s", openai.host, path)
key := fmt.Sprintf("Bearer %s", openai.key)
option["model"] = openai.model
req := http.New(url).
WithHeader(map[string][]string{
"Authorization": {key},
"Content-Type": {"multipart/form-data"},
})
for name, data := range files {
req.AddFileBytes(name, fmt.Sprintf("%s.mp3", name), data)
}
res := req.Send("POST", option)
if err := openai.isError(res); err != nil {
return nil, err
}
return res.Data, nil
}
// post post request with file without model
func (openai OpenAI) postFileWithoutModel(path string, files map[string][]byte, option map[string]interface{}) (interface{}, *exception.Exception) {
url := fmt.Sprintf("%s%s", openai.host, path)
key := fmt.Sprintf("Bearer %s", openai.key)
req := http.New(url).
WithHeader(map[string][]string{
"Authorization": {key},
"Content-Type": {"multipart/form-data"},
})
for name, data := range files {
req.AddFileBytes(name, fmt.Sprintf("%s.mp3", name), data)
}
res := req.Send("POST", option)
if err := openai.isError(res); err != nil {
return nil, err
}
return res.Data, nil
}
// stream post request
func (openai OpenAI) stream(ctx context.Context, path string, payload map[string]interface{}, cb func(data []byte) int) *exception.Exception {
url := fmt.Sprintf("%s%s", openai.host, path)
key := fmt.Sprintf("Bearer %s", openai.key)
payload["model"] = openai.model
req := http.New(url)
err := req.
WithHeader(map[string][]string{
"Content-Type": {"application/json; charset=utf-8"},
"Authorization": {key},
}).
Stream(ctx, "POST", payload, cb)
if err != nil {
return exception.New(err.Error(), 500)
}
return nil
}
func (openai OpenAI) isError(res *http.Response) *exception.Exception {
if res.Status != 200 {
message := "OpenAI Error"
if v, ok := res.Data.(string); ok {
message = v
}
if data, ok := res.Data.(map[string]interface{}); ok {
if err, has := data["error"]; has {
if err, ok := err.(map[string]interface{}); ok {
if msg, has := err["message"].(string); has {
message = msg
}
if code, has := err["code"].(string); has {
message = fmt.Sprintf("OpenAI %s %s", code, message)
}
}
}
}
return exception.New(message, res.Status)
}
return nil
}