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
|
assistant.Description = v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// capabilities
|
||||||
|
if v, ok := data["capabilities"].(string); ok {
|
||||||
|
assistant.Capabilities = v
|
||||||
|
}
|
||||||
|
|
||||||
// locales
|
// locales
|
||||||
if locales, ok := data["locales"].(i18n.Map); ok {
|
if locales, ok := data["locales"].(i18n.Map); ok {
|
||||||
assistant.Locales = locales
|
assistant.Locales = locales
|
||||||
|
|
@ -568,30 +573,39 @@ func loadMap(data map[string]interface{}) (*Assistant, error) {
|
||||||
if i18nObj.Messages == nil {
|
if i18nObj.Messages == nil {
|
||||||
i18nObj.Messages = make(map[string]any)
|
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 != "" {
|
if _, exists := i18nObj.Messages["name"]; !exists && assistant.Name != "" {
|
||||||
i18nObj.Messages["name"] = assistant.Name
|
i18nObj.Messages["name"] = assistant.Name
|
||||||
}
|
}
|
||||||
if _, exists := i18nObj.Messages["description"]; !exists && assistant.Description != "" {
|
if _, exists := i18nObj.Messages["description"]; !exists && assistant.Description != "" {
|
||||||
i18nObj.Messages["description"] = assistant.Description
|
i18nObj.Messages["description"] = assistant.Description
|
||||||
}
|
}
|
||||||
|
if _, exists := i18nObj.Messages["capabilities"]; !exists && assistant.Capabilities != "" {
|
||||||
|
i18nObj.Messages["capabilities"] = assistant.Capabilities
|
||||||
|
}
|
||||||
flattened[locale] = i18nObj
|
flattened[locale] = i18nObj
|
||||||
}
|
}
|
||||||
|
|
||||||
i18n.Locales[id] = flattened
|
i18n.Locales[id] = flattened
|
||||||
} else {
|
} else {
|
||||||
// No locales defined, create default with name and description for all common locales
|
// No locales defined, create default with name, description, and capabilities for all common locales
|
||||||
if assistant.Name != "" || assistant.Description != "" {
|
if assistant.Name != "" || assistant.Description != "" || assistant.Capabilities != "" {
|
||||||
defaultLocales := make(map[string]i18n.I18n)
|
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"}
|
commonLocales := []string{"en", "en-us", "zh", "zh-cn", "zh-tw"}
|
||||||
for _, locale := range commonLocales {
|
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{
|
defaultLocales[locale] = i18n.I18n{
|
||||||
Locale: locale,
|
Locale: locale,
|
||||||
Messages: map[string]any{
|
Messages: messages,
|
||||||
"name": assistant.Name,
|
|
||||||
"description": assistant.Description,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
i18n.Locales[id] = defaultLocales
|
i18n.Locales[id] = defaultLocales
|
||||||
|
|
|
||||||
|
|
@ -291,6 +291,9 @@ func ToAssistantModel(v interface{}) (*AssistantModel, error) {
|
||||||
if description, ok := data["description"].(string); ok {
|
if description, ok := data["description"].(string); ok {
|
||||||
model.Description = description
|
model.Description = description
|
||||||
}
|
}
|
||||||
|
if capabilities, ok := data["capabilities"].(string); ok {
|
||||||
|
model.Capabilities = capabilities
|
||||||
|
}
|
||||||
if share, ok := data["share"].(string); ok {
|
if share, ok := data["share"].(string); ok {
|
||||||
model.Share = share
|
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
|
// Placeholder
|
||||||
if placeholder, ok := data["placeholder"]; ok && placeholder != nil {
|
if placeholder, ok := data["placeholder"]; ok && placeholder != nil {
|
||||||
raw, err := jsoniter.Marshal(placeholder)
|
raw, err := jsoniter.Marshal(placeholder)
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,12 @@ var AssistantAllowedFields = map[string]bool{
|
||||||
"prompts": true,
|
"prompts": true,
|
||||||
"prompt_presets": true,
|
"prompt_presets": true,
|
||||||
"disable_global_prompts": true,
|
"disable_global_prompts": true,
|
||||||
|
"capabilities": true,
|
||||||
"workflow": true,
|
"workflow": true,
|
||||||
"kb": true,
|
"kb": true,
|
||||||
"db": true,
|
"db": true,
|
||||||
"mcp": true,
|
"mcp": true,
|
||||||
|
"sandbox": true,
|
||||||
"source": true,
|
"source": true,
|
||||||
"tags": true,
|
"tags": true,
|
||||||
"modes": true,
|
"modes": true,
|
||||||
|
|
@ -53,6 +55,7 @@ var AssistantDefaultFields = []string{
|
||||||
"avatar",
|
"avatar",
|
||||||
"connector",
|
"connector",
|
||||||
"description",
|
"description",
|
||||||
|
"capabilities", // Capabilities description for Robot orchestration (lightweight)
|
||||||
"tags", // Tags for categorization (lightweight)
|
"tags", // Tags for categorization (lightweight)
|
||||||
"modes", // Supported modes (lightweight)
|
"modes", // Supported modes (lightweight)
|
||||||
"default_mode", // Default mode (lightweight)
|
"default_mode", // Default mode (lightweight)
|
||||||
|
|
@ -63,9 +66,10 @@ var AssistantDefaultFields = []string{
|
||||||
"share",
|
"share",
|
||||||
"automated",
|
"automated",
|
||||||
"mentionable",
|
"mentionable",
|
||||||
"kb", // Knowledge base configuration (lightweight)
|
"sandbox", // Sandbox configuration presence (lightweight)
|
||||||
"db", // Database configuration (lightweight)
|
"kb", // Knowledge base configuration (lightweight)
|
||||||
"mcp", // MCP servers configuration (lightweight)
|
"db", // Database configuration (lightweight)
|
||||||
|
"mcp", // MCP servers configuration (lightweight)
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
"__yao_created_by", // Permission: creator user ID
|
"__yao_created_by", // Permission: creator user ID
|
||||||
|
|
@ -84,6 +88,7 @@ var AssistantFullFields = []string{
|
||||||
"connector",
|
"connector",
|
||||||
"connector_options",
|
"connector_options",
|
||||||
"description",
|
"description",
|
||||||
|
"capabilities",
|
||||||
"path",
|
"path",
|
||||||
"sort",
|
"sort",
|
||||||
"built_in",
|
"built_in",
|
||||||
|
|
@ -96,6 +101,7 @@ var AssistantFullFields = []string{
|
||||||
"kb",
|
"kb",
|
||||||
"db",
|
"db",
|
||||||
"mcp",
|
"mcp",
|
||||||
|
"sandbox",
|
||||||
"source",
|
"source",
|
||||||
"tags",
|
"tags",
|
||||||
"modes",
|
"modes",
|
||||||
|
|
|
||||||
|
|
@ -183,6 +183,7 @@ type AssistantFilter struct {
|
||||||
Mentionable *bool `json:"mentionable,omitempty"` // Filter by mentionable status
|
Mentionable *bool `json:"mentionable,omitempty"` // Filter by mentionable status
|
||||||
Automated *bool `json:"automated,omitempty"` // Filter by automation status
|
Automated *bool `json:"automated,omitempty"` // Filter by automation status
|
||||||
BuiltIn *bool `json:"built_in,omitempty"` // Filter by built-in 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
|
Page int `json:"page,omitempty"` // Page number, starting from 1
|
||||||
PageSize int `json:"pagesize,omitempty"` // Items per page
|
PageSize int `json:"pagesize,omitempty"` // Items per page
|
||||||
Select []string `json:"select,omitempty"` // Fields to return, returns all fields if empty
|
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
|
BuiltIn bool `json:"built_in,omitempty"` // Whether this is a built-in assistant
|
||||||
Sort int `json:"sort,omitempty"` // Assistant Sort
|
Sort int `json:"sort,omitempty"` // Assistant Sort
|
||||||
Description string `json:"description,omitempty"` // Assistant Description
|
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
|
Tags []string `json:"tags,omitempty"` // Assistant Tags
|
||||||
Modes []string `json:"modes,omitempty"` // Supported modes (e.g., ["task", "chat"]), null means all modes are supported
|
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
|
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 {
|
} else {
|
||||||
data["description"] = nil
|
data["description"] = nil
|
||||||
}
|
}
|
||||||
|
if assistant.Capabilities != "" {
|
||||||
|
data["capabilities"] = assistant.Capabilities
|
||||||
|
} else {
|
||||||
|
data["capabilities"] = nil
|
||||||
|
}
|
||||||
if assistant.Path != "" {
|
if assistant.Path != "" {
|
||||||
data["path"] = assistant.Path
|
data["path"] = assistant.Path
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -181,6 +186,7 @@ func (store *Xun) SaveAssistant(assistant *types.AssistantModel) (string, error)
|
||||||
"db": assistant.DB,
|
"db": assistant.DB,
|
||||||
"mcp": assistant.MCP,
|
"mcp": assistant.MCP,
|
||||||
"workflow": assistant.Workflow,
|
"workflow": assistant.Workflow,
|
||||||
|
"sandbox": assistant.Sandbox,
|
||||||
"placeholder": assistant.Placeholder,
|
"placeholder": assistant.Placeholder,
|
||||||
"locales": assistant.Locales,
|
"locales": assistant.Locales,
|
||||||
"uses": assistant.Uses,
|
"uses": assistant.Uses,
|
||||||
|
|
@ -243,14 +249,14 @@ func (store *Xun) UpdateAssistant(assistantID string, updates map[string]interfa
|
||||||
data := make(map[string]interface{})
|
data := make(map[string]interface{})
|
||||||
|
|
||||||
// List of fields that need JSON marshaling
|
// 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)
|
jsonFieldSet := make(map[string]bool)
|
||||||
for _, field := range jsonFields {
|
for _, field := range jsonFields {
|
||||||
jsonFieldSet[field] = true
|
jsonFieldSet[field] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// List of nullable string fields
|
// 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)
|
nullableFieldSet := make(map[string]bool)
|
||||||
for _, field := range nullableStringFields {
|
for _, field := range nullableStringFields {
|
||||||
nullableFieldSet[field] = true
|
nullableFieldSet[field] = true
|
||||||
|
|
@ -349,6 +355,7 @@ func (store *Xun) GetAssistants(filter types.AssistantFilter, locale ...string)
|
||||||
qb.Where(func(qb query.Query) {
|
qb.Where(func(qb query.Query) {
|
||||||
qb.Where("name", "like", fmt.Sprintf("%%%s%%", filter.Keywords)).
|
qb.Where("name", "like", fmt.Sprintf("%%%s%%", filter.Keywords)).
|
||||||
OrWhere("description", "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))
|
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)
|
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)
|
// Apply custom query filter function (for permission filtering)
|
||||||
if filter.QueryFilter != nil {
|
if filter.QueryFilter != nil {
|
||||||
qb.Where(filter.QueryFilter)
|
qb.Where(filter.QueryFilter)
|
||||||
|
|
@ -448,7 +470,7 @@ func (store *Xun) GetAssistants(filter types.AssistantFilter, locale ...string)
|
||||||
|
|
||||||
// Convert rows to types.AssistantModel slice
|
// Convert rows to types.AssistantModel slice
|
||||||
assistants := make([]*types.AssistantModel, 0, len(rows))
|
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 {
|
for _, row := range rows {
|
||||||
data := row.ToMap()
|
data := row.ToMap()
|
||||||
|
|
@ -521,7 +543,7 @@ func (store *Xun) GetAssistant(assistantID string, fields []string, locale ...st
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse JSON fields
|
// 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)
|
store.parseJSONFields(data, jsonFields)
|
||||||
|
|
||||||
// Convert map to types.AssistantModel
|
// Convert map to types.AssistantModel
|
||||||
|
|
@ -536,6 +558,7 @@ func (store *Xun) GetAssistant(assistantID string, fields []string, locale ...st
|
||||||
BuiltIn: getBool(data, "built_in"),
|
BuiltIn: getBool(data, "built_in"),
|
||||||
Sort: getInt(data, "sort"),
|
Sort: getInt(data, "sort"),
|
||||||
Description: getString(data, "description"),
|
Description: getString(data, "description"),
|
||||||
|
Capabilities: getString(data, "capabilities"),
|
||||||
DefaultMode: getString(data, "default_mode"),
|
DefaultMode: getString(data, "default_mode"),
|
||||||
Readonly: getBool(data, "readonly"),
|
Readonly: getBool(data, "readonly"),
|
||||||
Public: getBool(data, "public"),
|
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 {
|
if placeholder, has := data["placeholder"]; has && placeholder != nil {
|
||||||
raw, err := jsoniter.Marshal(placeholder)
|
raw, err := jsoniter.Marshal(placeholder)
|
||||||
if err == nil {
|
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
|
// Translate prompts
|
||||||
if model.Prompts != nil {
|
if model.Prompts != nil {
|
||||||
for i := range model.Prompts {
|
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.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) {
|
t.Run("AllNewFieldsTogether", func(t *testing.T) {
|
||||||
// Test assistant with all new fields together
|
// Test assistant with all new fields together
|
||||||
optionalFalse := false
|
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
|
// Parse boolean filters
|
||||||
var builtIn, mentionable, automated *bool
|
var builtIn, mentionable, automated, sandbox *bool
|
||||||
if builtInParam := c.Query("built_in"); builtInParam != "" {
|
if builtInParam := c.Query("built_in"); builtInParam != "" {
|
||||||
builtIn = parseBoolValue(builtInParam)
|
builtIn = parseBoolValue(builtInParam)
|
||||||
}
|
}
|
||||||
|
|
@ -127,6 +127,9 @@ func ListAssistants(c *gin.Context) {
|
||||||
if automatedParam := c.Query("automated"); automatedParam != "" {
|
if automatedParam := c.Query("automated"); automatedParam != "" {
|
||||||
automated = parseBoolValue(automatedParam)
|
automated = parseBoolValue(automatedParam)
|
||||||
}
|
}
|
||||||
|
if sandboxParam := c.Query("sandbox"); sandboxParam != "" {
|
||||||
|
sandbox = parseBoolValue(sandboxParam)
|
||||||
|
}
|
||||||
|
|
||||||
// Note: public and share filters are not yet supported in AssistantFilter
|
// Note: public and share filters are not yet supported in AssistantFilter
|
||||||
// They would need to be added to the store layer for proper filtering
|
// They would need to be added to the store layer for proper filtering
|
||||||
|
|
@ -152,6 +155,7 @@ func ListAssistants(c *gin.Context) {
|
||||||
BuiltIn: builtIn,
|
BuiltIn: builtIn,
|
||||||
Mentionable: mentionable,
|
Mentionable: mentionable,
|
||||||
Automated: automated,
|
Automated: automated,
|
||||||
|
Sandbox: sandbox,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Apply permission-based filtering (Scope filtering)
|
// Apply permission-based filtering (Scope filtering)
|
||||||
|
|
@ -169,12 +173,19 @@ func ListAssistants(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter sensitive fields for built-in assistants
|
// Convert sandbox to boolean and filter built-in sensitive fields
|
||||||
// For built-in assistants, clear code-level fields (prompts, workflow, tools, kb, mcp, options)
|
resp := map[string]interface{}{
|
||||||
FilterBuiltInFields(result.Data)
|
"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
|
// 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
|
// GetAssistant retrieves a single assistant by ID with permission verification
|
||||||
|
|
@ -260,11 +271,13 @@ func GetAssistant(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter sensitive fields for built-in assistants
|
// Convert sandbox to boolean and filter built-in sensitive fields
|
||||||
|
hasSandbox := assistant.Sandbox != nil
|
||||||
FilterBuiltInAssistant(assistant)
|
FilterBuiltInAssistant(assistant)
|
||||||
|
resp := AssistantToResponse(assistant, hasSandbox)
|
||||||
|
|
||||||
// Return the result with standard response format
|
// 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
|
// ListAssistantTags lists assistant tags with permission-based filtering
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package agent
|
package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/yaoapp/gou/model"
|
"github.com/yaoapp/gou/model"
|
||||||
"github.com/yaoapp/xun/dbal/query"
|
"github.com/yaoapp/xun/dbal/query"
|
||||||
|
|
@ -150,9 +152,49 @@ func FilterBuiltInAssistant(assistant *agenttypes.AssistantModel) {
|
||||||
assistant.Prompts = nil
|
assistant.Prompts = nil
|
||||||
assistant.PromptPresets = nil
|
assistant.PromptPresets = nil
|
||||||
assistant.Workflow = nil
|
assistant.Workflow = nil
|
||||||
|
assistant.Sandbox = nil
|
||||||
assistant.KB = nil
|
assistant.KB = nil
|
||||||
assistant.MCP = nil
|
assistant.MCP = nil
|
||||||
assistant.Options = nil
|
assistant.Options = nil
|
||||||
assistant.Source = ""
|
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 defines all available fields for security filtering
|
||||||
availableAssistantFields = map[string]bool{
|
availableAssistantFields = map[string]bool{
|
||||||
"id": true, "assistant_id": true, "type": true, "name": true, "avatar": true,
|
"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,
|
"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,
|
"readonly": true, "public": true, "share": true, "locales": true,
|
||||||
"automated": true, "mentionable": true,
|
"automated": true, "mentionable": true,
|
||||||
"created_at": true, "updated_at": true, "deleted_at": true,
|
"created_at": true, "updated_at": true, "deleted_at": true,
|
||||||
|
|
@ -23,9 +23,9 @@ var (
|
||||||
|
|
||||||
// defaultAssistantFields defines the default compact field list
|
// defaultAssistantFields defines the default compact field list
|
||||||
defaultAssistantFields = []string{
|
defaultAssistantFields = []string{
|
||||||
"assistant_id", "type", "name", "avatar", "connector", "description",
|
"assistant_id", "type", "name", "avatar", "connector", "description", "capabilities",
|
||||||
"sort", "built_in", "tags", "readonly", "public", "share",
|
"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
|
BuiltIn *bool
|
||||||
Mentionable *bool
|
Mentionable *bool
|
||||||
Automated *bool
|
Automated *bool
|
||||||
|
Sandbox *bool
|
||||||
Public *bool
|
Public *bool
|
||||||
Share string
|
Share string
|
||||||
}
|
}
|
||||||
|
|
@ -79,6 +80,7 @@ func BuildAssistantFilter(params AssistantFilterParams) agenttypes.AssistantFilt
|
||||||
BuiltIn: params.BuiltIn,
|
BuiltIn: params.BuiltIn,
|
||||||
Mentionable: params.Mentionable,
|
Mentionable: params.Mentionable,
|
||||||
Automated: params.Automated,
|
Automated: params.Automated,
|
||||||
|
Sandbox: params.Sandbox,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set default type if not specified (only when Types is also empty)
|
// 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) {
|
t.Run("ListAssistantsWithSelectFields", func(t *testing.T) {
|
||||||
// Test with select parameter to limit returned fields
|
// Test with select parameter to limit returned fields
|
||||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?select=assistant_id,name,avatar,type", nil)
|
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, "name", "Assistant should have name")
|
||||||
assert.Contains(t, responseAssistant, "type", "Assistant should have type")
|
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)
|
responseAssistantID := responseAssistant["assistant_id"].(string)
|
||||||
t.Logf("Response structure is correct for assistant: %s", responseAssistantID)
|
t.Logf("Response structure is correct for assistant: %s", responseAssistantID)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,15 @@
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"index": 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",
|
"name": "path",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|
@ -161,6 +170,13 @@
|
||||||
"comment": "MCP servers available for the assistant to use",
|
"comment": "MCP servers available for the assistant to use",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "sandbox",
|
||||||
|
"type": "json",
|
||||||
|
"label": "Sandbox",
|
||||||
|
"comment": "Sandbox configuration for coding agents (command, image, timeout, etc.)",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "source",
|
"name": "source",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue