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)
|
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
|
// GetPlaceholder returns the placeholder of the assistant
|
||||||
func (ast *Assistant) GetPlaceholder(locale string) *store.Placeholder {
|
func (ast *Assistant) GetPlaceholder(locale string) *store.Placeholder {
|
||||||
|
|
||||||
|
|
@ -88,30 +63,33 @@ func (ast *Assistant) Map() map[string]interface{} {
|
||||||
}
|
}
|
||||||
|
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"assistant_id": ast.ID,
|
"assistant_id": ast.ID,
|
||||||
"type": ast.Type,
|
"type": ast.Type,
|
||||||
"name": ast.Name,
|
"name": ast.Name,
|
||||||
"readonly": ast.Readonly,
|
"readonly": ast.Readonly,
|
||||||
"public": ast.Public,
|
"public": ast.Public,
|
||||||
"share": ast.Share,
|
"share": ast.Share,
|
||||||
"avatar": ast.Avatar,
|
"avatar": ast.Avatar,
|
||||||
"connector": ast.Connector,
|
"connector": ast.Connector,
|
||||||
"path": ast.Path,
|
"connector_options": ast.ConnectorOptions,
|
||||||
"built_in": ast.BuiltIn,
|
"path": ast.Path,
|
||||||
"sort": ast.Sort,
|
"built_in": ast.BuiltIn,
|
||||||
"description": ast.Description,
|
"sort": ast.Sort,
|
||||||
"options": ast.Options,
|
"description": ast.Description,
|
||||||
"prompts": ast.Prompts,
|
"options": ast.Options,
|
||||||
"kb": ast.KB,
|
"prompts": ast.Prompts,
|
||||||
"mcp": ast.MCP,
|
"prompt_presets": ast.PromptPresets,
|
||||||
"workflow": ast.Workflow,
|
"source": ast.Source,
|
||||||
"tags": ast.Tags,
|
"kb": ast.KB,
|
||||||
"mentionable": ast.Mentionable,
|
"mcp": ast.MCP,
|
||||||
"automated": ast.Automated,
|
"workflow": ast.Workflow,
|
||||||
"placeholder": ast.Placeholder,
|
"tags": ast.Tags,
|
||||||
"locales": ast.Locales,
|
"mentionable": ast.Mentionable,
|
||||||
"created_at": store.ToMySQLTime(ast.CreatedAt),
|
"automated": ast.Automated,
|
||||||
"updated_at": store.ToMySQLTime(ast.UpdatedAt),
|
"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,
|
Share: ast.Share,
|
||||||
Mentionable: ast.Mentionable,
|
Mentionable: ast.Mentionable,
|
||||||
Automated: ast.Automated,
|
Automated: ast.Automated,
|
||||||
|
Source: ast.Source,
|
||||||
CreatedAt: ast.CreatedAt,
|
CreatedAt: ast.CreatedAt,
|
||||||
UpdatedAt: ast.UpdatedAt,
|
UpdatedAt: ast.UpdatedAt,
|
||||||
},
|
},
|
||||||
|
|
@ -245,6 +224,31 @@ func (ast *Assistant) Clone() *Assistant {
|
||||||
copy(clone.Prompts, ast.Prompts)
|
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
|
// Deep copy workflow
|
||||||
if ast.Workflow != nil {
|
if ast.Workflow != nil {
|
||||||
clone.Workflow = &store.Workflow{}
|
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 {
|
if v, ok := data["options"].(map[string]interface{}); ok {
|
||||||
ast.Options = v
|
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
|
// KB
|
||||||
if v, has := data["kb"]; has {
|
if v, has := data["kb"]; has {
|
||||||
|
|
|
||||||
|
|
@ -268,7 +268,7 @@ func LoadPath(path string) (*Assistant, error) {
|
||||||
|
|
||||||
updatedAt := int64(0)
|
updatedAt := int64(0)
|
||||||
|
|
||||||
// prompts
|
// prompts (default prompts from prompts.yml)
|
||||||
promptsfile := filepath.Join(path, "prompts.yml")
|
promptsfile := filepath.Join(path, "prompts.yml")
|
||||||
if has, _ := app.Exists(promptsfile); has {
|
if has, _ := app.Exists(promptsfile); has {
|
||||||
prompts, ts, err := loadPrompts(promptsfile, path)
|
prompts, ts, err := loadPrompts(promptsfile, path)
|
||||||
|
|
@ -280,6 +280,19 @@ func LoadPath(path string) (*Assistant, error) {
|
||||||
updatedAt = ts
|
updatedAt = ts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prompt_presets (from prompts directory, key is filename without extension)
|
||||||
|
promptsDir := filepath.Join(path, "prompts")
|
||||||
|
if has, _ := app.Exists(promptsDir); has {
|
||||||
|
presets, ts, err := loadPromptPresets(promptsDir, path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(presets) > 0 {
|
||||||
|
data["prompt_presets"] = presets
|
||||||
|
updatedAt = max(updatedAt, ts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// load script
|
// load script
|
||||||
scriptfile := filepath.Join(path, "src", "index.ts")
|
scriptfile := filepath.Join(path, "src", "index.ts")
|
||||||
if has, _ := app.Exists(scriptfile); has {
|
if has, _ := app.Exists(scriptfile); has {
|
||||||
|
|
@ -419,6 +432,15 @@ func loadMap(data map[string]interface{}) (*Assistant, error) {
|
||||||
assistant.Connector = connector
|
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
|
// tags
|
||||||
if v, has := data["tags"]; has {
|
if v, has := data["tags"]; has {
|
||||||
switch vv := v.(type) {
|
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
|
// tools - deprecated, now handled by MCP
|
||||||
// if tools, has := data["tools"]; has {
|
// if tools, has := data["tools"]; has {
|
||||||
// ... removed ...
|
// ... removed ...
|
||||||
|
|
@ -542,7 +578,29 @@ func loadMap(data map[string]interface{}) (*Assistant, error) {
|
||||||
assistant.Workflow = wf
|
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 {
|
if data["script"] != nil {
|
||||||
switch v := data["script"].(type) {
|
switch v := data["script"].(type) {
|
||||||
case string:
|
case string:
|
||||||
|
|
@ -557,6 +615,13 @@ func loadMap(data map[string]interface{}) (*Assistant, error) {
|
||||||
case *v8.Script:
|
case *v8.Script:
|
||||||
assistant.Script = &hook.Script{Script: v}
|
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
|
// created_at
|
||||||
|
|
@ -603,6 +668,7 @@ func loadPrompts(file string, root string) (string, int64, error) {
|
||||||
return "", 0, err
|
return "", 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Replace @assets/xxx references with file content
|
||||||
re := regexp.MustCompile(`@assets/([^\s]+\.(md|yml|yaml|json|txt))`)
|
re := regexp.MustCompile(`@assets/([^\s]+\.(md|yml|yaml|json|txt))`)
|
||||||
prompts = re.ReplaceAllFunc(prompts, func(s []byte) []byte {
|
prompts = re.ReplaceAllFunc(prompts, func(s []byte) []byte {
|
||||||
asset := re.FindStringSubmatch(string(s))[1]
|
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
|
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) {
|
func loadScript(file string, root string) (*hook.Script, int64, error) {
|
||||||
|
|
||||||
app, err := fs.Get("app")
|
app, err := fs.Get("app")
|
||||||
|
|
|
||||||
|
|
@ -1,435 +1,501 @@
|
||||||
package assistant
|
package assistant
|
||||||
|
|
||||||
// func prepare(t *testing.T) {
|
import (
|
||||||
// test.Prepare(t, config.Conf)
|
"testing"
|
||||||
// }
|
|
||||||
|
|
||||||
// func TestLoad_LoadPath(t *testing.T) {
|
"github.com/stretchr/testify/assert"
|
||||||
// prepare(t)
|
"github.com/stretchr/testify/require"
|
||||||
// defer test.Clean()
|
store "github.com/yaoapp/yao/agent/store/types"
|
||||||
|
"github.com/yaoapp/yao/config"
|
||||||
|
"github.com/yaoapp/yao/test"
|
||||||
|
)
|
||||||
|
|
||||||
// assistant, err := LoadPath("/assistants/modi")
|
func prepare(t *testing.T) {
|
||||||
// if err != nil {
|
test.Prepare(t, config.Conf)
|
||||||
// t.Fatal(err)
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
// // Validate basic properties
|
// TestLoadPath tests loading assistant from path
|
||||||
// assert.NotNil(t, assistant)
|
func TestLoadPath(t *testing.T) {
|
||||||
// assert.Equal(t, "modi", assistant.ID)
|
prepare(t)
|
||||||
// assert.Equal(t, "Modi", assistant.Name)
|
defer test.Clean()
|
||||||
// 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)
|
|
||||||
|
|
||||||
// // Test non-existent assistant
|
t.Run("LoadFullFieldsAssistant", func(t *testing.T) {
|
||||||
// _, err = LoadPath("/assistants/non-existent")
|
assistant, err := LoadPath("/assistants/tests/fullfields")
|
||||||
// assert.Error(t, err)
|
require.NoError(t, err)
|
||||||
// }
|
require.NotNil(t, assistant)
|
||||||
|
|
||||||
// func TestLoad_LoadStore(t *testing.T) {
|
// Basic fields
|
||||||
// prepare(t)
|
assert.Equal(t, "tests.fullfields", assistant.ID)
|
||||||
// defer test.Clean()
|
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
|
// Boolean fields
|
||||||
// _, err := LoadStore("test-id")
|
assert.True(t, assistant.Public)
|
||||||
// assert.Error(t, err)
|
assert.True(t, assistant.Readonly)
|
||||||
// assert.Contains(t, err.Error(), "storage is not set")
|
assert.True(t, assistant.Mentionable)
|
||||||
|
assert.False(t, assistant.Automated)
|
||||||
|
|
||||||
// // Setup mock storage
|
// Share field
|
||||||
// mockStore := &mockStore{
|
assert.Equal(t, "team", assistant.Share)
|
||||||
// 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)
|
|
||||||
|
|
||||||
// // Test loading from store
|
// Sort field
|
||||||
// assistant, err := LoadStore("test-id")
|
assert.Equal(t, 100, assistant.Sort)
|
||||||
// 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)
|
|
||||||
|
|
||||||
// // Test cache functionality
|
// Tags
|
||||||
// assistant2, err := LoadStore("test-id")
|
assert.NotNil(t, assistant.Tags)
|
||||||
// assert.NoError(t, err)
|
assert.Contains(t, assistant.Tags, "Test")
|
||||||
// assert.Equal(t, assistant, assistant2) // Should be the same instance from cache
|
assert.Contains(t, assistant.Tags, "Development")
|
||||||
|
assert.Contains(t, assistant.Tags, "FullFields")
|
||||||
|
|
||||||
// // Test non-existent assistant
|
// Options
|
||||||
// _, err = LoadStore("non-existent")
|
assert.NotNil(t, assistant.Options)
|
||||||
// assert.Error(t, err)
|
assert.Equal(t, 0.7, assistant.Options["temperature"])
|
||||||
// }
|
assert.Equal(t, float64(2000), assistant.Options["max_tokens"])
|
||||||
|
|
||||||
// func TestLoad_Cache(t *testing.T) {
|
// Prompts (default prompts from prompts.yml)
|
||||||
// prepare(t)
|
assert.NotNil(t, assistant.Prompts)
|
||||||
// defer test.Clean()
|
assert.GreaterOrEqual(t, len(assistant.Prompts), 1)
|
||||||
|
assert.Equal(t, "system", assistant.Prompts[0].Role)
|
||||||
|
|
||||||
// // Clear any existing cache first
|
// Script (from src/index.ts)
|
||||||
// ClearCache()
|
assert.NotNil(t, assistant.Script)
|
||||||
|
})
|
||||||
|
|
||||||
// // Test cache operations
|
t.Run("LoadConnectorOptions", func(t *testing.T) {
|
||||||
// SetCache(2) // Set small cache size for testing
|
assistant, err := LoadPath("/assistants/tests/fullfields")
|
||||||
// assert.Equal(t, 2, loaded.capacity, "Cache capacity should be 2")
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, assistant)
|
||||||
|
|
||||||
// // Create test assistants
|
// ConnectorOptions
|
||||||
// assistant1 := &Assistant{ID: "id1", Name: "Assistant 1"}
|
assert.NotNil(t, assistant.ConnectorOptions)
|
||||||
// assistant2 := &Assistant{ID: "id2", Name: "Assistant 2"}
|
assert.True(t, assistant.ConnectorOptions.Optional)
|
||||||
// assistant3 := &Assistant{ID: "id3", Name: "Assistant 3"}
|
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
|
t.Run("LoadPromptPresets", func(t *testing.T) {
|
||||||
// loaded.Put(assistant1)
|
assistant, err := LoadPath("/assistants/tests/fullfields")
|
||||||
// assert.Equal(t, 1, loaded.Len(), "Cache should have 1 item")
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, assistant)
|
||||||
|
|
||||||
// loaded.Put(assistant2)
|
// PromptPresets (from prompts directory)
|
||||||
// assert.Equal(t, 2, loaded.Len(), "Cache should have 2 items")
|
assert.NotNil(t, assistant.PromptPresets)
|
||||||
|
|
||||||
// // Test cache hit
|
// Top-level presets: chat.yml -> "chat", task.yml -> "task"
|
||||||
// cached, exists := loaded.Get("id1")
|
chatPreset, hasChat := assistant.PromptPresets["chat"]
|
||||||
// assert.True(t, exists)
|
assert.True(t, hasChat, "Should have 'chat' preset")
|
||||||
// assert.Equal(t, assistant1, cached)
|
assert.NotEmpty(t, chatPreset)
|
||||||
|
|
||||||
// // Test cache eviction (LRU)
|
taskPreset, hasTask := assistant.PromptPresets["task"]
|
||||||
// // At this point: assistant1 is most recently used (due to Get), then assistant2
|
assert.True(t, hasTask, "Should have 'task' preset")
|
||||||
// loaded.Put(assistant3) // This should evict assistant2 since it's least recently used
|
assert.NotEmpty(t, taskPreset)
|
||||||
// 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)")
|
|
||||||
|
|
||||||
// // Test clear cache
|
// Nested presets: chat/friendly.yml -> "chat.friendly"
|
||||||
// ClearCache()
|
friendlyPreset, hasFriendly := assistant.PromptPresets["chat.friendly"]
|
||||||
// assert.Nil(t, loaded)
|
assert.True(t, hasFriendly, "Should have 'chat.friendly' preset")
|
||||||
|
assert.NotEmpty(t, friendlyPreset)
|
||||||
|
|
||||||
// // Test setting new cache capacity
|
professionalPreset, hasProfessional := assistant.PromptPresets["chat.professional"]
|
||||||
// SetCache(100)
|
assert.True(t, hasProfessional, "Should have 'chat.professional' preset")
|
||||||
// assert.NotNil(t, loaded)
|
assert.NotEmpty(t, professionalPreset)
|
||||||
// }
|
|
||||||
|
|
||||||
// func TestLoad_Validate(t *testing.T) {
|
// task/analysis.yml -> "task.analysis"
|
||||||
// tests := []struct {
|
analysisPreset, hasAnalysis := assistant.PromptPresets["task.analysis"]
|
||||||
// name string
|
assert.True(t, hasAnalysis, "Should have 'task.analysis' preset")
|
||||||
// ast *Assistant
|
assert.NotEmpty(t, analysisPreset)
|
||||||
// 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,
|
|
||||||
// },
|
|
||||||
// }
|
|
||||||
|
|
||||||
// for _, tt := range tests {
|
t.Run("LoadKnowledgeBase", func(t *testing.T) {
|
||||||
// t.Run(tt.name, func(t *testing.T) {
|
assistant, err := LoadPath("/assistants/tests/fullfields")
|
||||||
// err := tt.ast.Validate()
|
require.NoError(t, err)
|
||||||
// if (err != nil) != tt.wantErr {
|
require.NotNil(t, assistant)
|
||||||
// t.Errorf("Assistant.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func TestLoad_Clone(t *testing.T) {
|
// KB
|
||||||
// // Create a test assistant with all fields populated
|
assert.NotNil(t, assistant.KB)
|
||||||
// original := &Assistant{
|
assert.NotNil(t, assistant.KB.Collections)
|
||||||
// ID: "test-id",
|
assert.Contains(t, assistant.KB.Collections, "test-collection")
|
||||||
// Type: "test-type",
|
assert.NotNil(t, assistant.KB.Options)
|
||||||
// Name: "Test Assistant",
|
assert.Equal(t, float64(5), assistant.KB.Options["top_k"])
|
||||||
// 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"},
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Clone the assistant
|
t.Run("LoadMCPServers", func(t *testing.T) {
|
||||||
// clone := original.Clone()
|
assistant, err := LoadPath("/assistants/tests/fullfields")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, assistant)
|
||||||
|
|
||||||
// // Verify all fields are correctly cloned
|
// MCP
|
||||||
// assert.Equal(t, original.ID, clone.ID)
|
assert.NotNil(t, assistant.MCP)
|
||||||
// assert.Equal(t, original.Type, clone.Type)
|
assert.NotNil(t, assistant.MCP.Servers)
|
||||||
// assert.Equal(t, original.Name, clone.Name)
|
assert.Len(t, assistant.MCP.Servers, 1)
|
||||||
// assert.Equal(t, original.Avatar, clone.Avatar)
|
assert.Equal(t, "echo", assistant.MCP.Servers[0].ServerID)
|
||||||
// assert.Equal(t, original.Connector, clone.Connector)
|
assert.Contains(t, assistant.MCP.Servers[0].Tools, "ping")
|
||||||
// assert.Equal(t, original.Path, clone.Path)
|
assert.Contains(t, assistant.MCP.Servers[0].Tools, "echo")
|
||||||
// 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)
|
|
||||||
|
|
||||||
// // Verify deep copy by modifying original
|
t.Run("LoadWorkflow", func(t *testing.T) {
|
||||||
// original.Tags[0] = "modified"
|
assistant, err := LoadPath("/assistants/tests/fullfields")
|
||||||
// original.Options["key"] = "modified"
|
require.NoError(t, err)
|
||||||
// original.Workflow["step"] = "modified"
|
require.NotNil(t, assistant)
|
||||||
// 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"])
|
|
||||||
|
|
||||||
// // Test nil case
|
// Workflow
|
||||||
// var nilAssistant *Assistant
|
assert.NotNil(t, assistant.Workflow)
|
||||||
// assert.Nil(t, nilAssistant.Clone())
|
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) {
|
t.Run("LoadPlaceholder", func(t *testing.T) {
|
||||||
// // Create a test assistant
|
assistant, err := LoadPath("/assistants/tests/fullfields")
|
||||||
// ast := &Assistant{
|
require.NoError(t, err)
|
||||||
// ID: "test-id",
|
require.NotNil(t, assistant)
|
||||||
// Name: "Original Name",
|
|
||||||
// Connector: "original-connector",
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Test updating various fields
|
// Placeholder
|
||||||
// updates := map[string]interface{}{
|
assert.NotNil(t, assistant.Placeholder)
|
||||||
// "name": "Updated Name",
|
assert.Equal(t, "Full Fields Test", assistant.Placeholder.Title)
|
||||||
// "avatar": "updated-avatar",
|
assert.Equal(t, "Test assistant with complete field coverage", assistant.Placeholder.Description)
|
||||||
// "description": "Updated description",
|
assert.NotNil(t, assistant.Placeholder.Prompts)
|
||||||
// "connector": "updated-connector",
|
assert.Len(t, assistant.Placeholder.Prompts, 3)
|
||||||
// "type": "updated-type",
|
})
|
||||||
// "sort": 2,
|
|
||||||
// "mentionable": true,
|
|
||||||
// "automated": true,
|
|
||||||
// "tags": []string{"new-tag"},
|
|
||||||
// "options": map[string]interface{}{"new": "value"},
|
|
||||||
// }
|
|
||||||
|
|
||||||
// err := ast.Update(updates)
|
t.Run("LoadLocales", func(t *testing.T) {
|
||||||
// assert.NoError(t, err)
|
assistant, err := LoadPath("/assistants/tests/fullfields")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, assistant)
|
||||||
|
|
||||||
// // Verify updates
|
// Locales
|
||||||
// assert.Equal(t, "Updated Name", ast.Name)
|
assert.NotNil(t, assistant.Locales)
|
||||||
// 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)
|
|
||||||
|
|
||||||
// // Test nil assistant
|
enLocale, hasEn := assistant.Locales["en-us"]
|
||||||
// var nilAssistant *Assistant
|
assert.True(t, hasEn, "Should have en-us locale")
|
||||||
// err = nilAssistant.Update(updates)
|
assert.NotNil(t, enLocale)
|
||||||
// assert.Error(t, err)
|
|
||||||
|
|
||||||
// // Test invalid update that would make the assistant invalid
|
zhLocale, hasZh := assistant.Locales["zh-cn"]
|
||||||
// invalidUpdates := map[string]interface{}{
|
assert.True(t, hasZh, "Should have zh-cn locale")
|
||||||
// "name": "",
|
assert.NotNil(t, zhLocale)
|
||||||
// }
|
})
|
||||||
// err = ast.Update(invalidUpdates)
|
|
||||||
// assert.Error(t, err)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func TestLoadBuiltIn(t *testing.T) {
|
t.Run("LoadNonExistentAssistant", func(t *testing.T) {
|
||||||
// prepare(t)
|
_, err := LoadPath("/assistants/non-existent")
|
||||||
// defer test.Clean()
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// // Clear any existing cache and storage
|
// TestLoadPathMCPTest tests loading the MCP test assistant
|
||||||
// ClearCache()
|
func TestLoadPathMCPTest(t *testing.T) {
|
||||||
// SetStorage(nil)
|
prepare(t)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
// // Create a mock store to verify built-in assistants are saved
|
assistant, err := LoadPath("/assistants/tests/mcptest")
|
||||||
// mockStore := &mockStore{
|
require.NoError(t, err)
|
||||||
// data: make(map[string]map[string]interface{}),
|
require.NotNil(t, assistant)
|
||||||
// }
|
|
||||||
// SetStorage(mockStore)
|
|
||||||
// SetCache(100)
|
|
||||||
|
|
||||||
// // Test loading built-in assistants
|
assert.Equal(t, "tests.mcptest", assistant.ID)
|
||||||
// err := LoadBuiltIn()
|
assert.Equal(t, "MCP Test Assistant", assistant.Name)
|
||||||
// assert.NoError(t, err)
|
assert.Equal(t, "gpt-4o", assistant.Connector)
|
||||||
|
|
||||||
// // Verify Modi assistant was loaded
|
// MCP configuration
|
||||||
// assistant, exists := loaded.Get("modi")
|
assert.NotNil(t, assistant.MCP)
|
||||||
// assert.True(t, exists, "Modi assistant should be loaded in cache")
|
assert.Len(t, assistant.MCP.Servers, 1)
|
||||||
// if exists {
|
assert.Equal(t, "echo", assistant.MCP.Servers[0].ServerID)
|
||||||
// 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)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// }
|
// 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
|
// TestLoadPathBuildRequest tests loading the build request test assistant
|
||||||
// type mockStore struct {
|
func TestLoadPathBuildRequest(t *testing.T) {
|
||||||
// data map[string]map[string]interface{}
|
prepare(t)
|
||||||
// }
|
defer test.Clean()
|
||||||
|
|
||||||
// func (m *mockStore) GetAssistant(id string, locale ...string) (map[string]interface{}, error) {
|
assistant, err := LoadPath("/assistants/tests/buildrequest")
|
||||||
// if data, ok := m.data[id]; ok {
|
require.NoError(t, err)
|
||||||
// return data, nil
|
require.NotNil(t, assistant)
|
||||||
// }
|
|
||||||
// return nil, fmt.Errorf("assistant not found: %s", id)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Add other required interface methods with empty implementations
|
assert.Equal(t, "tests.buildrequest", assistant.ID)
|
||||||
// func (m *mockStore) GetThread(id string) (map[string]interface{}, error) { return nil, nil }
|
assert.Equal(t, "Build Request Test", assistant.Name)
|
||||||
// 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
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Attachment related methods
|
// Script should be loaded
|
||||||
// func (m *mockStore) SaveAttachment(attachment map[string]interface{}) (interface{}, error) {
|
assert.NotNil(t, assistant.Script)
|
||||||
// return attachment["file_id"], nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (m *mockStore) DeleteAttachment(fileID string) error {
|
// Options
|
||||||
// return nil
|
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) {
|
// TestCache tests the assistant cache functionality
|
||||||
// return &store.AttachmentResponse{}, nil
|
func TestCache(t *testing.T) {
|
||||||
// }
|
// Clear any existing cache
|
||||||
|
ClearCache()
|
||||||
|
|
||||||
// func (m *mockStore) GetAttachment(fileID string, locale ...string) (map[string]interface{}, error) {
|
// Set small cache for testing
|
||||||
// return nil, nil
|
SetCache(3)
|
||||||
// }
|
assert.NotNil(t, loaded)
|
||||||
|
|
||||||
// func (m *mockStore) DeleteAttachments(filter store.AttachmentFilter) (int64, error) {
|
// Create test assistants
|
||||||
// return 0, nil
|
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
|
t.Run("PutAndGet", func(t *testing.T) {
|
||||||
// func (m *mockStore) SaveKnowledge(knowledge map[string]interface{}) (interface{}, error) {
|
loaded.Put(ast1)
|
||||||
// return knowledge["collection_id"], nil
|
assert.Equal(t, 1, loaded.Len())
|
||||||
// }
|
|
||||||
|
|
||||||
// func (m *mockStore) DeleteKnowledge(collectionID string) error {
|
cached, exists := loaded.Get("id1")
|
||||||
// return nil
|
assert.True(t, exists)
|
||||||
// }
|
assert.Equal(t, ast1, cached)
|
||||||
|
})
|
||||||
|
|
||||||
// func (m *mockStore) GetKnowledges(filter store.KnowledgeFilter, locale ...string) (*store.KnowledgeResponse, error) {
|
t.Run("CacheEviction", func(t *testing.T) {
|
||||||
// return &store.KnowledgeResponse{}, nil
|
loaded.Put(ast2)
|
||||||
// }
|
loaded.Put(ast3)
|
||||||
|
assert.Equal(t, 3, loaded.Len())
|
||||||
|
|
||||||
// func (m *mockStore) GetKnowledge(collectionID string, locale ...string) (map[string]interface{}, error) {
|
// Access ast1 to make it recently used
|
||||||
// return nil, nil
|
loaded.Get("id1")
|
||||||
// }
|
|
||||||
|
|
||||||
// func (m *mockStore) DeleteKnowledges(filter store.KnowledgeFilter) (int64, error) {
|
// Add ast4, should evict ast2 (least recently used)
|
||||||
// return 0, nil
|
loaded.Put(ast4)
|
||||||
// }
|
assert.Equal(t, 3, loaded.Len())
|
||||||
|
|
||||||
// // Close closes the store and releases any resources
|
_, exists := loaded.Get("id2")
|
||||||
// func (m *mockStore) Close() error {
|
assert.False(t, exists, "ast2 should be evicted")
|
||||||
// return nil
|
|
||||||
// }
|
_, 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]
|
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
|
// TestParseModelID tests the ParseModelID function
|
||||||
func TestParseModelID(t *testing.T) {
|
func TestParseModelID(t *testing.T) {
|
||||||
t.Run("ValidModelID", func(t *testing.T) {
|
t.Run("ValidModelID", func(t *testing.T) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue