Refactor Neo API assistant management and remove obsolete functions
- Removed unused assistant-related functions and types from the Neo API, including local chat and file handling, to streamline the codebase. - Refactored the assistant initialization process, consolidating the loading and configuration of assistants, including Vision and Connector settings. - Updated the DSL struct to enhance clarity and maintainability, ensuring better configuration management for assistants. - Improved error handling and modularity in the assistant loading process, paving the way for future enhancements in AI functionalities.
This commit is contained in:
parent
966b0f601f
commit
55c0580fc3
15 changed files with 324 additions and 881 deletions
|
|
@ -1,5 +1,20 @@
|
||||||
package assistant
|
package assistant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/yaoapp/gou/fs"
|
||||||
|
chatMessage "github.com/yaoapp/yao/neo/message"
|
||||||
|
)
|
||||||
|
|
||||||
// Get get the assistant by id
|
// Get get the assistant by id
|
||||||
func Get(id string) (*Assistant, error) {
|
func Get(id string) (*Assistant, error) {
|
||||||
return LoadStore(id)
|
return LoadStore(id)
|
||||||
|
|
@ -25,14 +40,229 @@ func GetByConnector(connector string, name string) (*Assistant, error) {
|
||||||
assistant, err := loadMap(data)
|
assistant, err := loadMap(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
||||||
}
|
}
|
||||||
loaded.Put(assistant)
|
loaded.Put(assistant)
|
||||||
return assistant, nil
|
return assistant, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init init the assistant
|
// AllowedFileTypes the allowed file types
|
||||||
// Choose the connector and initialize the assistant
|
var AllowedFileTypes = map[string]string{
|
||||||
func (ast *Assistant) initialize() error {
|
"application/json": "json",
|
||||||
|
"application/pdf": "pdf",
|
||||||
|
"application/msword": "doc",
|
||||||
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
|
||||||
|
"application/vnd.oasis.opendocument.text": "odt",
|
||||||
|
"application/vnd.ms-excel": "xls",
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
|
||||||
|
"application/vnd.ms-powerpoint": "ppt",
|
||||||
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation": "pptx",
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaxSize 20M max file size
|
||||||
|
var MaxSize int64 = 20 * 1024 * 1024
|
||||||
|
|
||||||
|
// Chat implements the chat functionality
|
||||||
|
func (ast *Assistant) Chat(ctx context.Context, messages []map[string]interface{}, option map[string]interface{}, cb func(data []byte) int) error {
|
||||||
|
if ast.openai == nil {
|
||||||
|
return fmt.Errorf("openai is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
requestMessages, err := ast.requestMessages(ctx, messages)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("request messages error: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ext := ast.openai.ChatCompletionsWith(ctx, requestMessages, option, cb)
|
||||||
|
if ext != nil {
|
||||||
|
return fmt.Errorf("openai chat completions with error: %s", ext.Message)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ast *Assistant) requestMessages(ctx context.Context, messages []map[string]interface{}) ([]map[string]interface{}, error) {
|
||||||
|
newMessages := []map[string]interface{}{}
|
||||||
|
length := len(messages)
|
||||||
|
for index, message := range messages {
|
||||||
|
role, ok := message["role"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("role must be string")
|
||||||
|
}
|
||||||
|
|
||||||
|
content, ok := message["content"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("content must be string")
|
||||||
|
}
|
||||||
|
|
||||||
|
newMessage := map[string]interface{}{
|
||||||
|
"role": role,
|
||||||
|
"content": content,
|
||||||
|
}
|
||||||
|
|
||||||
|
if name, ok := message["name"].(string); ok {
|
||||||
|
newMessage["name"] = name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special handling for user messages with JSON content last message
|
||||||
|
if role == "user" && index == length-1 {
|
||||||
|
content = strings.TrimSpace(content)
|
||||||
|
msg, err := chatMessage.NewString(content)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("new string error: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
newMessage["content"] = msg.Text
|
||||||
|
if msg.Attachments != nil {
|
||||||
|
content, err := ast.withAttachments(ctx, msg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("with attachments error: %s", err.Error())
|
||||||
|
}
|
||||||
|
newMessage["content"] = content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newMessages = append(newMessages, newMessage)
|
||||||
|
}
|
||||||
|
return newMessages, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ast *Assistant) withAttachments(ctx context.Context, msg *chatMessage.Message) ([]map[string]interface{}, error) {
|
||||||
|
contents := []map[string]interface{}{{"type": "text", "text": msg.Text}}
|
||||||
|
images := []string{}
|
||||||
|
for _, attachment := range msg.Attachments {
|
||||||
|
if strings.HasPrefix(attachment.ContentType, "image/") {
|
||||||
|
images = append(images, attachment.FileID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(images) == 0 {
|
||||||
|
return contents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, image := range images {
|
||||||
|
bytes64, err := ast.ReadBase64(ctx, image)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read base64 error: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
contents = append(contents, map[string]interface{}{
|
||||||
|
"type": "image_url",
|
||||||
|
"image_url": map[string]string{
|
||||||
|
"url": fmt.Sprintf("data:image/jpeg;base64,%s", bytes64),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return contents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload implements file upload functionality
|
||||||
|
func (ast *Assistant) Upload(ctx context.Context, file *multipart.FileHeader, reader io.Reader, option map[string]interface{}) (*File, error) {
|
||||||
|
// check file size
|
||||||
|
if file.Size > MaxSize {
|
||||||
|
return nil, fmt.Errorf("file size %d exceeds the maximum size of %d", file.Size, MaxSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := file.Header.Get("Content-Type")
|
||||||
|
if !ast.allowed(contentType) {
|
||||||
|
return nil, fmt.Errorf("file type %s not allowed", contentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := fs.Get("data")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ext := filepath.Ext(file.Filename)
|
||||||
|
id, err := ast.id(file.Filename, ext)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := id
|
||||||
|
_, err = data.Write(filename, reader, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &File{
|
||||||
|
ID: filename,
|
||||||
|
Filename: filename,
|
||||||
|
ContentType: contentType,
|
||||||
|
Bytes: int(file.Size),
|
||||||
|
CreatedAt: int(time.Now().Unix()),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ast *Assistant) allowed(contentType string) bool {
|
||||||
|
if _, ok := AllowedFileTypes[contentType]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(contentType, "text/") || strings.HasPrefix(contentType, "image/") ||
|
||||||
|
strings.HasPrefix(contentType, "audio/") || strings.HasPrefix(contentType, "video/") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ast *Assistant) id(temp string, ext string) (string, error) {
|
||||||
|
date := time.Now().Format("20060102")
|
||||||
|
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(temp)))[:8]
|
||||||
|
return fmt.Sprintf("/__assistants/%s/%s/%s%s", ast.ID, date, hash, ext), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download implements file download functionality
|
||||||
|
func (ast *Assistant) Download(ctx context.Context, fileID string) (*FileResponse, error) {
|
||||||
|
data, err := fs.Get("data")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get filesystem error: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, err := data.Exists(fileID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("check file error: %s", err.Error())
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf("file %s not found", fileID)
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := data.ReadCloser(fileID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ext := filepath.Ext(fileID)
|
||||||
|
contentType := "application/octet-stream"
|
||||||
|
if v, err := data.MimeType(fileID); err == nil {
|
||||||
|
contentType = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return &FileResponse{
|
||||||
|
Reader: reader,
|
||||||
|
ContentType: contentType,
|
||||||
|
Extension: ext,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadBase64 implements base64 file reading functionality
|
||||||
|
func (ast *Assistant) ReadBase64(ctx context.Context, fileID string) (string, error) {
|
||||||
|
data, err := fs.Get("data")
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("get filesystem error: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, err := data.Exists(fileID)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("check file error: %s", err.Error())
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
return "", fmt.Errorf("file %s not found", fileID)
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := data.ReadFile(fileID)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("read file error: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64.StdEncoding.EncodeToString(content), nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -174,7 +174,7 @@ func (ast *Assistant) Clone() *Assistant {
|
||||||
Mentionable: ast.Mentionable,
|
Mentionable: ast.Mentionable,
|
||||||
Automated: ast.Automated,
|
Automated: ast.Automated,
|
||||||
Script: ast.Script,
|
Script: ast.Script,
|
||||||
API: ast.API,
|
openai: ast.openai,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deep copy tags
|
// Deep copy tags
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ import (
|
||||||
"github.com/yaoapp/gou/rag/driver"
|
"github.com/yaoapp/gou/rag/driver"
|
||||||
v8 "github.com/yaoapp/gou/runtime/v8"
|
v8 "github.com/yaoapp/gou/runtime/v8"
|
||||||
"github.com/yaoapp/yao/neo/store"
|
"github.com/yaoapp/yao/neo/store"
|
||||||
|
neovision "github.com/yaoapp/yao/neo/vision"
|
||||||
|
"github.com/yaoapp/yao/openai"
|
||||||
"github.com/yaoapp/yao/share"
|
"github.com/yaoapp/yao/share"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
@ -20,6 +22,8 @@ import (
|
||||||
var loaded = NewCache(200) // 200 is the default capacity
|
var loaded = NewCache(200) // 200 is the default capacity
|
||||||
var storage store.Store = nil
|
var storage store.Store = nil
|
||||||
var rag *RAG = nil
|
var rag *RAG = nil
|
||||||
|
var vision *neovision.Vision = nil
|
||||||
|
var defaultConnector string = "" // default connector
|
||||||
|
|
||||||
// LoadBuiltIn load the built-in assistants
|
// LoadBuiltIn load the built-in assistants
|
||||||
func LoadBuiltIn() error {
|
func LoadBuiltIn() error {
|
||||||
|
|
@ -73,6 +77,12 @@ func LoadBuiltIn() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize the assistant
|
||||||
|
err = assistant.initialize()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
sort++
|
sort++
|
||||||
loaded.Put(assistant)
|
loaded.Put(assistant)
|
||||||
|
|
||||||
|
|
@ -86,6 +96,16 @@ func SetStorage(s store.Store) {
|
||||||
storage = s
|
storage = s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetVision set the vision
|
||||||
|
func SetVision(v *neovision.Vision) {
|
||||||
|
vision = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConnector set the connector
|
||||||
|
func SetConnector(c string) {
|
||||||
|
defaultConnector = c
|
||||||
|
}
|
||||||
|
|
||||||
// SetRAG set the RAG engine
|
// SetRAG set the RAG engine
|
||||||
// e: the RAG engine
|
// e: the RAG engine
|
||||||
// u: the RAG file uploader
|
// u: the RAG file uploader
|
||||||
|
|
@ -115,6 +135,11 @@ func ClearCache() {
|
||||||
|
|
||||||
// LoadStore create a new assistant from store
|
// LoadStore create a new assistant from store
|
||||||
func LoadStore(id string) (*Assistant, error) {
|
func LoadStore(id string) (*Assistant, error) {
|
||||||
|
|
||||||
|
if id == "" {
|
||||||
|
return nil, fmt.Errorf("assistant_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
assistant, exists := loaded.Get(id)
|
assistant, exists := loaded.Get(id)
|
||||||
if exists {
|
if exists {
|
||||||
return assistant, nil
|
return assistant, nil
|
||||||
|
|
@ -331,6 +356,12 @@ func loadMap(data map[string]interface{}) (*Assistant, error) {
|
||||||
assistant.UpdatedAt = ts
|
assistant.UpdatedAt = ts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize the assistant
|
||||||
|
err := assistant.initialize()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return assistant, nil
|
return assistant, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -398,3 +429,21 @@ func loadScriptSource(source string, file string) (*v8.Script, error) {
|
||||||
}
|
}
|
||||||
return script, nil
|
return script, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Init init the assistant
|
||||||
|
// Choose the connector and initialize the assistant
|
||||||
|
func (ast *Assistant) initialize() error {
|
||||||
|
|
||||||
|
conn := defaultConnector
|
||||||
|
if ast.Connector != "" {
|
||||||
|
conn = ast.Connector
|
||||||
|
}
|
||||||
|
ast.Connector = conn
|
||||||
|
|
||||||
|
api, err := openai.New(conn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ast.openai = api
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
package local
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Chat the chat
|
|
||||||
func (ast *Local) Chat(ctx context.Context, messages []map[string]interface{}, option map[string]interface{}, cb func(data []byte) int) error {
|
|
||||||
|
|
||||||
if ast.openai == nil {
|
|
||||||
return fmt.Errorf("api is not initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, ext := ast.openai.ChatCompletionsWith(ctx, messages, option, cb)
|
|
||||||
if ext != nil {
|
|
||||||
return fmt.Errorf("openai chat completions with error: %s", ext.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,162 +0,0 @@
|
||||||
package local
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"mime/multipart"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/yaoapp/gou/fs"
|
|
||||||
"github.com/yaoapp/yao/neo/assistant"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AllowedFileTypes the allowed file types
|
|
||||||
var AllowedFileTypes = map[string]string{
|
|
||||||
"application/json": "json",
|
|
||||||
"application/pdf": "pdf",
|
|
||||||
"application/msword": "doc",
|
|
||||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
|
|
||||||
"application/vnd.oasis.opendocument.text": "odt",
|
|
||||||
"application/vnd.ms-excel": "xls",
|
|
||||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
|
|
||||||
"application/vnd.ms-powerpoint": "ppt",
|
|
||||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation": "pptx",
|
|
||||||
}
|
|
||||||
|
|
||||||
// MaxSize 20M max file size
|
|
||||||
var MaxSize int64 = 20 * 1024 * 1024
|
|
||||||
|
|
||||||
// Upload the file
|
|
||||||
func (ast *Local) Upload(ctx context.Context, file *multipart.FileHeader, reader io.Reader, option map[string]interface{}) (*assistant.File, error) {
|
|
||||||
|
|
||||||
// check file size
|
|
||||||
if file.Size > MaxSize {
|
|
||||||
return nil, fmt.Errorf("file size %d exceeds the maximum size of %d", file.Size, MaxSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
contentType := file.Header.Get("Content-Type")
|
|
||||||
if !ast.allowed(contentType) {
|
|
||||||
return nil, fmt.Errorf("file type %s not allowed", contentType)
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := fs.Get("data")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ext := filepath.Ext(file.Filename)
|
|
||||||
id, err := ast.id(file.Filename, ext)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
filename := fmt.Sprintf("%s%s", id, ext)
|
|
||||||
_, err = data.Write(filename, reader, 0644)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &assistant.File{
|
|
||||||
ID: filename,
|
|
||||||
Filename: filename,
|
|
||||||
ContentType: contentType,
|
|
||||||
Bytes: int(file.Size),
|
|
||||||
CreatedAt: int(time.Now().Unix()),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ast *Local) id(temp string, ext string) (string, error) {
|
|
||||||
date := time.Now().Format("20060102")
|
|
||||||
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(temp)))[:8]
|
|
||||||
return fmt.Sprintf("/__assistants/%s/%s/%s%s", ast.ID, date, hash, ext), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ast *Local) allowed(contentType string) bool {
|
|
||||||
if _, ok := AllowedFileTypes[contentType]; ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// text/* // image/* // audio/* // video/*
|
|
||||||
if strings.HasPrefix(contentType, "text/") || strings.HasPrefix(contentType, "image/") || strings.HasPrefix(contentType, "audio/") || strings.HasPrefix(contentType, "video/") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Download downloads a file
|
|
||||||
func (ast *Local) Download(ctx context.Context, fileID string) (*assistant.FileResponse, error) {
|
|
||||||
|
|
||||||
// Get the data filesystem
|
|
||||||
data, err := fs.Get("data")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("get filesystem error: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if file exists
|
|
||||||
exists, err := data.Exists(fileID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("check file error: %s", err.Error())
|
|
||||||
}
|
|
||||||
if !exists {
|
|
||||||
return nil, fmt.Errorf("file %s not found", fileID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open the file
|
|
||||||
reader, err := data.ReadCloser(fileID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get content type and extension
|
|
||||||
ext := filepath.Ext(fileID)
|
|
||||||
|
|
||||||
// Get content type from mime type
|
|
||||||
contentType := "application/octet-stream"
|
|
||||||
if v, err := data.MimeType(fileID); err == nil {
|
|
||||||
contentType = v
|
|
||||||
}
|
|
||||||
|
|
||||||
for mimeType, extension := range AllowedFileTypes {
|
|
||||||
if "."+extension == ext {
|
|
||||||
contentType = mimeType
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &assistant.FileResponse{
|
|
||||||
Reader: reader,
|
|
||||||
ContentType: contentType,
|
|
||||||
Extension: ext,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadBase64 reads a file and returns its base64 encoded content
|
|
||||||
func (ast *Local) ReadBase64(ctx context.Context, fileID string) (string, error) {
|
|
||||||
// Get the data filesystem
|
|
||||||
data, err := fs.Get("data")
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("get filesystem error: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if file exists
|
|
||||||
exists, err := data.Exists(fileID)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("check file error: %s", err.Error())
|
|
||||||
}
|
|
||||||
if !exists {
|
|
||||||
return "", fmt.Errorf("file %s not found", fileID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read file content
|
|
||||||
content, err := data.ReadFile(fileID)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("read file error: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encode to base64
|
|
||||||
return base64.StdEncoding.EncodeToString(content), nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
package local
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/yaoapp/gou/connector"
|
|
||||||
"github.com/yaoapp/yao/neo/assistant"
|
|
||||||
"github.com/yaoapp/yao/openai"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Local the local assistant
|
|
||||||
type Local struct {
|
|
||||||
ID string `json:"assistant_id"`
|
|
||||||
Prompts []assistant.Prompt `json:"prompts,omitempty"`
|
|
||||||
Connector connector.Connector `json:"-" yaml:"-"`
|
|
||||||
openai *openai.OpenAI
|
|
||||||
}
|
|
||||||
|
|
||||||
// New create a new local assistant
|
|
||||||
func New(connector connector.Connector, prompts []assistant.Prompt, id string) (*Local, error) {
|
|
||||||
|
|
||||||
setting := connector.Setting()
|
|
||||||
api, err := openai.NewOpenAI(setting)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Local{Connector: connector, ID: id, Prompts: prompts, openai: api}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// List list all assistants
|
|
||||||
func (ast *Local) List(ctx context.Context, param assistant.QueryParam) ([]assistant.Assistant, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,118 +0,0 @@
|
||||||
package openai
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
chatMessage "github.com/yaoapp/yao/neo/message"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Chat the chat struct
|
|
||||||
type Chat struct {
|
|
||||||
ID string `json:"chat_id"`
|
|
||||||
ThreadID string `json:"thread_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewChat create a new chat
|
|
||||||
func (ast *OpenAI) NewChat() {}
|
|
||||||
|
|
||||||
// Chat the chat
|
|
||||||
func (ast *OpenAI) Chat(ctx context.Context, messages []map[string]interface{}, option map[string]interface{}, cb func(data []byte) int) error {
|
|
||||||
|
|
||||||
if ast.openai == nil {
|
|
||||||
return fmt.Errorf("openai is not initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
requestMessages, err := ast.requestMessages(ctx, messages)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("request messages error: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
_, ext := ast.openai.ChatCompletionsWith(ctx, requestMessages, option, cb)
|
|
||||||
if ext != nil {
|
|
||||||
return fmt.Errorf("openai chat completions with error: %s", ext.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ast *OpenAI) requestMessages(ctx context.Context, messages []map[string]interface{}) ([]map[string]interface{}, error) {
|
|
||||||
newMessages := []map[string]interface{}{}
|
|
||||||
length := len(messages)
|
|
||||||
for index, message := range messages {
|
|
||||||
role, ok := message["role"].(string)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("role must be string")
|
|
||||||
}
|
|
||||||
|
|
||||||
content, ok := message["content"].(string)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("content must be string")
|
|
||||||
}
|
|
||||||
|
|
||||||
newMessage := map[string]interface{}{
|
|
||||||
"role": role,
|
|
||||||
"content": content,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle name if present
|
|
||||||
if name, ok := message["name"].(string); ok {
|
|
||||||
newMessage["name"] = name
|
|
||||||
}
|
|
||||||
|
|
||||||
newMessage["content"] = content
|
|
||||||
|
|
||||||
// Special handling for user messages with JSON content last message
|
|
||||||
if role == "user" && index == length-1 {
|
|
||||||
content = strings.TrimSpace(content)
|
|
||||||
msg, err := chatMessage.NewString(content)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("new string error: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
newMessage["content"] = msg.Text
|
|
||||||
if msg.Attachments != nil {
|
|
||||||
content, err := ast.withAttachments(ctx, msg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("with attachments error: %s", err.Error())
|
|
||||||
}
|
|
||||||
newMessage["content"] = content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newMessages = append(newMessages, newMessage)
|
|
||||||
}
|
|
||||||
return newMessages, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ast *OpenAI) withAttachments(ctx context.Context, msg *chatMessage.Message) ([]map[string]interface{}, error) {
|
|
||||||
contents := []map[string]interface{}{{"type": "text", "text": msg.Text}}
|
|
||||||
images := []string{}
|
|
||||||
for _, attachment := range msg.Attachments {
|
|
||||||
if strings.HasPrefix(attachment.ContentType, "image/") {
|
|
||||||
images = append(images, attachment.FileID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(images) == 0 {
|
|
||||||
return contents, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, image := range images {
|
|
||||||
bytes64, err := ast.ReadBase64(ctx, image)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("read base64 error: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
contents = append(contents, map[string]interface{}{
|
|
||||||
"type": "image_url",
|
|
||||||
"image_url": map[string]string{
|
|
||||||
"url": fmt.Sprintf("data:image/jpeg;base64,%s", bytes64),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return contents, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,167 +0,0 @@
|
||||||
package openai
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"mime/multipart"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/yaoapp/gou/fs"
|
|
||||||
"github.com/yaoapp/yao/neo/assistant"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AllowedFileTypes the allowed file types
|
|
||||||
var AllowedFileTypes = map[string]string{
|
|
||||||
"application/json": "json",
|
|
||||||
"application/pdf": "pdf",
|
|
||||||
"application/msword": "doc",
|
|
||||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
|
|
||||||
"application/vnd.oasis.opendocument.text": "odt",
|
|
||||||
"application/vnd.ms-excel": "xls",
|
|
||||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
|
|
||||||
"application/vnd.ms-powerpoint": "ppt",
|
|
||||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation": "pptx",
|
|
||||||
}
|
|
||||||
|
|
||||||
// MaxSize 20M max file size
|
|
||||||
var MaxSize int64 = 20 * 1024 * 1024
|
|
||||||
|
|
||||||
// Upload the file
|
|
||||||
func (ast *OpenAI) Upload(ctx context.Context, file *multipart.FileHeader, reader io.Reader, option map[string]interface{}) (*assistant.File, error) {
|
|
||||||
|
|
||||||
// check file size
|
|
||||||
if file.Size > MaxSize {
|
|
||||||
return nil, fmt.Errorf("file size %d exceeds the maximum size of %d", file.Size, MaxSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
contentType := file.Header.Get("Content-Type")
|
|
||||||
if !ast.allowed(contentType) {
|
|
||||||
return nil, fmt.Errorf("file type %s not allowed", contentType)
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := fs.Get("data")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ext := filepath.Ext(file.Filename)
|
|
||||||
id, err := ast.id(file.Filename, ext)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
filename := id
|
|
||||||
_, err = data.Write(filename, reader, 0644)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &assistant.File{
|
|
||||||
ID: filename,
|
|
||||||
Filename: filename,
|
|
||||||
ContentType: contentType,
|
|
||||||
Bytes: int(file.Size),
|
|
||||||
CreatedAt: int(time.Now().Unix()),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ast *OpenAI) id(temp string, ext string) (string, error) {
|
|
||||||
date := time.Now().Format("20060102")
|
|
||||||
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(temp)))[:8]
|
|
||||||
return fmt.Sprintf("/__assistants/%s/%s/%s%s", ast.ID, date, hash, ext), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ast *OpenAI) allowed(contentType string) bool {
|
|
||||||
if _, ok := AllowedFileTypes[contentType]; ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// text/* // image/* // audio/* // video/*
|
|
||||||
if strings.HasPrefix(contentType, "text/") || strings.HasPrefix(contentType, "image/") || strings.HasPrefix(contentType, "audio/") || strings.HasPrefix(contentType, "video/") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// FileLists list all files
|
|
||||||
func (ast *OpenAI) FileLists() {}
|
|
||||||
|
|
||||||
// FileDelete delete a file
|
|
||||||
func (ast *OpenAI) FileDelete() {}
|
|
||||||
|
|
||||||
// FileContent get the content of a file
|
|
||||||
func (ast *OpenAI) FileContent() {}
|
|
||||||
|
|
||||||
// FileInfo get the information of a file
|
|
||||||
func (ast *OpenAI) FileInfo() {}
|
|
||||||
|
|
||||||
// Download downloads a file
|
|
||||||
func (ast *OpenAI) Download(ctx context.Context, fileID string) (*assistant.FileResponse, error) {
|
|
||||||
|
|
||||||
// Get the data filesystem
|
|
||||||
data, err := fs.Get("data")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("get filesystem error: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if file exists
|
|
||||||
exists, err := data.Exists(fileID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("check file error: %s", err.Error())
|
|
||||||
}
|
|
||||||
if !exists {
|
|
||||||
return nil, fmt.Errorf("file %s not found", fileID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open the file
|
|
||||||
reader, err := data.ReadCloser(fileID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get content type and extension
|
|
||||||
ext := filepath.Ext(fileID)
|
|
||||||
|
|
||||||
// Get content type from mime type
|
|
||||||
contentType := "application/octet-stream"
|
|
||||||
if v, err := data.MimeType(fileID); err == nil {
|
|
||||||
contentType = v
|
|
||||||
}
|
|
||||||
|
|
||||||
return &assistant.FileResponse{
|
|
||||||
Reader: reader,
|
|
||||||
ContentType: contentType,
|
|
||||||
Extension: ext,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadBase64 reads a file and returns its base64 encoded content
|
|
||||||
func (ast *OpenAI) ReadBase64(ctx context.Context, fileID string) (string, error) {
|
|
||||||
// Get the data filesystem
|
|
||||||
data, err := fs.Get("data")
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("get filesystem error: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if file exists
|
|
||||||
exists, err := data.Exists(fileID)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("check file error: %s", err.Error())
|
|
||||||
}
|
|
||||||
if !exists {
|
|
||||||
return "", fmt.Errorf("file %s not found", fileID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read file content
|
|
||||||
content, err := data.ReadFile(fileID)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("read file error: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encode to base64
|
|
||||||
return base64.StdEncoding.EncodeToString(content), nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
package openai
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/yaoapp/gou/connector"
|
|
||||||
"github.com/yaoapp/yao/neo/assistant"
|
|
||||||
api "github.com/yaoapp/yao/openai"
|
|
||||||
)
|
|
||||||
|
|
||||||
// OpenAI the openai assistant
|
|
||||||
type OpenAI struct {
|
|
||||||
ID string `json:"assistant_id"` // the assistant id
|
|
||||||
Connector connector.Connector `json:"-" yaml:"-"`
|
|
||||||
openai *api.OpenAI
|
|
||||||
}
|
|
||||||
|
|
||||||
// New create a new openai assistant
|
|
||||||
func New(connector connector.Connector, id string) (*OpenAI, error) {
|
|
||||||
|
|
||||||
setting := connector.Setting()
|
|
||||||
openai, err := api.NewOpenAI(setting)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &OpenAI{ID: id, Connector: connector, openai: openai}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Current set the current assistant
|
|
||||||
func (ast *OpenAI) Current(id string) *OpenAI {
|
|
||||||
ast.ID = id
|
|
||||||
return ast
|
|
||||||
}
|
|
||||||
|
|
||||||
// List list all assistants
|
|
||||||
func (ast *OpenAI) List(ctx context.Context, param assistant.QueryParam) ([]assistant.Assistant, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create create a new assistant
|
|
||||||
func (ast *OpenAI) Create() {}
|
|
||||||
|
|
||||||
// Delete delete an assistant
|
|
||||||
func (ast *OpenAI) Delete() {}
|
|
||||||
|
|
||||||
// Update update an assistant
|
|
||||||
func (ast *OpenAI) Update() {}
|
|
||||||
|
|
||||||
// Get get an assistant
|
|
||||||
func (ast *OpenAI) Get() {}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
package openai
|
|
||||||
|
|
||||||
// Thread the thread struct
|
|
||||||
type Thread struct {
|
|
||||||
ID string `json:"thread_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ThreadList list all threads
|
|
||||||
func (ast *OpenAI) ThreadList() {}
|
|
||||||
|
|
||||||
// ThreadCreate create a new thread
|
|
||||||
func (ast *OpenAI) ThreadCreate() {}
|
|
||||||
|
|
||||||
// ThreadGet get a thread
|
|
||||||
func (ast *OpenAI) ThreadGet(id string) {}
|
|
||||||
|
|
||||||
// ThreadDelete delete a thread
|
|
||||||
func (ast *OpenAI) ThreadDelete() {}
|
|
||||||
|
|
||||||
// ThreadUpdate update a thread
|
|
||||||
func (ast *OpenAI) ThreadUpdate() {}
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/yaoapp/gou/rag/driver"
|
"github.com/yaoapp/gou/rag/driver"
|
||||||
v8 "github.com/yaoapp/gou/runtime/v8"
|
v8 "github.com/yaoapp/gou/runtime/v8"
|
||||||
|
api "github.com/yaoapp/yao/openai"
|
||||||
)
|
)
|
||||||
|
|
||||||
// API the assistant API interface
|
// API the assistant API interface
|
||||||
|
|
@ -64,9 +65,9 @@ type Assistant struct {
|
||||||
Prompts []Prompt `json:"prompts,omitempty"` // AI Prompts
|
Prompts []Prompt `json:"prompts,omitempty"` // AI Prompts
|
||||||
Flows []map[string]interface{} `json:"flows,omitempty"` // Assistant Flows
|
Flows []map[string]interface{} `json:"flows,omitempty"` // Assistant Flows
|
||||||
Script *v8.Script `json:"-" yaml:"-"` // Assistant Script
|
Script *v8.Script `json:"-" yaml:"-"` // Assistant Script
|
||||||
API API `json:"-" yaml:"-"` // Assistant API
|
|
||||||
CreatedAt int64 `json:"created_at"` // Creation timestamp
|
CreatedAt int64 `json:"created_at"` // Creation timestamp
|
||||||
UpdatedAt int64 `json:"updated_at"` // Last update timestamp
|
UpdatedAt int64 `json:"updated_at"` // Last update timestamp
|
||||||
|
openai *api.OpenAI // OpenAI API
|
||||||
}
|
}
|
||||||
|
|
||||||
// File the file
|
// File the file
|
||||||
|
|
|
||||||
103
neo/hooks.go
103
neo/hooks.go
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
"github.com/yaoapp/gou/process"
|
"github.com/yaoapp/gou/process"
|
||||||
"github.com/yaoapp/yao/neo/assistant"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// HookCreate create the assistant
|
// HookCreate create the assistant
|
||||||
|
|
@ -69,51 +68,6 @@ func (neo *DSL) HookCreate(ctx Context, messages []map[string]interface{}, c *gi
|
||||||
return CreateResponse{AssistantID: assistantID, ChatID: ctx.ChatID}, nil
|
return CreateResponse{AssistantID: assistantID, ChatID: ctx.ChatID}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// HookAssistants query the assistant list from the assistant list hook
|
|
||||||
func (neo *DSL) HookAssistants(ctx context.Context, param assistant.QueryParam) ([]assistant.Assistant, error) {
|
|
||||||
if neo.AssistantListHook == "" {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a context with 10 second timeout
|
|
||||||
timeoutCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
p, err := process.Of(neo.AssistantListHook, param)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = p.WithContext(timeoutCtx).Execute()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer p.Release()
|
|
||||||
|
|
||||||
// Check if context was canceled
|
|
||||||
if timeoutCtx.Err() != nil {
|
|
||||||
return nil, timeoutCtx.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
value := p.Value()
|
|
||||||
if value == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var list []assistant.Assistant
|
|
||||||
bytes, err := jsoniter.Marshal(value)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = jsoniter.Unmarshal(bytes, &list)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return list, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HookPrepare executes the prepare hook before AI is called
|
// HookPrepare executes the prepare hook before AI is called
|
||||||
func (neo *DSL) HookPrepare(ctx Context, messages []map[string]interface{}) ([]map[string]interface{}, error) {
|
func (neo *DSL) HookPrepare(ctx Context, messages []map[string]interface{}) ([]map[string]interface{}, error) {
|
||||||
if neo.Prepare == "" {
|
if neo.Prepare == "" {
|
||||||
|
|
@ -194,60 +148,3 @@ func (neo *DSL) HookWrite(ctx Context, messages []map[string]interface{}, respon
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// HookMention query the mention list
|
|
||||||
func (neo *DSL) HookMention(ctx context.Context, keywords string) ([]Mention, error) {
|
|
||||||
|
|
||||||
// Default Get the assistant list
|
|
||||||
if neo.MentionHook == "" {
|
|
||||||
var mentions []Mention
|
|
||||||
assistants := neo.GetAssistants()
|
|
||||||
for _, assistant := range assistants {
|
|
||||||
mentions = append(mentions, Mention{
|
|
||||||
ID: assistant.ID,
|
|
||||||
Name: assistant.Name,
|
|
||||||
Type: "assistant",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return mentions, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a context with 10 second timeout
|
|
||||||
timeoutCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
p, err := process.Of(neo.MentionHook, keywords)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = p.WithContext(timeoutCtx).Execute()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer p.Release()
|
|
||||||
|
|
||||||
// Check if context was canceled
|
|
||||||
if timeoutCtx.Err() != nil {
|
|
||||||
return nil, timeoutCtx.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
value := p.Value()
|
|
||||||
if value == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var list []Mention
|
|
||||||
bytes, err := jsoniter.Marshal(value)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = jsoniter.Unmarshal(bytes, &list)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return list, nil
|
|
||||||
}
|
|
||||||
|
|
|
||||||
10
neo/load.go
10
neo/load.go
|
|
@ -155,6 +155,14 @@ func (neo *DSL) initAssistant() error {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Assistant Vision
|
||||||
|
if Neo.Vision != nil {
|
||||||
|
assistant.SetVision(Neo.Vision)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default Connector
|
||||||
|
assistant.SetConnector(Neo.Connector)
|
||||||
|
|
||||||
// Load Built-in Assistants
|
// Load Built-in Assistants
|
||||||
err := assistant.LoadBuiltIn()
|
err := assistant.LoadBuiltIn()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -167,7 +175,7 @@ func (neo *DSL) initAssistant() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
Neo.Assistant = defaultAssistant.API
|
Neo.Assistant = defaultAssistant
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
184
neo/neo.go
184
neo/neo.go
|
|
@ -1,21 +1,15 @@
|
||||||
package neo
|
package neo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/yaoapp/gou/connector"
|
|
||||||
"github.com/yaoapp/kun/log"
|
"github.com/yaoapp/kun/log"
|
||||||
"github.com/yaoapp/yao/neo/assistant"
|
"github.com/yaoapp/yao/neo/assistant"
|
||||||
"github.com/yaoapp/yao/neo/assistant/local"
|
|
||||||
"github.com/yaoapp/yao/neo/assistant/openai"
|
|
||||||
"github.com/yaoapp/yao/neo/message"
|
"github.com/yaoapp/yao/neo/message"
|
||||||
"github.com/yaoapp/yao/neo/store"
|
|
||||||
"github.com/yaoapp/yao/share"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Lock the assistant list
|
// Lock the assistant list
|
||||||
|
|
@ -39,23 +33,20 @@ func (neo *DSL) Answer(ctx Context, question string, c *gin.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select Assistant
|
// Select Assistant
|
||||||
ast, err := neo.selectAssistant(res.AssistantID)
|
ast, err := neo.Select(res.AssistantID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chat with AI
|
// Chat with AI
|
||||||
return neo.chat(ast, ctx, messages, c)
|
return neo.chat(ast, ctx, messages, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAssistants returns the list of assistants
|
// Select select an assistant
|
||||||
func (neo *DSL) GetAssistants() []assistant.Assistant {
|
func (neo *DSL) Select(id string) (assistant.API, error) {
|
||||||
return neo.AssistantList
|
if id == "" {
|
||||||
}
|
return neo.Assistant, nil
|
||||||
|
}
|
||||||
// GetMentions returns the mention list
|
return assistant.Get(id)
|
||||||
func (neo *DSL) GetMentions(keywords string) ([]Mention, error) {
|
|
||||||
return neo.HookMention(context.Background(), keywords)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GeneratePrompts generate prompts for the AI assistant
|
// GeneratePrompts generate prompts for the AI assistant
|
||||||
|
|
@ -110,7 +101,7 @@ func (neo *DSL) GenerateWithAI(ctx Context, input string, messageType string, sy
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select Assistant
|
// Select Assistant
|
||||||
ast, err := neo.selectAssistant(res.AssistantID)
|
ast, err := neo.Select(res.AssistantID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
@ -234,7 +225,7 @@ func (neo *DSL) Upload(ctx Context, c *gin.Context) (*assistant.File, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select Assistant
|
// Select Assistant
|
||||||
ast, err := neo.selectAssistant(res.AssistantID)
|
ast, err := neo.Select(res.AssistantID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -257,7 +248,7 @@ func (neo *DSL) Download(ctx Context, c *gin.Context) (*assistant.FileResponse,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select Assistant
|
// Select Assistant
|
||||||
ast, err := neo.selectAssistant(res.AssistantID)
|
ast, err := neo.Select(res.AssistantID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -346,130 +337,6 @@ func (neo *DSL) chat(ast assistant.API, ctx Context, messages []map[string]inter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateAssistantList update the assistant list
|
|
||||||
func (neo *DSL) updateAssistantList(list []assistant.Assistant) {
|
|
||||||
lock.Lock()
|
|
||||||
defer lock.Unlock()
|
|
||||||
neo.AssistantList = list
|
|
||||||
neo.AssistantMaps = make(map[string]assistant.Assistant)
|
|
||||||
if list != nil {
|
|
||||||
for _, assistant := range list {
|
|
||||||
neo.AssistantMaps[assistant.ID] = assistant
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// selectAssistant select the assistant
|
|
||||||
func (neo *DSL) selectAssistant(assistantID string) (assistant.API, error) {
|
|
||||||
ast := neo.Assistant
|
|
||||||
if assistantID != "" {
|
|
||||||
ast, err := neo.newAssistant(assistantID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return ast, nil
|
|
||||||
}
|
|
||||||
return ast, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// newAssistant create a new assistant
|
|
||||||
func (neo *DSL) newAssistant(id string) (assistant.API, error) {
|
|
||||||
// Try to find assistant in AssistantList first
|
|
||||||
if id != "" && neo.AssistantMaps != nil {
|
|
||||||
if ast, ok := neo.AssistantMaps[id]; ok {
|
|
||||||
|
|
||||||
if ast.API != nil {
|
|
||||||
return ast.API, nil
|
|
||||||
}
|
|
||||||
api, err := neo.newAssistantByConfig(&ast)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ast.API = api
|
|
||||||
return api, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return neo.newAssistantByConnector(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// newAssistantByConfig create a new assistant from assistant configuration
|
|
||||||
func (neo *DSL) newAssistantByConfig(ast *assistant.Assistant) (assistant.API, error) {
|
|
||||||
return neo.newAssistantByConnector(ast.Connector)
|
|
||||||
}
|
|
||||||
|
|
||||||
// newAssistantByConnector create a new assistant from connector id
|
|
||||||
func (neo *DSL) newAssistantByConnector(id string) (assistant.API, error) {
|
|
||||||
// Moapi connector
|
|
||||||
if id == "" || strings.HasPrefix(id, "moapi") {
|
|
||||||
return neo.newMoapiAssistant(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Other connector
|
|
||||||
conn, err := connector.Select(id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Neo assistant connector %s not support", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
if conn.Is(connector.OPENAI) {
|
|
||||||
api, err := openai.New(conn, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Create openai assistant error: %s", err.Error())
|
|
||||||
}
|
|
||||||
return api, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Base on the assistant list hook
|
|
||||||
api, err := local.New(conn, neo.Prompts, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Create local assistant error: %s", err.Error())
|
|
||||||
}
|
|
||||||
return api, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// newMoapiAssistant creates a new moapi assistant
|
|
||||||
func (neo *DSL) newMoapiAssistant(id string) (assistant.API, error) {
|
|
||||||
model := "gpt-3.5-turbo"
|
|
||||||
if strings.HasPrefix(id, "moapi:") {
|
|
||||||
model = strings.TrimPrefix(id, "moapi:")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the moapi setting
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the moapi secret
|
|
||||||
if key == "" {
|
|
||||||
return nil, fmt.Errorf("The moapi secret is empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := connector.New(`moapi`, `__yao.moapi`, []byte(`{"name":"Moapi", "options":{"model": "`+model+`", "key": "`+key+`", "organization": "`+organization+`", "host": "`+url+`"}}`))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Create moapi assistant error: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
api, err := openai.New(conn, strings.ReplaceAll(id, ":", "_"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Create openai assistant error: %s", err.Error())
|
|
||||||
}
|
|
||||||
return api, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// createDefaultAssistant create a default assistant
|
|
||||||
func (neo *DSL) createDefaultAssistant() (assistant.API, error) {
|
|
||||||
if neo.Use != "" {
|
|
||||||
return neo.newAssistant(neo.Use)
|
|
||||||
}
|
|
||||||
return neo.newAssistant(neo.Connector)
|
|
||||||
}
|
|
||||||
|
|
||||||
// chatMessages get the chat messages
|
// chatMessages get the chat messages
|
||||||
func (neo *DSL) chatMessages(ctx Context, content ...string) ([]map[string]interface{}, error) {
|
func (neo *DSL) chatMessages(ctx Context, content ...string) ([]map[string]interface{}, error) {
|
||||||
|
|
||||||
|
|
@ -509,37 +376,6 @@ func (neo *DSL) saveHistory(sid string, chatID string, content []byte, messages
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// createStore create a new store
|
|
||||||
func (neo *DSL) createStore() error {
|
|
||||||
|
|
||||||
var err error
|
|
||||||
if neo.StoreSetting.Connector == "default" || neo.StoreSetting.Connector == "" {
|
|
||||||
neo.Store, err = store.NewXun(neo.StoreSetting)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// other connector
|
|
||||||
conn, err := connector.Select(neo.StoreSetting.Connector)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if conn.Is(connector.DATABASE) {
|
|
||||||
neo.Store, err = store.NewXun(neo.StoreSetting)
|
|
||||||
return err
|
|
||||||
|
|
||||||
} else if conn.Is(connector.REDIS) {
|
|
||||||
neo.Store = store.NewRedis()
|
|
||||||
return nil
|
|
||||||
|
|
||||||
} else if conn.Is(connector.MONGO) {
|
|
||||||
neo.Store = store.NewMongo()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("%s store connector %s not support", neo.ID, neo.StoreSetting.Connector)
|
|
||||||
}
|
|
||||||
|
|
||||||
// sendMessage sends a message to the client
|
// sendMessage sends a message to the client
|
||||||
func (neo *DSL) sendMessage(w gin.ResponseWriter, data interface{}) error {
|
func (neo *DSL) sendMessage(w gin.ResponseWriter, data interface{}) error {
|
||||||
if msg, ok := data.(map[string]interface{}); ok {
|
if msg, ok := data.(map[string]interface{}); ok {
|
||||||
|
|
|
||||||
42
neo/types.go
42
neo/types.go
|
|
@ -13,29 +13,25 @@ import (
|
||||||
|
|
||||||
// DSL AI assistant
|
// DSL AI assistant
|
||||||
type DSL struct {
|
type DSL struct {
|
||||||
ID string `json:"-" yaml:"-"`
|
ID string `json:"-" yaml:"-"`
|
||||||
Name string `json:"name,omitempty" yaml:"name,omitempty"`
|
Name string `json:"name,omitempty" yaml:"name,omitempty"`
|
||||||
Use string `json:"use,omitempty" yaml:"use,omitempty"` // Which assistant to use default
|
Use string `json:"use,omitempty" yaml:"use,omitempty"` // Which assistant to use default
|
||||||
Guard string `json:"guard,omitempty" yaml:"guard,omitempty"`
|
Guard string `json:"guard,omitempty" yaml:"guard,omitempty"`
|
||||||
Connector string `json:"connector" yaml:"connector"`
|
Connector string `json:"connector" yaml:"connector"`
|
||||||
StoreSetting store.Setting `json:"store" yaml:"store"`
|
StoreSetting store.Setting `json:"store" yaml:"store"`
|
||||||
RAGSetting rag.Setting `json:"rag" yaml:"rag"`
|
RAGSetting rag.Setting `json:"rag" yaml:"rag"`
|
||||||
VisionSetting VisionSetting `json:"vision" yaml:"vision"`
|
VisionSetting VisionSetting `json:"vision" yaml:"vision"`
|
||||||
Option map[string]interface{} `json:"option" yaml:"option"`
|
Option map[string]interface{} `json:"option" yaml:"option"`
|
||||||
Prepare string `json:"prepare,omitempty" yaml:"prepare,omitempty"`
|
Prepare string `json:"prepare,omitempty" yaml:"prepare,omitempty"`
|
||||||
Create string `json:"create,omitempty" yaml:"create,omitempty"`
|
Create string `json:"create,omitempty" yaml:"create,omitempty"`
|
||||||
Write string `json:"write,omitempty" yaml:"write,omitempty"`
|
Write string `json:"write,omitempty" yaml:"write,omitempty"`
|
||||||
AssistantListHook string `json:"assistants,omitempty" yaml:"assistants,omitempty"` // Get the assistant list from the hook
|
Prompts []assistant.Prompt `json:"prompts,omitempty" yaml:"prompts,omitempty"`
|
||||||
MentionHook string `json:"mentions,omitempty"` // Get the mention list from the hook
|
Allows []string `json:"allows,omitempty" yaml:"allows,omitempty"`
|
||||||
Prompts []assistant.Prompt `json:"prompts,omitempty" yaml:"prompts,omitempty"`
|
Assistant assistant.API `json:"-" yaml:"-"` // The default assistant
|
||||||
Allows []string `json:"allows,omitempty" yaml:"allows,omitempty"`
|
Store store.Store `json:"-" yaml:"-"`
|
||||||
Assistant assistant.API `json:"-" yaml:"-"` // The default assistant
|
RAG *rag.RAG `json:"-" yaml:"-"`
|
||||||
Store store.Store `json:"-" yaml:"-"`
|
Vision *vision.Vision `json:"-" yaml:"-"`
|
||||||
RAG *rag.RAG `json:"-" yaml:"-"`
|
GuardHandlers []gin.HandlerFunc `json:"-" yaml:"-"`
|
||||||
Vision *vision.Vision `json:"-" yaml:"-"`
|
|
||||||
GuardHandlers []gin.HandlerFunc `json:"-" yaml:"-"`
|
|
||||||
AssistantList []assistant.Assistant `json:"-" yaml:"-"`
|
|
||||||
AssistantMaps map[string]assistant.Assistant `json:"-" yaml:"-"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// VisionSetting the vision setting
|
// VisionSetting the vision setting
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue