feat: Refactor tenant configuration management to use KV API

- Updated tenant web search and agent configuration endpoints to utilize a KV-style API for better flexibility.
- Enhanced the tenant handler to support generic key-value operations for retrieving and updating configurations.
- Improved frontend API calls to align with the new KV API structure, ensuring consistent data handling.
- Removed deprecated direct configuration endpoints for a cleaner and more maintainable codebase.
This commit is contained in:
wizardchen
2025-11-10 20:11:45 +08:00
parent 07a8c1de4c
commit ac8c8e9af2
8 changed files with 201 additions and 178 deletions

View File

@@ -38,9 +38,9 @@ export function getSystemInfo(): Promise<{ data: SystemInfo }> {
} }
export function getAgentConfig(): Promise<{ data: AgentConfig }> { export function getAgentConfig(): Promise<{ data: AgentConfig }> {
return get('/api/v1/tenants/agent-config') return get('/api/v1/tenants/kv/agent-config')
} }
export function updateAgentConfig(config: AgentConfig): Promise<{ data: AgentConfig }> { export function updateAgentConfig(config: AgentConfig): Promise<{ data: AgentConfig }> {
return put('/api/v1/tenants/agent-config', config) return put('/api/v1/tenants/kv/agent-config', config)
} }

View File

@@ -29,13 +29,13 @@ export function getWebSearchProviders() {
return get('/api/v1/web-search/providers') return get('/api/v1/web-search/providers')
} }
// Get tenant web search config // Get tenant web search config via KV API
export function getTenantWebSearchConfig() { export function getTenantWebSearchConfig() {
return get('/api/v1/tenants/web-search-config') return get('/api/v1/tenants/kv/web-search-config')
} }
// Update tenant web search config // Update tenant web search config via KV API
export function updateTenantWebSearchConfig(config: WebSearchConfig) { export function updateTenantWebSearchConfig(config: WebSearchConfig) {
return put('/api/v1/tenants/web-search-config', config) return put('/api/v1/tenants/kv/web-search-config', config)
} }

View File

@@ -31,11 +31,6 @@
<div class="provider-option-wrapper"> <div class="provider-option-wrapper">
<div class="provider-option"> <div class="provider-option">
<span class="provider-name">{{ provider.name }}</span> <span class="provider-name">{{ provider.name }}</span>
<div class="provider-tags">
<t-tag v-if="provider.free" size="small" theme="success">免费</t-tag>
<t-tag v-else size="small" theme="warning">付费</t-tag>
<t-tag v-if="provider.requires_api_key" size="small" theme="default">需API密钥</t-tag>
</div>
</div> </div>
<div v-if="provider.description" class="provider-desc"> <div v-if="provider.description" class="provider-desc">
{{ provider.description }} {{ provider.description }}
@@ -401,5 +396,35 @@ onMounted(async () => {
line-height: 1.4; line-height: 1.4;
margin-top: 2px; margin-top: 2px;
} }
/* 修复下拉项描述与条目重叠:让选项支持多行自适应高度 */
:deep(.t-select-option) {
height: auto;
align-items: flex-start;
padding-top: 6px;
padding-bottom: 6px;
}
:deep(.t-select-option__content) {
white-space: normal;
}
</style>
<style lang="less">
.t-select__dropdown .t-select-option {
height: auto;
align-items: flex-start;
padding-top: 6px;
padding-bottom: 6px;
}
.t-select__dropdown .t-select-option__content {
white-space: normal;
}
.t-select__dropdown .provider-option-wrapper {
display: flex;
flex-direction: column;
gap: 4px;
padding: 2px 0;
}
</style> </style>

View File

@@ -5,6 +5,12 @@ import (
"strings" "strings"
) )
const (
DefaultAgentTemperature = 0.7
DefaultAgentMaxIterations = 20
DefaultAgentReflectionEnabled = false
)
// formatFileSize formats file size in human-readable format // formatFileSize formats file size in human-readable format
func formatFileSize(size int64) string { func formatFileSize(size int64) string {
const ( const (
@@ -42,6 +48,24 @@ type KnowledgeBaseInfo struct {
RecentDocs []RecentDocInfo // Recently added documents (up to 10) RecentDocs []RecentDocInfo // Recently added documents (up to 10)
} }
// PlaceholderDefinition defines a placeholder exposed to UI/configuration
type PlaceholderDefinition struct {
Name string `json:"name"`
Label string `json:"label"`
Description string `json:"description"`
}
// AvailablePlaceholders lists all supported prompt placeholders for UI hints
func AvailablePlaceholders() []PlaceholderDefinition {
return []PlaceholderDefinition{
{
Name: "knowledge_bases",
Label: "知识库列表",
Description: "自动格式化为表格形式的知识库列表,包含知识库名称、描述、文档数量、最近添加的文档等信息",
},
}
}
// formatKnowledgeBaseList formats knowledge base information for the prompt // formatKnowledgeBaseList formats knowledge base information for the prompt
func formatKnowledgeBaseList(kbInfos []*KnowledgeBaseInfo) string { func formatKnowledgeBaseList(kbInfos []*KnowledgeBaseInfo) string {
if len(kbInfos) == 0 { if len(kbInfos) == 0 {

View File

@@ -0,0 +1,35 @@
package tools
// AvailableTool defines a simple tool metadata used by settings APIs.
type AvailableTool struct {
Name string `json:"name"`
Label string `json:"label"`
Description string `json:"description"`
}
// AvailableToolDefinitions returns the list of tools exposed to the UI.
// Keep this in sync with registered tools in this package.
func AvailableToolDefinitions() []AvailableTool {
return []AvailableTool{
{Name: "thinking", Label: "思考", Description: "AI 进行深度思考和推理"},
{Name: "todo_write", Label: "制定计划", Description: "为复杂任务制定执行计划"},
{Name: "knowledge_search", Label: "知识搜索", Description: "在知识库中搜索相关信息"},
{Name: "get_related_chunks", Label: "获取相关片段", Description: "查找相关的知识片段"},
{Name: "query_knowledge_graph", Label: "查询知识图谱", Description: "从知识图谱中查询关系"},
{Name: "get_document_info", Label: "获取文档信息", Description: "查看文档元数据"},
{Name: "database_query", Label: "查询数据库", Description: "查询数据库中的信息"},
}
}
// DefaultAllowedTools returns the default allowed tools list.
func DefaultAllowedTools() []string {
return []string{
"thinking",
"todo_write",
"knowledge_search",
"get_related_chunks",
"query_knowledge_graph",
"get_document_info",
"database_query",
}
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/Tencent/WeKnora/internal/agent" "github.com/Tencent/WeKnora/internal/agent"
agenttools "github.com/Tencent/WeKnora/internal/agent/tools"
"github.com/Tencent/WeKnora/internal/errors" "github.com/Tencent/WeKnora/internal/errors"
"github.com/Tencent/WeKnora/internal/logger" "github.com/Tencent/WeKnora/internal/logger"
"github.com/Tencent/WeKnora/internal/types" "github.com/Tencent/WeKnora/internal/types"
@@ -240,45 +241,34 @@ type AgentConfigRequest struct {
// GetTenantAgentConfig retrieves the agent configuration for a tenant // GetTenantAgentConfig retrieves the agent configuration for a tenant
// This is the global agent configuration that applies to all sessions by default // This is the global agent configuration that applies to all sessions by default
// Tenant ID is obtained from the authentication context
func (h *TenantHandler) GetTenantAgentConfig(c *gin.Context) { func (h *TenantHandler) GetTenantAgentConfig(c *gin.Context) {
ctx := c.Request.Context() ctx := c.Request.Context()
logger.Info(ctx, "Start retrieving tenant agent config") logger.Info(ctx, "Start retrieving tenant agent config")
// Get tenant ID from authentication context tenant := ctx.Value(types.TenantInfoContextKey).(*types.Tenant)
tenantID := c.GetUint(types.TenantIDContextKey.String()) if tenant == nil {
if tenantID == 0 { logger.Error(ctx, "Tenant is empty")
logger.Error(ctx, "Tenant ID is empty") c.Error(errors.NewBadRequestError("Tenant is empty"))
c.Error(errors.NewBadRequestError("Tenant ID cannot be empty"))
return return
} }
// 从 tools 包集中配置可用工具列表
tenant, err := h.service.GetTenantByID(ctx, tenantID) availableTools := make([]gin.H, 0)
if err != nil { for _, t := range agenttools.AvailableToolDefinitions() {
if appErr, ok := errors.IsAppError(err); ok { availableTools = append(availableTools, gin.H{
logger.Error(ctx, "Failed to retrieve tenant: application error", appErr) "name": t.Name,
c.Error(appErr) "label": t.Label,
} else { "description": t.Description,
logger.ErrorWithFields(ctx, err, nil) })
c.Error(errors.NewInternalServerError("Failed to retrieve tenant").WithDetails(err.Error()))
}
return
}
// 定义所有可用工具及其描述(与 internal/agent/tools 注册的工具对应)
availableTools := []gin.H{
{"name": "thinking", "label": "思考", "description": "AI 进行深度思考和推理"},
{"name": "todo_write", "label": "制定计划", "description": "为复杂任务制定执行计划"},
{"name": "knowledge_search", "label": "知识搜索", "description": "在知识库中搜索相关信息"},
{"name": "get_related_chunks", "label": "获取相关片段", "description": "查找相关的知识片段"},
{"name": "query_knowledge_graph", "label": "查询知识图谱", "description": "从知识图谱中查询关系"},
{"name": "get_document_info", "label": "获取文档信息", "description": "查看文档元数据"},
{"name": "database_query", "label": "查询数据库", "description": "查询数据库中的信息"},
} }
// 定义可用的占位符列表 // 从 agent 包获取占位符定义
availablePlaceholders := []gin.H{ availablePlaceholders := make([]gin.H, 0)
{"name": "knowledge_bases", "label": "知识库列表", "description": "自动格式化为表格形式的知识库列表,包含知识库名称、描述、文档数量、最近添加的文档等信息"}, for _, p := range agent.AvailablePlaceholders() {
availablePlaceholders = append(availablePlaceholders, gin.H{
"name": p.Name,
"label": p.Label,
"description": p.Description,
})
} }
if tenant.AgentConfig == nil { if tenant.AgentConfig == nil {
// Return default config if not set // Return default config if not set
@@ -287,10 +277,10 @@ func (h *TenantHandler) GetTenantAgentConfig(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"data": gin.H{ "data": gin.H{
"max_iterations": 20, "max_iterations": agent.DefaultAgentMaxIterations,
"reflection_enabled": false, "reflection_enabled": agent.DefaultAgentReflectionEnabled,
"allowed_tools": []string{"thinking", "todo_write", "knowledge_search", "get_related_chunks", "query_knowledge_graph", "get_document_info", "database_query"}, "allowed_tools": agenttools.DefaultAllowedTools(),
"temperature": 0.7, "temperature": agent.DefaultAgentTemperature,
"thinking_model_id": "", "thinking_model_id": "",
"rerank_model_id": "", "rerank_model_id": "",
"system_prompt": agent.DefaultReActSystemPrompt, "system_prompt": agent.DefaultReActSystemPrompt,
@@ -307,7 +297,7 @@ func (h *TenantHandler) GetTenantAgentConfig(c *gin.Context) {
systemPrompt = agent.DefaultReActSystemPrompt systemPrompt = agent.DefaultReActSystemPrompt
} }
logger.Infof(ctx, "Retrieved tenant agent config successfully, Tenant ID: %d", tenantID) logger.Infof(ctx, "Retrieved tenant agent config successfully, Tenant ID: %d", tenant.ID)
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"data": gin.H{ "data": gin.H{
@@ -325,22 +315,11 @@ func (h *TenantHandler) GetTenantAgentConfig(c *gin.Context) {
}) })
} }
// UpdateTenantAgentConfig updates the agent configuration for a tenant // updateTenantAgentConfigInternal updates the agent configuration for a tenant
// This sets the global agent configuration for all sessions in this tenant // This sets the global agent configuration for all sessions in this tenant
// Tenant ID is obtained from the authentication context func (h *TenantHandler) updateTenantAgentConfigInternal(c *gin.Context) {
func (h *TenantHandler) UpdateTenantAgentConfig(c *gin.Context) {
ctx := c.Request.Context() ctx := c.Request.Context()
logger.Info(ctx, "Start updating tenant agent config") logger.Info(ctx, "Start updating tenant agent config")
// Get tenant ID from authentication context
tenantID := c.GetUint(types.TenantIDContextKey.String())
if tenantID == 0 {
logger.Error(ctx, "Tenant ID is empty")
c.Error(errors.NewBadRequestError("Tenant ID cannot be empty"))
return
}
var req AgentConfigRequest var req AgentConfigRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
logger.Error(ctx, "Failed to parse request parameters", err) logger.Error(ctx, "Failed to parse request parameters", err)
@@ -367,15 +346,10 @@ func (h *TenantHandler) UpdateTenantAgentConfig(c *gin.Context) {
} }
// Get existing tenant // Get existing tenant
tenant, err := h.service.GetTenantByID(ctx, tenantID) tenant := ctx.Value(types.TenantInfoContextKey).(*types.Tenant)
if err != nil { if tenant.AgentConfig == nil {
if appErr, ok := errors.IsAppError(err); ok { logger.Error(ctx, "Tenant has no agent config")
logger.Error(ctx, "Failed to retrieve tenant: application error", appErr) c.Error(errors.NewBadRequestError("Tenant has no agent config"))
c.Error(appErr)
} else {
logger.ErrorWithFields(ctx, err, nil)
c.Error(errors.NewInternalServerError("Failed to retrieve tenant").WithDetails(err.Error()))
}
return return
} }
@@ -403,7 +377,7 @@ func (h *TenantHandler) UpdateTenantAgentConfig(c *gin.Context) {
return return
} }
logger.Infof(ctx, "Tenant agent config updated successfully, Tenant ID: %d", tenantID) logger.Infof(ctx, "Tenant agent config updated successfully, Tenant ID: %d", tenant.ID)
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"data": updatedTenant.AgentConfig, "data": updatedTenant.AgentConfig,
@@ -411,111 +385,74 @@ func (h *TenantHandler) UpdateTenantAgentConfig(c *gin.Context) {
}) })
} }
// GetTenantWebSearchConfig returns the web search configuration for a tenant // GetTenantKV provides a generic KV-style getter for tenant-level configurations
// Tenant ID is obtained from the authentication context // Supported keys:
func (h *TenantHandler) GetTenantWebSearchConfig(c *gin.Context) { // - "agent-config": returns tenant.AgentConfig with additional available_* fields
// - "web-search-config": returns masked tenant.WebSearchConfig (API key masked)
func (h *TenantHandler) GetTenantKV(c *gin.Context) {
ctx := c.Request.Context() ctx := c.Request.Context()
key := c.Param("key")
logger.Info(ctx, "Start getting tenant web search config") switch key {
case "agent-config":
// Get tenant ID from authentication context h.GetTenantAgentConfig(c)
tenantID := c.GetUint(types.TenantIDContextKey.String()) return
if tenantID == 0 { case "web-search-config":
logger.Error(ctx, "Tenant ID is empty") h.GetTenantWebSearchConfig(c)
c.Error(errors.NewBadRequestError("Tenant ID cannot be empty")) return
default:
logger.Info(ctx, "KV key not supported", "key", key)
c.Error(errors.NewBadRequestError("unsupported key"))
return return
} }
// Get tenant
tenant, err := h.service.GetTenantByID(ctx, tenantID)
if err != nil {
if appErr, ok := errors.IsAppError(err); ok {
logger.Error(ctx, "Failed to retrieve tenant: application error", appErr)
c.Error(appErr)
} else {
logger.ErrorWithFields(ctx, err, nil)
c.Error(errors.NewInternalServerError("Failed to retrieve tenant").WithDetails(err.Error()))
}
return
}
// Hide API key in response
var responseConfig *types.WebSearchConfig
if tenant.WebSearchConfig != nil {
responseConfig = &types.WebSearchConfig{
Provider: tenant.WebSearchConfig.Provider,
APIKey: "", // Hide API key
MaxResults: tenant.WebSearchConfig.MaxResults,
IncludeDate: tenant.WebSearchConfig.IncludeDate,
CompressionMethod: tenant.WebSearchConfig.CompressionMethod,
Blacklist: tenant.WebSearchConfig.Blacklist,
EmbeddingModelID: tenant.WebSearchConfig.EmbeddingModelID,
EmbeddingDimension: tenant.WebSearchConfig.EmbeddingDimension,
RerankModelID: tenant.WebSearchConfig.RerankModelID,
DocumentFragments: tenant.WebSearchConfig.DocumentFragments,
}
// If API key exists, show a masked version
if tenant.WebSearchConfig.APIKey != "" {
responseConfig.APIKey = "***" // Masked API key
}
}
logger.Infof(ctx, "Tenant web search config retrieved successfully, Tenant ID: %d", tenantID)
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": responseConfig,
})
} }
// UpdateTenantWebSearchConfig updates the web search configuration for a tenant // UpdateTenantKV provides a generic KV-style updater for tenant-level configurations
// Tenant ID is obtained from the authentication context // Body is the JSON value to set for the key.
func (h *TenantHandler) UpdateTenantWebSearchConfig(c *gin.Context) { func (h *TenantHandler) UpdateTenantKV(c *gin.Context) {
ctx := c.Request.Context() ctx := c.Request.Context()
key := c.Param("key")
logger.Info(ctx, "Start updating tenant web search config") switch key {
case "agent-config":
// Get tenant ID from authentication context h.updateTenantAgentConfigInternal(c)
tenantID := c.GetUint(types.TenantIDContextKey.String()) return
if tenantID == 0 { case "web-search-config":
logger.Error(ctx, "Tenant ID is empty") h.updateTenantWebSearchConfigInternal(c)
c.Error(errors.NewBadRequestError("Tenant ID cannot be empty")) return
default:
logger.Info(ctx, "KV key not supported", "key", key)
c.Error(errors.NewBadRequestError("unsupported key"))
return return
} }
}
var req types.WebSearchConfig // updateTenantWebSearchConfigInternal updates tenant's web search config
if err := c.ShouldBindJSON(&req); err != nil { func (h *TenantHandler) updateTenantWebSearchConfigInternal(c *gin.Context) {
ctx := c.Request.Context()
// Bind directly into the strong typed struct
var cfg types.WebSearchConfig
if err := c.ShouldBindJSON(&cfg); err != nil {
logger.Error(ctx, "Failed to parse request parameters", err) logger.Error(ctx, "Failed to parse request parameters", err)
c.Error(errors.NewValidationError("Invalid request data").WithDetails(err.Error())) c.Error(errors.NewValidationError("Invalid request data").WithDetails(err.Error()))
return return
} }
// Validate configuration // Validate configuration
if req.MaxResults < 1 || req.MaxResults > 50 { if cfg.MaxResults < 1 || cfg.MaxResults > 50 {
c.Error(errors.NewBadRequestError("max_results must be between 1 and 50")) c.Error(errors.NewBadRequestError("max_results must be between 1 and 50"))
return return
} }
// Get existing tenant tenant := ctx.Value(types.TenantInfoContextKey).(*types.Tenant)
tenant, err := h.service.GetTenantByID(ctx, tenantID) if tenant == nil {
if err != nil { logger.Error(ctx, "Tenant is empty")
if appErr, ok := errors.IsAppError(err); ok { c.Error(errors.NewBadRequestError("Tenant is empty"))
logger.Error(ctx, "Failed to retrieve tenant: application error", appErr)
c.Error(appErr)
} else {
logger.ErrorWithFields(ctx, err, nil)
c.Error(errors.NewInternalServerError("Failed to retrieve tenant").WithDetails(err.Error()))
}
return return
} }
// Update web search configuration tenant.WebSearchConfig = &cfg
// If API key is "***", keep the existing API key
if req.APIKey == "***" && tenant.WebSearchConfig != nil {
req.APIKey = tenant.WebSearchConfig.APIKey
}
tenant.WebSearchConfig = &req
updatedTenant, err := h.service.UpdateTenant(ctx, tenant) updatedTenant, err := h.service.UpdateTenant(ctx, tenant)
if err != nil { if err != nil {
if appErr, ok := errors.IsAppError(err); ok { if appErr, ok := errors.IsAppError(err); ok {
@@ -527,28 +464,28 @@ func (h *TenantHandler) UpdateTenantWebSearchConfig(c *gin.Context) {
} }
return return
} }
// Hide API key in response
responseConfig := &types.WebSearchConfig{
Provider: updatedTenant.WebSearchConfig.Provider,
APIKey: "", // Hide API key
MaxResults: updatedTenant.WebSearchConfig.MaxResults,
IncludeDate: updatedTenant.WebSearchConfig.IncludeDate,
CompressionMethod: updatedTenant.WebSearchConfig.CompressionMethod,
Blacklist: updatedTenant.WebSearchConfig.Blacklist,
EmbeddingModelID: updatedTenant.WebSearchConfig.EmbeddingModelID,
EmbeddingDimension: updatedTenant.WebSearchConfig.EmbeddingDimension,
RerankModelID: updatedTenant.WebSearchConfig.RerankModelID,
DocumentFragments: updatedTenant.WebSearchConfig.DocumentFragments,
}
if updatedTenant.WebSearchConfig.APIKey != "" {
responseConfig.APIKey = "***" // Masked API key
}
logger.Infof(ctx, "Tenant web search config updated successfully, Tenant ID: %d", tenantID)
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"data": responseConfig, "data": updatedTenant.WebSearchConfig,
"message": "Web search configuration updated successfully", "message": "Web search configuration updated successfully",
}) })
} }
// GetTenantWebSearchConfig returns the web search configuration for a tenant
func (h *TenantHandler) GetTenantWebSearchConfig(c *gin.Context) {
ctx := c.Request.Context()
logger.Info(ctx, "Start getting tenant web search config")
// Get tenant
tenant := ctx.Value(types.TenantInfoContextKey).(*types.Tenant)
if tenant == nil {
logger.Error(ctx, "Tenant is empty")
c.Error(errors.NewBadRequestError("Tenant is empty"))
return
}
logger.Infof(ctx, "Tenant web search config retrieved successfully, Tenant ID: %d", tenant.ID)
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": tenant.WebSearchConfig,
})
}

View File

@@ -222,14 +222,10 @@ func RegisterTenantRoutes(r *gin.RouterGroup, handler *handler.TenantHandler) {
tenantRoutes.DELETE("/:id", handler.DeleteTenant) tenantRoutes.DELETE("/:id", handler.DeleteTenant)
tenantRoutes.GET("", handler.ListTenants) tenantRoutes.GET("", handler.ListTenants)
// Agent configuration management (tenant-level) // Generic KV configuration management (tenant-level)
// Tenant ID is obtained from authentication context // Tenant ID is obtained from authentication context
tenantRoutes.GET("/agent-config", handler.GetTenantAgentConfig) tenantRoutes.GET("/kv/:key", handler.GetTenantKV)
tenantRoutes.PUT("/agent-config", handler.UpdateTenantAgentConfig) tenantRoutes.PUT("/kv/:key", handler.UpdateTenantKV)
// Web search configuration management (tenant-level)
// Tenant ID is obtained from authentication context
tenantRoutes.GET("/web-search-config", handler.GetTenantWebSearchConfig)
tenantRoutes.PUT("/web-search-config", handler.UpdateTenantWebSearchConfig)
} }
} }

6
package-lock.json generated Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "WeKnora",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}