Add modes and default mode fields to Assistant model

- Introduced `modes` field to support multiple operational modes for the assistant, allowing for flexible functionality (e.g., chat, task).
- Added `default_mode` field to specify the primary mode of operation for the assistant.
- Updated the `Map`, `Clone`, and `Update` methods to handle the new fields appropriately.
- Enhanced the `ToAssistantModel` conversion function to include the new fields.
- Added tests to validate the integration and functionality of modes and default mode within the assistant model.
This commit is contained in:
Max 2025-12-02 19:02:31 +08:00
parent da48896bdf
commit ed62033761
10 changed files with 347 additions and 159 deletions

View file

@ -86,6 +86,8 @@ func (ast *Assistant) Map() map[string]interface{} {
"mcp": ast.MCP,
"workflow": ast.Workflow,
"tags": ast.Tags,
"modes": ast.Modes,
"default_mode": ast.DefaultMode,
"mentionable": ast.Mentionable,
"automated": ast.Automated,
"placeholder": ast.Placeholder,
@ -169,6 +171,15 @@ func (ast *Assistant) Clone() *Assistant {
copy(clone.Tags, ast.Tags)
}
// Deep copy modes
if ast.Modes != nil {
clone.Modes = make([]string, len(ast.Modes))
copy(clone.Modes, ast.Modes)
}
// Copy default_mode (simple string)
clone.DefaultMode = ast.DefaultMode
// Deep copy KB
if ast.KB != nil {
clone.KB = &store.KnowledgeBase{}
@ -363,6 +374,12 @@ func (ast *Assistant) Update(data map[string]interface{}) error {
if v, ok := data["tags"].([]string); ok {
ast.Tags = v
}
if v, ok := data["modes"].([]string); ok {
ast.Modes = v
}
if v, ok := data["default_mode"].(string); ok {
ast.DefaultMode = v
}
if v, ok := data["options"].(map[string]interface{}); ok {
ast.Options = v
}

View file

@ -302,6 +302,22 @@ func ToAssistantModel(v interface{}) (*AssistantModel, error) {
}
}
// Modes (string array)
if modes, ok := data["modes"]; ok && modes != nil {
raw, err := jsoniter.Marshal(modes)
if err == nil {
var m []string
if err := jsoniter.Unmarshal(raw, &m); err == nil {
model.Modes = m
}
}
}
// DefaultMode (string)
if defaultMode, ok := data["default_mode"].(string); ok {
model.DefaultMode = defaultMode
}
// Options (map)
if options, ok := data["options"].(map[string]interface{}); ok {
model.Options = options

View file

@ -533,18 +533,20 @@ func TestToAssistantModel(t *testing.T) {
"connectors": []string{"openai", "anthropic"},
"filters": []string{"vision", "tool_calls"},
},
"path": "/path/to/assistant",
"description": "Test description",
"share": "team",
"built_in": true,
"readonly": false,
"public": true,
"mentionable": true,
"automated": false,
"sort": 100,
"created_at": int64(1609459200),
"updated_at": int64(1609459300),
"tags": []string{"tag1", "tag2"},
"path": "/path/to/assistant",
"description": "Test description",
"share": "team",
"built_in": true,
"readonly": false,
"public": true,
"mentionable": true,
"automated": false,
"sort": 100,
"created_at": int64(1609459200),
"updated_at": int64(1609459300),
"tags": []string{"tag1", "tag2"},
"modes": []string{"chat", "task"},
"default_mode": "chat",
"options": map[string]interface{}{
"temperature": 0.7,
},
@ -656,6 +658,15 @@ func TestToAssistantModel(t *testing.T) {
if len(result.Tags) != 2 {
t.Errorf("Expected 2 tags, got %d", len(result.Tags))
}
if len(result.Modes) != 2 {
t.Errorf("Expected 2 modes, got %d", len(result.Modes))
}
if result.Modes[0] != "chat" {
t.Errorf("Expected first mode 'chat', got '%s'", result.Modes[0])
}
if result.DefaultMode != "chat" {
t.Errorf("Expected default_mode 'chat', got '%s'", result.DefaultMode)
}
if result.Options == nil {
t.Error("Expected Options to be set")
}
@ -729,6 +740,8 @@ func TestToAssistantModel(t *testing.T) {
data := map[string]interface{}{
"assistant_id": "test-id",
"tags": nil,
"modes": nil,
"default_mode": "",
"options": nil,
"prompts": nil,
"kb": nil,
@ -751,6 +764,12 @@ func TestToAssistantModel(t *testing.T) {
if result.Tags != nil {
t.Error("Expected Tags to be nil")
}
if result.Modes != nil {
t.Error("Expected Modes to be nil")
}
if result.DefaultMode != "" {
t.Error("Expected DefaultMode to be empty")
}
if result.Options != nil {
t.Error("Expected Options to be nil")
}

View file

@ -26,6 +26,8 @@ var AssistantAllowedFields = map[string]bool{
"mcp": true,
"source": true,
"tags": true,
"modes": true,
"default_mode": true,
"readonly": true,
"public": true,
"share": true,
@ -50,7 +52,9 @@ var AssistantDefaultFields = []string{
"avatar",
"connector",
"description",
"tags", // Tags for categorization (lightweight)
"tags", // Tags for categorization (lightweight)
"modes", // Supported modes (lightweight)
"default_mode", // Default mode (lightweight)
"sort",
"built_in",
"readonly",
@ -93,6 +97,8 @@ var AssistantFullFields = []string{
"mcp",
"source",
"tags",
"modes",
"default_mode",
"readonly",
"public",
"share",

View file

@ -129,6 +129,8 @@ func TestAssistantAllowedFields(t *testing.T) {
"uses",
"connector_options",
"source",
"modes",
"default_mode",
}
for _, field := range complexFields {
if !AssistantAllowedFields[field] {
@ -147,6 +149,8 @@ func TestAssistantDefaultFields(t *testing.T) {
"kb", // Knowledge base is essential for assistant functionality
"db", // Database is essential for assistant functionality
"mcp", // MCP servers are essential for assistant functionality
"modes", // Supported modes are essential for mode filtering
"default_mode", // Default mode is essential for mode selection
"__yao_created_by", // Permission fields are essential for access control
"__yao_updated_by",
"__yao_team_id",
@ -167,7 +171,7 @@ func TestAssistantDefaultFields(t *testing.T) {
t.Run("DoesNotContainSensitiveFields", func(t *testing.T) {
// Default fields should not include complex/large fields by default
// Note: kb, db, mcp, and tags are lightweight and included in defaults
// Note: kb, db, mcp, tags, modes, and default_mode are lightweight and included in defaults
sensitiveFields := []string{
"options",
"prompts",
@ -237,6 +241,8 @@ func TestAssistantFullFields(t *testing.T) {
"uses",
"connector_options",
"source",
"modes",
"default_mode",
}
fullFieldsMap := make(map[string]bool)

View file

@ -263,6 +263,8 @@ type AssistantModel struct {
Sort int `json:"sort,omitempty"` // Assistant Sort
Description string `json:"description,omitempty"` // Assistant Description
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
Readonly bool `json:"readonly,omitempty"` // Whether this assistant is readonly
Public bool `json:"public,omitempty"` // Whether this assistant is shared across all teams in the platform
Share string `json:"share,omitempty"` // Assistant sharing scope (private/team)

View file

@ -156,6 +156,21 @@ func (conv *Xun) SaveAssistant(assistant *types.AssistantModel) (string, error)
data["tags"] = jsonStr
}
if assistant.Modes != nil {
jsonStr, err := jsoniter.MarshalToString(assistant.Modes)
if err != nil {
return "", fmt.Errorf("failed to marshal modes: %w", err)
}
data["modes"] = jsonStr
}
// DefaultMode is a simple string field
if assistant.DefaultMode != "" {
data["default_mode"] = assistant.DefaultMode
} else {
data["default_mode"] = nil
}
// Handle interface{} fields - they should already be in the correct format
jsonFields := map[string]interface{}{
"prompts": assistant.Prompts,
@ -226,14 +241,14 @@ func (conv *Xun) UpdateAssistant(assistantID string, updates map[string]interfac
data := make(map[string]interface{})
// List of fields that need JSON marshaling
jsonFields := []string{"options", "tags", "prompts", "prompt_presets", "connector_options", "kb", "db", "mcp", "workflow", "placeholder", "locales", "uses"}
jsonFields := []string{"options", "tags", "modes", "prompts", "prompt_presets", "connector_options", "kb", "db", "mcp", "workflow", "placeholder", "locales", "uses"}
jsonFieldSet := make(map[string]bool)
for _, field := range jsonFields {
jsonFieldSet[field] = true
}
// List of nullable string fields
nullableStringFields := []string{"name", "avatar", "description", "path", "source", "__yao_created_by", "__yao_updated_by", "__yao_team_id", "__yao_tenant_id"}
nullableStringFields := []string{"name", "avatar", "description", "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
@ -499,7 +514,7 @@ func (conv *Xun) GetAssistant(assistantID string, fields []string, locale ...str
}
// Parse JSON fields
jsonFields := []string{"tags", "options", "prompts", "prompt_presets", "connector_options", "workflow", "kb", "db", "mcp", "placeholder", "locales", "uses"}
jsonFields := []string{"tags", "modes", "options", "prompts", "prompt_presets", "connector_options", "workflow", "kb", "db", "mcp", "placeholder", "locales", "uses"}
conv.parseJSONFields(data, jsonFields)
// Convert map to types.AssistantModel
@ -514,6 +529,7 @@ func (conv *Xun) GetAssistant(assistantID string, fields []string, locale ...str
BuiltIn: getBool(data, "built_in"),
Sort: getInt(data, "sort"),
Description: getString(data, "description"),
DefaultMode: getString(data, "default_mode"),
Readonly: getBool(data, "readonly"),
Public: getBool(data, "public"),
Share: getString(data, "share"),
@ -538,6 +554,16 @@ func (conv *Xun) GetAssistant(assistantID string, fields []string, locale ...str
}
}
// Handle Modes
if modes, ok := data["modes"].([]interface{}); ok {
model.Modes = make([]string, len(modes))
for i, mode := range modes {
if s, ok := mode.(string); ok {
model.Modes[i] = s
}
}
}
// Handle Options
if options, ok := data["options"].(map[string]interface{}); ok {
model.Options = options

View file

@ -2507,6 +2507,87 @@ func TestUpdateAssistant(t *testing.T) {
}
})
t.Run("UpdateModesAndDefaultMode", func(t *testing.T) {
// Create assistant
assistant := &types.AssistantModel{
Name: "Modes 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 modes and default_mode
updates := map[string]interface{}{
"modes": []string{"chat", "task", "analyze"},
"default_mode": "chat",
}
err = store.UpdateAssistant(id, updates)
if err != nil {
t.Fatalf("Failed to update modes: %v", err)
}
// Verify updates - modes and default_mode are in default fields
retrieved, err := store.GetAssistant(id, nil)
if err != nil {
t.Fatalf("Failed to retrieve assistant: %v", err)
}
if retrieved.Modes == nil || len(retrieved.Modes) != 3 {
t.Errorf("Expected 3 modes, got %v", retrieved.Modes)
}
if retrieved.Modes[0] != "chat" {
t.Errorf("Expected first mode 'chat', got '%s'", retrieved.Modes[0])
}
if retrieved.DefaultMode != "chat" {
t.Errorf("Expected default_mode 'chat', got '%s'", retrieved.DefaultMode)
}
})
t.Run("UpdateModesOnly", func(t *testing.T) {
// Create assistant with default_mode
assistant := &types.AssistantModel{
Name: "Modes Only Test",
Type: "assistant",
Connector: "openai",
Share: "private",
DefaultMode: "task",
}
id, err := store.SaveAssistant(assistant)
if err != nil {
t.Fatalf("Failed to create assistant: %v", err)
}
// Update only modes
updates := map[string]interface{}{
"modes": []string{"chat", "task"},
}
err = store.UpdateAssistant(id, updates)
if err != nil {
t.Fatalf("Failed to update modes: %v", err)
}
// Verify updates - default_mode should remain unchanged
retrieved, err := store.GetAssistant(id, nil)
if err != nil {
t.Fatalf("Failed to retrieve assistant: %v", err)
}
if len(retrieved.Modes) != 2 {
t.Errorf("Expected 2 modes, got %d", len(retrieved.Modes))
}
if retrieved.DefaultMode != "task" {
t.Errorf("Expected default_mode to remain 'task', got '%s'", retrieved.DefaultMode)
}
})
t.Run("UpdateMCPWithToolsAndResources", func(t *testing.T) {
// Create assistant
assistant := &types.AssistantModel{

File diff suppressed because one or more lines are too long

View file

@ -175,6 +175,21 @@
"comment": "Assistant tags",
"nullable": true
},
{
"name": "modes",
"type": "json",
"label": "Modes",
"comment": "Supported modes (e.g., chat, task), null means all modes are supported",
"nullable": true
},
{
"name": "default_mode",
"type": "string",
"label": "Default Mode",
"comment": "Default mode for the assistant",
"length": 50,
"nullable": true
},
{
"name": "readonly",
"type": "boolean",