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:
Max 2025-12-02 14:54:35 +08:00
parent 339a486eb4
commit 51092e3324
6 changed files with 1019 additions and 431 deletions

View file

@ -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 {

View file

@ -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")

View file

@ -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
View 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
// }

View file

@ -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
}
}

View file

@ -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) {