Add dependencies management to Assistant model and related functionalities
- Introduce a new field for dependencies in the Assistant model to manage external MCP client dependencies with version constraints. - Implement deep copy functionality for dependencies in the Clone method to ensure integrity during assistant cloning. - Enhance the Update method to handle dependencies input from data maps, supporting both string and interface types. - Update the Map method to include dependencies in the serialized output. - Add comprehensive tests to validate loading, cloning, and mapping of dependencies, ensuring correct behavior across various scenarios.
This commit is contained in:
parent
87f80b0ccc
commit
e8720f0421
11 changed files with 518 additions and 381 deletions
|
|
@ -146,6 +146,7 @@ func (ast *Assistant) Map() map[string]interface{} {
|
|||
"locales": ast.Locales,
|
||||
"uses": ast.Uses,
|
||||
"search": ast.Search,
|
||||
"dependencies": ast.Dependencies,
|
||||
"created_at": store.ToMySQLTime(ast.CreatedAt),
|
||||
"updated_at": store.ToMySQLTime(ast.UpdatedAt),
|
||||
}
|
||||
|
|
@ -455,6 +456,14 @@ func (ast *Assistant) Clone() *Assistant {
|
|||
}
|
||||
}
|
||||
|
||||
// Deep copy dependencies
|
||||
if ast.Dependencies != nil {
|
||||
clone.Dependencies = make(map[string]string, len(ast.Dependencies))
|
||||
for k, v := range ast.Dependencies {
|
||||
clone.Dependencies[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
|
|
@ -639,6 +648,26 @@ func (ast *Assistant) Update(data map[string]interface{}) error {
|
|||
ast.Search = search
|
||||
}
|
||||
|
||||
// Dependencies
|
||||
if v, has := data["dependencies"]; has {
|
||||
if v == nil {
|
||||
ast.Dependencies = nil
|
||||
} else {
|
||||
switch d := v.(type) {
|
||||
case map[string]string:
|
||||
ast.Dependencies = d
|
||||
case map[string]interface{}:
|
||||
deps := make(map[string]string, len(d))
|
||||
for k, val := range d {
|
||||
if s, ok := val.(string); ok {
|
||||
deps[k] = s
|
||||
}
|
||||
}
|
||||
ast.Dependencies = deps
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ast.Validate()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -729,6 +729,30 @@ func loadMap(data map[string]interface{}) (*Assistant, error) {
|
|||
assistant.Sandbox = sb
|
||||
}
|
||||
|
||||
// dependencies (name -> version constraint, like npm dependencies)
|
||||
if deps, has := data["dependencies"]; has {
|
||||
switch v := deps.(type) {
|
||||
case map[string]string:
|
||||
assistant.Dependencies = v
|
||||
case map[string]interface{}:
|
||||
d := make(map[string]string, len(v))
|
||||
for k, val := range v {
|
||||
d[k] = cast.ToString(val)
|
||||
}
|
||||
assistant.Dependencies = d
|
||||
default:
|
||||
raw, err := jsoniter.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var d map[string]string
|
||||
if err := jsoniter.Unmarshal(raw, &d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
assistant.Dependencies = d
|
||||
}
|
||||
}
|
||||
|
||||
// uses (wrapper configurations for vision, audio, etc.)
|
||||
// Merge hierarchy: global uses < assistant uses
|
||||
if uses, has := data["uses"]; has {
|
||||
|
|
|
|||
|
|
@ -494,6 +494,10 @@ func TestLoadStoreWithAllFields(t *testing.T) {
|
|||
Description: "This is a test placeholder",
|
||||
Prompts: []string{"Test prompt 1", "Test prompt 2"},
|
||||
},
|
||||
Dependencies: map[string]string{
|
||||
"echo": "^1.0.0",
|
||||
"customer": ">=2.0.0",
|
||||
},
|
||||
Source: `
|
||||
// @ts-nocheck
|
||||
function Create(ctx: any, messages: any[]): any {
|
||||
|
|
@ -571,6 +575,12 @@ function Create(ctx: any, messages: any[]): any {
|
|||
assert.NotNil(t, loaded.HookScript)
|
||||
assert.NotEmpty(t, loaded.Source)
|
||||
|
||||
// Dependencies
|
||||
require.NotNil(t, loaded.Dependencies)
|
||||
assert.Len(t, loaded.Dependencies, 2)
|
||||
assert.Equal(t, "^1.0.0", loaded.Dependencies["echo"])
|
||||
assert.Equal(t, ">=2.0.0", loaded.Dependencies["customer"])
|
||||
|
||||
// Execute the Create hook to verify it works
|
||||
ctx := newStoreTestContext("test-chat-all-fields", assistantID)
|
||||
messages := []context.Message{{Role: "user", Content: "Test message"}}
|
||||
|
|
|
|||
|
|
@ -193,6 +193,18 @@ func TestLoadPath(t *testing.T) {
|
|||
assert.NotNil(t, zhLocale)
|
||||
})
|
||||
|
||||
t.Run("LoadDependencies", func(t *testing.T) {
|
||||
assistant, err := assistant.LoadPath("/assistants/tests/fullfields")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, assistant)
|
||||
|
||||
// Dependencies
|
||||
assert.NotNil(t, assistant.Dependencies)
|
||||
assert.Len(t, assistant.Dependencies, 2)
|
||||
assert.Equal(t, "^1.0.0", assistant.Dependencies["echo"])
|
||||
assert.Equal(t, ">=2.0.0", assistant.Dependencies["customer"])
|
||||
})
|
||||
|
||||
t.Run("LoadNonExistentAssistant", func(t *testing.T) {
|
||||
_, err := assistant.LoadPath("/assistants/non-existent")
|
||||
assert.Error(t, err)
|
||||
|
|
@ -333,6 +345,13 @@ func TestClone(t *testing.T) {
|
|||
assert.False(t, exists, "Clone should not have modified key")
|
||||
delete(original.Options, "test_key") // cleanup
|
||||
}
|
||||
|
||||
if original.Dependencies != nil {
|
||||
original.Dependencies["test_dep"] = "^9.9.9"
|
||||
_, exists := clone.Dependencies["test_dep"]
|
||||
assert.False(t, exists, "Clone dependencies should not have modified key")
|
||||
delete(original.Dependencies, "test_dep") // cleanup
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CloneNil", func(t *testing.T) {
|
||||
|
|
@ -457,6 +476,7 @@ func TestMap(t *testing.T) {
|
|||
assert.Equal(t, assistant.ConnectorOptions, m["connector_options"])
|
||||
assert.Equal(t, assistant.PromptPresets, m["prompt_presets"])
|
||||
assert.Equal(t, assistant.Source, m["source"])
|
||||
assert.Equal(t, assistant.Dependencies, m["dependencies"])
|
||||
}
|
||||
|
||||
// TestLoadSystemAgents tests loading system agents from bindata
|
||||
|
|
|
|||
|
|
@ -454,6 +454,17 @@ func ToAssistantModel(v interface{}) (*AssistantModel, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// Dependencies
|
||||
if deps, ok := data["dependencies"]; ok && deps != nil {
|
||||
raw, err := jsoniter.Marshal(deps)
|
||||
if err == nil {
|
||||
var d map[string]string
|
||||
if err := jsoniter.Unmarshal(raw, &d); err == nil {
|
||||
model.Dependencies = d
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Permission fields
|
||||
if createdBy, ok := data["__yao_created_by"].(string); ok {
|
||||
model.YaoCreatedBy = createdBy
|
||||
|
|
|
|||
|
|
@ -583,6 +583,10 @@ func TestToAssistantModel(t *testing.T) {
|
|||
"name": "English Name",
|
||||
},
|
||||
},
|
||||
"dependencies": map[string]interface{}{
|
||||
"echo": "^1.0.0",
|
||||
"customer": ">=2.0.0",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := ToAssistantModel(data)
|
||||
|
|
@ -711,6 +715,19 @@ func TestToAssistantModel(t *testing.T) {
|
|||
if result.Locales == nil {
|
||||
t.Error("Expected Locales to be set")
|
||||
}
|
||||
if result.Dependencies == nil {
|
||||
t.Error("Expected Dependencies to be set")
|
||||
} else {
|
||||
if len(result.Dependencies) != 2 {
|
||||
t.Errorf("Expected 2 dependencies, got %d", len(result.Dependencies))
|
||||
}
|
||||
if result.Dependencies["echo"] != "^1.0.0" {
|
||||
t.Errorf("Expected echo dependency '^1.0.0', got '%s'", result.Dependencies["echo"])
|
||||
}
|
||||
if result.Dependencies["customer"] != ">=2.0.0" {
|
||||
t.Errorf("Expected customer dependency '>=2.0.0', got '%s'", result.Dependencies["customer"])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("MapWithFloatNumbers", func(t *testing.T) {
|
||||
|
|
@ -750,6 +767,7 @@ func TestToAssistantModel(t *testing.T) {
|
|||
"workflow": nil,
|
||||
"placeholder": nil,
|
||||
"locales": nil,
|
||||
"dependencies": nil,
|
||||
}
|
||||
|
||||
result, err := ToAssistantModel(data)
|
||||
|
|
@ -764,6 +782,9 @@ func TestToAssistantModel(t *testing.T) {
|
|||
if result.Tags != nil {
|
||||
t.Error("Expected Tags to be nil")
|
||||
}
|
||||
if result.Dependencies != nil {
|
||||
t.Error("Expected Dependencies to be nil")
|
||||
}
|
||||
if result.Modes != nil {
|
||||
t.Error("Expected Modes to be nil")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ var AssistantAllowedFields = map[string]bool{
|
|||
"locales": true,
|
||||
"uses": true,
|
||||
"search": true,
|
||||
"dependencies": true,
|
||||
"automated": true,
|
||||
"mentionable": true,
|
||||
"created_at": true,
|
||||
|
|
@ -66,10 +67,11 @@ var AssistantDefaultFields = []string{
|
|||
"share",
|
||||
"automated",
|
||||
"mentionable",
|
||||
"sandbox", // Sandbox configuration presence (lightweight)
|
||||
"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)
|
||||
"dependencies", // Dependencies on other MCP Clients (lightweight)
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"__yao_created_by", // Permission: creator user ID
|
||||
|
|
@ -112,6 +114,7 @@ var AssistantFullFields = []string{
|
|||
"locales",
|
||||
"uses",
|
||||
"search",
|
||||
"dependencies",
|
||||
"automated",
|
||||
"mentionable",
|
||||
"created_at",
|
||||
|
|
|
|||
|
|
@ -454,6 +454,7 @@ type AssistantModel struct {
|
|||
Locales i18n.Map `json:"locales,omitempty"` // Assistant Locales
|
||||
Uses *context.Uses `json:"uses,omitempty"` // Assistant-specific wrapper configurations for vision, audio, etc. If not set, use global settings
|
||||
Search *searchTypes.Config `json:"search,omitempty"` // Search configuration (web, kb, db, citation, weights, etc.)
|
||||
Dependencies map[string]string `json:"dependencies,omitempty"` // Dependencies on other MCP Clients (name -> version constraint)
|
||||
CreatedAt int64 `json:"created_at"` // Creation timestamp
|
||||
UpdatedAt int64 `json:"updated_at"` // Last update timestamp
|
||||
|
||||
|
|
|
|||
|
|
@ -191,6 +191,7 @@ func (store *Xun) SaveAssistant(assistant *types.AssistantModel) (string, error)
|
|||
"locales": assistant.Locales,
|
||||
"uses": assistant.Uses,
|
||||
"search": assistant.Search,
|
||||
"dependencies": assistant.Dependencies,
|
||||
}
|
||||
|
||||
for field, value := range jsonFields {
|
||||
|
|
@ -249,7 +250,7 @@ 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", "sandbox", "placeholder", "locales", "uses", "search"}
|
||||
jsonFields := []string{"options", "tags", "modes", "prompts", "prompt_presets", "connector_options", "kb", "db", "mcp", "workflow", "sandbox", "placeholder", "locales", "uses", "search", "dependencies"}
|
||||
jsonFieldSet := make(map[string]bool)
|
||||
for _, field := range jsonFields {
|
||||
jsonFieldSet[field] = true
|
||||
|
|
@ -470,7 +471,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", "sandbox", "kb", "mcp", "placeholder", "locales", "uses", "search"}
|
||||
jsonFields := []string{"tags", "options", "prompts", "prompt_presets", "connector_options", "workflow", "sandbox", "kb", "mcp", "placeholder", "locales", "uses", "search", "dependencies"}
|
||||
|
||||
for _, row := range rows {
|
||||
data := row.ToMap()
|
||||
|
|
@ -543,7 +544,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", "sandbox", "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", "dependencies"}
|
||||
store.parseJSONFields(data, jsonFields)
|
||||
|
||||
// Convert map to types.AssistantModel
|
||||
|
|
@ -706,6 +707,16 @@ func (store *Xun) GetAssistant(assistantID string, fields []string, locale ...st
|
|||
}
|
||||
}
|
||||
|
||||
if deps, has := data["dependencies"]; has && deps != nil {
|
||||
raw, err := jsoniter.Marshal(deps)
|
||||
if err == nil {
|
||||
var d map[string]string
|
||||
if err := jsoniter.Unmarshal(raw, &d); err == nil {
|
||||
model.Dependencies = d
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply i18n translation if locale is provided
|
||||
if len(locale) > 0 && locale[0] != "" {
|
||||
store.translate(model, assistantID, locale[0])
|
||||
|
|
|
|||
748
data/bindata.go
748
data/bindata.go
File diff suppressed because one or more lines are too long
|
|
@ -258,6 +258,13 @@
|
|||
"comment": "Search configuration (web, kb, db, citation, weights, etc.)",
|
||||
"nullable": true
|
||||
},
|
||||
{
|
||||
"name": "dependencies",
|
||||
"type": "json",
|
||||
"label": "Dependencies",
|
||||
"comment": "Dependencies on other MCP Clients (name -> version constraint)",
|
||||
"nullable": true
|
||||
},
|
||||
{
|
||||
"name": "automated",
|
||||
"type": "boolean",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue