mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-06-04 13:30:32 +08:00
- Introduced a new built-in agent, "Wiki Researcher," designed for navigating and answering questions based on Wiki knowledge bases, complete with multilingual support. - Added a corresponding system prompt that outlines the agent's role, mission, and workflow for effective knowledge graph traversal. - Updated the agent configuration to include specific tools and parameters tailored for the Wiki Researcher, enhancing its functionality and user interaction. - Removed deprecated wiki tools from the agent service to streamline the toolset and improve performance. These changes significantly enhance the capabilities of the agent system, providing users with a specialized tool for in-depth research and information retrieval from Wiki sources.
648 lines
21 KiB
Go
648 lines
21 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
|
|
"github.com/Tencent/WeKnora/internal/agent"
|
|
"github.com/Tencent/WeKnora/internal/agent/skills"
|
|
"github.com/Tencent/WeKnora/internal/agent/tools"
|
|
"github.com/Tencent/WeKnora/internal/config"
|
|
"github.com/Tencent/WeKnora/internal/event"
|
|
"github.com/Tencent/WeKnora/internal/logger"
|
|
"github.com/Tencent/WeKnora/internal/mcp"
|
|
"github.com/Tencent/WeKnora/internal/models/chat"
|
|
"github.com/Tencent/WeKnora/internal/models/rerank"
|
|
"github.com/Tencent/WeKnora/internal/sandbox"
|
|
"github.com/Tencent/WeKnora/internal/types"
|
|
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
|
secutils "github.com/Tencent/WeKnora/internal/utils"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const MAX_ITERATIONS = 100 // Max iterations for agent execution
|
|
|
|
// agentService implements agent-related business logic
|
|
type agentService struct {
|
|
cfg *config.Config
|
|
modelService interfaces.ModelService
|
|
mcpServiceService interfaces.MCPServiceService
|
|
mcpManager *mcp.MCPManager
|
|
eventBus *event.EventBus
|
|
db *gorm.DB
|
|
webSearchService interfaces.WebSearchService
|
|
knowledgeBaseService interfaces.KnowledgeBaseService
|
|
knowledgeService interfaces.KnowledgeService
|
|
fileService interfaces.FileService
|
|
chunkService interfaces.ChunkService
|
|
duckdb *sql.DB
|
|
webSearchStateService interfaces.WebSearchStateService
|
|
wikiPageService interfaces.WikiPageService
|
|
}
|
|
|
|
// NewAgentService creates a new agent service
|
|
func NewAgentService(
|
|
cfg *config.Config,
|
|
modelService interfaces.ModelService,
|
|
knowledgeBaseService interfaces.KnowledgeBaseService,
|
|
knowledgeService interfaces.KnowledgeService,
|
|
fileService interfaces.FileService,
|
|
chunkService interfaces.ChunkService,
|
|
mcpServiceService interfaces.MCPServiceService,
|
|
mcpManager *mcp.MCPManager,
|
|
eventBus *event.EventBus,
|
|
db *gorm.DB,
|
|
webSearchService interfaces.WebSearchService,
|
|
duckdb *sql.DB,
|
|
webSearchStateService interfaces.WebSearchStateService,
|
|
wikiPageService interfaces.WikiPageService,
|
|
) interfaces.AgentService {
|
|
return &agentService{
|
|
cfg: cfg,
|
|
modelService: modelService,
|
|
knowledgeBaseService: knowledgeBaseService,
|
|
knowledgeService: knowledgeService,
|
|
fileService: fileService,
|
|
chunkService: chunkService,
|
|
mcpServiceService: mcpServiceService,
|
|
mcpManager: mcpManager,
|
|
eventBus: eventBus,
|
|
db: db,
|
|
webSearchService: webSearchService,
|
|
duckdb: duckdb,
|
|
webSearchStateService: webSearchStateService,
|
|
wikiPageService: wikiPageService,
|
|
}
|
|
}
|
|
|
|
// CreateAgentEngineWithEventBus creates an agent engine with the given configuration and EventBus
|
|
func (s *agentService) CreateAgentEngine(
|
|
ctx context.Context,
|
|
config *types.AgentConfig,
|
|
chatModel chat.Chat,
|
|
rerankModel rerank.Reranker,
|
|
eventBus *event.EventBus,
|
|
contextManager interfaces.ContextManager,
|
|
sessionID string,
|
|
) (interfaces.AgentEngine, error) {
|
|
logger.Infof(ctx, "Creating agent engine with custom EventBus")
|
|
|
|
// 1. Validate config
|
|
if err := s.ValidateConfig(config); err != nil {
|
|
return nil, fmt.Errorf("invalid agent config: %w", err)
|
|
}
|
|
if chatModel == nil {
|
|
return nil, fmt.Errorf("chat model is nil after initialization")
|
|
}
|
|
|
|
// 2. Build tool registry
|
|
toolRegistry := tools.NewToolRegistry()
|
|
if config.MaxToolOutputChars > 0 {
|
|
toolRegistry.SetMaxToolOutputSize(config.MaxToolOutputChars)
|
|
}
|
|
if err := s.registerTools(ctx, toolRegistry, config, rerankModel, chatModel, sessionID); err != nil {
|
|
return nil, fmt.Errorf("failed to register tools: %w", err)
|
|
}
|
|
s.registerMCPTools(ctx, toolRegistry, config)
|
|
|
|
// 3. Resolve knowledge base and selected document metadata
|
|
kbInfos, selectedDocs := s.resolveKBAndDocInfos(ctx, config)
|
|
|
|
// 4. Resolve system prompt template
|
|
systemPromptTemplate := ""
|
|
if config.UseCustomSystemPrompt || config.SystemPrompt != "" {
|
|
systemPromptTemplate = config.ResolveSystemPrompt(config.WebSearchEnabled)
|
|
}
|
|
|
|
// 5. Create engine
|
|
engine := agent.NewAgentEngine(
|
|
config, chatModel, toolRegistry, eventBus,
|
|
kbInfos, selectedDocs, contextManager, sessionID,
|
|
systemPromptTemplate,
|
|
)
|
|
engine.SetAppConfig(s.cfg)
|
|
|
|
// Set VLM image describer for MCP tool result image analysis.
|
|
// When an MCP tool returns images, the engine uses VLM to generate text descriptions
|
|
// and appends them to the tool result content (since Chat Completions API does not
|
|
// reliably support images in tool role messages across providers).
|
|
if config.VLMModelID != "" {
|
|
if vlmModel, err := s.modelService.GetVLMModel(ctx, config.VLMModelID); err == nil {
|
|
engine.SetImageDescriber(func(ctx context.Context, imgBytes []byte, prompt string) (string, error) {
|
|
return vlmModel.Predict(ctx, [][]byte{imgBytes}, prompt)
|
|
})
|
|
logger.Infof(ctx, "VLM image describer set for MCP tool result analysis (model: %s)", config.VLMModelID)
|
|
} else {
|
|
logger.Warnf(ctx, "Failed to load VLM model %s for MCP image fallback: %v", config.VLMModelID, err)
|
|
}
|
|
}
|
|
|
|
// Initialize skills manager if skills are enabled
|
|
if config.SkillsEnabled && len(config.SkillDirs) > 0 {
|
|
skillsManager, err := s.initializeSkillsManager(ctx, config, toolRegistry)
|
|
if err != nil {
|
|
logger.Warnf(ctx, "Failed to initialize skills manager: %v", err)
|
|
} else if skillsManager != nil {
|
|
engine.SetSkillsManager(skillsManager)
|
|
logger.Infof(ctx, "Skills manager initialized with %d skills",
|
|
len(skillsManager.GetAllMetadata()))
|
|
}
|
|
}
|
|
|
|
return engine, nil
|
|
}
|
|
|
|
// registerMCPTools registers MCP tools from enabled services for this tenant.
|
|
func (s *agentService) registerMCPTools(
|
|
ctx context.Context,
|
|
toolRegistry *tools.ToolRegistry,
|
|
config *types.AgentConfig,
|
|
) {
|
|
tenantID := uint64(0)
|
|
if tid, ok := types.TenantIDFromContext(ctx); ok {
|
|
tenantID = tid
|
|
}
|
|
if tenantID == 0 || s.mcpServiceService == nil || s.mcpManager == nil {
|
|
return
|
|
}
|
|
|
|
mcpMode := config.MCPSelectionMode
|
|
if mcpMode == "" {
|
|
mcpMode = "all"
|
|
}
|
|
if mcpMode == "none" {
|
|
logger.Infof(ctx, "MCP services disabled by agent config (mode: none)")
|
|
return
|
|
}
|
|
|
|
var mcpServices []*types.MCPService
|
|
var err error
|
|
|
|
if mcpMode == "selected" && len(config.MCPServices) > 0 {
|
|
mcpServices, err = s.mcpServiceService.ListMCPServicesByIDs(ctx, tenantID, config.MCPServices)
|
|
if err != nil {
|
|
logger.Warnf(ctx, "Failed to list selected MCP services: %v", err)
|
|
return
|
|
}
|
|
logger.Infof(ctx, "Using %d selected MCP services from agent config", len(mcpServices))
|
|
} else {
|
|
mcpServices, err = s.mcpServiceService.ListMCPServices(ctx, tenantID)
|
|
if err != nil {
|
|
logger.Warnf(ctx, "Failed to list MCP services: %v", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
enabledServices := make([]*types.MCPService, 0)
|
|
for _, svc := range mcpServices {
|
|
if svc != nil && svc.Enabled {
|
|
enabledServices = append(enabledServices, svc)
|
|
}
|
|
}
|
|
if len(enabledServices) > 0 {
|
|
if err := tools.RegisterMCPTools(ctx, toolRegistry, enabledServices, s.mcpManager); err != nil {
|
|
logger.Warnf(ctx, "Failed to register MCP tools: %v", err)
|
|
} else {
|
|
logger.Infof(ctx, "Registered MCP tools from %d enabled services", len(enabledServices))
|
|
}
|
|
}
|
|
}
|
|
|
|
// resolveKBAndDocInfos loads knowledge base metadata and selected document info for prompt.
|
|
func (s *agentService) resolveKBAndDocInfos(
|
|
ctx context.Context,
|
|
config *types.AgentConfig,
|
|
) ([]*agent.KnowledgeBaseInfo, []*agent.SelectedDocumentInfo) {
|
|
kbInfos, err := s.getKnowledgeBaseInfos(ctx, config.KnowledgeBases)
|
|
if err != nil {
|
|
logger.Warnf(ctx, "Failed to get knowledge base details, using IDs only: %v", err)
|
|
kbInfos = make([]*agent.KnowledgeBaseInfo, 0, len(config.KnowledgeBases))
|
|
for _, kbID := range config.KnowledgeBases {
|
|
kbInfos = append(kbInfos, &agent.KnowledgeBaseInfo{
|
|
ID: kbID,
|
|
Name: kbID,
|
|
Description: "",
|
|
DocCount: 0,
|
|
})
|
|
}
|
|
}
|
|
|
|
selectedDocs, err := s.getSelectedDocumentInfos(ctx, config.KnowledgeIDs)
|
|
if err != nil {
|
|
logger.Warnf(ctx, "Failed to get selected document details: %v", err)
|
|
selectedDocs = []*agent.SelectedDocumentInfo{}
|
|
}
|
|
|
|
return kbInfos, selectedDocs
|
|
}
|
|
|
|
// initializeSkillsManager creates and initializes the skills manager
|
|
func (s *agentService) initializeSkillsManager(
|
|
ctx context.Context,
|
|
config *types.AgentConfig,
|
|
toolRegistry *tools.ToolRegistry,
|
|
) (*skills.Manager, error) {
|
|
// Initialize sandbox manager based on environment variables
|
|
// WEKNORA_SANDBOX_MODE: "docker", "local", "disabled" (default: "disabled")
|
|
// WEKNORA_SANDBOX_TIMEOUT: timeout in seconds (default: 60)
|
|
// WEKNORA_SANDBOX_DOCKER_IMAGE: custom Docker image (default: wechatopenai/weknora-sandbox:latest)
|
|
var sandboxMgr sandbox.Manager
|
|
var err error
|
|
|
|
sandboxMode := os.Getenv("WEKNORA_SANDBOX_MODE")
|
|
if sandboxMode == "" {
|
|
sandboxMode = "disabled"
|
|
}
|
|
dockerImage := os.Getenv("WEKNORA_SANDBOX_DOCKER_IMAGE")
|
|
if dockerImage == "" {
|
|
dockerImage = sandbox.DefaultDockerImage
|
|
}
|
|
sandboxTimeoutStr := os.Getenv("WEKNORA_SANDBOX_TIMEOUT")
|
|
sandboxTimeout := 60
|
|
if sandboxTimeoutStr != "" {
|
|
if v, err := strconv.Atoi(sandboxTimeoutStr); err == nil && v > 0 {
|
|
sandboxTimeout = v
|
|
}
|
|
}
|
|
|
|
switch sandboxMode {
|
|
case "docker":
|
|
sandboxMgr, err = sandbox.NewManagerFromType("docker", true, dockerImage) // Enable fallback to local
|
|
if err != nil {
|
|
logger.Warnf(ctx, "Failed to initialize Docker sandbox, falling back to disabled: %v", err)
|
|
sandboxMgr = sandbox.NewDisabledManager()
|
|
}
|
|
case "local":
|
|
sandboxMgr, err = sandbox.NewManagerFromType("local", false, "")
|
|
if err != nil {
|
|
logger.Warnf(ctx, "Failed to initialize local sandbox: %v", err)
|
|
sandboxMgr = sandbox.NewDisabledManager()
|
|
}
|
|
default:
|
|
sandboxMgr = sandbox.NewDisabledManager()
|
|
}
|
|
logger.Infof(ctx, "Sandbox configured: mode=%s, timeout=%ds, image=%s", sandboxMode, sandboxTimeout, dockerImage)
|
|
|
|
// Create skills manager
|
|
skillsConfig := &skills.ManagerConfig{
|
|
SkillDirs: config.SkillDirs,
|
|
AllowedSkills: config.AllowedSkills,
|
|
Enabled: config.SkillsEnabled,
|
|
}
|
|
|
|
skillsManager := skills.NewManager(skillsConfig, sandboxMgr)
|
|
|
|
// Initialize (discover skills)
|
|
if err := skillsManager.Initialize(ctx); err != nil {
|
|
return nil, fmt.Errorf("failed to initialize skills: %w", err)
|
|
}
|
|
|
|
// Register skills tools
|
|
readSkillTool := tools.NewReadSkillTool(skillsManager)
|
|
toolRegistry.RegisterTool(readSkillTool)
|
|
logger.Infof(ctx, "Registered read_skill tool")
|
|
|
|
if sandboxMode != "disabled" {
|
|
executeSkillTool := tools.NewExecuteSkillScriptTool(skillsManager)
|
|
toolRegistry.RegisterTool(executeSkillTool)
|
|
logger.Infof(ctx, "Registered execute_skill_script tool")
|
|
}
|
|
|
|
return skillsManager, nil
|
|
}
|
|
|
|
// registerTools registers tools based on the agent configuration
|
|
func (s *agentService) registerTools(
|
|
ctx context.Context,
|
|
registry *tools.ToolRegistry,
|
|
config *types.AgentConfig,
|
|
rerankModel rerank.Reranker,
|
|
chatModel chat.Chat,
|
|
sessionID string,
|
|
) error {
|
|
// Use config's allowed tools if specified, otherwise use defaults
|
|
var allowedTools []string
|
|
if len(config.AllowedTools) > 0 {
|
|
allowedTools = make([]string, len(config.AllowedTools))
|
|
copy(allowedTools, config.AllowedTools)
|
|
logger.Infof(ctx, "Using custom allowed tools from config: %v", allowedTools)
|
|
} else {
|
|
allowedTools = tools.DefaultAllowedTools()
|
|
logger.Infof(ctx, "Using default allowed tools: %v", allowedTools)
|
|
}
|
|
|
|
// Filter out knowledge base tools if no knowledge bases or knowledge IDs are configured
|
|
hasKnowledge := len(config.KnowledgeBases) > 0 || len(config.KnowledgeIDs) > 0
|
|
if !hasKnowledge {
|
|
filteredTools := make([]string, 0)
|
|
kbTools := map[string]bool{
|
|
tools.ToolKnowledgeSearch: true,
|
|
tools.ToolGrepChunks: true,
|
|
tools.ToolListKnowledgeChunks: true,
|
|
tools.ToolQueryKnowledgeGraph: true,
|
|
tools.ToolGetDocumentInfo: true,
|
|
tools.ToolDatabaseQuery: true,
|
|
tools.ToolDataAnalysis: true,
|
|
tools.ToolDataSchema: true,
|
|
}
|
|
|
|
// If no knowledge and no web search, also disable todo_write (not useful for simple chat)
|
|
if !config.WebSearchEnabled {
|
|
kbTools[tools.ToolTodoWrite] = true
|
|
}
|
|
|
|
for _, toolName := range allowedTools {
|
|
if !kbTools[toolName] {
|
|
filteredTools = append(filteredTools, toolName)
|
|
}
|
|
}
|
|
allowedTools = filteredTools
|
|
logger.Infof(ctx, "Pure Agent Mode: Knowledge base tools filtered out, remaining: %v", allowedTools)
|
|
}
|
|
|
|
// If web search is enabled, add web_search to allowedTools
|
|
if config.WebSearchEnabled {
|
|
allowedTools = append(allowedTools, tools.ToolWebSearch)
|
|
allowedTools = append(allowedTools, tools.ToolWebFetch)
|
|
}
|
|
|
|
// If any search target is a wiki KB, add wiki tools automatically
|
|
var wikiKBIDs []string
|
|
for _, target := range config.SearchTargets {
|
|
kb, err := s.knowledgeBaseService.GetKnowledgeBaseByIDOnly(ctx, target.KnowledgeBaseID)
|
|
if err == nil && kb.IsWikiEnabled() {
|
|
wikiKBIDs = append(wikiKBIDs, kb.ID)
|
|
}
|
|
}
|
|
if len(wikiKBIDs) > 0 {
|
|
allowedTools = append(allowedTools,
|
|
tools.ToolWikiReadPage,
|
|
tools.ToolWikiSearch,
|
|
)
|
|
logger.Infof(ctx, "Wiki KBs detected (%d), wiki tools added", len(wikiKBIDs))
|
|
}
|
|
|
|
logger.Infof(ctx, "Registering tools: %v, webSearchEnabled: %v", allowedTools, config.WebSearchEnabled)
|
|
allowedTools = append(allowedTools, tools.ToolFinalAnswer)
|
|
// Register each allowed tool
|
|
for _, toolName := range allowedTools {
|
|
var toolToRegister types.Tool
|
|
|
|
switch toolName {
|
|
case tools.ToolThinking:
|
|
toolToRegister = tools.NewSequentialThinkingTool()
|
|
case tools.ToolTodoWrite:
|
|
toolToRegister = tools.NewTodoWriteTool()
|
|
case tools.ToolKnowledgeSearch:
|
|
toolToRegister = tools.NewKnowledgeSearchTool(
|
|
s.knowledgeBaseService,
|
|
s.knowledgeService,
|
|
s.chunkService,
|
|
config.SearchTargets,
|
|
rerankModel,
|
|
chatModel,
|
|
s.cfg,
|
|
)
|
|
case tools.ToolGrepChunks:
|
|
toolToRegister = tools.NewGrepChunksTool(s.db, config.SearchTargets)
|
|
logger.Infof(ctx, "Registered grep_chunks tool with searchTargets: %d targets", len(config.SearchTargets))
|
|
case tools.ToolListKnowledgeChunks:
|
|
toolToRegister = tools.NewListKnowledgeChunksTool(s.knowledgeService, s.chunkService, config.SearchTargets)
|
|
case tools.ToolQueryKnowledgeGraph:
|
|
toolToRegister = tools.NewQueryKnowledgeGraphTool(s.knowledgeBaseService)
|
|
case tools.ToolGetDocumentInfo:
|
|
toolToRegister = tools.NewGetDocumentInfoTool(s.knowledgeService, s.chunkService, config.SearchTargets)
|
|
case tools.ToolDatabaseQuery:
|
|
toolToRegister = tools.NewDatabaseQueryTool(s.db, config.SearchTargets)
|
|
case tools.ToolWebSearch:
|
|
toolToRegister = tools.NewWebSearchTool(
|
|
s.webSearchService,
|
|
s.knowledgeBaseService,
|
|
s.knowledgeService,
|
|
s.webSearchStateService,
|
|
sessionID,
|
|
config.WebSearchMaxResults,
|
|
config.WebSearchProviderID,
|
|
)
|
|
logger.Infof(ctx, "Registered web_search tool for session: %s, maxResults: %d, providerID: %s", sessionID, config.WebSearchMaxResults, config.WebSearchProviderID)
|
|
|
|
case tools.ToolWebFetch:
|
|
toolToRegister = tools.NewWebFetchTool(chatModel)
|
|
logger.Infof(ctx, "Registered web_fetch tool for session: %s", sessionID)
|
|
|
|
case tools.ToolDataAnalysis:
|
|
toolToRegister = tools.NewDataAnalysisTool(s.knowledgeService, s.fileService, s.duckdb, sessionID)
|
|
logger.Infof(ctx, "Registered data_analysis tool for session: %s", sessionID)
|
|
|
|
case tools.ToolDataSchema:
|
|
toolToRegister = tools.NewDataSchemaTool(s.knowledgeService, s.chunkService.GetRepository())
|
|
logger.Infof(ctx, "Registered data_schema tool")
|
|
|
|
case tools.ToolFinalAnswer:
|
|
toolToRegister = tools.NewFinalAnswerTool()
|
|
logger.Infof(ctx, "Registered final_answer tool")
|
|
|
|
// Wiki tools — only registered when wiki KBs are detected
|
|
case tools.ToolWikiReadPage:
|
|
toolToRegister = tools.NewWikiReadPageTool(s.wikiPageService, wikiKBIDs)
|
|
case tools.ToolWikiSearch:
|
|
toolToRegister = tools.NewWikiSearchTool(s.wikiPageService, wikiKBIDs)
|
|
|
|
default:
|
|
logger.Warnf(ctx, "Unknown tool: %s", toolName)
|
|
}
|
|
|
|
if toolToRegister != nil {
|
|
if toolToRegister.Name() != toolName {
|
|
logger.Warnf(ctx, "Tool name mismatch: expected %s, got %s", toolName, toolToRegister.Name())
|
|
}
|
|
registry.RegisterTool(toolToRegister)
|
|
}
|
|
}
|
|
|
|
logger.Infof(ctx, "Registered %d tools", len(registry.ListTools()))
|
|
return nil
|
|
}
|
|
|
|
// ValidateConfig validates the agent configuration
|
|
func (s *agentService) ValidateConfig(config *types.AgentConfig) error {
|
|
if config == nil {
|
|
return fmt.Errorf("config cannot be nil")
|
|
}
|
|
|
|
if config.MaxIterations <= 0 {
|
|
config.MaxIterations = 5 // Default
|
|
}
|
|
|
|
if config.MaxIterations > MAX_ITERATIONS {
|
|
return fmt.Errorf("max iterations too high: %d (max %d)", config.MaxIterations, MAX_ITERATIONS)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getKnowledgeBaseInfos retrieves detailed information for knowledge bases
|
|
func (s *agentService) getKnowledgeBaseInfos(ctx context.Context, kbIDs []string) ([]*agent.KnowledgeBaseInfo, error) {
|
|
if len(kbIDs) == 0 {
|
|
return []*agent.KnowledgeBaseInfo{}, nil
|
|
}
|
|
|
|
kbInfos := make([]*agent.KnowledgeBaseInfo, 0, len(kbIDs))
|
|
|
|
for _, kbID := range kbIDs {
|
|
// Get knowledge base details
|
|
kb, err := s.knowledgeBaseService.GetKnowledgeBaseByID(ctx, kbID)
|
|
if err != nil {
|
|
logger.Warnf(ctx, "Failed to get knowledge base %s: %v", secutils.SanitizeForLog(kbID), err)
|
|
kbInfos = append(kbInfos, &agent.KnowledgeBaseInfo{
|
|
ID: kbID,
|
|
Name: kbID,
|
|
Type: "document",
|
|
Description: "",
|
|
DocCount: 0,
|
|
RecentDocs: []agent.RecentDocInfo{},
|
|
})
|
|
continue
|
|
}
|
|
|
|
// Skip hidden/system-managed knowledge bases (e.g., __chat_history__)
|
|
if kb.IsTemporary {
|
|
logger.Debugf(ctx, "Skipping temporary knowledge base %s (%s) from prompt", kb.ID, kb.Name)
|
|
continue
|
|
}
|
|
|
|
// Get document count and recent documents
|
|
docCount := 0
|
|
recentDocs := []agent.RecentDocInfo{}
|
|
|
|
if kb.Type == types.KnowledgeBaseTypeFAQ {
|
|
pageResult, err := s.knowledgeService.ListFAQEntries(ctx, kbID, &types.Pagination{
|
|
Page: 1,
|
|
PageSize: 10,
|
|
}, 0, "", "", "")
|
|
if err == nil && pageResult != nil {
|
|
docCount = int(pageResult.Total)
|
|
if entries, ok := pageResult.Data.([]*types.FAQEntry); ok {
|
|
for _, entry := range entries {
|
|
if len(recentDocs) >= 10 {
|
|
break
|
|
}
|
|
recentDocs = append(recentDocs, agent.RecentDocInfo{
|
|
ChunkID: entry.ChunkID,
|
|
KnowledgeID: entry.KnowledgeID,
|
|
KnowledgeBaseID: entry.KnowledgeBaseID,
|
|
Title: entry.StandardQuestion,
|
|
Type: string(types.ChunkTypeFAQ),
|
|
CreatedAt: entry.CreatedAt.Format("2006-01-02"),
|
|
FAQStandardQuestion: entry.StandardQuestion,
|
|
FAQSimilarQuestions: entry.SimilarQuestions,
|
|
FAQAnswers: entry.Answers,
|
|
})
|
|
}
|
|
}
|
|
} else if err != nil {
|
|
logger.Warnf(ctx, "Failed to list FAQ entries for %s: %v", kbID, err)
|
|
}
|
|
}
|
|
|
|
// Fallback to generic knowledge listing when not FAQ or FAQ retrieval failed
|
|
if kb.Type != types.KnowledgeBaseTypeFAQ || len(recentDocs) == 0 {
|
|
pageResult, err := s.knowledgeService.ListPagedKnowledgeByKnowledgeBaseID(ctx, kbID, &types.Pagination{
|
|
Page: 1,
|
|
PageSize: 10,
|
|
}, "", "", "")
|
|
|
|
if err == nil && pageResult != nil {
|
|
docCount = int(pageResult.Total)
|
|
|
|
// Convert to Knowledge slice
|
|
if knowledges, ok := pageResult.Data.([]*types.Knowledge); ok {
|
|
for _, k := range knowledges {
|
|
if len(recentDocs) >= 10 {
|
|
break
|
|
}
|
|
recentDocs = append(recentDocs, agent.RecentDocInfo{
|
|
KnowledgeID: k.ID,
|
|
Title: k.Title,
|
|
Description: k.Description,
|
|
FileName: k.FileName,
|
|
Type: k.FileType,
|
|
CreatedAt: k.CreatedAt.Format("2006-01-02"),
|
|
FileSize: k.FileSize,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
kbType := kb.Type
|
|
if kbType == "" {
|
|
kbType = "document" // Default type
|
|
}
|
|
kbInfos = append(kbInfos, &agent.KnowledgeBaseInfo{
|
|
ID: kb.ID,
|
|
Name: kb.Name,
|
|
Type: kbType,
|
|
Description: kb.Description,
|
|
DocCount: docCount,
|
|
RecentDocs: recentDocs,
|
|
})
|
|
}
|
|
|
|
return kbInfos, nil
|
|
}
|
|
|
|
// getSelectedDocumentInfos retrieves detailed information for user-selected documents (via @ mention)
|
|
// This loads the actual content of the documents to include in the system prompt
|
|
func (s *agentService) getSelectedDocumentInfos(ctx context.Context, knowledgeIDs []string) ([]*agent.SelectedDocumentInfo, error) {
|
|
if len(knowledgeIDs) == 0 {
|
|
return []*agent.SelectedDocumentInfo{}, nil
|
|
}
|
|
|
|
// Get tenant ID from context
|
|
tenantID := uint64(0)
|
|
if tid, ok := types.TenantIDFromContext(ctx); ok {
|
|
tenantID = tid
|
|
}
|
|
|
|
// Fetch knowledge metadata (include docs from shared KBs the user has access to)
|
|
knowledges, err := s.knowledgeService.GetKnowledgeBatchWithSharedAccess(ctx, tenantID, knowledgeIDs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get knowledge batch: %w", err)
|
|
}
|
|
|
|
// Build map for quick lookup
|
|
knowledgeMap := make(map[string]*types.Knowledge)
|
|
for _, k := range knowledges {
|
|
if k != nil {
|
|
knowledgeMap[k.ID] = k
|
|
}
|
|
}
|
|
|
|
selectedDocs := make([]*agent.SelectedDocumentInfo, 0, len(knowledgeIDs))
|
|
|
|
for _, kid := range knowledgeIDs {
|
|
k, ok := knowledgeMap[kid]
|
|
if !ok {
|
|
logger.Warnf(ctx, "Selected knowledge %s not found", kid)
|
|
continue
|
|
}
|
|
|
|
docInfo := &agent.SelectedDocumentInfo{
|
|
KnowledgeID: k.ID,
|
|
KnowledgeBaseID: k.KnowledgeBaseID,
|
|
Title: k.Title,
|
|
FileName: k.FileName,
|
|
FileType: k.FileType,
|
|
}
|
|
|
|
selectedDocs = append(selectedDocs, docInfo)
|
|
}
|
|
|
|
logger.Infof(ctx, "Loaded %d selected documents metadata for prompt", len(selectedDocs))
|
|
return selectedDocs, nil
|
|
}
|