Add provider management functionality and enhance schema retrieval

- Implemented GetProviders and GetProvider methods to retrieve providers based on type and ID, improving the knowledge base's provider management capabilities.
- Enhanced the Schema methods for chunking providers to retrieve schemas from bindata, ensuring better integration with the overall system.
- Updated OpenAPI routes to include endpoints for provider management, allowing for easier access to provider information via HTTP requests.
This commit is contained in:
Max 2025-08-09 11:23:51 +08:00
parent 08c83e09e6
commit 0251bdc8d7
18 changed files with 1242 additions and 107 deletions

File diff suppressed because one or more lines are too long

137
kb/kb.go
View file

@ -1,7 +1,9 @@
package kb
import (
"fmt"
"path/filepath"
"slices"
"github.com/yaoapp/gou/application"
"github.com/yaoapp/gou/graphrag"
@ -73,3 +75,138 @@ func Load(appConfig config.Config) (*KnowledgeBase, error) {
Instance = instance
return instance, nil
}
// GetProviders returns all providers
func GetProviders(typ string, ids []string, locale string) ([]kbtypes.Provider, error) {
if Instance == nil {
return nil, fmt.Errorf("knowledge base not initialized")
}
// Get the providers from the instance
knowledgeBase, ok := Instance.(*KnowledgeBase)
if !ok {
return nil, fmt.Errorf("knowledge base not initialized")
}
// Get the configuration
conf := knowledgeBase.Config
if conf == nil {
return nil, fmt.Errorf("configuration not found")
}
providers := []*kbtypes.Provider{}
switch typ {
case "chunking":
providers = conf.Chunkings
case "converter":
providers = conf.Converters
case "embedding":
providers = conf.Embeddings
case "extractor":
providers = conf.Extractors
case "fetcher":
providers = conf.Fetchers
case "searcher":
providers = conf.Searchers
case "reranker":
providers = conf.Rerankers
case "vote":
providers = conf.Votes
case "weight":
providers = conf.Weights
case "score":
providers = conf.Scores
default:
return nil, fmt.Errorf("invalid provider type: %s", typ)
}
// Filter empty ids
filteredIds := []string{}
for _, id := range ids {
if id != "" {
filteredIds = append(filteredIds, id)
}
}
// Filter the providers by ids
filteredProviders := []kbtypes.Provider{}
for _, provider := range providers {
if len(filteredIds) == 0 || slices.Contains(ids, provider.ID) {
filteredProviders = append(filteredProviders, *provider)
}
}
return filteredProviders, nil
}
// GetProvider returns a provider by id
func GetProvider(typ string, id string) (*kbtypes.Provider, error) {
if Instance == nil {
return nil, fmt.Errorf("knowledge base not initialized")
}
knowledgeBase, ok := Instance.(*KnowledgeBase)
if !ok {
return nil, fmt.Errorf("knowledge base not initialized")
}
conf := knowledgeBase.Config
if conf == nil {
return nil, fmt.Errorf("configuration not found")
}
providers := []*kbtypes.Provider{}
switch typ {
case "chunking":
providers = conf.Chunkings
case "converter":
providers = conf.Converters
case "embedding":
providers = conf.Embeddings
case "extractor":
providers = conf.Extractors
case "fetcher":
providers = conf.Fetchers
case "searcher":
providers = conf.Searchers
case "reranker":
providers = conf.Rerankers
case "vote":
providers = conf.Votes
case "weight":
providers = conf.Weights
case "score":
providers = conf.Scores
default:
return nil, fmt.Errorf("invalid provider type: %s", typ)
}
// Find the provider by id
for _, provider := range providers {
if provider.ID == id {
return provider, nil
}
}
return nil, fmt.Errorf("provider %s not found", id)
}

View file

@ -96,7 +96,7 @@ func (s *Structured) Options(option *kbtypes.ProviderOption) (*types.ChunkingOpt
// Schema returns the schema for the structured chunking provider
func (s *Structured) Schema(provider *kbtypes.Provider, locale string) (*kbtypes.ProviderSchema, error) {
return nil, nil
return factory.GetSchemaFromBindata(factory.ProviderTypeChunking, "structured", locale)
}
// === Semantic Chunking ===
@ -241,5 +241,5 @@ func (s *Semantic) Options(option *kbtypes.ProviderOption) (*types.ChunkingOptio
// Schema returns the schema for the semantic chunking provider
func (s *Semantic) Schema(provider *kbtypes.Provider, locale string) (*kbtypes.ProviderSchema, error) {
return nil, nil
return factory.GetSchemaFromBindata(factory.ProviderTypeChunking, "semantic", locale)
}

View file

@ -0,0 +1,91 @@
package factory
import (
"fmt"
"regexp"
"strings"
jsoniter "github.com/json-iterator/go"
"github.com/yaoapp/yao/data"
kbtypes "github.com/yaoapp/yao/kb/types"
)
// GetSchemaFromBindata reads the schema from bindata
func GetSchemaFromBindata(typ ProviderType, name string, locale string) (*kbtypes.ProviderSchema, error) {
local := strings.ToLower(locale)
if local == "" {
local = "en"
}
// Read the schema from bindata
raw, err := data.Asset("yao/data/kb/providers/" + string(typ) + "/" + name + "/" + local + ".json")
if err != nil {
// fallback to en
raw, err = data.Asset("yao/data/kb/providers/" + string(typ) + "/" + name + "/en.json")
if err != nil {
return nil, err
}
}
// Replace the {{ $limit... }} with the actual limit values
raw, err = replaceVars(raw, map[string]interface{}{
"limit.max_concurrent": 10,
"limit.task.max_concurrent": 10,
})
if err != nil {
return nil, err
}
schema := &kbtypes.ProviderSchema{}
if err := jsoniter.Unmarshal(raw, schema); err != nil {
return nil, err
}
return schema, nil
}
// ReplaceVars replaces the variables in the raw data {{ $... }}
func replaceVars(raw []byte, vars map[string]interface{}) ([]byte, error) {
result := string(raw)
// Regular expressions to match quoted and unquoted variables
// Match "{{ $variable }}" (quoted) or {{ $variable }} (unquoted)
var regQuoted = regexp.MustCompile(`"\{\{\s*\$([a-zA-Z_][a-zA-Z0-9_.]*)\s*\}\}"`)
var regUnquoted = regexp.MustCompile(`\{\{\s*\$([a-zA-Z_][a-zA-Z0-9_.]*)\s*\}\}`)
// First, process quoted variables
quotedMatches := regQuoted.FindAllStringSubmatch(result, -1)
for _, match := range quotedMatches {
fullMatch := match[0] // Full match, e.g. "{{ $limit.max_concurrent }}"
varName := match[1] // Variable name, e.g. "limit.max_concurrent"
if value, exists := vars[varName]; exists {
// For quoted variables, replace the entire quoted part with JSON-encoded value
valueBytes, err := jsoniter.Marshal(value)
if err != nil {
return nil, fmt.Errorf("failed to marshal variable %s: %v", varName, err)
}
replacement := string(valueBytes)
result = strings.ReplaceAll(result, fullMatch, replacement)
}
}
// Then, process unquoted variables
unquotedMatches := regUnquoted.FindAllStringSubmatch(result, -1)
for _, match := range unquotedMatches {
fullMatch := match[0] // Full match, e.g. {{ $limit.max_concurrent }}
varName := match[1] // Variable name, e.g. "limit.max_concurrent"
if value, exists := vars[varName]; exists {
// For unquoted variables, also use JSON encoding
valueBytes, err := jsoniter.Marshal(value)
if err != nil {
return nil, fmt.Errorf("failed to marshal variable %s: %v", varName, err)
}
replacement := string(valueBytes)
result = strings.ReplaceAll(result, fullMatch, replacement)
}
}
return []byte(result), nil
}

View file

@ -57,4 +57,8 @@ func Attach(group *gin.RouterGroup, oauth types.OAuth) {
// Collection Backup and Restore
group.POST("/collections/:collectionID/backup", Backup)
group.POST("/collections/:collectionID/restore", Restore)
// Provider Management (Chunking, Converter, Embedding, Extractor, Fetcher ...)
group.GET("/providers/:providerType", GetProviders)
group.GET("/providers/:providerType/:providerID/schema", GetProviderSchema)
}

86
openapi/kb/provider.go Normal file
View file

@ -0,0 +1,86 @@
package kb
import (
"strings"
"github.com/gin-gonic/gin"
"github.com/yaoapp/yao/kb"
"github.com/yaoapp/yao/kb/providers/factory"
"github.com/yaoapp/yao/openapi/response"
)
// GetProviders get all providers
func GetProviders(c *gin.Context) {
providerType := c.Param("providerType")
locale := c.Query("locale")
if locale == "" {
locale = "en"
}
if providerType == "" {
// Create a custom error with the same structure but specific message
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "Invalid request format: providerType is required",
}
response.RespondWithError(c, response.StatusBadRequest, errorResp)
return
}
// Filter providers by ids
ids := strings.Split(c.Query("ids"), ",")
providers, err := kb.GetProviders(providerType, ids, locale)
if err != nil {
// Create a custom error with the same structure but specific message
errorResp := &response.ErrorResponse{
Code: response.ErrServerError.Code,
ErrorDescription: "Failed to get providers: " + err.Error(),
}
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
return
}
// response with success
response.RespondWithSuccess(c, response.StatusOK, providers)
}
// GetProviderSchema get provider schema
func GetProviderSchema(c *gin.Context) {
providerType := c.Param("providerType")
providerID := c.Param("providerID")
locale := c.Query("locale")
if locale == "" {
locale = "en"
}
if providerType == "" || providerID == "" {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "Invalid request format: providerType and providerID are required",
}
response.RespondWithError(c, response.StatusBadRequest, errorResp)
return
}
provider, err := kb.GetProvider(providerType, providerID)
if err != nil {
errorResp := &response.ErrorResponse{
Code: response.ErrServerError.Code,
ErrorDescription: "Failed to get provider: " + err.Error(),
}
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
return
}
schema, err := factory.GetSchema(factory.ProviderType(providerType), provider, locale)
if err != nil {
errorResp := &response.ErrorResponse{
Code: response.ErrServerError.Code,
ErrorDescription: "Failed to get provider schema: " + err.Error(),
}
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
return
}
response.RespondWithSuccess(c, response.StatusOK, schema)
}

View file

@ -0,0 +1,176 @@
{
"id": "__yao.semantic",
"title": "Smart Text Splitting",
"description": "AI-powered intelligent document splitting that understands content meaning and context. Uses large language models to split text at natural topic boundaries, preserving subject coherence and logical flow for better search retrieval quality.",
"required": ["size", "overlap", "max_depth"],
"properties": {
"size": {
"type": "integer",
"title": "Segment Size",
"description": "Target characters per text segment.",
"default": 300,
"minimum": 50,
"maximum": 4000,
"component": "InputNumber",
"width": "half",
"order": 1
},
"overlap": {
"type": "integer",
"title": "Overlap",
"description": "Overlapping characters between adjacent segments.",
"default": 50,
"minimum": 0,
"maximum": 1000,
"component": "InputNumber",
"width": "half",
"order": 2
},
"max_depth": {
"type": "integer",
"title": "Max Depth",
"description": "Maximum hierarchy depth to traverse for splitting.",
"default": 3,
"minimum": 1,
"maximum": 10,
"component": "InputNumber",
"width": "half",
"order": 3
},
"size_multiplier": {
"type": "integer",
"title": "Size Multiplier",
"description": "Multiplier to adjust effective size at deeper levels.",
"default": 3,
"minimum": 1,
"maximum": 10,
"component": "InputNumber",
"width": "half",
"order": 4
},
"max_concurrent": {
"type": "integer",
"title": "Max Concurrent",
"description": "Parallelism when splitting documents.",
"default": 1,
"minimum": 1,
"maximum": "{{ $limit.max_concurrent }}",
"component": "InputNumber",
"width": "half",
"order": 5
},
"semantic": {
"type": "object",
"title": "Semantic Options",
"description": "Advanced options for semantic model calls.",
"component": "Nested",
"order": 6,
"required": false,
"requiredFields": ["connector", "context_size"],
"properties": {
"connector": {
"type": "string",
"title": "Connector",
"description": "Connector ID for the AI model (e.g. openai.gpt-4o-mini, deepseek.v3).",
"default": "",
"component": "Select",
"enum": [
{
"groupLabel": "OpenAI Models",
"options": [
{
"label": "GPT-4o Mini",
"value": "openai.gpt-4o-mini",
"description": "Fast and cost-effective, best for most use cases",
"default": true
},
{
"label": "GPT-4o",
"value": "openai.gpt-4o",
"description": "Highest quality AI splitting for complex documents"
}
]
},
{
"groupLabel": "Alternative Models",
"options": [
{
"label": "Deepseek V3",
"value": "deepseek.v3",
"description": "Cost-effective alternative with good performance"
},
{
"label": "Claude 3.5 Sonnet",
"value": "anthropic.claude-3-5-sonnet",
"description": "Excellent reasoning and context understanding"
}
]
}
],
"width": "half",
"order": 1
},
"toolcall": {
"type": "boolean",
"title": "Enable Tool Call",
"description": "Allow tool calls during semantic analysis.",
"default": false,
"component": "Switch",
"width": "half",
"order": 2
},
"context_size": {
"type": "integer",
"title": "Context Size",
"description": "Approximate characters provided to the model for context (defaults to size * 6).",
"default": 1800,
"minimum": 200,
"maximum": 32000,
"component": "InputNumber",
"width": "half",
"order": 3
},
"options": {
"type": "string",
"title": "Model Options (JSON)",
"description": "Optional model-specific options in JSON string.",
"default": "",
"component": "CodeEditor",
"width": "full",
"order": 4
},
"prompt": {
"type": "string",
"title": "Custom Prompt",
"description": "Override default prompting for semantic splitting.",
"default": "",
"component": "TextArea",
"width": "full",
"order": 5
},
"max_retry": {
"type": "integer",
"title": "Max Retry",
"description": "Maximum retries for model calls.",
"default": 3,
"minimum": 0,
"maximum": 10,
"component": "InputNumber",
"width": "half",
"order": 6
},
"semantic_max_concurrent": {
"type": "integer",
"title": "Semantic Max Concurrent",
"description": "Parallelism for semantic model calls.",
"default": 1,
"minimum": 1,
"maximum": "{{ $limit.task.max_concurrent }}",
"component": "InputNumber",
"width": "half",
"order": 7
}
}
}
}
}

View file

@ -0,0 +1,176 @@
{
"id": "__yao.semantic",
"title": "智能文本分割",
"description": "AI驱动的智能文档分割能够理解内容含义和上下文。使用大语言模型在自然主题边界处分割文本保持主题连贯性和逻辑流程提供更好的搜索检索质量。",
"required": ["size", "overlap", "max_depth"],
"properties": {
"size": {
"type": "integer",
"title": "片段大小",
"description": "每个片段的目标字符数。",
"default": 300,
"minimum": 50,
"maximum": 4000,
"component": "InputNumber",
"width": "half",
"order": 1
},
"overlap": {
"type": "integer",
"title": "重叠字符",
"description": "相邻片段间的重叠字符数。",
"default": 50,
"minimum": 0,
"maximum": 1000,
"component": "InputNumber",
"width": "half",
"order": 2
},
"max_depth": {
"type": "integer",
"title": "最大深度",
"description": "分割时遍历的最大层级深度。",
"default": 3,
"minimum": 1,
"maximum": 10,
"component": "InputNumber",
"width": "half",
"order": 3
},
"size_multiplier": {
"type": "integer",
"title": "大小倍数",
"description": "调整更深层级有效大小的倍数。",
"default": 3,
"minimum": 1,
"maximum": 10,
"component": "InputNumber",
"width": "half",
"order": 4
},
"max_concurrent": {
"type": "integer",
"title": "最大并发数",
"description": "分割文档时的并行度。",
"default": 1,
"minimum": 1,
"maximum": "{{ $limit.max_concurrent }}",
"component": "InputNumber",
"width": "half",
"order": 5
},
"semantic": {
"type": "object",
"title": "语义选项",
"description": "语义模型调用的高级选项。",
"component": "Nested",
"order": 6,
"required": false,
"requiredFields": ["connector", "context_size"],
"properties": {
"connector": {
"type": "string",
"title": "连接器",
"description": "AI模型的连接器ID例如openai.gpt-4o-mini, deepseek.v3。",
"default": "",
"component": "Select",
"enum": [
{
"groupLabel": "OpenAI 模型",
"options": [
{
"label": "GPT-4o Mini",
"value": "openai.gpt-4o-mini",
"description": "快速且经济,适合大多数使用场景",
"default": true
},
{
"label": "GPT-4o",
"value": "openai.gpt-4o",
"description": "最高质量的AI分割适用于复杂文档"
}
]
},
{
"groupLabel": "替代模型",
"options": [
{
"label": "Deepseek V3",
"value": "deepseek.v3",
"description": "经济实惠的替代方案,性能良好"
},
{
"label": "Claude 3.5 Sonnet",
"value": "anthropic.claude-3-5-sonnet",
"description": "出色的推理和上下文理解能力"
}
]
}
],
"width": "half",
"order": 1
},
"toolcall": {
"type": "boolean",
"title": "启用工具调用",
"description": "在语义分析过程中允许工具调用。",
"default": false,
"component": "Switch",
"width": "half",
"order": 2
},
"context_size": {
"type": "integer",
"title": "上下文大小",
"description": "提供给模型的近似字符数上下文默认为大小的6倍。",
"default": 1800,
"minimum": 200,
"maximum": 32000,
"component": "InputNumber",
"width": "half",
"order": 3
},
"options": {
"type": "string",
"title": "模型选项JSON",
"description": "可选的模型特定选项JSON字符串格式。",
"default": "",
"component": "CodeEditor",
"width": "full",
"order": 4
},
"prompt": {
"type": "string",
"title": "自定义提示词",
"description": "覆盖语义分割的默认提示词。",
"default": "",
"component": "TextArea",
"width": "full",
"order": 5
},
"max_retry": {
"type": "integer",
"title": "最大重试次数",
"description": "模型调用的最大重试次数。",
"default": 3,
"minimum": 0,
"maximum": 10,
"component": "InputNumber",
"width": "half",
"order": 6
},
"semantic_max_concurrent": {
"type": "integer",
"title": "语义最大并发数",
"description": "语义模型调用的并行度。",
"default": 1,
"minimum": 1,
"maximum": "{{ $limit.task.max_concurrent }}",
"component": "InputNumber",
"width": "half",
"order": 7
}
}
}
}
}

View file

@ -0,0 +1,82 @@
{
"id": "__yao.structured",
"title": "Hierarchical Text Splitting",
"description": "Splits documents into multiple layers of text segments with configurable sizes and overlaps. Creates parent-child relationships between segments at different depths, allowing for both detailed and broader context retrieval in search applications.",
"required": ["size", "overlap", "max_depth"],
"properties": {
"size": {
"type": "integer",
"title": "Segment Size",
"description": "Target characters per text segment.",
"default": 300,
"minimum": 50,
"maximum": 4000,
"component": "InputNumber",
"width": "half",
"order": 1
},
"overlap": {
"type": "integer",
"title": "Overlap",
"description": "Overlapping characters between adjacent segments.",
"default": 20,
"minimum": 0,
"maximum": 1000,
"component": "InputNumber",
"width": "half",
"order": 2
},
"max_depth": {
"type": "integer",
"title": "Max Depth",
"description": "Maximum hierarchy depth to traverse for splitting.",
"default": 3,
"minimum": 1,
"maximum": 10,
"component": "InputNumber",
"width": "half",
"order": 3
},
"separator": {
"type": "string",
"title": "Custom Separator",
"description": "Custom separator pattern (regex supported).",
"default": "",
"placeholder": "e.g. \\n\\n, ---",
"component": "Input",
"width": "half",
"order": 4
},
"enable_debug": {
"type": "boolean",
"title": "Enable Debug Mode",
"description": "Output detailed splitting information for debugging.",
"default": false,
"component": "Switch",
"width": "half",
"order": 5
},
"size_multiplier": {
"type": "integer",
"title": "Size Multiplier",
"description": "Multiplier to adjust effective size at deeper levels.",
"default": 3,
"minimum": 1,
"maximum": 10,
"component": "InputNumber",
"width": "half",
"order": 6
},
"max_concurrent": {
"type": "integer",
"title": "Max Concurrent",
"description": "Parallelism when splitting documents.",
"default": 1,
"minimum": 1,
"maximum": "{{ $limit.max_concurrent }}",
"component": "InputNumber",
"width": "half",
"order": 7
}
}
}

View file

@ -0,0 +1,82 @@
{
"id": "__yao.structured",
"title": "分层文本分割",
"description": "将文档分割为多层文本片段,可配置大小和重叠度。在不同深度的片段间创建父子关系,支持在搜索应用中进行详细和宽泛的上下文检索。",
"required": ["size", "overlap", "max_depth"],
"properties": {
"size": {
"type": "integer",
"title": "片段大小",
"description": "每个片段的目标字符数。",
"default": 300,
"minimum": 50,
"maximum": 4000,
"component": "InputNumber",
"width": "half",
"order": 1
},
"overlap": {
"type": "integer",
"title": "重叠字符",
"description": "相邻片段间的重叠字符数。",
"default": 20,
"minimum": 0,
"maximum": 1000,
"component": "InputNumber",
"width": "half",
"order": 2
},
"max_depth": {
"type": "integer",
"title": "最大深度",
"description": "分割时遍历的最大层级深度。",
"default": 3,
"minimum": 1,
"maximum": 10,
"component": "InputNumber",
"width": "half",
"order": 3
},
"separator": {
"type": "string",
"title": "自定义分隔符",
"description": "自定义分隔符模式(支持正则表达式)。",
"default": "",
"placeholder": "例如:\\n\\n, ---",
"component": "Input",
"width": "half",
"order": 4
},
"enable_debug": {
"type": "boolean",
"title": "启用调试模式",
"description": "输出详细的分割信息用于调试。",
"default": false,
"component": "Switch",
"width": "half",
"order": 5
},
"size_multiplier": {
"type": "integer",
"title": "大小倍数",
"description": "调整更深层级有效大小的倍数。",
"default": 3,
"minimum": 1,
"maximum": 10,
"component": "InputNumber",
"width": "half",
"order": 6
},
"max_concurrent": {
"type": "integer",
"title": "最大并发数",
"description": "分割文档时的并行度。",
"default": 1,
"minimum": 1,
"maximum": "{{ $limit.max_concurrent }}",
"component": "InputNumber",
"width": "half",
"order": 7
}
}
}

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1 @@
{}

View file

View file

@ -0,0 +1 @@
{}