yao/neo/assistant/load.go
Max 24b1d11528 Enhance error handling and message processing in Neo API assistant
- Added support for parsing and handling pending and error messages in stream processing
- Improved error message extraction and writing for better error visibility
- Updated message type handling to capture and process raw error messages
- Introduced more flexible input parsing in withHistory and execute methods
- Added environment variable support for connector configuration in assistant loading

This change improves the robustness of error handling and message processing in the Neo API assistant, providing more comprehensive error reporting and input flexibility.
2025-01-30 16:11:29 +08:00

631 lines
12 KiB
Go

package assistant
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"time"
jsoniter "github.com/json-iterator/go"
"github.com/spf13/cast"
"github.com/yaoapp/gou/application"
"github.com/yaoapp/gou/fs"
"github.com/yaoapp/gou/rag/driver"
v8 "github.com/yaoapp/gou/runtime/v8"
"github.com/yaoapp/yao/neo/store"
neovision "github.com/yaoapp/yao/neo/vision"
"github.com/yaoapp/yao/openai"
"github.com/yaoapp/yao/share"
"gopkg.in/yaml.v3"
)
// loaded the loaded assistant
var loaded = NewCache(200) // 200 is the default capacity
var storage store.Store = nil
var rag *RAG = nil
var vision *neovision.Vision = nil
var defaultConnector string = "" // default connector
// LoadBuiltIn load the built-in assistants
func LoadBuiltIn() error {
// Clear the cache
loaded.Clear()
root := `/assistants`
app, err := fs.Get("app")
if err != nil {
return err
}
// Remove the built-in assistants
if storage != nil {
builtIn := true
_, err := storage.DeleteAssistants(store.AssistantFilter{BuiltIn: &builtIn})
if err != nil {
return err
}
}
// Check if the assistant is built-in
if exists, _ := app.Exists(root); !exists {
return nil
}
paths, err := app.ReadDir(root, true)
if err != nil {
return err
}
sort := 1
for _, path := range paths {
pkgfile := filepath.Join(path, "package.yao")
if has, _ := app.Exists(pkgfile); !has {
continue
}
assistant, err := LoadPath(path)
if err != nil {
return err
}
assistant.Readonly = true
assistant.BuiltIn = true
if assistant.Sort == 0 {
assistant.Sort = sort
}
if assistant.Tags == nil {
assistant.Tags = []string{"Built-in"}
}
// Check if the assistant has Built-in tag
hasBuiltIn := false
for _, tag := range assistant.Tags {
if tag == "Built-in" {
hasBuiltIn = true
break
}
}
// add Built-in tag if not exists
if !hasBuiltIn {
assistant.Tags = append(assistant.Tags, "Built-in")
}
// Save the assistant
err = assistant.Save()
if err != nil {
return err
}
// Initialize the assistant
err = assistant.initialize()
if err != nil {
return err
}
sort++
loaded.Put(assistant)
}
return nil
}
// SetStorage set the storage
func SetStorage(s store.Store) {
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
// e: the RAG engine
// u: the RAG file uploader
// v: the RAG vectorizer
func SetRAG(e driver.Engine, u driver.FileUpload, v driver.Vectorizer, setting RAGSetting) {
rag = &RAG{
Engine: e,
Uploader: u,
Vectorizer: v,
Setting: setting,
}
}
// SetCache set the cache
func SetCache(capacity int) {
ClearCache()
loaded = NewCache(capacity)
}
// ClearCache clear the cache
func ClearCache() {
if loaded != nil {
loaded.Clear()
loaded = nil
}
}
// LoadStore create a new assistant from store
func LoadStore(id string) (*Assistant, error) {
if id == "" {
return nil, fmt.Errorf("assistant_id is required")
}
assistant, exists := loaded.Get(id)
if exists {
return assistant, nil
}
if storage == nil {
return nil, fmt.Errorf("storage is not set")
}
data, err := storage.GetAssistant(id)
if err != nil {
return nil, err
}
// Load from path
if data["path"] != nil {
assistant, err = LoadPath(data["path"].(string))
if err != nil {
return nil, err
}
loaded.Put(assistant)
return assistant, nil
}
// Load from store
assistant, err = loadMap(data)
if err != nil {
return nil, err
}
loaded.Put(assistant)
return assistant, nil
}
// loadPackage loads and parses the package.yao file
func loadPackage(path string) (map[string]interface{}, error) {
app, err := fs.Get("app")
if err != nil {
return nil, err
}
pkgfile := filepath.Join(path, "package.yao")
if has, _ := app.Exists(pkgfile); !has {
return nil, fmt.Errorf("package.yao not found in %s", path)
}
pkgraw, err := app.ReadFile(pkgfile)
if err != nil {
return nil, err
}
var data map[string]interface{}
err = application.Parse(pkgfile, pkgraw, &data)
if err != nil {
return nil, err
}
// Process connector environment variable
if connector, ok := data["connector"].(string); ok {
if strings.HasPrefix(connector, "$ENV.") {
envKey := strings.TrimPrefix(connector, "$ENV.")
if envValue := os.Getenv(envKey); envValue != "" {
data["connector"] = envValue
}
}
}
return data, nil
}
// LoadPath load assistant from path
func LoadPath(path string) (*Assistant, error) {
app, err := fs.Get("app")
if err != nil {
return nil, err
}
data, err := loadPackage(path)
if err != nil {
return nil, err
}
// assistant_id
id := strings.ReplaceAll(strings.TrimPrefix(path, "/assistants/"), "/", ".")
data["assistant_id"] = id
data["type"] = "assistant"
data["path"] = path
updatedAt := int64(0)
// prompts
promptsfile := filepath.Join(path, "prompts.yml")
if has, _ := app.Exists(promptsfile); has {
prompts, ts, err := loadPrompts(promptsfile, path)
if err != nil {
return nil, err
}
data["prompts"] = prompts
data["updated_at"] = ts
updatedAt = ts
}
// load script
scriptfile := filepath.Join(path, "src", "index.ts")
if has, _ := app.Exists(scriptfile); has {
script, ts, err := loadScript(scriptfile, path)
if err != nil {
return nil, err
}
data["script"] = script
data["updated_at"] = max(updatedAt, ts)
}
// load functions
functionsfile := filepath.Join(path, "functions.json")
if has, _ := app.Exists(functionsfile); has {
functions, ts, err := loadFunctions(functionsfile)
if err != nil {
return nil, err
}
data["functions"] = functions
updatedAt = max(updatedAt, ts)
data["updated_at"] = updatedAt
}
// load flow
return loadMap(data)
}
func loadMap(data map[string]interface{}) (*Assistant, error) {
assistant := &Assistant{}
// assistant_id is required
id, ok := data["assistant_id"].(string)
if !ok {
return nil, fmt.Errorf("assistant_id is required")
}
assistant.ID = id
// name is required
name, ok := data["name"].(string)
if !ok {
return nil, fmt.Errorf("name is required")
}
assistant.Name = name
// avatar
if avatar, ok := data["avatar"].(string); ok {
assistant.Avatar = avatar
}
// Type
if v, ok := data["type"].(string); ok {
assistant.Type = v
}
// Placeholder
if v, ok := data["placeholder"]; ok {
switch vv := v.(type) {
case string:
placeholder, err := jsoniter.Marshal(vv)
if err != nil {
return nil, err
}
assistant.Placeholder = &Placeholder{}
err = jsoniter.Unmarshal(placeholder, assistant.Placeholder)
if err != nil {
return nil, err
}
case map[string]interface{}:
raw, err := jsoniter.Marshal(vv)
if err != nil {
return nil, err
}
assistant.Placeholder = &Placeholder{}
err = jsoniter.Unmarshal(raw, assistant.Placeholder)
if err != nil {
return nil, err
}
case *Placeholder:
assistant.Placeholder = vv
case nil:
assistant.Placeholder = nil
}
}
// Mentionable
if v, ok := data["mentionable"].(bool); ok {
assistant.Mentionable = v
}
// Automated
if v, ok := data["automated"].(bool); ok {
assistant.Automated = v
}
// Readonly
if v, ok := data["readonly"].(bool); ok {
assistant.Readonly = v
}
// built_in
if v, ok := data["built_in"].(bool); ok {
assistant.BuiltIn = v
}
// sort
if v, has := data["sort"]; has {
assistant.Sort = cast.ToInt(v)
}
// path
if v, ok := data["path"].(string); ok {
assistant.Path = v
}
// connector
if connector, ok := data["connector"].(string); ok {
assistant.Connector = connector
}
// tags
if v, has := data["tags"]; has {
switch vv := v.(type) {
case []string:
assistant.Tags = vv
case []interface{}:
var tags []string
for _, tag := range vv {
tags = append(tags, cast.ToString(tag))
}
assistant.Tags = tags
case interface{}:
raw, err := jsoniter.Marshal(vv)
if err != nil {
return nil, err
}
var tags []string
err = jsoniter.Unmarshal(raw, &tags)
if err != nil {
return nil, err
}
assistant.Tags = tags
case string:
assistant.Tags = []string{vv}
}
}
// options
if v, ok := data["options"].(map[string]interface{}); ok {
assistant.Options = v
}
// description
if v, ok := data["description"].(string); ok {
assistant.Description = v
}
// prompts
if v, ok := data["prompts"].(string); ok {
var prompts []Prompt
err := yaml.Unmarshal([]byte(v), &prompts)
if err != nil {
return nil, err
}
assistant.Prompts = prompts
}
// functions
if funcs, has := data["functions"]; has {
switch vv := funcs.(type) {
case []Function:
assistant.Functions = vv
default:
raw, err := jsoniter.Marshal(vv)
if err != nil {
return nil, err
}
var functions []Function
err = jsoniter.Unmarshal(raw, &functions)
if err != nil {
return nil, err
}
assistant.Functions = functions
}
}
// script
if data["script"] != nil {
switch v := data["script"].(type) {
case string:
file := fmt.Sprintf("assistants/%s/src/index.ts", assistant.ID)
script, err := loadScriptSource(v, file)
if err != nil {
return nil, err
}
assistant.Script = script
case *v8.Script:
assistant.Script = v
}
}
// created_at
if v, has := data["created_at"]; has {
ts, err := getTimestamp(v)
if err != nil {
return nil, err
}
assistant.CreatedAt = ts
}
// updated_at
if v, has := data["updated_at"]; has {
ts, err := getTimestamp(v)
if err != nil {
return nil, err
}
assistant.UpdatedAt = ts
}
// Initialize the assistant
err := assistant.initialize()
if err != nil {
return nil, err
}
return assistant, nil
}
func loadFunctions(file string) ([]Function, int64, error) {
app, err := fs.Get("app")
if err != nil {
return nil, 0, err
}
ts, err := app.ModTime(file)
if err != nil {
return nil, 0, err
}
raw, err := app.ReadFile(file)
if err != nil {
return nil, 0, err
}
var functions []Function
err = jsoniter.Unmarshal(raw, &functions)
if err != nil {
return nil, 0, err
}
return functions, ts.UnixNano(), nil
}
func loadPrompts(file string, root string) (string, int64, error) {
app, err := fs.Get("app")
if err != nil {
return "", 0, err
}
ts, err := app.ModTime(file)
if err != nil {
return "", 0, err
}
prompts, err := app.ReadFile(file)
if err != nil {
return "", 0, err
}
re := regexp.MustCompile(`@assets/([^\s]+\.(md|yml|yaml|json|txt))`)
prompts = re.ReplaceAllFunc(prompts, func(s []byte) []byte {
asset := re.FindStringSubmatch(string(s))[1]
assetFile := filepath.Join(root, "assets", asset)
assetContent, err := app.ReadFile(assetFile)
if err != nil {
return []byte("")
}
// Add proper YAML formatting for content
lines := strings.Split(string(assetContent), "\n")
formattedContent := "|\n"
for _, line := range lines {
formattedContent += " " + line + "\n"
}
return []byte(formattedContent)
})
return string(prompts), ts.UnixNano(), nil
}
func loadScript(file string, root string) (*v8.Script, int64, error) {
app, err := fs.Get("app")
if err != nil {
return nil, 0, err
}
ts, err := app.ModTime(file)
if err != nil {
return nil, 0, err
}
script, err := v8.Load(file, share.ID(root, file))
if err != nil {
return nil, 0, err
}
return script, ts.UnixNano(), nil
}
func loadScriptSource(source string, file string) (*v8.Script, error) {
script, err := v8.MakeScript([]byte(source), file, 5*time.Second, true)
if err != nil {
return nil, err
}
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
// Check if the assistant supports vision
model := api.Model()
if v, ok := ast.Options["model"].(string); ok {
model = strings.TrimLeft(v, "moapi:")
}
if _, ok := VisionCapableModels[model]; ok {
ast.vision = true
}
// Check if the assistant has an init hook
if ast.Script != nil {
scriptCtx, err := ast.Script.NewContext("", nil)
if err != nil {
return err
}
defer scriptCtx.Close()
ast.initHook = scriptCtx.Global().Has("init")
}
return nil
}