Implement System Agents Configuration and Loading Mechanism

- Added configuration support for system agents in the assistant initialization process, allowing for custom connectors for agents like __yao.keyword and __yao.querydsl.
- Implemented the loading mechanism for system agents from bindata, ensuring that essential agents are available during runtime.
- Updated the LoadBuiltIn function to exclude system agents from being removed, enhancing the management of built-in and system agents.
- Enhanced test coverage by introducing tests for loading system agents, verifying their presence and correctness in the cache.
- Updated documentation to reflect the new system agents configuration and loading processes.
This commit is contained in:
Max 2025-12-16 15:32:20 +08:00
parent a2182eda1d
commit c86ccb55b1
18 changed files with 1205 additions and 193 deletions

View file

@ -45,7 +45,7 @@ func LoadBuiltIn() error {
// Get all existing built-in assistants
deletedBuiltIn := map[string]bool{}
// Remove the built-in assistants
// Remove the built-in assistants (exclude system agents with __yao. prefix)
if storage != nil {
builtIn := true
@ -54,8 +54,12 @@ func LoadBuiltIn() error {
return err
}
// Get all existing built-in assistants
// Get all existing built-in assistants (exclude system agents)
for _, assistant := range res.Data {
// Skip system agents (they are managed by LoadSystemAgents)
if strings.HasPrefix(assistant.ID, "__yao.") {
continue
}
deletedBuiltIn[assistant.ID] = true
}
}

View file

@ -0,0 +1,367 @@
package assistant
import (
"fmt"
"path/filepath"
"strings"
"github.com/yaoapp/gou/application"
"github.com/yaoapp/gou/connector"
gouOpenAI "github.com/yaoapp/gou/connector/openai"
"github.com/yaoapp/kun/log"
"github.com/yaoapp/yao/agent/i18n"
store "github.com/yaoapp/yao/agent/store/types"
"github.com/yaoapp/yao/data"
"gopkg.in/yaml.v3"
)
// systemAgents defines the system agents loaded from bindata
// These are internal agents used by the system (e.g., keyword extraction, querydsl generation)
// The directory name is without __yao. prefix, prefix is added during loading
// Format: directory name -> bindata path prefix
var systemAgents = []string{
"keyword",
"querydsl",
"title",
"prompt",
"needsearch",
"entity",
}
// SystemConfig holds the system agents connector configuration
// This is set from agent.yml system block
type SystemConfig struct {
Default string // Default connector for all system agents
Keyword string // Connector for __yao.keyword agent
QueryDSL string // Connector for __yao.querydsl agent
Title string // Connector for __yao.title agent
Prompt string // Connector for __yao.prompt agent
NeedSearch string // Connector for __yao.needsearch agent
Entity string // Connector for __yao.entity agent
}
// systemConfig holds the system agents configuration (global variable like others in load.go)
var systemConfig *SystemConfig = nil
// SetSystemConfig sets the system agents configuration
func SetSystemConfig(config *SystemConfig) {
systemConfig = config
}
// GetSystemConfig returns the system agents configuration
func GetSystemConfig() *SystemConfig {
return systemConfig
}
// LoadSystemAgents loads the system agents from bindata
// These are internal agents like __yao.keyword and __yao.querydsl
// They are loaded before application assistants
// Behavior is same as LoadBuiltIn, just reads from bindata instead of filesystem
func LoadSystemAgents() error {
// Get all existing system agents (for cleanup)
deletedSystem := map[string]bool{}
if storage != nil {
// System agents have "system" tag
tags := []string{"system"}
builtIn := true
res, err := storage.GetAssistants(store.AssistantFilter{
Tags: tags,
BuiltIn: &builtIn,
Select: []string{"assistant_id", "id"},
})
if err != nil {
log.Warn("Failed to get existing system agents: %v", err)
} else {
for _, assistant := range res.Data {
deletedSystem[assistant.ID] = true
}
}
}
sort := 1
for _, name := range systemAgents {
// Build agent ID with __yao. prefix
id := "__yao." + name
pathPrefix := "yao/assistants/" + name
assistant, err := loadSystemAgent(id, pathPrefix)
if err != nil {
log.Warn("Failed to load system agent %s: %v", id, err)
continue
}
// Set sort order
if assistant.Sort == 0 {
assistant.Sort = sort
}
// Save to storage
if err := assistant.Save(); err != nil {
log.Warn("Failed to save system agent %s: %v", id, err)
continue
}
// Initialize the assistant
if err := assistant.initialize(); err != nil {
log.Warn("Failed to initialize system agent %s: %v", id, err)
continue
}
sort++
loaded.Put(assistant)
log.Trace("Loaded system agent: %s", id)
// Remove from deleted list
delete(deletedSystem, id)
}
// Remove deleted system agents
if len(deletedSystem) > 0 {
assistantIDs := []string{}
for assistantID := range deletedSystem {
assistantIDs = append(assistantIDs, assistantID)
}
if _, err := storage.DeleteAssistants(store.AssistantFilter{AssistantIDs: assistantIDs}); err != nil {
log.Warn("Failed to delete obsolete system agents: %v", err)
}
}
return nil
}
// loadSystemAgent loads a single system agent from bindata
// This follows the same pattern as LoadPath but reads from bindata
func loadSystemAgent(id, pathPrefix string) (*Assistant, error) {
// Read package.yao from bindata
pkgPath := pathPrefix + "/package.yao"
pkgContent, err := data.Read(pkgPath)
if err != nil {
return nil, fmt.Errorf("failed to read %s: %w", pkgPath, err)
}
// Parse package.yao
var pkgData map[string]interface{}
if err := application.Parse(pkgPath, pkgContent, &pkgData); err != nil {
return nil, fmt.Errorf("failed to parse %s: %w", pkgPath, err)
}
// Set assistant_id and path
pkgData["assistant_id"] = id
pkgData["path"] = "/" + pathPrefix
// Set type if not specified
if _, has := pkgData["type"]; !has {
pkgData["type"] = "assistant"
}
// Resolve connector for this system agent
connectorID := resolveSystemConnector(id)
if connectorID != "" {
pkgData["connector"] = connectorID
}
// Read prompts.yml from bindata (default prompts)
promptsPath := pathPrefix + "/prompts.yml"
promptsContent, err := data.Read(promptsPath)
if err == nil {
var prompts []store.Prompt
if err := yaml.Unmarshal(promptsContent, &prompts); err == nil && len(prompts) > 0 {
pkgData["prompts"] = prompts
}
}
// Read prompt_presets from prompts directory
presets := loadSystemPromptPresets(pathPrefix)
if len(presets) > 0 {
pkgData["prompt_presets"] = presets
}
// Load scripts from src directory (hook script source and other scripts sources)
// These will be compiled by loadMap -> LoadScriptsFromData
hookScriptSource, scriptsSource := loadSystemScripts(pathPrefix)
if hookScriptSource != "" {
pkgData["script"] = hookScriptSource
}
if len(scriptsSource) > 0 {
pkgData["scripts"] = scriptsSource
}
// Read locales
locales, err := loadSystemLocales(pathPrefix)
if err == nil && len(locales) > 0 {
pkgData["locales"] = locales
}
// Mark as system agent
pkgData["readonly"] = true
pkgData["built_in"] = true
pkgData["tags"] = []string{"system"}
// Load from map (same as LoadPath, includes initialize())
return loadMap(pkgData)
}
// resolveSystemConnector resolves the connector for a system agent
// Priority: specific agent config > system.default > defaultConnector > fallback to first capable connector
func resolveSystemConnector(agentID string) string {
// Try specific agent config first
if systemConfig != nil {
switch agentID {
case "__yao.keyword":
if systemConfig.Keyword != "" {
return systemConfig.Keyword
}
case "__yao.querydsl":
if systemConfig.QueryDSL != "" {
return systemConfig.QueryDSL
}
case "__yao.title":
if systemConfig.Title != "" {
return systemConfig.Title
}
case "__yao.prompt":
if systemConfig.Prompt != "" {
return systemConfig.Prompt
}
case "__yao.needsearch":
if systemConfig.NeedSearch != "" {
return systemConfig.NeedSearch
}
case "__yao.entity":
if systemConfig.Entity != "" {
return systemConfig.Entity
}
}
// Try system default
if systemConfig.Default != "" {
return systemConfig.Default
}
}
// Try global default connector
if defaultConnector != "" {
return defaultConnector
}
// Fallback: find first connector that supports tool calling
return findCapableConnector()
}
// findCapableConnector finds the first connector that supports tool calling
func findCapableConnector() string {
// Get all registered connectors
for id, conn := range connector.Connectors {
if !conn.Is(connector.OPENAI) {
continue
}
// Check from modelCapabilities (user-defined in models.yml)
if caps, exists := modelCapabilities[id]; exists {
if caps.ToolCalls {
return id
}
}
// Check capabilities from connector's Options
if connOpenAI, ok := conn.(*gouOpenAI.Connector); ok {
if connOpenAI.Options.Capabilities != nil && connOpenAI.Options.Capabilities.ToolCalls {
return id
}
}
}
// No capable connector found, return empty
return ""
}
// loadSystemPromptPresets loads prompt presets from bindata prompts directory
func loadSystemPromptPresets(pathPrefix string) map[string][]store.Prompt {
presets := make(map[string][]store.Prompt)
promptsDir := pathPrefix + "/prompts"
// Try common preset files
presetFiles := []string{"chat.yml", "task.yml", "code.yml", "analysis.yml"}
for _, filename := range presetFiles {
presetPath := promptsDir + "/" + filename
content, err := data.Read(presetPath)
if err != nil {
continue
}
var prompts []store.Prompt
if err := yaml.Unmarshal(content, &prompts); err == nil && len(prompts) > 0 {
presetName := strings.TrimSuffix(filename, ".yml")
presets[presetName] = prompts
}
}
return presets
}
// loadSystemScripts loads scripts source from bindata src directory
// Returns hook script source and other scripts sources (as strings)
// These will be compiled by loadMap -> LoadScriptsFromData
func loadSystemScripts(pathPrefix string) (string, map[string]string) {
srcDir := pathPrefix + "/src"
// Try to load hook script (index.ts)
var hookScriptSource string
indexPath := srcDir + "/index.ts"
indexContent, err := data.Read(indexPath)
if err == nil && len(indexContent) > 0 {
hookScriptSource = string(indexContent)
}
// Try to load other scripts
scripts := make(map[string]string)
scriptFiles := []string{"utils.ts", "helpers.ts", "tools.ts"}
for _, filename := range scriptFiles {
scriptPath := srcDir + "/" + filename
content, err := data.Read(scriptPath)
if err != nil {
continue
}
scriptName := strings.TrimSuffix(filename, ".ts")
scripts[scriptName] = string(content)
}
if len(scripts) == 0 {
scripts = nil
}
return hookScriptSource, scripts
}
// loadSystemLocales loads locales from bindata
func loadSystemLocales(pathPrefix string) (i18n.Map, error) {
locales := make(i18n.Map)
// Try to load common locale files
localeFiles := []string{"en-us.yml", "zh-cn.yml", "en.yml", "zh.yml"}
localesDir := pathPrefix + "/locales"
for _, filename := range localeFiles {
localePath := filepath.Join(localesDir, filename)
content, err := data.Read(localePath)
if err != nil {
continue
}
// Parse locale file
locale := strings.TrimSuffix(filename, ".yml")
var messages map[string]any
if err := yaml.Unmarshal(content, &messages); err != nil {
continue
}
locales[locale] = i18n.I18n{
Locale: locale,
Messages: messages,
}
}
return locales, nil
}

View file

@ -1,10 +1,12 @@
package assistant
package assistant_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/yaoapp/yao/agent"
"github.com/yaoapp/yao/agent/assistant"
store "github.com/yaoapp/yao/agent/store/types"
"github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/test"
@ -14,13 +16,19 @@ func prepare(t *testing.T) {
test.Prepare(t, config.Conf)
}
func prepareAgent(t *testing.T) {
test.Prepare(t, config.Conf)
err := agent.Load(config.Conf)
require.NoError(t, err, "agent.Load should succeed")
}
// TestLoadPath tests loading assistant from path
func TestLoadPath(t *testing.T) {
prepare(t)
defer test.Clean()
t.Run("LoadFullFieldsAssistant", func(t *testing.T) {
assistant, err := LoadPath("/assistants/tests/fullfields")
assistant, err := assistant.LoadPath("/assistants/tests/fullfields")
require.NoError(t, err)
require.NotNil(t, assistant)
@ -67,7 +75,7 @@ func TestLoadPath(t *testing.T) {
})
t.Run("LoadConnectorOptions", func(t *testing.T) {
assistant, err := LoadPath("/assistants/tests/fullfields")
assistant, err := assistant.LoadPath("/assistants/tests/fullfields")
require.NoError(t, err)
require.NotNil(t, assistant)
@ -84,7 +92,7 @@ func TestLoadPath(t *testing.T) {
})
t.Run("LoadPromptPresets", func(t *testing.T) {
assistant, err := LoadPath("/assistants/tests/fullfields")
assistant, err := assistant.LoadPath("/assistants/tests/fullfields")
require.NoError(t, err)
require.NotNil(t, assistant)
@ -116,7 +124,7 @@ func TestLoadPath(t *testing.T) {
})
t.Run("LoadKnowledgeBase", func(t *testing.T) {
assistant, err := LoadPath("/assistants/tests/fullfields")
assistant, err := assistant.LoadPath("/assistants/tests/fullfields")
require.NoError(t, err)
require.NotNil(t, assistant)
@ -129,7 +137,7 @@ func TestLoadPath(t *testing.T) {
})
t.Run("LoadMCPServers", func(t *testing.T) {
assistant, err := LoadPath("/assistants/tests/fullfields")
assistant, err := assistant.LoadPath("/assistants/tests/fullfields")
require.NoError(t, err)
require.NotNil(t, assistant)
@ -143,7 +151,7 @@ func TestLoadPath(t *testing.T) {
})
t.Run("LoadWorkflow", func(t *testing.T) {
assistant, err := LoadPath("/assistants/tests/fullfields")
assistant, err := assistant.LoadPath("/assistants/tests/fullfields")
require.NoError(t, err)
require.NotNil(t, assistant)
@ -156,7 +164,7 @@ func TestLoadPath(t *testing.T) {
})
t.Run("LoadPlaceholder", func(t *testing.T) {
assistant, err := LoadPath("/assistants/tests/fullfields")
assistant, err := assistant.LoadPath("/assistants/tests/fullfields")
require.NoError(t, err)
require.NotNil(t, assistant)
@ -169,7 +177,7 @@ func TestLoadPath(t *testing.T) {
})
t.Run("LoadLocales", func(t *testing.T) {
assistant, err := LoadPath("/assistants/tests/fullfields")
assistant, err := assistant.LoadPath("/assistants/tests/fullfields")
require.NoError(t, err)
require.NotNil(t, assistant)
@ -186,7 +194,7 @@ func TestLoadPath(t *testing.T) {
})
t.Run("LoadNonExistentAssistant", func(t *testing.T) {
_, err := LoadPath("/assistants/non-existent")
_, err := assistant.LoadPath("/assistants/non-existent")
assert.Error(t, err)
})
}
@ -196,7 +204,7 @@ func TestLoadPathMCPTest(t *testing.T) {
prepare(t)
defer test.Clean()
assistant, err := LoadPath("/assistants/tests/mcptest")
assistant, err := assistant.LoadPath("/assistants/tests/mcptest")
require.NoError(t, err)
require.NotNil(t, assistant)
@ -220,7 +228,7 @@ func TestLoadPathBuildRequest(t *testing.T) {
prepare(t)
defer test.Clean()
assistant, err := LoadPath("/assistants/tests/buildrequest")
assistant, err := assistant.LoadPath("/assistants/tests/buildrequest")
require.NoError(t, err)
require.NotNil(t, assistant)
@ -238,57 +246,57 @@ func TestLoadPathBuildRequest(t *testing.T) {
// TestCache tests the assistant cache functionality
func TestCache(t *testing.T) {
// Clear any existing cache
ClearCache()
assistant.ClearCache()
// Set small cache for testing
SetCache(3)
assert.NotNil(t, loaded)
assistant.SetCache(3)
assert.NotNil(t, assistant.GetCache())
// Create test assistants
ast1 := &Assistant{AssistantModel: store.AssistantModel{ID: "id1", Name: "Assistant 1"}}
ast2 := &Assistant{AssistantModel: store.AssistantModel{ID: "id2", Name: "Assistant 2"}}
ast3 := &Assistant{AssistantModel: store.AssistantModel{ID: "id3", Name: "Assistant 3"}}
ast4 := &Assistant{AssistantModel: store.AssistantModel{ID: "id4", Name: "Assistant 4"}}
ast1 := &assistant.Assistant{AssistantModel: store.AssistantModel{ID: "id1", Name: "Assistant 1"}}
ast2 := &assistant.Assistant{AssistantModel: store.AssistantModel{ID: "id2", Name: "Assistant 2"}}
ast3 := &assistant.Assistant{AssistantModel: store.AssistantModel{ID: "id3", Name: "Assistant 3"}}
ast4 := &assistant.Assistant{AssistantModel: store.AssistantModel{ID: "id4", Name: "Assistant 4"}}
t.Run("PutAndGet", func(t *testing.T) {
loaded.Put(ast1)
assert.Equal(t, 1, loaded.Len())
assistant.GetCache().Put(ast1)
assert.Equal(t, 1, assistant.GetCache().Len())
cached, exists := loaded.Get("id1")
cached, exists := assistant.GetCache().Get("id1")
assert.True(t, exists)
assert.Equal(t, ast1, cached)
})
t.Run("CacheEviction", func(t *testing.T) {
loaded.Put(ast2)
loaded.Put(ast3)
assert.Equal(t, 3, loaded.Len())
assistant.GetCache().Put(ast2)
assistant.GetCache().Put(ast3)
assert.Equal(t, 3, assistant.GetCache().Len())
// Access ast1 to make it recently used
loaded.Get("id1")
assistant.GetCache().Get("id1")
// Add ast4, should evict ast2 (least recently used)
loaded.Put(ast4)
assert.Equal(t, 3, loaded.Len())
assistant.GetCache().Put(ast4)
assert.Equal(t, 3, assistant.GetCache().Len())
_, exists := loaded.Get("id2")
_, exists := assistant.GetCache().Get("id2")
assert.False(t, exists, "ast2 should be evicted")
_, exists = loaded.Get("id1")
_, exists = assistant.GetCache().Get("id1")
assert.True(t, exists, "ast1 should still exist")
_, exists = loaded.Get("id4")
_, exists = assistant.GetCache().Get("id4")
assert.True(t, exists, "ast4 should exist")
})
t.Run("ClearCache", func(t *testing.T) {
ClearCache()
assert.Nil(t, loaded)
assistant.ClearCache()
assert.Nil(t, assistant.GetCache())
})
t.Run("SetCacheAfterClear", func(t *testing.T) {
SetCache(100)
assert.NotNil(t, loaded)
assistant.SetCache(100)
assert.NotNil(t, assistant.GetCache())
})
}
@ -298,7 +306,7 @@ func TestClone(t *testing.T) {
defer test.Clean()
t.Run("CloneFullFieldsAssistant", func(t *testing.T) {
original, err := LoadPath("/assistants/tests/fullfields")
original, err := assistant.LoadPath("/assistants/tests/fullfields")
require.NoError(t, err)
clone := original.Clone()
@ -328,7 +336,7 @@ func TestClone(t *testing.T) {
})
t.Run("CloneNil", func(t *testing.T) {
var nilAssistant *Assistant
var nilAssistant *assistant.Assistant
assert.Nil(t, nilAssistant.Clone())
})
}
@ -339,7 +347,7 @@ func TestUpdate(t *testing.T) {
defer test.Clean()
t.Run("UpdateBasicFields", func(t *testing.T) {
assistant, err := LoadPath("/assistants/tests/fullfields")
assistant, err := assistant.LoadPath("/assistants/tests/fullfields")
require.NoError(t, err)
updates := map[string]interface{}{
@ -357,7 +365,7 @@ func TestUpdate(t *testing.T) {
})
t.Run("UpdateConnectorOptions", func(t *testing.T) {
assistant, err := LoadPath("/assistants/tests/fullfields")
assistant, err := assistant.LoadPath("/assistants/tests/fullfields")
require.NoError(t, err)
updates := map[string]interface{}{
@ -377,7 +385,7 @@ func TestUpdate(t *testing.T) {
})
t.Run("UpdatePromptPresets", func(t *testing.T) {
assistant, err := LoadPath("/assistants/tests/fullfields")
assistant, err := assistant.LoadPath("/assistants/tests/fullfields")
require.NoError(t, err)
updates := map[string]interface{}{
@ -398,7 +406,7 @@ func TestUpdate(t *testing.T) {
})
t.Run("UpdateSource", func(t *testing.T) {
assistant, err := LoadPath("/assistants/tests/fullfields")
assistant, err := assistant.LoadPath("/assistants/tests/fullfields")
require.NoError(t, err)
updates := map[string]interface{}{
@ -412,7 +420,7 @@ func TestUpdate(t *testing.T) {
})
t.Run("UpdateNilAssistant", func(t *testing.T) {
var nilAssistant *Assistant
var nilAssistant *assistant.Assistant
err := nilAssistant.Update(map[string]interface{}{"name": "test"})
assert.Error(t, err)
})
@ -423,7 +431,7 @@ func TestMap(t *testing.T) {
prepare(t)
defer test.Clean()
assistant, err := LoadPath("/assistants/tests/fullfields")
assistant, err := assistant.LoadPath("/assistants/tests/fullfields")
require.NoError(t, err)
m := assistant.Map()
@ -451,16 +459,103 @@ func TestMap(t *testing.T) {
assert.Equal(t, assistant.Source, m["source"])
}
// TestLoadSystemAgents tests loading system agents from bindata
func TestLoadSystemAgents(t *testing.T) {
prepareAgent(t)
defer test.Clean()
// Clear cache first
assistant.ClearCache()
assistant.SetCache(200)
t.Run("LoadSystemAgents", func(t *testing.T) {
err := assistant.LoadSystemAgents()
require.NoError(t, err)
// Check __yao.keyword
keywordAst, keywordExists := assistant.GetCache().Get("__yao.keyword")
require.True(t, keywordExists, "__yao.keyword should be loaded")
assert.Equal(t, "__yao.keyword", keywordAst.ID)
assert.Equal(t, "Keyword Extraction", keywordAst.Name)
assert.True(t, keywordAst.Readonly)
assert.True(t, keywordAst.BuiltIn)
assert.Contains(t, keywordAst.Tags, "system")
assert.NotNil(t, keywordAst.Prompts)
assert.Greater(t, len(keywordAst.Prompts), 0)
// Check __yao.querydsl
querydslAst, querydslExists := assistant.GetCache().Get("__yao.querydsl")
require.True(t, querydslExists, "__yao.querydsl should be loaded")
assert.Equal(t, "__yao.querydsl", querydslAst.ID)
assert.Equal(t, "QueryDSL Generator", querydslAst.Name)
assert.True(t, querydslAst.Readonly)
assert.True(t, querydslAst.BuiltIn)
assert.Contains(t, querydslAst.Tags, "system")
assert.NotNil(t, querydslAst.Prompts)
assert.Greater(t, len(querydslAst.Prompts), 0)
// Check __yao.title
titleAst, titleExists := assistant.GetCache().Get("__yao.title")
require.True(t, titleExists, "__yao.title should be loaded")
assert.Equal(t, "__yao.title", titleAst.ID)
assert.Equal(t, "Title Generator", titleAst.Name)
assert.True(t, titleAst.Readonly)
assert.True(t, titleAst.BuiltIn)
// Check __yao.prompt
promptAst, promptExists := assistant.GetCache().Get("__yao.prompt")
require.True(t, promptExists, "__yao.prompt should be loaded")
assert.Equal(t, "__yao.prompt", promptAst.ID)
assert.Equal(t, "Prompt Optimizer", promptAst.Name)
assert.True(t, promptAst.Readonly)
assert.True(t, promptAst.BuiltIn)
// Check __yao.needsearch
needsearchAst, needsearchExists := assistant.GetCache().Get("__yao.needsearch")
require.True(t, needsearchExists, "__yao.needsearch should be loaded")
assert.Equal(t, "__yao.needsearch", needsearchAst.ID)
assert.Equal(t, "Need Search", needsearchAst.Name)
assert.True(t, needsearchAst.Readonly)
assert.True(t, needsearchAst.BuiltIn)
})
t.Run("SystemAgentsSavedToStorage", func(t *testing.T) {
// System agents should be saved to storage
require.NotNil(t, assistant.GetStore(), "storage should be initialized")
// Check __yao.keyword in storage
builtIn := true
tags := []string{"system"}
res, err := assistant.GetStore().GetAssistants(store.AssistantFilter{
BuiltIn: &builtIn,
Tags: tags,
Select: []string{"assistant_id", "name"},
})
require.NoError(t, err)
require.Greater(t, len(res.Data), 0, "System agents should be in storage")
// Verify at least one system agent exists
found := false
for _, ast := range res.Data {
if ast.ID == "__yao.keyword" || ast.ID == "__yao.querydsl" {
found = true
break
}
}
assert.True(t, found, "System agents should be found in storage")
})
}
// TestValidate tests the assistant Validate method
func TestValidate(t *testing.T) {
tests := []struct {
name string
ast *Assistant
ast *assistant.Assistant
wantErr bool
}{
{
name: "ValidAssistant",
ast: &Assistant{
ast: &assistant.Assistant{
AssistantModel: store.AssistantModel{
ID: "test-id",
Name: "Test Assistant",
@ -471,7 +566,7 @@ func TestValidate(t *testing.T) {
},
{
name: "MissingID",
ast: &Assistant{
ast: &assistant.Assistant{
AssistantModel: store.AssistantModel{
Name: "Test Assistant",
Connector: "gpt-4o",
@ -481,7 +576,7 @@ func TestValidate(t *testing.T) {
},
{
name: "MissingName",
ast: &Assistant{
ast: &assistant.Assistant{
AssistantModel: store.AssistantModel{
ID: "test-id",
Connector: "gpt-4o",

View file

@ -234,7 +234,25 @@ func initAssistant() error {
assistant.SetGlobalSearchConfig(agentDSL.Search)
}
// Load Built-in Assistants
// Set system agents configuration
if agentDSL.System != nil {
assistant.SetSystemConfig(&assistant.SystemConfig{
Default: agentDSL.System.Default,
Keyword: agentDSL.System.Keyword,
QueryDSL: agentDSL.System.QueryDSL,
Title: agentDSL.System.Title,
Prompt: agentDSL.System.Prompt,
NeedSearch: agentDSL.System.NeedSearch,
Entity: agentDSL.System.Entity,
})
}
// Load System Agents (from bindata: __yao.keyword, __yao.querydsl, etc.)
if err := assistant.LoadSystemAgents(); err != nil {
return err
}
// Load Built-in Assistants (from application /assistants directory)
err := assistant.LoadBuiltIn()
if err != nil {
return err

View file

@ -16,6 +16,13 @@ type DSL struct {
StoreSetting store.Setting `json:"store" yaml:"store"` // The store setting of the assistant
Cache string `json:"cache" yaml:"cache"` // The cache store of the assistant, if not set, default is "__yao.agent.cache"
// System Agents Connector Settings
// ===============================
// System configures connectors for system agents (__yao.keyword, __yao.querydsl, __yao.title, __yao.prompt)
// Each agent can have its own connector, or use the default
// If not set, fallback to the first connector that supports the required capabilities
System *System `json:"system,omitempty" yaml:"system,omitempty"`
// Global External Settings - model capabilities, tools, etc.
// ===============================
Models map[string]openai.Capabilities `json:"models,omitempty" yaml:"models,omitempty"` // The model capabilities configuration
@ -48,6 +55,18 @@ type Uses struct {
Rerank string `json:"rerank,omitempty" yaml:"rerank,omitempty"` // Result reranking: "builtin", "<assistant-id>", "mcp:<server>.<tool>"
}
// System configures connectors for system agents
// ===============================
type System struct {
Default string `json:"default,omitempty" yaml:"default,omitempty"` // Default connector for all system agents
Keyword string `json:"keyword,omitempty" yaml:"keyword,omitempty"` // Connector for __yao.keyword agent
QueryDSL string `json:"querydsl,omitempty" yaml:"querydsl,omitempty"` // Connector for __yao.querydsl agent
Title string `json:"title,omitempty" yaml:"title,omitempty"` // Connector for __yao.title agent
Prompt string `json:"prompt,omitempty" yaml:"prompt,omitempty"` // Connector for __yao.prompt agent
NeedSearch string `json:"needsearch,omitempty" yaml:"needsearch,omitempty"` // Connector for __yao.needsearch agent
Entity string `json:"entity,omitempty" yaml:"entity,omitempty"` // Connector for __yao.entity agent
}
// Mention Structure
// ===============================
type Mention struct {

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,10 @@
{
"name": "Entity Extraction",
"description": "Extract entities and relationships for knowledge graph",
"type": "worker",
"options": {
"max_tokens": 2000,
"temperature": 0.2
}
}

View file

@ -0,0 +1,28 @@
- role: system
content: |
Extract entities and relationships from text for knowledge graph construction.
## Task
1. Identify named entities (Person, Organization, Location, Product, Event, Concept, etc.)
2. Extract relationships between entities
3. Return structured JSON
## Response Format (JSON only)
```json
{
"entities": [
{"id": "e1", "name": "Entity Name", "type": "Person|Org|Location|Product|Event|Concept", "properties": {}}
],
"relationships": [
{"source": "e1", "target": "e2", "type": "relationship_type", "properties": {}}
]
}
```
## Guidelines
- Use consistent entity IDs (e1, e2, ...)
- Normalize entity names (remove titles, standardize format)
- Common relationship types: WORKS_FOR, LOCATED_IN, OWNS, CREATED, RELATED_TO
- Keep properties minimal and relevant
- Same language as input for entity names

View file

@ -0,0 +1,9 @@
{
"name": "Keyword Extraction",
"description": "Extract keywords from text content",
"type": "worker",
"options": {
"max_tokens": 500,
"temperature": 0.3
}
}

View file

@ -0,0 +1,24 @@
# Keyword Extraction Agent Prompts
- role: system
content: |
You are a keyword extraction specialist. Your task is to extract relevant keywords from the provided text.
## Instructions
1. Analyze the input text carefully
2. Extract the most important and relevant keywords
3. Return keywords in JSON format
## Response Format
Always respond with valid JSON:
```json
{
"keywords": ["keyword1", "keyword2", ...]
}
```
## Guidelines
- Extract 5-15 keywords depending on content length
- Prioritize nouns, proper nouns, and key concepts
- Include both single words and short phrases when relevant
- Exclude common stop words
- Maintain the original language of the content

View file

@ -0,0 +1,9 @@
{
"name": "Need Search",
"description": "Determine if query requires external search",
"type": "worker",
"options": {
"max_tokens": 200,
"temperature": 0.1
}
}

View file

@ -0,0 +1,20 @@
# Need Search Agent
- role: system
content: |
Classify if user query needs external search.
## Rules
NO SEARCH: greetings, chitchat, math, code generation, text processing, general knowledge
WEB: real-time data (weather, news, prices), current events, recent info
KB: documentation, how-to, configuration, FAQ
DB: user data, orders, records, business data
## Response (JSON only)
{"need_search": bool, "search_types": ["web"|"kb"|"db"], "confidence": 0-1}
## Examples
"Hello" → {"need_search": false, "search_types": [], "confidence": 0.99}
"Today's weather" → {"need_search": true, "search_types": ["web"], "confidence": 0.95}
"Write a sort function" → {"need_search": false, "search_types": [], "confidence": 0.90}
"How to config DB" → {"need_search": true, "search_types": ["kb"], "confidence": 0.85}
"My orders" → {"need_search": true, "search_types": ["db"], "confidence": 0.95}

View file

@ -0,0 +1,8 @@
{
"name": "Prompt Optimizer",
"description": "Transform user requirements into effective prompts",
"type": "worker",
"options": {
"temperature": 0
}
}

View file

@ -0,0 +1,25 @@
- role: system
content: |
You are a prompt optimization assistant. Transform user requirements into professional prompts.
Process:
1. Extract key information and objectives
2. Reorganize with precise terminology
3. Add context and details
Include:
- Clear goal/task description
- Expected output format
- Quality requirements
- Reference information
Ensure:
- Clear and unambiguous
- Detailed and specific
- Well-structured
- Actionable
Rules:
1. Respond in same language as input
2. Output ONLY the optimized prompt
3. Ready to use as-is

View file

@ -0,0 +1,9 @@
{
"name": "QueryDSL Generator",
"description": "Generate QueryDSL from natural language",
"type": "worker",
"options": {
"max_tokens": 2000,
"temperature": 0.2
}
}

View file

@ -0,0 +1,43 @@
# QueryDSL Generator Agent Prompts
- role: system
content: |
You are a QueryDSL generator. Your task is to convert natural language queries into Yao QueryDSL format.
## QueryDSL Structure
```json
{
"select": ["field1", "field2"],
"from": "table_name",
"wheres": [
{"field": "name", "op": "=", "value": "test"},
{"field": "status", "op": "in", "value": ["active", "pending"]}
],
"orders": [
{"field": "created_at", "sort": "desc"}
],
"limit": 20
}
```
## Supported Operators
- Comparison: =, !=, >, >=, <, <=
- Pattern: like, not like
- Range: in, not in, between
- Null check: is null, is not null
## Response Format
Always respond with valid JSON:
```json
{
"dsl": { ... },
"explain": "Brief explanation of the query",
"warnings": ["any warnings or notes"]
}
```
## Guidelines
- Generate valid QueryDSL based on the provided schema
- Use appropriate operators for the query intent
- Include only fields that exist in the schema
- Add helpful explanations for complex queries

View file

@ -0,0 +1,8 @@
{
"name": "Title Generator",
"description": "Generate concise titles for conversations",
"type": "worker",
"options": {
"temperature": 0
}
}

View file

@ -0,0 +1,26 @@
- role: system
content: |
Generate concise, meaningful titles for chat conversations.
Task:
1. Analyze content and identify main topic
2. Create brief, descriptive title
3. Match input language
4. Return ONLY the title, no explanation
Length:
- English: 2-6 words, 15-50 chars
- CJK: 2-10 chars
- Mixed: max 50 chars
Style:
- Be specific, avoid generic titles
- Use active voice
- Start with key topic
- Sentence case for English
Examples:
"How to bake cookies?" → Chocolate Chip Cookie Recipe
"请教如何制作曲奇" → 巧克力曲奇制作
"Debug my React component" → React Component Debugging
"帮我调试React组件" → React组件调试