- Enhanced the extractSandboxVersion function to support multiple input types, including *sandboxTypes.SandboxConfig and map[string]any, for better flexibility in version retrieval. - Simplified the version extraction process, ensuring consistent handling of sandbox configurations.
1117 lines
25 KiB
Go
1117 lines
25 KiB
Go
package assistant
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
jsoniter "github.com/json-iterator/go"
|
|
"github.com/spf13/cast"
|
|
"github.com/yaoapp/gou/application"
|
|
"github.com/yaoapp/gou/fs"
|
|
"github.com/yaoapp/yao/agent/context"
|
|
"github.com/yaoapp/yao/agent/i18n"
|
|
sandboxTypes "github.com/yaoapp/yao/agent/sandbox/v2/types"
|
|
searchTypes "github.com/yaoapp/yao/agent/search/types"
|
|
store "github.com/yaoapp/yao/agent/store/types"
|
|
"github.com/yaoapp/yao/config"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// loaded the loaded assistant
|
|
var loaded = NewCache(200) // 200 is the default capacity
|
|
var storage store.Store = nil
|
|
var storeSetting *store.Setting = nil // store setting from agent.yml
|
|
var defaultConnector string = "" // default connector
|
|
var globalUses *context.Uses = nil // global uses configuration from agent.yml
|
|
var globalPrompts []store.Prompt = nil // global prompts from agent/prompts.yml
|
|
var globalKBSetting *store.KBSetting = nil // global KB setting from agent/kb.yml
|
|
var globalSearchConfig *searchTypes.Config = nil // global search config from agent/search.yml
|
|
|
|
// LoadBuiltIn load the built-in assistants
|
|
func LoadBuiltIn() error {
|
|
|
|
// Clear non-system agents from cache (preserve system agents loaded by LoadSystemAgents)
|
|
loaded.ClearExcept(func(id string) bool {
|
|
return strings.HasPrefix(id, "__yao.") // Keep system agents
|
|
})
|
|
|
|
root := `/assistants`
|
|
app, err := fs.Get("app")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get all existing built-in assistants
|
|
deletedBuiltIn := map[string]bool{}
|
|
|
|
// Remove the built-in assistants (exclude system agents with __yao. prefix)
|
|
if storage != nil {
|
|
|
|
builtIn := true
|
|
res, err := storage.GetAssistants(store.AssistantFilter{BuiltIn: &builtIn, Select: []string{"assistant_id", "id"}})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get all existing built-in assistants (exclude system agents)
|
|
for _, assistant := range res.Data {
|
|
// Skip system agents (they are managed by LoadSystemAgents)
|
|
if strings.HasPrefix(assistant.ID, "__yao.") {
|
|
continue
|
|
}
|
|
deletedBuiltIn[assistant.ID] = true
|
|
}
|
|
}
|
|
|
|
// 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{}
|
|
}
|
|
|
|
// 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)
|
|
|
|
// Remove the built-in assistant from the store
|
|
delete(deletedBuiltIn, assistant.ID)
|
|
}
|
|
|
|
// Remove deleted built-in assistants
|
|
if len(deletedBuiltIn) > 0 {
|
|
assistantIDs := []string{}
|
|
for assistantID := range deletedBuiltIn {
|
|
assistantIDs = append(assistantIDs, assistantID)
|
|
}
|
|
_, err := storage.DeleteAssistants(store.AssistantFilter{AssistantIDs: assistantIDs})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetStorage set the storage
|
|
func SetStorage(s store.Store) {
|
|
storage = s
|
|
}
|
|
|
|
// GetStorage returns the storage (for testing purposes)
|
|
func GetStorage() store.Store {
|
|
return storage
|
|
}
|
|
|
|
// SetConnector set the connector
|
|
func SetConnector(c string) {
|
|
defaultConnector = c
|
|
}
|
|
|
|
// SetGlobalUses set the global uses configuration
|
|
func SetGlobalUses(uses *context.Uses) {
|
|
globalUses = uses
|
|
}
|
|
|
|
// SetGlobalPrompts set the global prompts from agent/prompts.yml
|
|
func SetGlobalPrompts(prompts []store.Prompt) {
|
|
globalPrompts = prompts
|
|
}
|
|
|
|
// SetStoreSetting set the store setting from agent.yml
|
|
func SetStoreSetting(setting *store.Setting) {
|
|
storeSetting = setting
|
|
}
|
|
|
|
// GetStoreSetting returns the store setting
|
|
func GetStoreSetting() *store.Setting {
|
|
return storeSetting
|
|
}
|
|
|
|
// GetGlobalPrompts returns the global prompts with variables parsed
|
|
// ctx: context variables for parsing $CTX.* variables
|
|
func GetGlobalPrompts(ctx map[string]string) []store.Prompt {
|
|
if len(globalPrompts) == 0 {
|
|
return nil
|
|
}
|
|
return store.Prompts(globalPrompts).Parse(ctx)
|
|
}
|
|
|
|
// SetGlobalKBSetting set the global KB setting from agent/kb.yml
|
|
func SetGlobalKBSetting(kbSetting *store.KBSetting) {
|
|
globalKBSetting = kbSetting
|
|
}
|
|
|
|
// GetGlobalKBSetting returns the global KB setting
|
|
func GetGlobalKBSetting() *store.KBSetting {
|
|
return globalKBSetting
|
|
}
|
|
|
|
// SetGlobalSearchConfig set the global search config from agent/search.yml
|
|
func SetGlobalSearchConfig(config *searchTypes.Config) {
|
|
globalSearchConfig = config
|
|
}
|
|
|
|
// GetGlobalSearchConfig returns the global search config
|
|
func GetGlobalSearchConfig() *searchTypes.Config {
|
|
return globalSearchConfig
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
// GetCache returns the loaded cache
|
|
func GetCache() *Cache {
|
|
return loaded
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
|
|
// Request all fields when loading assistant from store
|
|
storeModel, err := storage.GetAssistant(id, store.AssistantFullFields)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Load from path
|
|
if storeModel.Path != "" {
|
|
assistant, err = LoadPath(storeModel.Path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
loaded.Put(assistant)
|
|
return assistant, nil
|
|
}
|
|
|
|
// Create assistant from store model
|
|
assistant = &Assistant{AssistantModel: *storeModel}
|
|
|
|
// Load script from source field if present
|
|
if assistant.Source != "" {
|
|
script, err := loadSource(assistant.Source, assistant.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
assistant.HookScript = script
|
|
}
|
|
|
|
// Initialize the assistant
|
|
err = assistant.initialize()
|
|
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["path"] = path
|
|
if _, has := data["type"]; !has {
|
|
data["type"] = "assistant"
|
|
}
|
|
|
|
updatedAt := int64(0)
|
|
|
|
// prompts (default prompts from prompts.yml)
|
|
promptsfile := filepath.Join(path, "prompts.yml")
|
|
if has, _ := app.Exists(promptsfile); has {
|
|
prompts, ts, err := store.LoadPrompts(promptsfile, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data["prompts"] = prompts
|
|
data["updated_at"] = ts
|
|
updatedAt = ts
|
|
}
|
|
|
|
// prompt_presets (from prompts directory, key is filename without extension)
|
|
promptsDir := filepath.Join(path, "prompts")
|
|
if has, _ := app.Exists(promptsDir); has {
|
|
presets, ts, err := store.LoadPromptPresets(promptsDir, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(presets) > 0 {
|
|
data["prompt_presets"] = presets
|
|
updatedAt = max(updatedAt, ts)
|
|
}
|
|
}
|
|
|
|
// load scripts (hook script and other scripts) from src directory
|
|
srcDir := filepath.Join(path, "src")
|
|
if has, _ := app.Exists(srcDir); has {
|
|
hookScript, scripts, err := LoadScripts(srcDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Set hook script and update timestamp
|
|
if hookScript != nil {
|
|
data["script"] = hookScript
|
|
// Get timestamp from index.ts if exists
|
|
scriptfile := filepath.Join(srcDir, "index.ts")
|
|
if ts, err := app.ModTime(scriptfile); err == nil {
|
|
data["updated_at"] = max(updatedAt, ts.UnixNano())
|
|
}
|
|
}
|
|
|
|
// Set other scripts
|
|
if len(scripts) > 0 {
|
|
data["scripts"] = scripts
|
|
}
|
|
}
|
|
|
|
// i18ns
|
|
locales, err := i18n.GetLocales(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data["locales"] = locales
|
|
|
|
// V2 sandbox: load standalone sandbox.yao if present (Path A).
|
|
sandboxFile := filepath.Join(path, "sandbox.yao")
|
|
if has, _ := app.Exists(sandboxFile); has {
|
|
absFile := filepath.Join(config.Conf.AppSource, sandboxFile)
|
|
sbCfg, sbErr := store.LoadSandboxConfig(absFile)
|
|
if sbErr != nil {
|
|
return nil, fmt.Errorf("load sandbox.yao: %w", sbErr)
|
|
}
|
|
data["__sandbox_v2"] = sbCfg
|
|
data["sandbox"] = sbCfg
|
|
}
|
|
|
|
ast, err := loadMap(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If V2 sandbox was loaded via Path A, assign it now.
|
|
if sbCfg, ok := data["__sandbox_v2"].(*sandboxTypes.SandboxConfig); ok && sbCfg != nil {
|
|
ast.SandboxV2 = sbCfg
|
|
}
|
|
|
|
// Extract Sandbox flag and ComputerFilter from V2 sandbox config.
|
|
if ast.SandboxV2 != nil {
|
|
ast.IsSandbox = true
|
|
ast.ComputerFilter = ast.SandboxV2.Filter
|
|
}
|
|
|
|
// Compute config hash for V2 sandbox.
|
|
if ast.SandboxV2 != nil {
|
|
var mcpServers []store.MCPServerConfig
|
|
if ast.MCP != nil {
|
|
mcpServers = ast.MCP.Servers
|
|
}
|
|
skillsDir := ""
|
|
if ast.Path != "" {
|
|
dir := filepath.Join(config.Conf.AppSource, ast.Path, "skills")
|
|
if info, e := os.Stat(dir); e == nil && info.IsDir() {
|
|
skillsDir = dir
|
|
}
|
|
}
|
|
ast.ConfigHash = store.ComputeConfigHash(ast.SandboxV2, mcpServers, skillsDir)
|
|
}
|
|
|
|
return ast, nil
|
|
}
|
|
|
|
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 = &store.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 = &store.Placeholder{}
|
|
err = jsoniter.Unmarshal(raw, assistant.Placeholder)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
case *store.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
|
|
}
|
|
|
|
// modes
|
|
if v, has := data["modes"]; has {
|
|
modes, err := store.ToModes(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
assistant.Modes = modes
|
|
}
|
|
|
|
// default_mode
|
|
if v, ok := data["default_mode"].(string); ok {
|
|
assistant.DefaultMode = v
|
|
}
|
|
|
|
// DisableGlobalPrompts
|
|
if v, ok := data["disable_global_prompts"].(bool); ok {
|
|
assistant.DisableGlobalPrompts = v
|
|
}
|
|
|
|
// Readonly
|
|
if v, ok := data["readonly"].(bool); ok {
|
|
assistant.Readonly = v
|
|
}
|
|
|
|
// Public
|
|
if v, ok := data["public"].(bool); ok {
|
|
assistant.Public = v
|
|
}
|
|
|
|
// Share
|
|
if v, ok := data["share"].(string); ok {
|
|
assistant.Share = 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
|
|
}
|
|
|
|
// connector_options
|
|
if connOpts, has := data["connector_options"]; has {
|
|
opts, err := store.ToConnectorOptions(connOpts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
assistant.ConnectorOptions = opts
|
|
}
|
|
|
|
// 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 string:
|
|
assistant.Tags = []string{vv}
|
|
|
|
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
|
|
|
|
}
|
|
}
|
|
|
|
// options
|
|
if v, ok := data["options"].(map[string]interface{}); ok {
|
|
assistant.Options = v
|
|
}
|
|
|
|
// description
|
|
if v, ok := data["description"].(string); ok {
|
|
assistant.Description = v
|
|
}
|
|
|
|
// capabilities
|
|
if v, ok := data["capabilities"].(string); ok {
|
|
assistant.Capabilities = v
|
|
}
|
|
|
|
// locales
|
|
if locales, ok := data["locales"].(i18n.Map); ok {
|
|
assistant.Locales = locales
|
|
flattened := locales.FlattenWithGlobal()
|
|
|
|
// Auto-inject assistant name and description into all locales
|
|
// so that {{name}} and {{description}} templates can be resolved
|
|
for locale, i18nObj := range flattened {
|
|
if i18nObj.Messages == nil {
|
|
i18nObj.Messages = make(map[string]any)
|
|
}
|
|
// Add name, description, and capabilities if not already present
|
|
if _, exists := i18nObj.Messages["name"]; !exists && assistant.Name != "" {
|
|
i18nObj.Messages["name"] = assistant.Name
|
|
}
|
|
if _, exists := i18nObj.Messages["description"]; !exists && assistant.Description != "" {
|
|
i18nObj.Messages["description"] = assistant.Description
|
|
}
|
|
if _, exists := i18nObj.Messages["capabilities"]; !exists && assistant.Capabilities != "" {
|
|
i18nObj.Messages["capabilities"] = assistant.Capabilities
|
|
}
|
|
flattened[locale] = i18nObj
|
|
}
|
|
|
|
i18n.Locales[id] = flattened
|
|
} else {
|
|
// No locales defined, create default with name, description, and capabilities for all common locales
|
|
if assistant.Name != "" || assistant.Description != "" || assistant.Capabilities != "" {
|
|
defaultLocales := make(map[string]i18n.I18n)
|
|
commonLocales := []string{"en", "en-us", "zh", "zh-cn", "zh-tw"}
|
|
for _, locale := range commonLocales {
|
|
messages := map[string]any{}
|
|
if assistant.Name != "" {
|
|
messages["name"] = assistant.Name
|
|
}
|
|
if assistant.Description != "" {
|
|
messages["description"] = assistant.Description
|
|
}
|
|
if assistant.Capabilities != "" {
|
|
messages["capabilities"] = assistant.Capabilities
|
|
}
|
|
defaultLocales[locale] = i18n.I18n{
|
|
Locale: locale,
|
|
Messages: messages,
|
|
}
|
|
}
|
|
i18n.Locales[id] = defaultLocales
|
|
}
|
|
}
|
|
|
|
// Search configuration (from package.yao search block)
|
|
// This contains search options like web.max_results, kb.threshold, citation.format, etc.
|
|
// Merge hierarchy: global config < assistant config
|
|
switch v := data["search"].(type) {
|
|
|
|
case *searchTypes.Config:
|
|
assistant.Search = v
|
|
|
|
case searchTypes.Config:
|
|
assistant.Search = &v
|
|
|
|
case map[string]interface{}:
|
|
var assistantSearch searchTypes.Config
|
|
raw, err := jsoniter.Marshal(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = jsoniter.Unmarshal(raw, &assistantSearch)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Merge with global search config
|
|
assistant.Search = mergeSearchConfig(globalSearchConfig, &assistantSearch)
|
|
|
|
default:
|
|
assistant.Search = globalSearchConfig
|
|
}
|
|
|
|
// prompts
|
|
if prompts, has := data["prompts"]; has {
|
|
|
|
switch v := prompts.(type) {
|
|
case []store.Prompt:
|
|
assistant.Prompts = v
|
|
|
|
case string:
|
|
var prompts []store.Prompt
|
|
err := yaml.Unmarshal([]byte(v), &prompts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
assistant.Prompts = prompts
|
|
|
|
default:
|
|
raw, err := jsoniter.Marshal(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var prompts []store.Prompt
|
|
err = jsoniter.Unmarshal(raw, &prompts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
assistant.Prompts = prompts
|
|
}
|
|
}
|
|
|
|
// prompt_presets
|
|
if presets, has := data["prompt_presets"]; has {
|
|
promptPresets, err := store.ToPromptPresets(presets)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
assistant.PromptPresets = promptPresets
|
|
}
|
|
|
|
// source (hook script code) - store the source code
|
|
if source, ok := data["source"].(string); ok {
|
|
assistant.Source = source
|
|
}
|
|
|
|
// kb
|
|
if kb, has := data["kb"]; has {
|
|
knowledgeBase, err := store.ToKnowledgeBase(kb)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
assistant.KB = knowledgeBase
|
|
}
|
|
|
|
// db
|
|
if db, has := data["db"]; has {
|
|
database, err := store.ToDatabase(db)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
assistant.DB = database
|
|
}
|
|
|
|
// mcp
|
|
if mcp, has := data["mcp"]; has {
|
|
mcpServers, err := store.ToMCPServers(mcp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
assistant.MCP = mcpServers
|
|
}
|
|
|
|
// workflow
|
|
if workflow, has := data["workflow"]; has {
|
|
wf, err := store.ToWorkflow(workflow)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
assistant.Workflow = wf
|
|
}
|
|
|
|
// sandbox (for coding agents like Claude CLI, Cursor CLI)
|
|
// V2 sandbox via independent sandbox.yao is loaded in LoadPath (below).
|
|
// This block handles the package.yao embedded "sandbox" field with version dispatch.
|
|
if assistant.SandboxV2 == nil {
|
|
if sandbox, has := data["sandbox"]; has {
|
|
version := extractSandboxVersion(sandbox)
|
|
if version == sandboxTypes.SandboxVersionV2 {
|
|
sb, err := store.ToSandboxV2(sandbox)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
assistant.SandboxV2 = sb
|
|
assistant.IsSandbox = true
|
|
assistant.ComputerFilter = sb.Filter
|
|
} else {
|
|
sb, err := store.ToSandbox(sandbox)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
assistant.Sandbox = sb
|
|
}
|
|
}
|
|
}
|
|
|
|
// dependencies (name -> version constraint, like npm dependencies)
|
|
if deps, has := data["dependencies"]; has {
|
|
switch v := deps.(type) {
|
|
case map[string]string:
|
|
assistant.Dependencies = v
|
|
case map[string]interface{}:
|
|
d := make(map[string]string, len(v))
|
|
for k, val := range v {
|
|
d[k] = cast.ToString(val)
|
|
}
|
|
assistant.Dependencies = d
|
|
default:
|
|
raw, err := jsoniter.Marshal(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var d map[string]string
|
|
if err := jsoniter.Unmarshal(raw, &d); err != nil {
|
|
return nil, err
|
|
}
|
|
assistant.Dependencies = d
|
|
}
|
|
}
|
|
|
|
// uses (wrapper configurations for vision, audio, etc.)
|
|
// Merge hierarchy: global uses < assistant uses
|
|
if uses, has := data["uses"]; has {
|
|
var assistantUses *context.Uses
|
|
switch v := uses.(type) {
|
|
case *context.Uses:
|
|
assistantUses = v
|
|
case context.Uses:
|
|
assistantUses = &v
|
|
default:
|
|
raw, err := jsoniter.Marshal(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var usesConfig context.Uses
|
|
err = jsoniter.Unmarshal(raw, &usesConfig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
assistantUses = &usesConfig
|
|
}
|
|
// Merge with global uses
|
|
assistant.Uses = mergeUses(globalUses, assistantUses)
|
|
} else if globalUses != nil {
|
|
// No assistant-specific uses, use global
|
|
assistant.Uses = globalUses
|
|
}
|
|
|
|
// Load scripts (hook script and other scripts)
|
|
hookScript, scripts, scriptErr := LoadScriptsFromData(data, assistant.ID)
|
|
if scriptErr != nil {
|
|
return nil, scriptErr
|
|
}
|
|
assistant.HookScript = hookScript
|
|
assistant.Scripts = scripts
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
|
|
// Register scripts as process handlers
|
|
if len(ast.Scripts) > 0 {
|
|
if err := ast.RegisterScripts(); err != nil {
|
|
return fmt.Errorf("failed to register scripts: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// mergeUses merges two Uses configs (base < override)
|
|
func mergeUses(base, override *context.Uses) *context.Uses {
|
|
if base == nil {
|
|
return override
|
|
}
|
|
if override == nil {
|
|
return base
|
|
}
|
|
|
|
result := *base // Copy base
|
|
|
|
// Override with non-empty values
|
|
if override.Vision != "" {
|
|
result.Vision = override.Vision
|
|
}
|
|
if override.Audio != "" {
|
|
result.Audio = override.Audio
|
|
}
|
|
if override.Search != "" {
|
|
result.Search = override.Search
|
|
}
|
|
if override.Fetch != "" {
|
|
result.Fetch = override.Fetch
|
|
}
|
|
if override.Web != "" {
|
|
result.Web = override.Web
|
|
}
|
|
if override.Keyword != "" {
|
|
result.Keyword = override.Keyword
|
|
}
|
|
if override.QueryDSL != "" {
|
|
result.QueryDSL = override.QueryDSL
|
|
}
|
|
if override.Rerank != "" {
|
|
result.Rerank = override.Rerank
|
|
}
|
|
|
|
return &result
|
|
}
|
|
|
|
// mergeSearchConfig merges two search configs (base < override)
|
|
func mergeSearchConfig(base, override *searchTypes.Config) *searchTypes.Config {
|
|
if base == nil {
|
|
return override
|
|
}
|
|
if override == nil {
|
|
return base
|
|
}
|
|
|
|
result := *base // Copy base
|
|
|
|
// Merge Web config
|
|
if override.Web != nil {
|
|
if result.Web == nil {
|
|
result.Web = override.Web
|
|
} else {
|
|
merged := *result.Web
|
|
if override.Web.Provider != "" {
|
|
merged.Provider = override.Web.Provider
|
|
}
|
|
if override.Web.APIKeyEnv != "" {
|
|
merged.APIKeyEnv = override.Web.APIKeyEnv
|
|
}
|
|
if override.Web.MaxResults > 0 {
|
|
merged.MaxResults = override.Web.MaxResults
|
|
}
|
|
result.Web = &merged
|
|
}
|
|
}
|
|
|
|
// Merge KB config
|
|
if override.KB != nil {
|
|
if result.KB == nil {
|
|
result.KB = override.KB
|
|
} else {
|
|
merged := *result.KB
|
|
if len(override.KB.Collections) > 0 {
|
|
merged.Collections = override.KB.Collections
|
|
}
|
|
if override.KB.Threshold > 0 {
|
|
merged.Threshold = override.KB.Threshold
|
|
}
|
|
if override.KB.Graph {
|
|
merged.Graph = override.KB.Graph
|
|
}
|
|
result.KB = &merged
|
|
}
|
|
}
|
|
|
|
// Merge DB config
|
|
if override.DB != nil {
|
|
if result.DB == nil {
|
|
result.DB = override.DB
|
|
} else {
|
|
merged := *result.DB
|
|
if len(override.DB.Models) > 0 {
|
|
merged.Models = override.DB.Models
|
|
}
|
|
if override.DB.MaxResults > 0 {
|
|
merged.MaxResults = override.DB.MaxResults
|
|
}
|
|
result.DB = &merged
|
|
}
|
|
}
|
|
|
|
// Merge Keyword config
|
|
if override.Keyword != nil {
|
|
if result.Keyword == nil {
|
|
result.Keyword = override.Keyword
|
|
} else {
|
|
merged := *result.Keyword
|
|
if override.Keyword.MaxKeywords > 0 {
|
|
merged.MaxKeywords = override.Keyword.MaxKeywords
|
|
}
|
|
if override.Keyword.Language != "" {
|
|
merged.Language = override.Keyword.Language
|
|
}
|
|
result.Keyword = &merged
|
|
}
|
|
}
|
|
|
|
// Merge QueryDSL config
|
|
if override.QueryDSL != nil {
|
|
if result.QueryDSL == nil {
|
|
result.QueryDSL = override.QueryDSL
|
|
} else {
|
|
merged := *result.QueryDSL
|
|
if override.QueryDSL.Strict {
|
|
merged.Strict = override.QueryDSL.Strict
|
|
}
|
|
result.QueryDSL = &merged
|
|
}
|
|
}
|
|
|
|
// Merge Rerank config
|
|
if override.Rerank != nil {
|
|
if result.Rerank == nil {
|
|
result.Rerank = override.Rerank
|
|
} else {
|
|
merged := *result.Rerank
|
|
if override.Rerank.TopN > 0 {
|
|
merged.TopN = override.Rerank.TopN
|
|
}
|
|
result.Rerank = &merged
|
|
}
|
|
}
|
|
|
|
// Merge Citation config
|
|
if override.Citation != nil {
|
|
if result.Citation == nil {
|
|
result.Citation = override.Citation
|
|
} else {
|
|
merged := *result.Citation
|
|
if override.Citation.Format != "" {
|
|
merged.Format = override.Citation.Format
|
|
}
|
|
// AutoInjectPrompt is a bool, so we check if it's explicitly set
|
|
// by checking if the whole Citation block was provided
|
|
merged.AutoInjectPrompt = override.Citation.AutoInjectPrompt
|
|
if override.Citation.CustomPrompt != "" {
|
|
merged.CustomPrompt = override.Citation.CustomPrompt
|
|
}
|
|
result.Citation = &merged
|
|
}
|
|
}
|
|
|
|
// Merge Weights config
|
|
if override.Weights != nil {
|
|
if result.Weights == nil {
|
|
result.Weights = override.Weights
|
|
} else {
|
|
merged := *result.Weights
|
|
if override.Weights.User > 0 {
|
|
merged.User = override.Weights.User
|
|
}
|
|
if override.Weights.Hook > 0 {
|
|
merged.Hook = override.Weights.Hook
|
|
}
|
|
if override.Weights.Auto > 0 {
|
|
merged.Auto = override.Weights.Auto
|
|
}
|
|
result.Weights = &merged
|
|
}
|
|
}
|
|
|
|
// Merge Options config
|
|
if override.Options != nil {
|
|
if result.Options == nil {
|
|
result.Options = override.Options
|
|
} else {
|
|
merged := *result.Options
|
|
if override.Options.SkipThreshold > 0 {
|
|
merged.SkipThreshold = override.Options.SkipThreshold
|
|
}
|
|
result.Options = &merged
|
|
}
|
|
}
|
|
|
|
return &result
|
|
}
|
|
|
|
// extractSandboxVersion tries to read the "version" field from a sandbox config value.
|
|
func extractSandboxVersion(v any) string {
|
|
switch sb := v.(type) {
|
|
case *sandboxTypes.SandboxConfig:
|
|
if sb != nil {
|
|
return sb.Version
|
|
}
|
|
case sandboxTypes.SandboxConfig:
|
|
return sb.Version
|
|
case map[string]any:
|
|
if ver, ok := sb["version"].(string); ok {
|
|
return ver
|
|
}
|
|
}
|
|
return ""
|
|
}
|