Refactor assistant model and enhance data handling
- Removed the GetByConnector method from the Assistant model to streamline the retrieval process. - Introduced new fields for connector options and prompt presets, allowing for more flexible configurations. - Updated the Map method to include additional fields such as connector options and prompt presets. - Enhanced the Clone method to support deep copying of new fields. - Improved the Update method to handle updates for the new source, connector options, and prompt presets fields. - Refactored tests to ensure comprehensive coverage of the new functionalities and maintain clarity in the assistant structure.
This commit is contained in:
parent
339a486eb4
commit
51092e3324
6 changed files with 1019 additions and 431 deletions
|
|
@ -15,31 +15,6 @@ func Get(id string) (*Assistant, error) {
|
|||
return LoadStore(id)
|
||||
}
|
||||
|
||||
// GetByConnector get the assistant by connector
|
||||
func GetByConnector(connector string, name string) (*Assistant, error) {
|
||||
id := "connector:" + connector
|
||||
|
||||
assistant, exists := loaded.Get(id)
|
||||
if exists {
|
||||
return assistant, nil
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"assistant_id": id,
|
||||
"connector": connector,
|
||||
"description": "Default assistant for " + connector,
|
||||
"name": name,
|
||||
"type": "assistant",
|
||||
}
|
||||
|
||||
assistant, err := loadMap(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
loaded.Put(assistant)
|
||||
return assistant, nil
|
||||
}
|
||||
|
||||
// GetPlaceholder returns the placeholder of the assistant
|
||||
func (ast *Assistant) GetPlaceholder(locale string) *store.Placeholder {
|
||||
|
||||
|
|
@ -88,30 +63,33 @@ func (ast *Assistant) Map() map[string]interface{} {
|
|||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"assistant_id": ast.ID,
|
||||
"type": ast.Type,
|
||||
"name": ast.Name,
|
||||
"readonly": ast.Readonly,
|
||||
"public": ast.Public,
|
||||
"share": ast.Share,
|
||||
"avatar": ast.Avatar,
|
||||
"connector": ast.Connector,
|
||||
"path": ast.Path,
|
||||
"built_in": ast.BuiltIn,
|
||||
"sort": ast.Sort,
|
||||
"description": ast.Description,
|
||||
"options": ast.Options,
|
||||
"prompts": ast.Prompts,
|
||||
"kb": ast.KB,
|
||||
"mcp": ast.MCP,
|
||||
"workflow": ast.Workflow,
|
||||
"tags": ast.Tags,
|
||||
"mentionable": ast.Mentionable,
|
||||
"automated": ast.Automated,
|
||||
"placeholder": ast.Placeholder,
|
||||
"locales": ast.Locales,
|
||||
"created_at": store.ToMySQLTime(ast.CreatedAt),
|
||||
"updated_at": store.ToMySQLTime(ast.UpdatedAt),
|
||||
"assistant_id": ast.ID,
|
||||
"type": ast.Type,
|
||||
"name": ast.Name,
|
||||
"readonly": ast.Readonly,
|
||||
"public": ast.Public,
|
||||
"share": ast.Share,
|
||||
"avatar": ast.Avatar,
|
||||
"connector": ast.Connector,
|
||||
"connector_options": ast.ConnectorOptions,
|
||||
"path": ast.Path,
|
||||
"built_in": ast.BuiltIn,
|
||||
"sort": ast.Sort,
|
||||
"description": ast.Description,
|
||||
"options": ast.Options,
|
||||
"prompts": ast.Prompts,
|
||||
"prompt_presets": ast.PromptPresets,
|
||||
"source": ast.Source,
|
||||
"kb": ast.KB,
|
||||
"mcp": ast.MCP,
|
||||
"workflow": ast.Workflow,
|
||||
"tags": ast.Tags,
|
||||
"mentionable": ast.Mentionable,
|
||||
"automated": ast.Automated,
|
||||
"placeholder": ast.Placeholder,
|
||||
"locales": ast.Locales,
|
||||
"created_at": store.ToMySQLTime(ast.CreatedAt),
|
||||
"updated_at": store.ToMySQLTime(ast.UpdatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -173,6 +151,7 @@ func (ast *Assistant) Clone() *Assistant {
|
|||
Share: ast.Share,
|
||||
Mentionable: ast.Mentionable,
|
||||
Automated: ast.Automated,
|
||||
Source: ast.Source,
|
||||
CreatedAt: ast.CreatedAt,
|
||||
UpdatedAt: ast.UpdatedAt,
|
||||
},
|
||||
|
|
@ -245,6 +224,31 @@ func (ast *Assistant) Clone() *Assistant {
|
|||
copy(clone.Prompts, ast.Prompts)
|
||||
}
|
||||
|
||||
// Deep copy prompt presets
|
||||
if ast.PromptPresets != nil {
|
||||
clone.PromptPresets = make(map[string][]store.Prompt)
|
||||
for k, v := range ast.PromptPresets {
|
||||
prompts := make([]store.Prompt, len(v))
|
||||
copy(prompts, v)
|
||||
clone.PromptPresets[k] = prompts
|
||||
}
|
||||
}
|
||||
|
||||
// Deep copy connector options
|
||||
if ast.ConnectorOptions != nil {
|
||||
clone.ConnectorOptions = &store.ConnectorOptions{
|
||||
Optional: ast.ConnectorOptions.Optional,
|
||||
}
|
||||
if ast.ConnectorOptions.Connectors != nil {
|
||||
clone.ConnectorOptions.Connectors = make([]string, len(ast.ConnectorOptions.Connectors))
|
||||
copy(clone.ConnectorOptions.Connectors, ast.ConnectorOptions.Connectors)
|
||||
}
|
||||
if ast.ConnectorOptions.Filters != nil {
|
||||
clone.ConnectorOptions.Filters = make([]store.ModelCapability, len(ast.ConnectorOptions.Filters))
|
||||
copy(clone.ConnectorOptions.Filters, ast.ConnectorOptions.Filters)
|
||||
}
|
||||
}
|
||||
|
||||
// Deep copy workflow
|
||||
if ast.Workflow != nil {
|
||||
clone.Workflow = &store.Workflow{}
|
||||
|
|
@ -341,6 +345,27 @@ func (ast *Assistant) Update(data map[string]interface{}) error {
|
|||
if v, ok := data["options"].(map[string]interface{}); ok {
|
||||
ast.Options = v
|
||||
}
|
||||
if v, ok := data["source"].(string); ok {
|
||||
ast.Source = v
|
||||
}
|
||||
|
||||
// ConnectorOptions
|
||||
if v, has := data["connector_options"]; has {
|
||||
connOpts, err := store.ToConnectorOptions(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ast.ConnectorOptions = connOpts
|
||||
}
|
||||
|
||||
// PromptPresets
|
||||
if v, has := data["prompt_presets"]; has {
|
||||
presets, err := store.ToPromptPresets(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ast.PromptPresets = presets
|
||||
}
|
||||
|
||||
// KB
|
||||
if v, has := data["kb"]; has {
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@ func LoadPath(path string) (*Assistant, error) {
|
|||
|
||||
updatedAt := int64(0)
|
||||
|
||||
// prompts
|
||||
// prompts (default prompts from prompts.yml)
|
||||
promptsfile := filepath.Join(path, "prompts.yml")
|
||||
if has, _ := app.Exists(promptsfile); has {
|
||||
prompts, ts, err := loadPrompts(promptsfile, path)
|
||||
|
|
@ -280,6 +280,19 @@ func LoadPath(path string) (*Assistant, error) {
|
|||
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 := loadPromptPresets(promptsDir, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(presets) > 0 {
|
||||
data["prompt_presets"] = presets
|
||||
updatedAt = max(updatedAt, ts)
|
||||
}
|
||||
}
|
||||
|
||||
// load script
|
||||
scriptfile := filepath.Join(path, "src", "index.ts")
|
||||
if has, _ := app.Exists(scriptfile); has {
|
||||
|
|
@ -419,6 +432,15 @@ func loadMap(data map[string]interface{}) (*Assistant, error) {
|
|||
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) {
|
||||
|
|
@ -510,6 +532,20 @@ func loadMap(data map[string]interface{}) (*Assistant, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// tools - deprecated, now handled by MCP
|
||||
// if tools, has := data["tools"]; has {
|
||||
// ... removed ...
|
||||
|
|
@ -542,7 +578,29 @@ func loadMap(data map[string]interface{}) (*Assistant, error) {
|
|||
assistant.Workflow = wf
|
||||
}
|
||||
|
||||
// script
|
||||
// uses (wrapper configurations for vision, audio, etc.)
|
||||
if uses, has := data["uses"]; has {
|
||||
switch v := uses.(type) {
|
||||
case *context.Uses:
|
||||
assistant.Uses = v
|
||||
case context.Uses:
|
||||
assistant.Uses = &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
|
||||
}
|
||||
assistant.Uses = &usesConfig
|
||||
}
|
||||
}
|
||||
|
||||
// script loading priority: script field > source field
|
||||
// If script field exists, use it; otherwise try source field
|
||||
if data["script"] != nil {
|
||||
switch v := data["script"].(type) {
|
||||
case string:
|
||||
|
|
@ -557,6 +615,13 @@ func loadMap(data map[string]interface{}) (*Assistant, error) {
|
|||
case *v8.Script:
|
||||
assistant.Script = &hook.Script{Script: v}
|
||||
}
|
||||
} else if assistant.Source != "" {
|
||||
// Load from source field if script is not provided
|
||||
script, err := loadSource(assistant.Source, assistant.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
assistant.Script = script
|
||||
}
|
||||
|
||||
// created_at
|
||||
|
|
@ -603,6 +668,7 @@ func loadPrompts(file string, root string) (string, int64, error) {
|
|||
return "", 0, err
|
||||
}
|
||||
|
||||
// Replace @assets/xxx references with file content
|
||||
re := regexp.MustCompile(`@assets/([^\s]+\.(md|yml|yaml|json|txt))`)
|
||||
prompts = re.ReplaceAllFunc(prompts, func(s []byte) []byte {
|
||||
asset := re.FindStringSubmatch(string(s))[1]
|
||||
|
|
@ -623,6 +689,81 @@ func loadPrompts(file string, root string) (string, int64, error) {
|
|||
return string(prompts), ts.UnixNano(), nil
|
||||
}
|
||||
|
||||
// loadPromptPresets loads prompt presets from the prompts directory
|
||||
// Supports multi-level directories, key is path with "/" replaced by "."
|
||||
// e.g., prompts/chat/default.yml -> "chat.default"
|
||||
func loadPromptPresets(dir string, root string) (map[string][]store.Prompt, int64, error) {
|
||||
app, err := fs.Get("app")
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// Read directory recursively - returns full paths relative to app root
|
||||
files, err := app.ReadDir(dir, true)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
presets := make(map[string][]store.Prompt)
|
||||
var latestTs int64
|
||||
|
||||
for _, file := range files {
|
||||
// Only process .yml/.yaml files
|
||||
if !strings.HasSuffix(file, ".yml") && !strings.HasSuffix(file, ".yaml") {
|
||||
continue
|
||||
}
|
||||
|
||||
// file is already full path relative to app root (e.g., /assistants/tests/fullfields/prompts/chat/friendly.yml)
|
||||
ts, err := app.ModTime(file)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if ts.UnixNano() > latestTs {
|
||||
latestTs = ts.UnixNano()
|
||||
}
|
||||
|
||||
// Read file content directly
|
||||
content, err := app.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// Replace @assets/xxx references with file content
|
||||
re := regexp.MustCompile(`@assets/([^\s]+\.(md|yml|yaml|json|txt))`)
|
||||
content = re.ReplaceAllFunc(content, 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)
|
||||
})
|
||||
|
||||
// Parse prompts
|
||||
var prompts []store.Prompt
|
||||
err = yaml.Unmarshal(content, &prompts)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to parse prompt preset %s: %w", file, err)
|
||||
}
|
||||
|
||||
// Build key: get relative path from dir, remove extension and replace "/" with "."
|
||||
// e.g., "/assistants/tests/fullfields/prompts/chat/friendly.yml" -> "chat.friendly"
|
||||
relPath := strings.TrimPrefix(file, dir+"/")
|
||||
key := strings.TrimSuffix(relPath, filepath.Ext(relPath))
|
||||
key = strings.ReplaceAll(key, "/", ".")
|
||||
presets[key] = prompts
|
||||
}
|
||||
|
||||
return presets, latestTs, nil
|
||||
}
|
||||
|
||||
func loadScript(file string, root string) (*hook.Script, int64, error) {
|
||||
|
||||
app, err := fs.Get("app")
|
||||
|
|
|
|||
|
|
@ -1,435 +1,501 @@
|
|||
package assistant
|
||||
|
||||
// func prepare(t *testing.T) {
|
||||
// test.Prepare(t, config.Conf)
|
||||
// }
|
||||
import (
|
||||
"testing"
|
||||
|
||||
// func TestLoad_LoadPath(t *testing.T) {
|
||||
// prepare(t)
|
||||
// defer test.Clean()
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
store "github.com/yaoapp/yao/agent/store/types"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/test"
|
||||
)
|
||||
|
||||
// assistant, err := LoadPath("/assistants/modi")
|
||||
// if err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
func prepare(t *testing.T) {
|
||||
test.Prepare(t, config.Conf)
|
||||
}
|
||||
|
||||
// // Validate basic properties
|
||||
// assert.NotNil(t, assistant)
|
||||
// assert.Equal(t, "modi", assistant.ID)
|
||||
// assert.Equal(t, "Modi", assistant.Name)
|
||||
// assert.Equal(t, "https://api.dicebear.com/7.x/bottts/svg?seed=Modi", assistant.Avatar)
|
||||
// assert.Equal(t, "deepseek", assistant.Connector)
|
||||
// assert.NotNil(t, assistant.Prompts)
|
||||
// assert.NotNil(t, assistant.Script)
|
||||
// TestLoadPath tests loading assistant from path
|
||||
func TestLoadPath(t *testing.T) {
|
||||
prepare(t)
|
||||
defer test.Clean()
|
||||
|
||||
// // Test non-existent assistant
|
||||
// _, err = LoadPath("/assistants/non-existent")
|
||||
// assert.Error(t, err)
|
||||
// }
|
||||
t.Run("LoadFullFieldsAssistant", func(t *testing.T) {
|
||||
assistant, err := LoadPath("/assistants/tests/fullfields")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, assistant)
|
||||
|
||||
// func TestLoad_LoadStore(t *testing.T) {
|
||||
// prepare(t)
|
||||
// defer test.Clean()
|
||||
// Basic fields
|
||||
assert.Equal(t, "tests.fullfields", assistant.ID)
|
||||
assert.Equal(t, "Full Fields Test Assistant", assistant.Name)
|
||||
assert.Equal(t, "assistant", assistant.Type)
|
||||
assert.Equal(t, "/api/__yao/app/icons/app.png", assistant.Avatar)
|
||||
assert.Equal(t, "gpt-4o", assistant.Connector)
|
||||
assert.Equal(t, "/assistants/tests/fullfields", assistant.Path)
|
||||
assert.Equal(t, "Test assistant with all available fields for unit testing", assistant.Description)
|
||||
|
||||
// // Test with nil storage
|
||||
// _, err := LoadStore("test-id")
|
||||
// assert.Error(t, err)
|
||||
// assert.Contains(t, err.Error(), "storage is not set")
|
||||
// Boolean fields
|
||||
assert.True(t, assistant.Public)
|
||||
assert.True(t, assistant.Readonly)
|
||||
assert.True(t, assistant.Mentionable)
|
||||
assert.False(t, assistant.Automated)
|
||||
|
||||
// // Setup mock storage
|
||||
// mockStore := &mockStore{
|
||||
// data: map[string]map[string]interface{}{
|
||||
// "test-id": {
|
||||
// "assistant_id": "test-id",
|
||||
// "name": "Test Assistant",
|
||||
// "avatar": "test-avatar",
|
||||
// "connector": "gpt-3_5-turbo",
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
// SetStorage(mockStore)
|
||||
// defer SetStorage(nil)
|
||||
// Share field
|
||||
assert.Equal(t, "team", assistant.Share)
|
||||
|
||||
// // Test loading from store
|
||||
// assistant, err := LoadStore("test-id")
|
||||
// assert.NoError(t, err)
|
||||
// assert.NotNil(t, assistant)
|
||||
// assert.Equal(t, "test-id", assistant.ID)
|
||||
// assert.Equal(t, "Test Assistant", assistant.Name)
|
||||
// assert.Equal(t, "test-avatar", assistant.Avatar)
|
||||
// assert.Equal(t, "gpt-3_5-turbo", assistant.Connector)
|
||||
// Sort field
|
||||
assert.Equal(t, 100, assistant.Sort)
|
||||
|
||||
// // Test cache functionality
|
||||
// assistant2, err := LoadStore("test-id")
|
||||
// assert.NoError(t, err)
|
||||
// assert.Equal(t, assistant, assistant2) // Should be the same instance from cache
|
||||
// Tags
|
||||
assert.NotNil(t, assistant.Tags)
|
||||
assert.Contains(t, assistant.Tags, "Test")
|
||||
assert.Contains(t, assistant.Tags, "Development")
|
||||
assert.Contains(t, assistant.Tags, "FullFields")
|
||||
|
||||
// // Test non-existent assistant
|
||||
// _, err = LoadStore("non-existent")
|
||||
// assert.Error(t, err)
|
||||
// }
|
||||
// Options
|
||||
assert.NotNil(t, assistant.Options)
|
||||
assert.Equal(t, 0.7, assistant.Options["temperature"])
|
||||
assert.Equal(t, float64(2000), assistant.Options["max_tokens"])
|
||||
|
||||
// func TestLoad_Cache(t *testing.T) {
|
||||
// prepare(t)
|
||||
// defer test.Clean()
|
||||
// Prompts (default prompts from prompts.yml)
|
||||
assert.NotNil(t, assistant.Prompts)
|
||||
assert.GreaterOrEqual(t, len(assistant.Prompts), 1)
|
||||
assert.Equal(t, "system", assistant.Prompts[0].Role)
|
||||
|
||||
// // Clear any existing cache first
|
||||
// ClearCache()
|
||||
// Script (from src/index.ts)
|
||||
assert.NotNil(t, assistant.Script)
|
||||
})
|
||||
|
||||
// // Test cache operations
|
||||
// SetCache(2) // Set small cache size for testing
|
||||
// assert.Equal(t, 2, loaded.capacity, "Cache capacity should be 2")
|
||||
t.Run("LoadConnectorOptions", func(t *testing.T) {
|
||||
assistant, err := LoadPath("/assistants/tests/fullfields")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, assistant)
|
||||
|
||||
// // Create test assistants
|
||||
// assistant1 := &Assistant{ID: "id1", Name: "Assistant 1"}
|
||||
// assistant2 := &Assistant{ID: "id2", Name: "Assistant 2"}
|
||||
// assistant3 := &Assistant{ID: "id3", Name: "Assistant 3"}
|
||||
// ConnectorOptions
|
||||
assert.NotNil(t, assistant.ConnectorOptions)
|
||||
assert.True(t, assistant.ConnectorOptions.Optional)
|
||||
assert.NotNil(t, assistant.ConnectorOptions.Connectors)
|
||||
assert.Contains(t, assistant.ConnectorOptions.Connectors, "gpt-4o")
|
||||
assert.Contains(t, assistant.ConnectorOptions.Connectors, "gpt-4o-mini")
|
||||
assert.Contains(t, assistant.ConnectorOptions.Connectors, "deepseek")
|
||||
assert.NotNil(t, assistant.ConnectorOptions.Filters)
|
||||
assert.Len(t, assistant.ConnectorOptions.Filters, 2)
|
||||
})
|
||||
|
||||
// // Test Put and Get
|
||||
// loaded.Put(assistant1)
|
||||
// assert.Equal(t, 1, loaded.Len(), "Cache should have 1 item")
|
||||
t.Run("LoadPromptPresets", func(t *testing.T) {
|
||||
assistant, err := LoadPath("/assistants/tests/fullfields")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, assistant)
|
||||
|
||||
// loaded.Put(assistant2)
|
||||
// assert.Equal(t, 2, loaded.Len(), "Cache should have 2 items")
|
||||
// PromptPresets (from prompts directory)
|
||||
assert.NotNil(t, assistant.PromptPresets)
|
||||
|
||||
// // Test cache hit
|
||||
// cached, exists := loaded.Get("id1")
|
||||
// assert.True(t, exists)
|
||||
// assert.Equal(t, assistant1, cached)
|
||||
// Top-level presets: chat.yml -> "chat", task.yml -> "task"
|
||||
chatPreset, hasChat := assistant.PromptPresets["chat"]
|
||||
assert.True(t, hasChat, "Should have 'chat' preset")
|
||||
assert.NotEmpty(t, chatPreset)
|
||||
|
||||
// // Test cache eviction (LRU)
|
||||
// // At this point: assistant1 is most recently used (due to Get), then assistant2
|
||||
// loaded.Put(assistant3) // This should evict assistant2 since it's least recently used
|
||||
// assert.Equal(t, 2, loaded.Len(), "Cache should still have 2 items")
|
||||
// _, exists = loaded.Get("id2")
|
||||
// assert.False(t, exists, "assistant2 should have been evicted (least recently used)")
|
||||
// _, exists = loaded.Get("id1")
|
||||
// assert.True(t, exists, "assistant1 should still be in cache (was accessed recently)")
|
||||
// _, exists = loaded.Get("id3")
|
||||
// assert.True(t, exists, "assistant3 should be in cache (most recently added)")
|
||||
taskPreset, hasTask := assistant.PromptPresets["task"]
|
||||
assert.True(t, hasTask, "Should have 'task' preset")
|
||||
assert.NotEmpty(t, taskPreset)
|
||||
|
||||
// // Test clear cache
|
||||
// ClearCache()
|
||||
// assert.Nil(t, loaded)
|
||||
// Nested presets: chat/friendly.yml -> "chat.friendly"
|
||||
friendlyPreset, hasFriendly := assistant.PromptPresets["chat.friendly"]
|
||||
assert.True(t, hasFriendly, "Should have 'chat.friendly' preset")
|
||||
assert.NotEmpty(t, friendlyPreset)
|
||||
|
||||
// // Test setting new cache capacity
|
||||
// SetCache(100)
|
||||
// assert.NotNil(t, loaded)
|
||||
// }
|
||||
professionalPreset, hasProfessional := assistant.PromptPresets["chat.professional"]
|
||||
assert.True(t, hasProfessional, "Should have 'chat.professional' preset")
|
||||
assert.NotEmpty(t, professionalPreset)
|
||||
|
||||
// func TestLoad_Validate(t *testing.T) {
|
||||
// tests := []struct {
|
||||
// name string
|
||||
// ast *Assistant
|
||||
// wantErr bool
|
||||
// }{
|
||||
// {
|
||||
// name: "valid assistant",
|
||||
// ast: &Assistant{
|
||||
// ID: "test-id",
|
||||
// Name: "Test Assistant",
|
||||
// Connector: "test-connector",
|
||||
// },
|
||||
// wantErr: false,
|
||||
// },
|
||||
// {
|
||||
// name: "missing id",
|
||||
// ast: &Assistant{
|
||||
// Name: "Test Assistant",
|
||||
// Connector: "test-connector",
|
||||
// },
|
||||
// wantErr: true,
|
||||
// },
|
||||
// {
|
||||
// name: "missing name",
|
||||
// ast: &Assistant{
|
||||
// ID: "test-id",
|
||||
// Connector: "test-connector",
|
||||
// },
|
||||
// wantErr: true,
|
||||
// },
|
||||
// {
|
||||
// name: "missing connector",
|
||||
// ast: &Assistant{
|
||||
// ID: "test-id",
|
||||
// Name: "Test Assistant",
|
||||
// },
|
||||
// wantErr: true,
|
||||
// },
|
||||
// }
|
||||
// task/analysis.yml -> "task.analysis"
|
||||
analysisPreset, hasAnalysis := assistant.PromptPresets["task.analysis"]
|
||||
assert.True(t, hasAnalysis, "Should have 'task.analysis' preset")
|
||||
assert.NotEmpty(t, analysisPreset)
|
||||
})
|
||||
|
||||
// for _, tt := range tests {
|
||||
// t.Run(tt.name, func(t *testing.T) {
|
||||
// err := tt.ast.Validate()
|
||||
// if (err != nil) != tt.wantErr {
|
||||
// t.Errorf("Assistant.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
t.Run("LoadKnowledgeBase", func(t *testing.T) {
|
||||
assistant, err := LoadPath("/assistants/tests/fullfields")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, assistant)
|
||||
|
||||
// func TestLoad_Clone(t *testing.T) {
|
||||
// // Create a test assistant with all fields populated
|
||||
// original := &Assistant{
|
||||
// ID: "test-id",
|
||||
// Type: "test-type",
|
||||
// Name: "Test Assistant",
|
||||
// Avatar: "test-avatar",
|
||||
// Connector: "test-connector",
|
||||
// Path: "test-path",
|
||||
// BuiltIn: true,
|
||||
// Sort: 1,
|
||||
// Description: "test description",
|
||||
// Tags: []string{"tag1", "tag2"},
|
||||
// Readonly: true,
|
||||
// Mentionable: true,
|
||||
// Automated: true,
|
||||
// Options: map[string]interface{}{"key": "value"},
|
||||
// Prompts: []Prompt{{Role: "system", Content: "test"}},
|
||||
// Workflow: map[string]interface{}{"step": "test"},
|
||||
// }
|
||||
// KB
|
||||
assert.NotNil(t, assistant.KB)
|
||||
assert.NotNil(t, assistant.KB.Collections)
|
||||
assert.Contains(t, assistant.KB.Collections, "test-collection")
|
||||
assert.NotNil(t, assistant.KB.Options)
|
||||
assert.Equal(t, float64(5), assistant.KB.Options["top_k"])
|
||||
})
|
||||
|
||||
// // Clone the assistant
|
||||
// clone := original.Clone()
|
||||
t.Run("LoadMCPServers", func(t *testing.T) {
|
||||
assistant, err := LoadPath("/assistants/tests/fullfields")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, assistant)
|
||||
|
||||
// // Verify all fields are correctly cloned
|
||||
// assert.Equal(t, original.ID, clone.ID)
|
||||
// assert.Equal(t, original.Type, clone.Type)
|
||||
// assert.Equal(t, original.Name, clone.Name)
|
||||
// assert.Equal(t, original.Avatar, clone.Avatar)
|
||||
// assert.Equal(t, original.Connector, clone.Connector)
|
||||
// assert.Equal(t, original.Path, clone.Path)
|
||||
// assert.Equal(t, original.BuiltIn, clone.BuiltIn)
|
||||
// assert.Equal(t, original.Sort, clone.Sort)
|
||||
// assert.Equal(t, original.Description, clone.Description)
|
||||
// assert.Equal(t, original.Tags, clone.Tags)
|
||||
// assert.Equal(t, original.Readonly, clone.Readonly)
|
||||
// assert.Equal(t, original.Mentionable, clone.Mentionable)
|
||||
// assert.Equal(t, original.Automated, clone.Automated)
|
||||
// assert.Equal(t, original.Options, clone.Options)
|
||||
// assert.Equal(t, original.Prompts, clone.Prompts)
|
||||
// assert.Equal(t, original.Workflow, clone.Workflow)
|
||||
// MCP
|
||||
assert.NotNil(t, assistant.MCP)
|
||||
assert.NotNil(t, assistant.MCP.Servers)
|
||||
assert.Len(t, assistant.MCP.Servers, 1)
|
||||
assert.Equal(t, "echo", assistant.MCP.Servers[0].ServerID)
|
||||
assert.Contains(t, assistant.MCP.Servers[0].Tools, "ping")
|
||||
assert.Contains(t, assistant.MCP.Servers[0].Tools, "echo")
|
||||
})
|
||||
|
||||
// // Verify deep copy by modifying original
|
||||
// original.Tags[0] = "modified"
|
||||
// original.Options["key"] = "modified"
|
||||
// original.Workflow["step"] = "modified"
|
||||
// assert.NotEqual(t, original.Tags[0], clone.Tags[0])
|
||||
// assert.NotEqual(t, original.Options["key"], clone.Options["key"])
|
||||
// assert.NotEqual(t, original.Workflow["step"], clone.Workflow["step"])
|
||||
t.Run("LoadWorkflow", func(t *testing.T) {
|
||||
assistant, err := LoadPath("/assistants/tests/fullfields")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, assistant)
|
||||
|
||||
// // Test nil case
|
||||
// var nilAssistant *Assistant
|
||||
// assert.Nil(t, nilAssistant.Clone())
|
||||
// }
|
||||
// Workflow
|
||||
assert.NotNil(t, assistant.Workflow)
|
||||
assert.NotNil(t, assistant.Workflow.Workflows)
|
||||
assert.Contains(t, assistant.Workflow.Workflows, "test-workflow")
|
||||
assert.NotNil(t, assistant.Workflow.Options)
|
||||
assert.Equal(t, float64(10), assistant.Workflow.Options["max_steps"])
|
||||
})
|
||||
|
||||
// func TestLoad_Update(t *testing.T) {
|
||||
// // Create a test assistant
|
||||
// ast := &Assistant{
|
||||
// ID: "test-id",
|
||||
// Name: "Original Name",
|
||||
// Connector: "original-connector",
|
||||
// }
|
||||
t.Run("LoadPlaceholder", func(t *testing.T) {
|
||||
assistant, err := LoadPath("/assistants/tests/fullfields")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, assistant)
|
||||
|
||||
// // Test updating various fields
|
||||
// updates := map[string]interface{}{
|
||||
// "name": "Updated Name",
|
||||
// "avatar": "updated-avatar",
|
||||
// "description": "Updated description",
|
||||
// "connector": "updated-connector",
|
||||
// "type": "updated-type",
|
||||
// "sort": 2,
|
||||
// "mentionable": true,
|
||||
// "automated": true,
|
||||
// "tags": []string{"new-tag"},
|
||||
// "options": map[string]interface{}{"new": "value"},
|
||||
// }
|
||||
// Placeholder
|
||||
assert.NotNil(t, assistant.Placeholder)
|
||||
assert.Equal(t, "Full Fields Test", assistant.Placeholder.Title)
|
||||
assert.Equal(t, "Test assistant with complete field coverage", assistant.Placeholder.Description)
|
||||
assert.NotNil(t, assistant.Placeholder.Prompts)
|
||||
assert.Len(t, assistant.Placeholder.Prompts, 3)
|
||||
})
|
||||
|
||||
// err := ast.Update(updates)
|
||||
// assert.NoError(t, err)
|
||||
t.Run("LoadLocales", func(t *testing.T) {
|
||||
assistant, err := LoadPath("/assistants/tests/fullfields")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, assistant)
|
||||
|
||||
// // Verify updates
|
||||
// assert.Equal(t, "Updated Name", ast.Name)
|
||||
// assert.Equal(t, "updated-avatar", ast.Avatar)
|
||||
// assert.Equal(t, "Updated description", ast.Description)
|
||||
// assert.Equal(t, "updated-connector", ast.Connector)
|
||||
// assert.Equal(t, "updated-type", ast.Type)
|
||||
// assert.Equal(t, 2, ast.Sort)
|
||||
// assert.True(t, ast.Mentionable)
|
||||
// assert.True(t, ast.Automated)
|
||||
// assert.Equal(t, []string{"new-tag"}, ast.Tags)
|
||||
// assert.Equal(t, map[string]interface{}{"new": "value"}, ast.Options)
|
||||
// Locales
|
||||
assert.NotNil(t, assistant.Locales)
|
||||
|
||||
// // Test nil assistant
|
||||
// var nilAssistant *Assistant
|
||||
// err = nilAssistant.Update(updates)
|
||||
// assert.Error(t, err)
|
||||
enLocale, hasEn := assistant.Locales["en-us"]
|
||||
assert.True(t, hasEn, "Should have en-us locale")
|
||||
assert.NotNil(t, enLocale)
|
||||
|
||||
// // Test invalid update that would make the assistant invalid
|
||||
// invalidUpdates := map[string]interface{}{
|
||||
// "name": "",
|
||||
// }
|
||||
// err = ast.Update(invalidUpdates)
|
||||
// assert.Error(t, err)
|
||||
// }
|
||||
zhLocale, hasZh := assistant.Locales["zh-cn"]
|
||||
assert.True(t, hasZh, "Should have zh-cn locale")
|
||||
assert.NotNil(t, zhLocale)
|
||||
})
|
||||
|
||||
// func TestLoadBuiltIn(t *testing.T) {
|
||||
// prepare(t)
|
||||
// defer test.Clean()
|
||||
t.Run("LoadNonExistentAssistant", func(t *testing.T) {
|
||||
_, err := LoadPath("/assistants/non-existent")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// // Clear any existing cache and storage
|
||||
// ClearCache()
|
||||
// SetStorage(nil)
|
||||
// TestLoadPathMCPTest tests loading the MCP test assistant
|
||||
func TestLoadPathMCPTest(t *testing.T) {
|
||||
prepare(t)
|
||||
defer test.Clean()
|
||||
|
||||
// // Create a mock store to verify built-in assistants are saved
|
||||
// mockStore := &mockStore{
|
||||
// data: make(map[string]map[string]interface{}),
|
||||
// }
|
||||
// SetStorage(mockStore)
|
||||
// SetCache(100)
|
||||
assistant, err := LoadPath("/assistants/tests/mcptest")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, assistant)
|
||||
|
||||
// // Test loading built-in assistants
|
||||
// err := LoadBuiltIn()
|
||||
// assert.NoError(t, err)
|
||||
assert.Equal(t, "tests.mcptest", assistant.ID)
|
||||
assert.Equal(t, "MCP Test Assistant", assistant.Name)
|
||||
assert.Equal(t, "gpt-4o", assistant.Connector)
|
||||
|
||||
// // Verify Modi assistant was loaded
|
||||
// assistant, exists := loaded.Get("modi")
|
||||
// assert.True(t, exists, "Modi assistant should be loaded in cache")
|
||||
// if exists {
|
||||
// assert.Equal(t, "modi", assistant.ID)
|
||||
// assert.Equal(t, "Modi", assistant.Name)
|
||||
// assert.Equal(t, "deepseek", assistant.Connector)
|
||||
// assert.True(t, assistant.BuiltIn)
|
||||
// assert.True(t, assistant.Readonly)
|
||||
// assert.NotNil(t, assistant.Prompts)
|
||||
// assert.NotNil(t, assistant.Script)
|
||||
// }
|
||||
// MCP configuration
|
||||
assert.NotNil(t, assistant.MCP)
|
||||
assert.Len(t, assistant.MCP.Servers, 1)
|
||||
assert.Equal(t, "echo", assistant.MCP.Servers[0].ServerID)
|
||||
|
||||
// }
|
||||
// Locales
|
||||
assert.NotNil(t, assistant.Locales)
|
||||
assert.Contains(t, assistant.Locales, "en-us")
|
||||
assert.Contains(t, assistant.Locales, "zh-cn")
|
||||
}
|
||||
|
||||
// // mockStore implements store.Store interface for testing
|
||||
// type mockStore struct {
|
||||
// data map[string]map[string]interface{}
|
||||
// }
|
||||
// TestLoadPathBuildRequest tests loading the build request test assistant
|
||||
func TestLoadPathBuildRequest(t *testing.T) {
|
||||
prepare(t)
|
||||
defer test.Clean()
|
||||
|
||||
// func (m *mockStore) GetAssistant(id string, locale ...string) (map[string]interface{}, error) {
|
||||
// if data, ok := m.data[id]; ok {
|
||||
// return data, nil
|
||||
// }
|
||||
// return nil, fmt.Errorf("assistant not found: %s", id)
|
||||
// }
|
||||
assistant, err := LoadPath("/assistants/tests/buildrequest")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, assistant)
|
||||
|
||||
// // Add other required interface methods with empty implementations
|
||||
// func (m *mockStore) GetThread(id string) (map[string]interface{}, error) { return nil, nil }
|
||||
// func (m *mockStore) GetMessage(id string) (map[string]interface{}, error) { return nil, nil }
|
||||
// func (m *mockStore) GetFile(id string) (map[string]interface{}, error) { return nil, nil }
|
||||
// func (m *mockStore) CreateAssistant(data map[string]interface{}) (map[string]interface{}, error) {
|
||||
// return nil, nil
|
||||
// }
|
||||
// func (m *mockStore) CreateThread(data map[string]interface{}) (map[string]interface{}, error) {
|
||||
// return nil, nil
|
||||
// }
|
||||
// func (m *mockStore) CreateMessage(data map[string]interface{}) (map[string]interface{}, error) {
|
||||
// return nil, nil
|
||||
// }
|
||||
// func (m *mockStore) CreateFile(data map[string]interface{}) (map[string]interface{}, error) {
|
||||
// return nil, nil
|
||||
// }
|
||||
// func (m *mockStore) UpdateAssistant(id string, data map[string]interface{}) error { return nil }
|
||||
// func (m *mockStore) UpdateThread(id string, data map[string]interface{}) error { return nil }
|
||||
// func (m *mockStore) UpdateMessage(id string, data map[string]interface{}) error { return nil }
|
||||
// func (m *mockStore) UpdateFile(id string, data map[string]interface{}) error { return nil }
|
||||
// func (m *mockStore) DeleteAssistant(id string) error { return nil }
|
||||
// func (m *mockStore) DeleteThread(id string) error { return nil }
|
||||
// func (m *mockStore) DeleteMessage(id string) error { return nil }
|
||||
// func (m *mockStore) DeleteFile(id string) error { return nil }
|
||||
// func (m *mockStore) ListAssistants(query map[string]interface{}) ([]map[string]interface{}, error) {
|
||||
// return nil, nil
|
||||
// }
|
||||
// func (m *mockStore) ListThreads(query map[string]interface{}) ([]map[string]interface{}, error) {
|
||||
// return nil, nil
|
||||
// }
|
||||
// func (m *mockStore) ListMessages(query map[string]interface{}) ([]map[string]interface{}, error) {
|
||||
// return nil, nil
|
||||
// }
|
||||
// func (m *mockStore) ListFiles(query map[string]interface{}) ([]map[string]interface{}, error) {
|
||||
// return nil, nil
|
||||
// }
|
||||
// func (m *mockStore) DeleteAllChats(id string) error { return nil }
|
||||
// func (m *mockStore) DeleteChat(id string, chatID string) error { return nil }
|
||||
// func (m *mockStore) GetAssistants(filter store.AssistantFilter, locale ...string) (*store.AssistantResponse, error) {
|
||||
// return nil, nil
|
||||
// }
|
||||
// func (m *mockStore) GetChat(id string, chatID string, locale ...string) (*store.ChatInfo, error) {
|
||||
// return nil, nil
|
||||
// }
|
||||
// func (m *mockStore) GetChatWithFilter(id string, chatID string, filter store.ChatFilter, locale ...string) (*store.ChatInfo, error) {
|
||||
// return nil, nil
|
||||
// }
|
||||
// func (m *mockStore) GetChats(id string, filter store.ChatFilter, locale ...string) (*store.ChatGroupResponse, error) {
|
||||
// return nil, nil
|
||||
// }
|
||||
// func (m *mockStore) GetHistory(id string, chatID string, locale ...string) ([]map[string]interface{}, error) {
|
||||
// return nil, nil
|
||||
// }
|
||||
// func (m *mockStore) GetHistoryWithFilter(id string, chatID string, filter store.ChatFilter, locale ...string) ([]map[string]interface{}, error) {
|
||||
// return nil, nil
|
||||
// }
|
||||
// func (m *mockStore) SaveAssistant(assistant map[string]interface{}) (interface{}, error) {
|
||||
// return nil, nil
|
||||
// }
|
||||
// func (m *mockStore) SaveHistory(sid string, messages []map[string]interface{}, cid string, context map[string]interface{}) error {
|
||||
// return nil
|
||||
// }
|
||||
// func (m *mockStore) UpdateChatTitle(sid string, cid string, title string) error { return nil }
|
||||
// func (m *mockStore) DeleteAssistants(filter store.AssistantFilter) (int64, error) { return 0, nil }
|
||||
// func (m *mockStore) GetAssistantTags(locale ...string) ([]store.Tag, error) {
|
||||
// return []store.Tag{}, nil
|
||||
// }
|
||||
assert.Equal(t, "tests.buildrequest", assistant.ID)
|
||||
assert.Equal(t, "Build Request Test", assistant.Name)
|
||||
|
||||
// // Attachment related methods
|
||||
// func (m *mockStore) SaveAttachment(attachment map[string]interface{}) (interface{}, error) {
|
||||
// return attachment["file_id"], nil
|
||||
// }
|
||||
// Script should be loaded
|
||||
assert.NotNil(t, assistant.Script)
|
||||
|
||||
// func (m *mockStore) DeleteAttachment(fileID string) error {
|
||||
// return nil
|
||||
// }
|
||||
// Options
|
||||
assert.NotNil(t, assistant.Options)
|
||||
assert.Equal(t, 0.5, assistant.Options["temperature"])
|
||||
}
|
||||
|
||||
// func (m *mockStore) GetAttachments(filter store.AttachmentFilter, locale ...string) (*store.AttachmentResponse, error) {
|
||||
// return &store.AttachmentResponse{}, nil
|
||||
// }
|
||||
// TestCache tests the assistant cache functionality
|
||||
func TestCache(t *testing.T) {
|
||||
// Clear any existing cache
|
||||
ClearCache()
|
||||
|
||||
// func (m *mockStore) GetAttachment(fileID string, locale ...string) (map[string]interface{}, error) {
|
||||
// return nil, nil
|
||||
// }
|
||||
// Set small cache for testing
|
||||
SetCache(3)
|
||||
assert.NotNil(t, loaded)
|
||||
|
||||
// func (m *mockStore) DeleteAttachments(filter store.AttachmentFilter) (int64, error) {
|
||||
// return 0, nil
|
||||
// }
|
||||
// Create test assistants
|
||||
ast1 := &Assistant{AssistantModel: store.AssistantModel{ID: "id1", Name: "Assistant 1"}}
|
||||
ast2 := &Assistant{AssistantModel: store.AssistantModel{ID: "id2", Name: "Assistant 2"}}
|
||||
ast3 := &Assistant{AssistantModel: store.AssistantModel{ID: "id3", Name: "Assistant 3"}}
|
||||
ast4 := &Assistant{AssistantModel: store.AssistantModel{ID: "id4", Name: "Assistant 4"}}
|
||||
|
||||
// // Knowledge related methods
|
||||
// func (m *mockStore) SaveKnowledge(knowledge map[string]interface{}) (interface{}, error) {
|
||||
// return knowledge["collection_id"], nil
|
||||
// }
|
||||
t.Run("PutAndGet", func(t *testing.T) {
|
||||
loaded.Put(ast1)
|
||||
assert.Equal(t, 1, loaded.Len())
|
||||
|
||||
// func (m *mockStore) DeleteKnowledge(collectionID string) error {
|
||||
// return nil
|
||||
// }
|
||||
cached, exists := loaded.Get("id1")
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, ast1, cached)
|
||||
})
|
||||
|
||||
// func (m *mockStore) GetKnowledges(filter store.KnowledgeFilter, locale ...string) (*store.KnowledgeResponse, error) {
|
||||
// return &store.KnowledgeResponse{}, nil
|
||||
// }
|
||||
t.Run("CacheEviction", func(t *testing.T) {
|
||||
loaded.Put(ast2)
|
||||
loaded.Put(ast3)
|
||||
assert.Equal(t, 3, loaded.Len())
|
||||
|
||||
// func (m *mockStore) GetKnowledge(collectionID string, locale ...string) (map[string]interface{}, error) {
|
||||
// return nil, nil
|
||||
// }
|
||||
// Access ast1 to make it recently used
|
||||
loaded.Get("id1")
|
||||
|
||||
// func (m *mockStore) DeleteKnowledges(filter store.KnowledgeFilter) (int64, error) {
|
||||
// return 0, nil
|
||||
// }
|
||||
// Add ast4, should evict ast2 (least recently used)
|
||||
loaded.Put(ast4)
|
||||
assert.Equal(t, 3, loaded.Len())
|
||||
|
||||
// // Close closes the store and releases any resources
|
||||
// func (m *mockStore) Close() error {
|
||||
// return nil
|
||||
// }
|
||||
_, exists := loaded.Get("id2")
|
||||
assert.False(t, exists, "ast2 should be evicted")
|
||||
|
||||
_, exists = loaded.Get("id1")
|
||||
assert.True(t, exists, "ast1 should still exist")
|
||||
|
||||
_, exists = loaded.Get("id4")
|
||||
assert.True(t, exists, "ast4 should exist")
|
||||
})
|
||||
|
||||
t.Run("ClearCache", func(t *testing.T) {
|
||||
ClearCache()
|
||||
assert.Nil(t, loaded)
|
||||
})
|
||||
|
||||
t.Run("SetCacheAfterClear", func(t *testing.T) {
|
||||
SetCache(100)
|
||||
assert.NotNil(t, loaded)
|
||||
})
|
||||
}
|
||||
|
||||
// TestClone tests the assistant Clone method
|
||||
func TestClone(t *testing.T) {
|
||||
prepare(t)
|
||||
defer test.Clean()
|
||||
|
||||
t.Run("CloneFullFieldsAssistant", func(t *testing.T) {
|
||||
original, err := LoadPath("/assistants/tests/fullfields")
|
||||
require.NoError(t, err)
|
||||
|
||||
clone := original.Clone()
|
||||
require.NotNil(t, clone)
|
||||
|
||||
// Basic fields should be equal
|
||||
assert.Equal(t, original.ID, clone.ID)
|
||||
assert.Equal(t, original.Name, clone.Name)
|
||||
assert.Equal(t, original.Type, clone.Type)
|
||||
assert.Equal(t, original.Connector, clone.Connector)
|
||||
assert.Equal(t, original.Description, clone.Description)
|
||||
|
||||
// Verify deep copy - modifying original should not affect clone
|
||||
if len(original.Tags) > 0 {
|
||||
originalTag := original.Tags[0]
|
||||
original.Tags[0] = "modified"
|
||||
assert.NotEqual(t, original.Tags[0], clone.Tags[0])
|
||||
original.Tags[0] = originalTag // restore
|
||||
}
|
||||
|
||||
if original.Options != nil {
|
||||
original.Options["test_key"] = "test_value"
|
||||
_, exists := clone.Options["test_key"]
|
||||
assert.False(t, exists, "Clone should not have modified key")
|
||||
delete(original.Options, "test_key") // cleanup
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CloneNil", func(t *testing.T) {
|
||||
var nilAssistant *Assistant
|
||||
assert.Nil(t, nilAssistant.Clone())
|
||||
})
|
||||
}
|
||||
|
||||
// TestUpdate tests the assistant Update method
|
||||
func TestUpdate(t *testing.T) {
|
||||
prepare(t)
|
||||
defer test.Clean()
|
||||
|
||||
t.Run("UpdateBasicFields", func(t *testing.T) {
|
||||
assistant, err := LoadPath("/assistants/tests/fullfields")
|
||||
require.NoError(t, err)
|
||||
|
||||
updates := map[string]interface{}{
|
||||
"name": "Updated Name",
|
||||
"description": "Updated description",
|
||||
"tags": []string{"updated", "tags"},
|
||||
}
|
||||
|
||||
err = assistant.Update(updates)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "Updated Name", assistant.Name)
|
||||
assert.Equal(t, "Updated description", assistant.Description)
|
||||
assert.Equal(t, []string{"updated", "tags"}, assistant.Tags)
|
||||
})
|
||||
|
||||
t.Run("UpdateConnectorOptions", func(t *testing.T) {
|
||||
assistant, err := LoadPath("/assistants/tests/fullfields")
|
||||
require.NoError(t, err)
|
||||
|
||||
updates := map[string]interface{}{
|
||||
"connector_options": map[string]interface{}{
|
||||
"optional": false,
|
||||
"connectors": []string{"new-connector"},
|
||||
},
|
||||
}
|
||||
|
||||
err = assistant.Update(updates)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotNil(t, assistant.ConnectorOptions)
|
||||
assert.False(t, assistant.ConnectorOptions.Optional)
|
||||
assert.Contains(t, assistant.ConnectorOptions.Connectors, "new-connector")
|
||||
})
|
||||
|
||||
t.Run("UpdatePromptPresets", func(t *testing.T) {
|
||||
assistant, err := LoadPath("/assistants/tests/fullfields")
|
||||
require.NoError(t, err)
|
||||
|
||||
updates := map[string]interface{}{
|
||||
"prompt_presets": map[string]interface{}{
|
||||
"custom": []map[string]interface{}{
|
||||
{"role": "system", "content": "Custom preset"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err = assistant.Update(updates)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotNil(t, assistant.PromptPresets)
|
||||
customPreset, exists := assistant.PromptPresets["custom"]
|
||||
assert.True(t, exists)
|
||||
assert.Len(t, customPreset, 1)
|
||||
})
|
||||
|
||||
t.Run("UpdateSource", func(t *testing.T) {
|
||||
assistant, err := LoadPath("/assistants/tests/fullfields")
|
||||
require.NoError(t, err)
|
||||
|
||||
updates := map[string]interface{}{
|
||||
"source": "function Create(ctx, messages) { return { messages: messages }; }",
|
||||
}
|
||||
|
||||
err = assistant.Update(updates)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "function Create(ctx, messages) { return { messages: messages }; }", assistant.Source)
|
||||
})
|
||||
|
||||
t.Run("UpdateNilAssistant", func(t *testing.T) {
|
||||
var nilAssistant *Assistant
|
||||
err := nilAssistant.Update(map[string]interface{}{"name": "test"})
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMap tests the assistant Map method
|
||||
func TestMap(t *testing.T) {
|
||||
prepare(t)
|
||||
defer test.Clean()
|
||||
|
||||
assistant, err := LoadPath("/assistants/tests/fullfields")
|
||||
require.NoError(t, err)
|
||||
|
||||
m := assistant.Map()
|
||||
require.NotNil(t, m)
|
||||
|
||||
// Check all fields are present
|
||||
assert.Equal(t, assistant.ID, m["assistant_id"])
|
||||
assert.Equal(t, assistant.Name, m["name"])
|
||||
assert.Equal(t, assistant.Type, m["type"])
|
||||
assert.Equal(t, assistant.Connector, m["connector"])
|
||||
assert.Equal(t, assistant.Description, m["description"])
|
||||
assert.Equal(t, assistant.Path, m["path"])
|
||||
assert.Equal(t, assistant.Tags, m["tags"])
|
||||
assert.Equal(t, assistant.Options, m["options"])
|
||||
assert.Equal(t, assistant.Prompts, m["prompts"])
|
||||
assert.Equal(t, assistant.KB, m["kb"])
|
||||
assert.Equal(t, assistant.MCP, m["mcp"])
|
||||
assert.Equal(t, assistant.Workflow, m["workflow"])
|
||||
assert.Equal(t, assistant.Placeholder, m["placeholder"])
|
||||
assert.Equal(t, assistant.Locales, m["locales"])
|
||||
|
||||
// New fields
|
||||
assert.Equal(t, assistant.ConnectorOptions, m["connector_options"])
|
||||
assert.Equal(t, assistant.PromptPresets, m["prompt_presets"])
|
||||
assert.Equal(t, assistant.Source, m["source"])
|
||||
}
|
||||
|
||||
// TestValidate tests the assistant Validate method
|
||||
func TestValidate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ast *Assistant
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "ValidAssistant",
|
||||
ast: &Assistant{
|
||||
AssistantModel: store.AssistantModel{
|
||||
ID: "test-id",
|
||||
Name: "Test Assistant",
|
||||
Connector: "gpt-4o",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "MissingID",
|
||||
ast: &Assistant{
|
||||
AssistantModel: store.AssistantModel{
|
||||
Name: "Test Assistant",
|
||||
Connector: "gpt-4o",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "MissingName",
|
||||
ast: &Assistant{
|
||||
AssistantModel: store.AssistantModel{
|
||||
ID: "test-id",
|
||||
Connector: "gpt-4o",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.ast.Validate()
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
36
agent/assistant/source.go
Normal file
36
agent/assistant/source.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package assistant
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
v8 "github.com/yaoapp/gou/runtime/v8"
|
||||
"github.com/yaoapp/yao/agent/assistant/hook"
|
||||
)
|
||||
|
||||
// loadSource loads hook script from source code string
|
||||
// The source field stores TypeScript code directly
|
||||
// Priority: script field > source field (if script exists, source is ignored)
|
||||
func loadSource(source string, assistantID string) (*hook.Script, error) {
|
||||
if source == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Generate a virtual file path for the script
|
||||
file := fmt.Sprintf("assistants/%s/source.ts", assistantID)
|
||||
|
||||
script, err := v8.MakeScript([]byte(source), file, 5*time.Second, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to compile source script: %w", err)
|
||||
}
|
||||
|
||||
return &hook.Script{Script: script}, nil
|
||||
}
|
||||
|
||||
// TODO: Future enhancement - support multiple files merged with special comment delimiter
|
||||
// Format: // file: index.ts
|
||||
// This would allow splitting large scripts into multiple logical files while storing as single source
|
||||
// func loadSourceMultiFile(source string, assistantID string) (*hook.Script, error) {
|
||||
// // Parse source by "// file: xxx.ts" delimiter
|
||||
// // Merge and compile
|
||||
// }
|
||||
|
|
@ -462,3 +462,56 @@ func ParseModelID(modelID string) string {
|
|||
}
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
|
||||
// ToConnectorOptions converts various types to ConnectorOptions
|
||||
func ToConnectorOptions(v interface{}) (*ConnectorOptions, error) {
|
||||
if v == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
switch opts := v.(type) {
|
||||
case *ConnectorOptions:
|
||||
return opts, nil
|
||||
|
||||
case ConnectorOptions:
|
||||
return &opts, nil
|
||||
|
||||
default:
|
||||
raw, err := jsoniter.Marshal(opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connector_options format error: %s", err.Error())
|
||||
}
|
||||
|
||||
var connOpts ConnectorOptions
|
||||
err = jsoniter.Unmarshal(raw, &connOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connector_options format error: %s", err.Error())
|
||||
}
|
||||
return &connOpts, nil
|
||||
}
|
||||
}
|
||||
|
||||
// ToPromptPresets converts various types to map[string][]Prompt
|
||||
func ToPromptPresets(v interface{}) (map[string][]Prompt, error) {
|
||||
if v == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
switch presets := v.(type) {
|
||||
case map[string][]Prompt:
|
||||
return presets, nil
|
||||
|
||||
default:
|
||||
raw, err := jsoniter.Marshal(presets)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("prompt_presets format error: %s", err.Error())
|
||||
}
|
||||
|
||||
var result map[string][]Prompt
|
||||
err = jsoniter.Unmarshal(raw, &result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("prompt_presets format error: %s", err.Error())
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1148,6 +1148,273 @@ func TestModelID(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
// TestToConnectorOptions tests the ToConnectorOptions conversion function
|
||||
func TestToConnectorOptions(t *testing.T) {
|
||||
t.Run("NilInput", func(t *testing.T) {
|
||||
result, err := ToConnectorOptions(nil)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got: %v", err)
|
||||
}
|
||||
if result != nil {
|
||||
t.Errorf("Expected nil result, got: %v", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ConnectorOptionsPointer", func(t *testing.T) {
|
||||
opts := &ConnectorOptions{
|
||||
Optional: true,
|
||||
Connectors: []string{"openai", "anthropic"},
|
||||
Filters: []ModelCapability{CapVision, CapToolCalls},
|
||||
}
|
||||
result, err := ToConnectorOptions(opts)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got: %v", err)
|
||||
}
|
||||
if result != opts {
|
||||
t.Errorf("Expected same pointer")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ConnectorOptionsValue", func(t *testing.T) {
|
||||
opts := ConnectorOptions{
|
||||
Optional: true,
|
||||
Connectors: []string{"openai", "anthropic"},
|
||||
Filters: []ModelCapability{CapVision, CapToolCalls},
|
||||
}
|
||||
result, err := ToConnectorOptions(opts)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got: %v", err)
|
||||
}
|
||||
if !result.Optional {
|
||||
t.Error("Expected Optional to be true")
|
||||
}
|
||||
if len(result.Connectors) != 2 {
|
||||
t.Errorf("Expected 2 connectors, got %d", len(result.Connectors))
|
||||
}
|
||||
if len(result.Filters) != 2 {
|
||||
t.Errorf("Expected 2 filters, got %d", len(result.Filters))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("MapInput", func(t *testing.T) {
|
||||
data := map[string]interface{}{
|
||||
"optional": true,
|
||||
"connectors": []string{"openai", "anthropic", "azure"},
|
||||
"filters": []string{"vision", "tool_calls", "audio"},
|
||||
}
|
||||
result, err := ToConnectorOptions(data)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got: %v", err)
|
||||
}
|
||||
if !result.Optional {
|
||||
t.Error("Expected Optional to be true")
|
||||
}
|
||||
if len(result.Connectors) != 3 {
|
||||
t.Errorf("Expected 3 connectors, got %d", len(result.Connectors))
|
||||
}
|
||||
if len(result.Filters) != 3 {
|
||||
t.Errorf("Expected 3 filters, got %d", len(result.Filters))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("MapInputOptionalOnly", func(t *testing.T) {
|
||||
data := map[string]interface{}{
|
||||
"optional": true,
|
||||
}
|
||||
result, err := ToConnectorOptions(data)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got: %v", err)
|
||||
}
|
||||
if !result.Optional {
|
||||
t.Error("Expected Optional to be true")
|
||||
}
|
||||
if result.Connectors != nil {
|
||||
t.Error("Expected Connectors to be nil")
|
||||
}
|
||||
if result.Filters != nil {
|
||||
t.Error("Expected Filters to be nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("InvalidInput", func(t *testing.T) {
|
||||
// Test with data that can't be marshaled
|
||||
invalidData := make(chan int)
|
||||
_, err := ToConnectorOptions(invalidData)
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid input")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("InvalidJSONUnmarshal", func(t *testing.T) {
|
||||
// Test with data that marshals but can't unmarshal to ConnectorOptions
|
||||
data := map[string]interface{}{
|
||||
"invalid_field": "should cause unmarshal to fail gracefully",
|
||||
}
|
||||
result, err := ToConnectorOptions(data)
|
||||
// Should not error, just return empty ConnectorOptions
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got: %v", err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Error("Expected non-nil result")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestToPromptPresets tests the ToPromptPresets conversion function
|
||||
func TestToPromptPresets(t *testing.T) {
|
||||
t.Run("NilInput", func(t *testing.T) {
|
||||
result, err := ToPromptPresets(nil)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got: %v", err)
|
||||
}
|
||||
if result != nil {
|
||||
t.Errorf("Expected nil result, got: %v", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("MapStringPromptSlice", func(t *testing.T) {
|
||||
presets := map[string][]Prompt{
|
||||
"chat": {
|
||||
{Role: "system", Content: "You are a chat assistant"},
|
||||
},
|
||||
"task": {
|
||||
{Role: "system", Content: "You are a task assistant"},
|
||||
},
|
||||
}
|
||||
result, err := ToPromptPresets(presets)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got: %v", err)
|
||||
}
|
||||
if len(result) != 2 {
|
||||
t.Errorf("Expected 2 presets, got %d", len(result))
|
||||
}
|
||||
if len(result["chat"]) != 1 {
|
||||
t.Errorf("Expected 1 chat prompt, got %d", len(result["chat"]))
|
||||
}
|
||||
if len(result["task"]) != 1 {
|
||||
t.Errorf("Expected 1 task prompt, got %d", len(result["task"]))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("MapInput", func(t *testing.T) {
|
||||
data := map[string]interface{}{
|
||||
"chat": []interface{}{
|
||||
map[string]interface{}{"role": "system", "content": "Chat mode system prompt"},
|
||||
map[string]interface{}{"role": "user", "content": "Example user message"},
|
||||
},
|
||||
"task": []interface{}{
|
||||
map[string]interface{}{"role": "system", "content": "Task mode system prompt"},
|
||||
},
|
||||
"analyze": []interface{}{
|
||||
map[string]interface{}{"role": "system", "content": "Analyze mode system prompt"},
|
||||
},
|
||||
}
|
||||
result, err := ToPromptPresets(data)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got: %v", err)
|
||||
}
|
||||
if len(result) != 3 {
|
||||
t.Errorf("Expected 3 presets, got %d", len(result))
|
||||
}
|
||||
if len(result["chat"]) != 2 {
|
||||
t.Errorf("Expected 2 chat prompts, got %d", len(result["chat"]))
|
||||
}
|
||||
if len(result["task"]) != 1 {
|
||||
t.Errorf("Expected 1 task prompt, got %d", len(result["task"]))
|
||||
}
|
||||
if len(result["analyze"]) != 1 {
|
||||
t.Errorf("Expected 1 analyze prompt, got %d", len(result["analyze"]))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EmptyMap", func(t *testing.T) {
|
||||
data := map[string]interface{}{}
|
||||
result, err := ToPromptPresets(data)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got: %v", err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Error("Expected non-nil result")
|
||||
}
|
||||
if len(result) != 0 {
|
||||
t.Errorf("Expected empty map, got %d entries", len(result))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SinglePreset", func(t *testing.T) {
|
||||
data := map[string]interface{}{
|
||||
"default": []interface{}{
|
||||
map[string]interface{}{"role": "system", "content": "Default prompt"},
|
||||
},
|
||||
}
|
||||
result, err := ToPromptPresets(data)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got: %v", err)
|
||||
}
|
||||
if len(result) != 1 {
|
||||
t.Errorf("Expected 1 preset, got %d", len(result))
|
||||
}
|
||||
if _, ok := result["default"]; !ok {
|
||||
t.Error("Expected 'default' key in result")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("InvalidInput", func(t *testing.T) {
|
||||
// Test with data that can't be marshaled
|
||||
invalidData := make(chan int)
|
||||
_, err := ToPromptPresets(invalidData)
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid input")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("InvalidJSONUnmarshal", func(t *testing.T) {
|
||||
// Test with data that marshals but can't unmarshal to map[string][]Prompt
|
||||
// This is a string that can be marshaled but won't unmarshal to the expected type
|
||||
data := "not a map"
|
||||
_, err := ToPromptPresets(data)
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid JSON unmarshal")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("PromptWithAllFields", func(t *testing.T) {
|
||||
data := map[string]interface{}{
|
||||
"advanced": []interface{}{
|
||||
map[string]interface{}{
|
||||
"role": "system",
|
||||
"content": "Advanced system prompt",
|
||||
"name": "system-prompt",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": "User example",
|
||||
"name": "user-example",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"role": "assistant",
|
||||
"content": "Assistant response",
|
||||
"name": "assistant-response",
|
||||
},
|
||||
},
|
||||
}
|
||||
result, err := ToPromptPresets(data)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got: %v", err)
|
||||
}
|
||||
if len(result["advanced"]) != 3 {
|
||||
t.Errorf("Expected 3 prompts in advanced, got %d", len(result["advanced"]))
|
||||
}
|
||||
if result["advanced"][0].Role != "system" {
|
||||
t.Errorf("Expected role 'system', got '%s'", result["advanced"][0].Role)
|
||||
}
|
||||
if result["advanced"][0].Content != "Advanced system prompt" {
|
||||
t.Errorf("Expected content 'Advanced system prompt', got '%s'", result["advanced"][0].Content)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestParseModelID tests the ParseModelID function
|
||||
func TestParseModelID(t *testing.T) {
|
||||
t.Run("ValidModelID", func(t *testing.T) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue