Enhance Assistant model with capabilities and sandbox configuration
- Introduce `Capabilities` and `Sandbox` fields in the Assistant model, allowing for detailed descriptions of assistant capabilities and sandbox configurations. - Update loading and conversion functions to handle the new fields, ensuring they are correctly parsed and stored. - Modify filtering and response handling to include the new fields, providing better integration with the API. - Add comprehensive tests to validate the functionality of the new fields, ensuring they are correctly processed in various scenarios.
This commit is contained in:
parent
a28d61ed78
commit
b4ded8a3ed
12 changed files with 2007 additions and 433 deletions
|
|
@ -557,6 +557,11 @@ func loadMap(data map[string]interface{}) (*Assistant, error) {
|
|||
assistant.Description = v
|
||||
}
|
||||
|
||||
// capabilities
|
||||
if v, ok := data["capabilities"].(string); ok {
|
||||
assistant.Capabilities = v
|
||||
}
|
||||
|
||||
// locales
|
||||
if locales, ok := data["locales"].(i18n.Map); ok {
|
||||
assistant.Locales = locales
|
||||
|
|
@ -568,30 +573,39 @@ func loadMap(data map[string]interface{}) (*Assistant, error) {
|
|||
if i18nObj.Messages == nil {
|
||||
i18nObj.Messages = make(map[string]any)
|
||||
}
|
||||
// Add name and description if not already present
|
||||
// Add name, description, and capabilities if not already present
|
||||
if _, exists := i18nObj.Messages["name"]; !exists && assistant.Name != "" {
|
||||
i18nObj.Messages["name"] = assistant.Name
|
||||
}
|
||||
if _, exists := i18nObj.Messages["description"]; !exists && assistant.Description != "" {
|
||||
i18nObj.Messages["description"] = assistant.Description
|
||||
}
|
||||
if _, exists := i18nObj.Messages["capabilities"]; !exists && assistant.Capabilities != "" {
|
||||
i18nObj.Messages["capabilities"] = assistant.Capabilities
|
||||
}
|
||||
flattened[locale] = i18nObj
|
||||
}
|
||||
|
||||
i18n.Locales[id] = flattened
|
||||
} else {
|
||||
// No locales defined, create default with name and description for all common locales
|
||||
if assistant.Name != "" || assistant.Description != "" {
|
||||
// No locales defined, create default with name, description, and capabilities for all common locales
|
||||
if assistant.Name != "" || assistant.Description != "" || assistant.Capabilities != "" {
|
||||
defaultLocales := make(map[string]i18n.I18n)
|
||||
// Create entries for all common locales so {{name}} can be resolved
|
||||
commonLocales := []string{"en", "en-us", "zh", "zh-cn", "zh-tw"}
|
||||
for _, locale := range commonLocales {
|
||||
messages := map[string]any{}
|
||||
if assistant.Name != "" {
|
||||
messages["name"] = assistant.Name
|
||||
}
|
||||
if assistant.Description != "" {
|
||||
messages["description"] = assistant.Description
|
||||
}
|
||||
if assistant.Capabilities != "" {
|
||||
messages["capabilities"] = assistant.Capabilities
|
||||
}
|
||||
defaultLocales[locale] = i18n.I18n{
|
||||
Locale: locale,
|
||||
Messages: map[string]any{
|
||||
"name": assistant.Name,
|
||||
"description": assistant.Description,
|
||||
},
|
||||
Locale: locale,
|
||||
Messages: messages,
|
||||
}
|
||||
}
|
||||
i18n.Locales[id] = defaultLocales
|
||||
|
|
|
|||
|
|
@ -291,6 +291,9 @@ func ToAssistantModel(v interface{}) (*AssistantModel, error) {
|
|||
if description, ok := data["description"].(string); ok {
|
||||
model.Description = description
|
||||
}
|
||||
if capabilities, ok := data["capabilities"].(string); ok {
|
||||
model.Capabilities = capabilities
|
||||
}
|
||||
if share, ok := data["share"].(string); ok {
|
||||
model.Share = share
|
||||
}
|
||||
|
|
@ -421,6 +424,14 @@ func ToAssistantModel(v interface{}) (*AssistantModel, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// Sandbox
|
||||
if sandbox, ok := data["sandbox"]; ok && sandbox != nil {
|
||||
sb, err := ToSandbox(sandbox)
|
||||
if err == nil {
|
||||
model.Sandbox = sb
|
||||
}
|
||||
}
|
||||
|
||||
// Placeholder
|
||||
if placeholder, ok := data["placeholder"]; ok && placeholder != nil {
|
||||
raw, err := jsoniter.Marshal(placeholder)
|
||||
|
|
|
|||
|
|
@ -20,10 +20,12 @@ var AssistantAllowedFields = map[string]bool{
|
|||
"prompts": true,
|
||||
"prompt_presets": true,
|
||||
"disable_global_prompts": true,
|
||||
"capabilities": true,
|
||||
"workflow": true,
|
||||
"kb": true,
|
||||
"db": true,
|
||||
"mcp": true,
|
||||
"sandbox": true,
|
||||
"source": true,
|
||||
"tags": true,
|
||||
"modes": true,
|
||||
|
|
@ -53,6 +55,7 @@ var AssistantDefaultFields = []string{
|
|||
"avatar",
|
||||
"connector",
|
||||
"description",
|
||||
"capabilities", // Capabilities description for Robot orchestration (lightweight)
|
||||
"tags", // Tags for categorization (lightweight)
|
||||
"modes", // Supported modes (lightweight)
|
||||
"default_mode", // Default mode (lightweight)
|
||||
|
|
@ -63,9 +66,10 @@ var AssistantDefaultFields = []string{
|
|||
"share",
|
||||
"automated",
|
||||
"mentionable",
|
||||
"kb", // Knowledge base configuration (lightweight)
|
||||
"db", // Database configuration (lightweight)
|
||||
"mcp", // MCP servers configuration (lightweight)
|
||||
"sandbox", // Sandbox configuration presence (lightweight)
|
||||
"kb", // Knowledge base configuration (lightweight)
|
||||
"db", // Database configuration (lightweight)
|
||||
"mcp", // MCP servers configuration (lightweight)
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"__yao_created_by", // Permission: creator user ID
|
||||
|
|
@ -84,6 +88,7 @@ var AssistantFullFields = []string{
|
|||
"connector",
|
||||
"connector_options",
|
||||
"description",
|
||||
"capabilities",
|
||||
"path",
|
||||
"sort",
|
||||
"built_in",
|
||||
|
|
@ -96,6 +101,7 @@ var AssistantFullFields = []string{
|
|||
"kb",
|
||||
"db",
|
||||
"mcp",
|
||||
"sandbox",
|
||||
"source",
|
||||
"tags",
|
||||
"modes",
|
||||
|
|
|
|||
|
|
@ -183,6 +183,7 @@ type AssistantFilter struct {
|
|||
Mentionable *bool `json:"mentionable,omitempty"` // Filter by mentionable status
|
||||
Automated *bool `json:"automated,omitempty"` // Filter by automation status
|
||||
BuiltIn *bool `json:"built_in,omitempty"` // Filter by built-in status
|
||||
Sandbox *bool `json:"sandbox,omitempty"` // Filter by sandbox configuration (true=has sandbox, false=no sandbox)
|
||||
Page int `json:"page,omitempty"` // Page number, starting from 1
|
||||
PageSize int `json:"pagesize,omitempty"` // Items per page
|
||||
Select []string `json:"select,omitempty"` // Fields to return, returns all fields if empty
|
||||
|
|
@ -429,6 +430,7 @@ type AssistantModel struct {
|
|||
BuiltIn bool `json:"built_in,omitempty"` // Whether this is a built-in assistant
|
||||
Sort int `json:"sort,omitempty"` // Assistant Sort
|
||||
Description string `json:"description,omitempty"` // Assistant Description
|
||||
Capabilities string `json:"capabilities,omitempty"` // Assistant capabilities description (useful for Robot orchestration)
|
||||
Tags []string `json:"tags,omitempty"` // Assistant Tags
|
||||
Modes []string `json:"modes,omitempty"` // Supported modes (e.g., ["task", "chat"]), null means all modes are supported
|
||||
DefaultMode string `json:"default_mode,omitempty"` // Default mode, can be empty
|
||||
|
|
|
|||
|
|
@ -99,6 +99,11 @@ func (store *Xun) SaveAssistant(assistant *types.AssistantModel) (string, error)
|
|||
} else {
|
||||
data["description"] = nil
|
||||
}
|
||||
if assistant.Capabilities != "" {
|
||||
data["capabilities"] = assistant.Capabilities
|
||||
} else {
|
||||
data["capabilities"] = nil
|
||||
}
|
||||
if assistant.Path != "" {
|
||||
data["path"] = assistant.Path
|
||||
} else {
|
||||
|
|
@ -181,6 +186,7 @@ func (store *Xun) SaveAssistant(assistant *types.AssistantModel) (string, error)
|
|||
"db": assistant.DB,
|
||||
"mcp": assistant.MCP,
|
||||
"workflow": assistant.Workflow,
|
||||
"sandbox": assistant.Sandbox,
|
||||
"placeholder": assistant.Placeholder,
|
||||
"locales": assistant.Locales,
|
||||
"uses": assistant.Uses,
|
||||
|
|
@ -243,14 +249,14 @@ func (store *Xun) UpdateAssistant(assistantID string, updates map[string]interfa
|
|||
data := make(map[string]interface{})
|
||||
|
||||
// List of fields that need JSON marshaling
|
||||
jsonFields := []string{"options", "tags", "modes", "prompts", "prompt_presets", "connector_options", "kb", "db", "mcp", "workflow", "placeholder", "locales", "uses", "search"}
|
||||
jsonFields := []string{"options", "tags", "modes", "prompts", "prompt_presets", "connector_options", "kb", "db", "mcp", "workflow", "sandbox", "placeholder", "locales", "uses", "search"}
|
||||
jsonFieldSet := make(map[string]bool)
|
||||
for _, field := range jsonFields {
|
||||
jsonFieldSet[field] = true
|
||||
}
|
||||
|
||||
// List of nullable string fields
|
||||
nullableStringFields := []string{"name", "avatar", "description", "path", "source", "default_mode", "__yao_created_by", "__yao_updated_by", "__yao_team_id", "__yao_tenant_id"}
|
||||
nullableStringFields := []string{"name", "avatar", "description", "capabilities", "path", "source", "default_mode", "__yao_created_by", "__yao_updated_by", "__yao_team_id", "__yao_tenant_id"}
|
||||
nullableFieldSet := make(map[string]bool)
|
||||
for _, field := range nullableStringFields {
|
||||
nullableFieldSet[field] = true
|
||||
|
|
@ -349,6 +355,7 @@ func (store *Xun) GetAssistants(filter types.AssistantFilter, locale ...string)
|
|||
qb.Where(func(qb query.Query) {
|
||||
qb.Where("name", "like", fmt.Sprintf("%%%s%%", filter.Keywords)).
|
||||
OrWhere("description", "like", fmt.Sprintf("%%%s%%", filter.Keywords)).
|
||||
OrWhere("capabilities", "like", fmt.Sprintf("%%%s%%", filter.Keywords)).
|
||||
OrWhere("locales", "like", fmt.Sprintf("%%%s%%", filter.Keywords))
|
||||
})
|
||||
}
|
||||
|
|
@ -393,6 +400,21 @@ func (store *Xun) GetAssistants(filter types.AssistantFilter, locale ...string)
|
|||
qb.Where("built_in", *filter.BuiltIn)
|
||||
}
|
||||
|
||||
// Apply sandbox filter (true = has sandbox config, false = no sandbox config)
|
||||
// MySQL JSON columns distinguish between SQL NULL and JSON literal null.
|
||||
// CAST(sandbox AS CHAR) returns 'null' for JSON null and NULL for SQL NULL.
|
||||
if filter.Sandbox != nil {
|
||||
if *filter.Sandbox {
|
||||
qb.WhereNotNull("sandbox").
|
||||
WhereRaw("CAST(`sandbox` AS CHAR) <> 'null'")
|
||||
} else {
|
||||
qb.Where(func(qb query.Query) {
|
||||
qb.WhereNull("sandbox").
|
||||
OrWhereRaw("CAST(`sandbox` AS CHAR) = 'null'")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Apply custom query filter function (for permission filtering)
|
||||
if filter.QueryFilter != nil {
|
||||
qb.Where(filter.QueryFilter)
|
||||
|
|
@ -448,7 +470,7 @@ func (store *Xun) GetAssistants(filter types.AssistantFilter, locale ...string)
|
|||
|
||||
// Convert rows to types.AssistantModel slice
|
||||
assistants := make([]*types.AssistantModel, 0, len(rows))
|
||||
jsonFields := []string{"tags", "options", "prompts", "prompt_presets", "connector_options", "workflow", "kb", "mcp", "placeholder", "locales", "uses", "search"}
|
||||
jsonFields := []string{"tags", "options", "prompts", "prompt_presets", "connector_options", "workflow", "sandbox", "kb", "mcp", "placeholder", "locales", "uses", "search"}
|
||||
|
||||
for _, row := range rows {
|
||||
data := row.ToMap()
|
||||
|
|
@ -521,7 +543,7 @@ func (store *Xun) GetAssistant(assistantID string, fields []string, locale ...st
|
|||
}
|
||||
|
||||
// Parse JSON fields
|
||||
jsonFields := []string{"tags", "modes", "options", "prompts", "prompt_presets", "connector_options", "workflow", "kb", "db", "mcp", "placeholder", "locales", "uses", "search"}
|
||||
jsonFields := []string{"tags", "modes", "options", "prompts", "prompt_presets", "connector_options", "workflow", "sandbox", "kb", "db", "mcp", "placeholder", "locales", "uses", "search"}
|
||||
store.parseJSONFields(data, jsonFields)
|
||||
|
||||
// Convert map to types.AssistantModel
|
||||
|
|
@ -536,6 +558,7 @@ func (store *Xun) GetAssistant(assistantID string, fields []string, locale ...st
|
|||
BuiltIn: getBool(data, "built_in"),
|
||||
Sort: getInt(data, "sort"),
|
||||
Description: getString(data, "description"),
|
||||
Capabilities: getString(data, "capabilities"),
|
||||
DefaultMode: getString(data, "default_mode"),
|
||||
Readonly: getBool(data, "readonly"),
|
||||
Public: getBool(data, "public"),
|
||||
|
|
@ -636,6 +659,13 @@ func (store *Xun) GetAssistant(assistantID string, fields []string, locale ...st
|
|||
}
|
||||
}
|
||||
|
||||
if sandbox, has := data["sandbox"]; has && sandbox != nil {
|
||||
sb, err := types.ToSandbox(sandbox)
|
||||
if err == nil {
|
||||
model.Sandbox = sb
|
||||
}
|
||||
}
|
||||
|
||||
if placeholder, has := data["placeholder"]; has && placeholder != nil {
|
||||
raw, err := jsoniter.Marshal(placeholder)
|
||||
if err == nil {
|
||||
|
|
@ -839,6 +869,13 @@ func (store *Xun) translate(model *types.AssistantModel, assistantID string, loc
|
|||
}
|
||||
}
|
||||
|
||||
// Translate capabilities
|
||||
if translated := i18n.Translate(assistantID, locale, model.Capabilities); translated != nil {
|
||||
if s, ok := translated.(string); ok {
|
||||
model.Capabilities = s
|
||||
}
|
||||
}
|
||||
|
||||
// Translate prompts
|
||||
if model.Prompts != nil {
|
||||
for i := range model.Prompts {
|
||||
|
|
|
|||
|
|
@ -788,6 +788,467 @@ func TestSaveAssistant(t *testing.T) {
|
|||
t.Logf("Successfully saved and retrieved source code for assistant %s", id)
|
||||
})
|
||||
|
||||
t.Run("SandboxConfiguration", func(t *testing.T) {
|
||||
assistant := &types.AssistantModel{
|
||||
Name: "Sandbox Test Assistant",
|
||||
Type: "assistant",
|
||||
Connector: "openai",
|
||||
Share: "private",
|
||||
Sandbox: &types.Sandbox{
|
||||
Command: "claude",
|
||||
Timeout: "5m",
|
||||
Arguments: map[string]interface{}{
|
||||
"max_turns": 10,
|
||||
"permission_mode": "bypassPermissions",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
id, err := store.SaveAssistant(assistant)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to save assistant with sandbox: %v", err)
|
||||
}
|
||||
|
||||
retrieved, err := store.GetAssistant(id, types.AssistantFullFields)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve assistant: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Sandbox == nil {
|
||||
t.Fatal("Expected sandbox to be set")
|
||||
}
|
||||
|
||||
if retrieved.Sandbox.Command != "claude" {
|
||||
t.Errorf("Expected command 'claude', got '%s'", retrieved.Sandbox.Command)
|
||||
}
|
||||
|
||||
if retrieved.Sandbox.Timeout != "5m" {
|
||||
t.Errorf("Expected timeout '5m', got '%s'", retrieved.Sandbox.Timeout)
|
||||
}
|
||||
|
||||
if retrieved.Sandbox.Arguments == nil {
|
||||
t.Fatal("Expected sandbox arguments to be set")
|
||||
}
|
||||
|
||||
if maxTurns, ok := retrieved.Sandbox.Arguments["max_turns"].(float64); !ok || maxTurns != 10 {
|
||||
t.Errorf("Expected max_turns 10, got %v", retrieved.Sandbox.Arguments["max_turns"])
|
||||
}
|
||||
|
||||
t.Logf("Successfully saved and retrieved sandbox configuration for assistant %s", id)
|
||||
})
|
||||
|
||||
t.Run("SandboxWithImage", func(t *testing.T) {
|
||||
assistant := &types.AssistantModel{
|
||||
Name: "Sandbox Image Test",
|
||||
Type: "assistant",
|
||||
Connector: "openai",
|
||||
Share: "private",
|
||||
Sandbox: &types.Sandbox{
|
||||
Command: "claude",
|
||||
Image: "yaoapp/sandbox-claude-desktop:latest",
|
||||
Timeout: "20m",
|
||||
MaxMemory: "4g",
|
||||
MaxCPU: 2.0,
|
||||
Arguments: map[string]interface{}{
|
||||
"max_turns": 500,
|
||||
"permission_mode": "bypassPermissions",
|
||||
"disallowed_tools": "WebSearch",
|
||||
},
|
||||
Secrets: map[string]string{
|
||||
"GITHUB_TOKEN": "$ENV.GITHUB_TOKEN",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
id, err := store.SaveAssistant(assistant)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to save assistant with sandbox image: %v", err)
|
||||
}
|
||||
|
||||
retrieved, err := store.GetAssistant(id, types.AssistantFullFields)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve assistant: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Sandbox == nil {
|
||||
t.Fatal("Expected sandbox to be set")
|
||||
}
|
||||
|
||||
if retrieved.Sandbox.Image != "yaoapp/sandbox-claude-desktop:latest" {
|
||||
t.Errorf("Expected image 'yaoapp/sandbox-claude-desktop:latest', got '%s'", retrieved.Sandbox.Image)
|
||||
}
|
||||
|
||||
if retrieved.Sandbox.MaxMemory != "4g" {
|
||||
t.Errorf("Expected max_memory '4g', got '%s'", retrieved.Sandbox.MaxMemory)
|
||||
}
|
||||
|
||||
if retrieved.Sandbox.MaxCPU != 2.0 {
|
||||
t.Errorf("Expected max_cpu 2.0, got %f", retrieved.Sandbox.MaxCPU)
|
||||
}
|
||||
|
||||
if retrieved.Sandbox.Secrets == nil || retrieved.Sandbox.Secrets["GITHUB_TOKEN"] != "$ENV.GITHUB_TOKEN" {
|
||||
t.Errorf("Expected secrets to contain GITHUB_TOKEN, got %v", retrieved.Sandbox.Secrets)
|
||||
}
|
||||
|
||||
t.Logf("Successfully saved and retrieved sandbox with image for assistant %s", id)
|
||||
})
|
||||
|
||||
t.Run("NilSandbox", func(t *testing.T) {
|
||||
assistant := &types.AssistantModel{
|
||||
Name: "No Sandbox Assistant",
|
||||
Type: "assistant",
|
||||
Connector: "openai",
|
||||
Share: "private",
|
||||
}
|
||||
|
||||
id, err := store.SaveAssistant(assistant)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to save assistant without sandbox: %v", err)
|
||||
}
|
||||
|
||||
retrieved, err := store.GetAssistant(id, types.AssistantFullFields)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve assistant: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Sandbox != nil {
|
||||
t.Errorf("Expected sandbox to be nil, got %+v", retrieved.Sandbox)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CapabilitiesField", func(t *testing.T) {
|
||||
assistant := &types.AssistantModel{
|
||||
Name: "Capabilities Test",
|
||||
Type: "assistant",
|
||||
Connector: "openai",
|
||||
Share: "private",
|
||||
Description: "A test assistant",
|
||||
Capabilities: "Can search the web, analyze data, write code, and summarize documents.",
|
||||
}
|
||||
|
||||
id, err := store.SaveAssistant(assistant)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to save assistant with capabilities: %v", err)
|
||||
}
|
||||
|
||||
retrieved, err := store.GetAssistant(id, types.AssistantFullFields)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve assistant: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Capabilities != "Can search the web, analyze data, write code, and summarize documents." {
|
||||
t.Errorf("Expected capabilities to match, got '%s'", retrieved.Capabilities)
|
||||
}
|
||||
|
||||
if retrieved.Description != "A test assistant" {
|
||||
t.Errorf("Expected description 'A test assistant', got '%s'", retrieved.Description)
|
||||
}
|
||||
|
||||
t.Logf("Successfully saved and retrieved capabilities for assistant %s", id)
|
||||
})
|
||||
|
||||
t.Run("EmptyCapabilities", func(t *testing.T) {
|
||||
assistant := &types.AssistantModel{
|
||||
Name: "No Capabilities Assistant",
|
||||
Type: "assistant",
|
||||
Connector: "openai",
|
||||
Share: "private",
|
||||
}
|
||||
|
||||
id, err := store.SaveAssistant(assistant)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to save assistant without capabilities: %v", err)
|
||||
}
|
||||
|
||||
retrieved, err := store.GetAssistant(id, types.AssistantFullFields)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve assistant: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Capabilities != "" {
|
||||
t.Errorf("Expected empty capabilities, got '%s'", retrieved.Capabilities)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CapabilitiesWithI18n", func(t *testing.T) {
|
||||
assistant := &types.AssistantModel{
|
||||
Name: "{{name}}",
|
||||
Type: "assistant",
|
||||
Connector: "openai",
|
||||
Share: "private",
|
||||
Description: "{{description}}",
|
||||
Capabilities: "{{capabilities}}",
|
||||
}
|
||||
|
||||
id, err := store.SaveAssistant(assistant)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to save assistant with i18n capabilities: %v", err)
|
||||
}
|
||||
|
||||
// Setup i18n
|
||||
i18n.Locales[id] = map[string]i18n.I18n{
|
||||
"en": {
|
||||
Locale: "en",
|
||||
Messages: map[string]any{
|
||||
"name": "i18n Test",
|
||||
"description": "Description in English",
|
||||
"capabilities": "Can do X, Y, and Z",
|
||||
},
|
||||
},
|
||||
"zh-cn": {
|
||||
Locale: "zh-cn",
|
||||
Messages: map[string]any{
|
||||
"name": "国际化测试",
|
||||
"description": "中文描述",
|
||||
"capabilities": "可以做X、Y和Z",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
retrievedEN, err := store.GetAssistant(id, types.AssistantFullFields, "en")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get assistant with EN locale: %v", err)
|
||||
}
|
||||
|
||||
if retrievedEN.Capabilities != "Can do X, Y, and Z" {
|
||||
t.Errorf("Expected capabilities 'Can do X, Y, and Z', got '%s'", retrievedEN.Capabilities)
|
||||
}
|
||||
|
||||
retrievedZH, err := store.GetAssistant(id, types.AssistantFullFields, "zh-cn")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get assistant with ZH locale: %v", err)
|
||||
}
|
||||
|
||||
if retrievedZH.Capabilities != "可以做X、Y和Z" {
|
||||
t.Errorf("Expected capabilities '可以做X、Y和Z', got '%s'", retrievedZH.Capabilities)
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
delete(i18n.Locales, id)
|
||||
t.Logf("Successfully tested capabilities i18n for assistant %s", id)
|
||||
})
|
||||
|
||||
t.Run("CapabilitiesInKeywordSearch", func(t *testing.T) {
|
||||
uniqueCapability := fmt.Sprintf("unique-cap-%d", time.Now().UnixNano())
|
||||
assistant := &types.AssistantModel{
|
||||
Name: "Capabilities Search Test",
|
||||
Type: "assistant",
|
||||
Connector: "openai",
|
||||
Share: "private",
|
||||
Capabilities: uniqueCapability,
|
||||
}
|
||||
|
||||
_, err := store.SaveAssistant(assistant)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to save assistant: %v", err)
|
||||
}
|
||||
|
||||
response, err := store.GetAssistants(types.AssistantFilter{
|
||||
Keywords: uniqueCapability,
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to search by capabilities keyword: %v", err)
|
||||
}
|
||||
|
||||
if len(response.Data) < 1 {
|
||||
t.Error("Expected to find assistant by capabilities keyword search")
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, a := range response.Data {
|
||||
if a.Capabilities == uniqueCapability {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Error("Expected to find assistant with matching capabilities")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("UpdateSandbox", func(t *testing.T) {
|
||||
assistant := &types.AssistantModel{
|
||||
Name: "Update Sandbox Test",
|
||||
Type: "assistant",
|
||||
Connector: "openai",
|
||||
Share: "private",
|
||||
}
|
||||
|
||||
id, err := store.SaveAssistant(assistant)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create assistant: %v", err)
|
||||
}
|
||||
|
||||
// Update with sandbox
|
||||
updates := map[string]interface{}{
|
||||
"sandbox": &types.Sandbox{
|
||||
Command: "claude",
|
||||
Timeout: "10m",
|
||||
},
|
||||
}
|
||||
|
||||
err = store.UpdateAssistant(id, updates)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to update sandbox: %v", err)
|
||||
}
|
||||
|
||||
retrieved, err := store.GetAssistant(id, types.AssistantFullFields)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve assistant: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Sandbox == nil {
|
||||
t.Fatal("Expected sandbox to be set")
|
||||
}
|
||||
|
||||
if retrieved.Sandbox.Command != "claude" {
|
||||
t.Errorf("Expected command 'claude', got '%s'", retrieved.Sandbox.Command)
|
||||
}
|
||||
|
||||
// Update to remove sandbox
|
||||
updates2 := map[string]interface{}{
|
||||
"sandbox": nil,
|
||||
}
|
||||
|
||||
err = store.UpdateAssistant(id, updates2)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to remove sandbox: %v", err)
|
||||
}
|
||||
|
||||
retrieved2, err := store.GetAssistant(id, types.AssistantFullFields)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve assistant: %v", err)
|
||||
}
|
||||
|
||||
if retrieved2.Sandbox != nil {
|
||||
t.Errorf("Expected sandbox to be nil, got %+v", retrieved2.Sandbox)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("UpdateCapabilities", func(t *testing.T) {
|
||||
assistant := &types.AssistantModel{
|
||||
Name: "Update Capabilities Test",
|
||||
Type: "assistant",
|
||||
Connector: "openai",
|
||||
Share: "private",
|
||||
Capabilities: "Original capabilities",
|
||||
}
|
||||
|
||||
id, err := store.SaveAssistant(assistant)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create assistant: %v", err)
|
||||
}
|
||||
|
||||
// Update capabilities
|
||||
updates := map[string]interface{}{
|
||||
"capabilities": "Updated capabilities: can search, analyze, and write code",
|
||||
}
|
||||
|
||||
err = store.UpdateAssistant(id, updates)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to update capabilities: %v", err)
|
||||
}
|
||||
|
||||
retrieved, err := store.GetAssistant(id, types.AssistantFullFields)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve assistant: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Capabilities != "Updated capabilities: can search, analyze, and write code" {
|
||||
t.Errorf("Expected updated capabilities, got '%s'", retrieved.Capabilities)
|
||||
}
|
||||
|
||||
// Update to clear capabilities
|
||||
updates2 := map[string]interface{}{
|
||||
"capabilities": "",
|
||||
}
|
||||
|
||||
err = store.UpdateAssistant(id, updates2)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to clear capabilities: %v", err)
|
||||
}
|
||||
|
||||
retrieved2, err := store.GetAssistant(id, types.AssistantFullFields)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve assistant: %v", err)
|
||||
}
|
||||
|
||||
if retrieved2.Capabilities != "" {
|
||||
t.Errorf("Expected empty capabilities, got '%s'", retrieved2.Capabilities)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FilterBySandbox", func(t *testing.T) {
|
||||
// Create one assistant with sandbox
|
||||
withSandbox := &types.AssistantModel{
|
||||
Name: "Filter Sandbox Yes",
|
||||
Type: "assistant",
|
||||
Connector: "openai",
|
||||
Share: "private",
|
||||
Sandbox: &types.Sandbox{
|
||||
Command: "claude",
|
||||
Timeout: "5m",
|
||||
},
|
||||
}
|
||||
idWith, err := store.SaveAssistant(withSandbox)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to save assistant with sandbox: %v", err)
|
||||
}
|
||||
|
||||
// Create one assistant without sandbox
|
||||
withoutSandbox := &types.AssistantModel{
|
||||
Name: "Filter Sandbox No",
|
||||
Type: "assistant",
|
||||
Connector: "openai",
|
||||
Share: "private",
|
||||
}
|
||||
idWithout, err := store.SaveAssistant(withoutSandbox)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to save assistant without sandbox: %v", err)
|
||||
}
|
||||
|
||||
testIDs := []string{idWith, idWithout}
|
||||
|
||||
// Filter: sandbox=true, scoped to test IDs
|
||||
trueVal := true
|
||||
result, err := store.GetAssistants(types.AssistantFilter{
|
||||
Page: 1,
|
||||
PageSize: 100,
|
||||
Sandbox: &trueVal,
|
||||
AssistantIDs: testIDs,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to filter with sandbox=true: %v", err)
|
||||
}
|
||||
if len(result.Data) != 1 {
|
||||
t.Errorf("Expected 1 result for sandbox=true, got %d", len(result.Data))
|
||||
} else if result.Data[0].ID != idWith {
|
||||
t.Errorf("Expected assistant %s, got %s", idWith, result.Data[0].ID)
|
||||
}
|
||||
|
||||
// Filter: sandbox=false, scoped to test IDs
|
||||
falseVal := false
|
||||
result2, err := store.GetAssistants(types.AssistantFilter{
|
||||
Page: 1,
|
||||
PageSize: 100,
|
||||
Sandbox: &falseVal,
|
||||
AssistantIDs: testIDs,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to filter with sandbox=false: %v", err)
|
||||
}
|
||||
if len(result2.Data) != 1 {
|
||||
t.Errorf("Expected 1 result for sandbox=false, got %d", len(result2.Data))
|
||||
} else if result2.Data[0].ID != idWithout {
|
||||
t.Errorf("Expected assistant %s, got %s", idWithout, result2.Data[0].ID)
|
||||
}
|
||||
|
||||
t.Logf("Sandbox filter test passed: sandbox=true returned %d, sandbox=false returned %d", len(result.Data), len(result2.Data))
|
||||
})
|
||||
|
||||
t.Run("AllNewFieldsTogether", func(t *testing.T) {
|
||||
// Test assistant with all new fields together
|
||||
optionalFalse := false
|
||||
|
|
|
|||
1717
data/bindata.go
1717
data/bindata.go
File diff suppressed because one or more lines are too long
|
|
@ -117,7 +117,7 @@ func ListAssistants(c *gin.Context) {
|
|||
}
|
||||
|
||||
// Parse boolean filters
|
||||
var builtIn, mentionable, automated *bool
|
||||
var builtIn, mentionable, automated, sandbox *bool
|
||||
if builtInParam := c.Query("built_in"); builtInParam != "" {
|
||||
builtIn = parseBoolValue(builtInParam)
|
||||
}
|
||||
|
|
@ -127,6 +127,9 @@ func ListAssistants(c *gin.Context) {
|
|||
if automatedParam := c.Query("automated"); automatedParam != "" {
|
||||
automated = parseBoolValue(automatedParam)
|
||||
}
|
||||
if sandboxParam := c.Query("sandbox"); sandboxParam != "" {
|
||||
sandbox = parseBoolValue(sandboxParam)
|
||||
}
|
||||
|
||||
// Note: public and share filters are not yet supported in AssistantFilter
|
||||
// They would need to be added to the store layer for proper filtering
|
||||
|
|
@ -152,6 +155,7 @@ func ListAssistants(c *gin.Context) {
|
|||
BuiltIn: builtIn,
|
||||
Mentionable: mentionable,
|
||||
Automated: automated,
|
||||
Sandbox: sandbox,
|
||||
})
|
||||
|
||||
// Apply permission-based filtering (Scope filtering)
|
||||
|
|
@ -169,12 +173,19 @@ func ListAssistants(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// Filter sensitive fields for built-in assistants
|
||||
// For built-in assistants, clear code-level fields (prompts, workflow, tools, kb, mcp, options)
|
||||
FilterBuiltInFields(result.Data)
|
||||
// Convert sandbox to boolean and filter built-in sensitive fields
|
||||
resp := map[string]interface{}{
|
||||
"data": AssistantsToResponse(result.Data),
|
||||
"total": result.Total,
|
||||
"page": result.Page,
|
||||
"pagesize": result.PageSize,
|
||||
"pagecount": result.PageCount,
|
||||
"next": result.Next,
|
||||
"prev": result.Prev,
|
||||
}
|
||||
|
||||
// Return the result with standard response format
|
||||
response.RespondWithSuccess(c, response.StatusOK, result)
|
||||
response.RespondWithSuccess(c, response.StatusOK, resp)
|
||||
}
|
||||
|
||||
// GetAssistant retrieves a single assistant by ID with permission verification
|
||||
|
|
@ -260,11 +271,13 @@ func GetAssistant(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// Filter sensitive fields for built-in assistants
|
||||
// Convert sandbox to boolean and filter built-in sensitive fields
|
||||
hasSandbox := assistant.Sandbox != nil
|
||||
FilterBuiltInAssistant(assistant)
|
||||
resp := AssistantToResponse(assistant, hasSandbox)
|
||||
|
||||
// Return the result with standard response format
|
||||
response.RespondWithSuccess(c, response.StatusOK, assistant)
|
||||
response.RespondWithSuccess(c, response.StatusOK, resp)
|
||||
}
|
||||
|
||||
// ListAssistantTags lists assistant tags with permission-based filtering
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package agent
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yaoapp/gou/model"
|
||||
"github.com/yaoapp/xun/dbal/query"
|
||||
|
|
@ -150,9 +152,49 @@ func FilterBuiltInAssistant(assistant *agenttypes.AssistantModel) {
|
|||
assistant.Prompts = nil
|
||||
assistant.PromptPresets = nil
|
||||
assistant.Workflow = nil
|
||||
assistant.Sandbox = nil
|
||||
assistant.KB = nil
|
||||
assistant.MCP = nil
|
||||
assistant.Options = nil
|
||||
assistant.Source = ""
|
||||
}
|
||||
}
|
||||
|
||||
// AssistantToResponse converts an AssistantModel to a response map,
|
||||
// replacing the sandbox JSON object with a boolean indicating whether sandbox is configured.
|
||||
// hasSandbox must be captured before FilterBuiltInAssistant clears the Sandbox field.
|
||||
func AssistantToResponse(assistant *agenttypes.AssistantModel, hasSandbox bool) map[string]interface{} {
|
||||
if assistant == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
raw, err := json.Marshal(assistant)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal(raw, &result); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result["sandbox"] = hasSandbox
|
||||
return result
|
||||
}
|
||||
|
||||
// AssistantsToResponse converts a slice of AssistantModel to response maps,
|
||||
// replacing sandbox with a boolean for each assistant.
|
||||
// Captures sandbox state before filtering, then applies FilterBuiltInAssistant.
|
||||
func AssistantsToResponse(assistants []*agenttypes.AssistantModel) []map[string]interface{} {
|
||||
if assistants == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]map[string]interface{}, 0, len(assistants))
|
||||
for _, a := range assistants {
|
||||
hasSandbox := a.Sandbox != nil
|
||||
FilterBuiltInAssistant(a)
|
||||
result = append(result, AssistantToResponse(a, hasSandbox))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ var (
|
|||
// availableAssistantFields defines all available fields for security filtering
|
||||
availableAssistantFields = map[string]bool{
|
||||
"id": true, "assistant_id": true, "type": true, "name": true, "avatar": true,
|
||||
"connector": true, "description": true, "path": true, "sort": true,
|
||||
"connector": true, "description": true, "capabilities": true, "path": true, "sort": true,
|
||||
"built_in": true, "placeholder": true, "options": true, "prompts": true,
|
||||
"workflow": true, "kb": true, "mcp": true, "tools": true, "tags": true,
|
||||
"workflow": true, "sandbox": true, "kb": true, "mcp": true, "tools": true, "tags": true,
|
||||
"readonly": true, "public": true, "share": true, "locales": true,
|
||||
"automated": true, "mentionable": true,
|
||||
"created_at": true, "updated_at": true, "deleted_at": true,
|
||||
|
|
@ -23,9 +23,9 @@ var (
|
|||
|
||||
// defaultAssistantFields defines the default compact field list
|
||||
defaultAssistantFields = []string{
|
||||
"assistant_id", "type", "name", "avatar", "connector", "description",
|
||||
"assistant_id", "type", "name", "avatar", "connector", "description", "capabilities",
|
||||
"sort", "built_in", "tags", "readonly", "public", "share",
|
||||
"automated", "mentionable", "created_at", "updated_at",
|
||||
"automated", "mentionable", "sandbox", "created_at", "updated_at",
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -59,6 +59,7 @@ type AssistantFilterParams struct {
|
|||
BuiltIn *bool
|
||||
Mentionable *bool
|
||||
Automated *bool
|
||||
Sandbox *bool
|
||||
Public *bool
|
||||
Share string
|
||||
}
|
||||
|
|
@ -79,6 +80,7 @@ func BuildAssistantFilter(params AssistantFilterParams) agenttypes.AssistantFilt
|
|||
BuiltIn: params.BuiltIn,
|
||||
Mentionable: params.Mentionable,
|
||||
Automated: params.Automated,
|
||||
Sandbox: params.Sandbox,
|
||||
}
|
||||
|
||||
// Set default type if not specified (only when Types is also empty)
|
||||
|
|
|
|||
|
|
@ -264,6 +264,67 @@ func TestListAssistants(t *testing.T) {
|
|||
}
|
||||
})
|
||||
|
||||
t.Run("ListAssistantsWithSandboxFilter", func(t *testing.T) {
|
||||
// Test with sandbox=true filter
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?sandbox=true&types=assistant", nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var response map[string]interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||
assert.NoError(t, err)
|
||||
|
||||
data, hasData := response["data"].([]interface{})
|
||||
if hasData {
|
||||
for _, item := range data {
|
||||
a, ok := item.(map[string]interface{})
|
||||
if ok {
|
||||
sandboxVal, exists := a["sandbox"]
|
||||
assert.True(t, exists, "sandbox field should be present in list response")
|
||||
assert.Equal(t, true, sandboxVal, "sandbox should be true when filtering sandbox=true")
|
||||
}
|
||||
}
|
||||
t.Logf("Successfully retrieved %d assistants with sandbox filter", len(data))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ListAssistantsSandboxReturnsBool", func(t *testing.T) {
|
||||
// Verify sandbox field is returned as boolean (not JSON object) in list response
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?pagesize=5&types=assistant", nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var response map[string]interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||
assert.NoError(t, err)
|
||||
|
||||
data, hasData := response["data"].([]interface{})
|
||||
if hasData && len(data) > 0 {
|
||||
a, ok := data[0].(map[string]interface{})
|
||||
if ok {
|
||||
sandboxVal, exists := a["sandbox"]
|
||||
assert.True(t, exists, "sandbox field should be present in default list fields")
|
||||
_, isBool := sandboxVal.(bool)
|
||||
assert.True(t, isBool, "sandbox should be a boolean value, got %T", sandboxVal)
|
||||
t.Logf("sandbox field correctly returned as bool: %v", sandboxVal)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ListAssistantsWithSelectFields", func(t *testing.T) {
|
||||
// Test with select parameter to limit returned fields
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?select=assistant_id,name,avatar,type", nil)
|
||||
|
|
@ -1413,6 +1474,10 @@ func TestGetAssistantResponseStructure(t *testing.T) {
|
|||
assert.Contains(t, responseAssistant, "name", "Assistant should have name")
|
||||
assert.Contains(t, responseAssistant, "type", "Assistant should have type")
|
||||
|
||||
// Verify capabilities field is present in response (may be empty/null)
|
||||
// capabilities is a default field that should always be returned
|
||||
t.Logf("capabilities field value: %v", responseAssistant["capabilities"])
|
||||
|
||||
responseAssistantID := responseAssistant["assistant_id"].(string)
|
||||
t.Logf("Response structure is correct for assistant: %s", responseAssistantID)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -73,6 +73,15 @@
|
|||
"nullable": true,
|
||||
"index": true
|
||||
},
|
||||
{
|
||||
"name": "capabilities",
|
||||
"type": "string",
|
||||
"label": "Capabilities",
|
||||
"comment": "Assistant capabilities description, useful for Robot orchestration",
|
||||
"length": 600,
|
||||
"nullable": true,
|
||||
"index": true
|
||||
},
|
||||
{
|
||||
"name": "path",
|
||||
"type": "string",
|
||||
|
|
@ -161,6 +170,13 @@
|
|||
"comment": "MCP servers available for the assistant to use",
|
||||
"nullable": true
|
||||
},
|
||||
{
|
||||
"name": "sandbox",
|
||||
"type": "json",
|
||||
"label": "Sandbox",
|
||||
"comment": "Sandbox configuration for coding agents (command, image, timeout, etc.)",
|
||||
"nullable": true
|
||||
},
|
||||
{
|
||||
"name": "source",
|
||||
"type": "text",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue