139a9c40ff
- Added timezone and language settings to the environment configuration. - Introduced built-in agent configurations with multilingual support for various agents. - Updated Docker Compose to utilize new environment variables for timezone and language. - Created new prompt templates for question generation, summary generation, and keywords extraction.
385 lines
14 KiB
Go
385 lines
14 KiB
Go
package agent
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Tencent/WeKnora/internal/agent/skills"
|
|
"github.com/Tencent/WeKnora/internal/config"
|
|
"github.com/Tencent/WeKnora/internal/types"
|
|
)
|
|
|
|
// formatFileSize formats file size in human-readable format
|
|
func formatFileSize(size int64) string {
|
|
const (
|
|
KB = 1024
|
|
MB = 1024 * KB
|
|
GB = 1024 * MB
|
|
)
|
|
|
|
if size < KB {
|
|
return fmt.Sprintf("%d B", size)
|
|
} else if size < MB {
|
|
return fmt.Sprintf("%.2f KB", float64(size)/KB)
|
|
} else if size < GB {
|
|
return fmt.Sprintf("%.2f MB", float64(size)/MB)
|
|
}
|
|
return fmt.Sprintf("%.2f GB", float64(size)/GB)
|
|
}
|
|
|
|
// formatDocSummary cleans and truncates document summaries for table display
|
|
func formatDocSummary(summary string, maxLen int) string {
|
|
cleaned := strings.TrimSpace(summary)
|
|
if cleaned == "" {
|
|
return "-"
|
|
}
|
|
cleaned = strings.ReplaceAll(cleaned, "\n", " ")
|
|
cleaned = strings.ReplaceAll(cleaned, "\r", " ")
|
|
cleaned = strings.Join(strings.Fields(cleaned), " ")
|
|
|
|
runes := []rune(cleaned)
|
|
if len(runes) <= maxLen {
|
|
return cleaned
|
|
}
|
|
return strings.TrimSpace(string(runes[:maxLen])) + "..."
|
|
}
|
|
|
|
// RecentDocInfo contains brief information about a recently added document
|
|
type RecentDocInfo struct {
|
|
ChunkID string
|
|
KnowledgeBaseID string
|
|
KnowledgeID string
|
|
Title string
|
|
Description string
|
|
FileName string
|
|
FileSize int64
|
|
Type string
|
|
CreatedAt string // Formatted time string
|
|
FAQStandardQuestion string
|
|
FAQSimilarQuestions []string
|
|
FAQAnswers []string
|
|
}
|
|
|
|
// SelectedDocumentInfo contains summary information about a user-selected document (via @ mention)
|
|
// Only metadata is included; content will be fetched via tools when needed
|
|
type SelectedDocumentInfo struct {
|
|
KnowledgeID string // Knowledge ID
|
|
KnowledgeBaseID string // Knowledge base ID
|
|
Title string // Document title
|
|
FileName string // Original file name
|
|
FileType string // File type (pdf, docx, etc.)
|
|
}
|
|
|
|
// KnowledgeBaseInfo contains essential information about a knowledge base for agent prompt
|
|
type KnowledgeBaseInfo struct {
|
|
ID string
|
|
Name string
|
|
Type string // Knowledge base type: "document" or "faq"
|
|
Description string
|
|
DocCount int
|
|
RecentDocs []RecentDocInfo // Recently added documents (up to 10)
|
|
}
|
|
|
|
// PlaceholderDefinition defines a placeholder exposed to UI/configuration
|
|
// Deprecated: Use types.PromptPlaceholder instead
|
|
type PlaceholderDefinition struct {
|
|
Name string `json:"name"`
|
|
Label string `json:"label"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
// AvailablePlaceholders lists all supported prompt placeholders for UI hints
|
|
// This returns agent mode specific placeholders
|
|
func AvailablePlaceholders() []PlaceholderDefinition {
|
|
// Use centralized placeholder definitions from types package
|
|
placeholders := types.PlaceholdersByField(types.PromptFieldAgentSystemPrompt)
|
|
result := make([]PlaceholderDefinition, len(placeholders))
|
|
for i, p := range placeholders {
|
|
result[i] = PlaceholderDefinition{
|
|
Name: p.Name,
|
|
Label: p.Label,
|
|
Description: p.Description,
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// formatKnowledgeBaseList formats knowledge base information for the prompt
|
|
func formatKnowledgeBaseList(kbInfos []*KnowledgeBaseInfo) string {
|
|
if len(kbInfos) == 0 {
|
|
return "None"
|
|
}
|
|
|
|
var builder strings.Builder
|
|
builder.WriteString("\nThe following knowledge bases have been selected by the user for this conversation. ")
|
|
builder.WriteString("You should search within these knowledge bases to find relevant information.\n\n")
|
|
for i, kb := range kbInfos {
|
|
// Display knowledge base name and ID
|
|
builder.WriteString(fmt.Sprintf("%d. **%s** (knowledge_base_id: `%s`)\n", i+1, kb.Name, kb.ID))
|
|
|
|
// Display knowledge base type
|
|
kbType := kb.Type
|
|
if kbType == "" {
|
|
kbType = "document" // Default type
|
|
}
|
|
builder.WriteString(fmt.Sprintf(" - Type: %s\n", kbType))
|
|
|
|
if kb.Description != "" {
|
|
builder.WriteString(fmt.Sprintf(" - Description: %s\n", kb.Description))
|
|
}
|
|
builder.WriteString(fmt.Sprintf(" - Document count: %d\n", kb.DocCount))
|
|
|
|
// Display recent documents if available
|
|
// For FAQ type knowledge bases, adjust the display format
|
|
if len(kb.RecentDocs) > 0 {
|
|
if kbType == "faq" {
|
|
// FAQ knowledge base: show Q&A pairs in a more compact format
|
|
builder.WriteString(" - Recent FAQ entries:\n\n")
|
|
builder.WriteString(" | # | Question | Answers | Chunk ID | Knowledge ID | Created At |\n")
|
|
builder.WriteString(" |---|-------------------|---------|----------|--------------|------------|\n")
|
|
for j, doc := range kb.RecentDocs {
|
|
if j >= 10 { // Limit to 10 documents
|
|
break
|
|
}
|
|
question := doc.FAQStandardQuestion
|
|
if question == "" {
|
|
question = doc.FileName
|
|
}
|
|
answers := "-"
|
|
if len(doc.FAQAnswers) > 0 {
|
|
answers = strings.Join(doc.FAQAnswers, " | ")
|
|
}
|
|
builder.WriteString(fmt.Sprintf(" | %d | %s | %s | `%s` | `%s` | %s |\n",
|
|
j+1, question, answers, doc.ChunkID, doc.KnowledgeID, doc.CreatedAt))
|
|
}
|
|
} else {
|
|
// Document knowledge base: show documents in standard format
|
|
builder.WriteString(" - Recently added documents:\n\n")
|
|
builder.WriteString(" | # | Document Name | Type | Created At | Knowledge ID | File Size | Summary |\n")
|
|
builder.WriteString(" |---|---------------|------|------------|--------------|----------|---------|\n")
|
|
for j, doc := range kb.RecentDocs {
|
|
if j >= 10 { // Limit to 10 documents
|
|
break
|
|
}
|
|
docName := doc.Title
|
|
if docName == "" {
|
|
docName = doc.FileName
|
|
}
|
|
// Format file size
|
|
fileSize := formatFileSize(doc.FileSize)
|
|
summary := formatDocSummary(doc.Description, 120)
|
|
builder.WriteString(fmt.Sprintf(" | %d | %s | %s | %s | `%s` | %s | %s |\n",
|
|
j+1, docName, doc.Type, doc.CreatedAt, doc.KnowledgeID, fileSize, summary))
|
|
}
|
|
}
|
|
builder.WriteString("\n")
|
|
}
|
|
builder.WriteString("\n")
|
|
}
|
|
return builder.String()
|
|
}
|
|
|
|
// renderPromptPlaceholders renders placeholders in the prompt template
|
|
// Supported placeholders:
|
|
// - {{knowledge_bases}} - Replaced with formatted knowledge base list
|
|
func renderPromptPlaceholders(template string, knowledgeBases []*KnowledgeBaseInfo) string {
|
|
result := template
|
|
|
|
// Replace {{knowledge_bases}} placeholder
|
|
if strings.Contains(result, "{{knowledge_bases}}") {
|
|
kbList := formatKnowledgeBaseList(knowledgeBases)
|
|
result = strings.ReplaceAll(result, "{{knowledge_bases}}", kbList)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// formatSkillsMetadata formats skills metadata for the system prompt (Level 1 - Progressive Disclosure)
|
|
// This is a lightweight representation that only includes skill name and description
|
|
func formatSkillsMetadata(skillsMetadata []*skills.SkillMetadata) string {
|
|
if len(skillsMetadata) == 0 {
|
|
return ""
|
|
}
|
|
|
|
var builder strings.Builder
|
|
builder.WriteString("\n### Available Skills (IMPORTANT - READ CAREFULLY)\n\n")
|
|
builder.WriteString("**You MUST actively consider using these skills for EVERY user request.**\n\n")
|
|
|
|
builder.WriteString("#### Skill Matching Protocol (MANDATORY)\n\n")
|
|
builder.WriteString("Before responding to ANY user query, follow this checklist:\n\n")
|
|
builder.WriteString("1. **SCAN**: Read each skill's description and trigger conditions below\n")
|
|
builder.WriteString("2. **MATCH**: Check if the user's intent matches ANY skill's triggers (keywords, scenarios, or task types)\n")
|
|
builder.WriteString("3. **LOAD**: If a match is found, call `read_skill(skill_name=\"...\")` BEFORE generating your response\n")
|
|
builder.WriteString("4. **APPLY**: Follow the skill's instructions to provide a higher-quality, structured response\n\n")
|
|
|
|
builder.WriteString("**⚠️ CRITICAL**: Skill usage is MANDATORY when applicable. Do NOT skip skills to save time or tokens.\n\n")
|
|
|
|
builder.WriteString("#### Available Skills\n\n")
|
|
for i, skill := range skillsMetadata {
|
|
builder.WriteString(fmt.Sprintf("%d. **%s**\n", i+1, skill.Name))
|
|
builder.WriteString(fmt.Sprintf(" %s\n\n", skill.Description))
|
|
}
|
|
|
|
builder.WriteString("#### Tool Reference\n\n")
|
|
builder.WriteString("- `read_skill(skill_name)`: Load full skill instructions (MUST call before using a skill)\n")
|
|
builder.WriteString("- `execute_skill_script(skill_name, script_path, args, input)`: Run utility scripts bundled with a skill\n")
|
|
builder.WriteString(" - `input`: Pass data directly via stdin (use this when you have data in memory, e.g. JSON string)\n")
|
|
builder.WriteString(" - `args`: Command-line arguments (only use `--file` if you have an actual file path in the skill directory)\n")
|
|
|
|
return builder.String()
|
|
}
|
|
|
|
// formatSelectedDocuments formats selected documents for the prompt (summary only, no content)
|
|
func formatSelectedDocuments(docs []*SelectedDocumentInfo) string {
|
|
if len(docs) == 0 {
|
|
return ""
|
|
}
|
|
|
|
var builder strings.Builder
|
|
builder.WriteString("\n### User Selected Documents (via @ mention)\n")
|
|
builder.WriteString("The user has explicitly selected the following documents. ")
|
|
builder.WriteString("**You should prioritize searching and retrieving information from these documents when answering.**\n")
|
|
builder.WriteString("Use `list_knowledge_chunks` with the provided Knowledge IDs to fetch their content.\n\n")
|
|
|
|
builder.WriteString("| # | Document Name | Type | Knowledge ID |\n")
|
|
builder.WriteString("|---|---------------|------|---------------|\n")
|
|
|
|
for i, doc := range docs {
|
|
title := doc.Title
|
|
if title == "" {
|
|
title = doc.FileName
|
|
}
|
|
fileType := doc.FileType
|
|
if fileType == "" {
|
|
fileType = "-"
|
|
}
|
|
builder.WriteString(fmt.Sprintf("| %d | %s | %s | `%s` |\n",
|
|
i+1, title, fileType, doc.KnowledgeID))
|
|
}
|
|
builder.WriteString("\n")
|
|
|
|
return builder.String()
|
|
}
|
|
|
|
// renderPromptPlaceholdersWithStatus renders placeholders including web search status
|
|
// Supported placeholders:
|
|
// - {{knowledge_bases}}
|
|
// - {{web_search_status}} -> "Enabled" or "Disabled"
|
|
// - {{current_time}} -> current time string
|
|
// - {{language}} -> user language name (e.g. "Chinese (Simplified)", "English")
|
|
// - {{skills}} -> formatted skills metadata (if any)
|
|
func renderPromptPlaceholdersWithStatus(
|
|
template string,
|
|
knowledgeBases []*KnowledgeBaseInfo,
|
|
webSearchEnabled bool,
|
|
currentTime string,
|
|
language string,
|
|
) string {
|
|
// Knowledge bases need special formatting, so handle it first
|
|
result := renderPromptPlaceholders(template, knowledgeBases)
|
|
|
|
status := "Disabled"
|
|
if webSearchEnabled {
|
|
status = "Enabled"
|
|
}
|
|
|
|
result = types.RenderPromptPlaceholders(result, types.PlaceholderValues{
|
|
"web_search_status": status,
|
|
"current_time": currentTime,
|
|
"language": language,
|
|
"skills": "", // Remove {{skills}} placeholder; skills are appended separately if present
|
|
})
|
|
return result
|
|
}
|
|
|
|
// BuildSystemPromptOptions contains optional parameters for BuildSystemPrompt
|
|
type BuildSystemPromptOptions struct {
|
|
SkillsMetadata []*skills.SkillMetadata
|
|
Language string // User language name for {{language}} placeholder (e.g. "Chinese (Simplified)")
|
|
Config *config.Config // Config for reading prompt templates; nil falls back to hardcoded defaults
|
|
}
|
|
|
|
// BuildSystemPrompt builds the progressive RAG system prompt
|
|
// This is the main function to use - it uses a unified template with dynamic web search status
|
|
func BuildSystemPrompt(
|
|
knowledgeBases []*KnowledgeBaseInfo,
|
|
webSearchEnabled bool,
|
|
selectedDocs []*SelectedDocumentInfo,
|
|
systemPromptTemplate ...string,
|
|
) string {
|
|
return BuildSystemPromptWithOptions(knowledgeBases, webSearchEnabled, selectedDocs, nil, systemPromptTemplate...)
|
|
}
|
|
|
|
// BuildSystemPromptWithOptions builds the system prompt with additional options like skills
|
|
func BuildSystemPromptWithOptions(
|
|
knowledgeBases []*KnowledgeBaseInfo,
|
|
webSearchEnabled bool,
|
|
selectedDocs []*SelectedDocumentInfo,
|
|
options *BuildSystemPromptOptions,
|
|
systemPromptTemplate ...string,
|
|
) string {
|
|
var basePrompt string
|
|
var template string
|
|
|
|
// Determine template to use
|
|
if len(systemPromptTemplate) > 0 && systemPromptTemplate[0] != "" {
|
|
template = systemPromptTemplate[0]
|
|
} else if len(knowledgeBases) == 0 {
|
|
var cfg *config.Config
|
|
if options != nil {
|
|
cfg = options.Config
|
|
}
|
|
template = GetPureAgentSystemPrompt(cfg)
|
|
} else {
|
|
var cfg *config.Config
|
|
if options != nil {
|
|
cfg = options.Config
|
|
}
|
|
template = GetProgressiveRAGSystemPrompt(cfg)
|
|
}
|
|
|
|
currentTime := time.Now().Format(time.RFC3339)
|
|
language := ""
|
|
if options != nil {
|
|
language = options.Language
|
|
}
|
|
basePrompt = renderPromptPlaceholdersWithStatus(template, knowledgeBases, webSearchEnabled, currentTime, language)
|
|
|
|
// Append selected documents section if any
|
|
if len(selectedDocs) > 0 {
|
|
basePrompt += formatSelectedDocuments(selectedDocs)
|
|
}
|
|
|
|
// Append skills metadata if available (Level 1 - Progressive Disclosure)
|
|
if options != nil && len(options.SkillsMetadata) > 0 {
|
|
basePrompt += formatSkillsMetadata(options.SkillsMetadata)
|
|
}
|
|
|
|
return basePrompt
|
|
}
|
|
|
|
// GetPureAgentSystemPrompt returns the Pure Agent system prompt from config templates.
|
|
// The template must be defined in config/prompt_templates/agent_system_prompt.yaml
|
|
// with mode "pure". Returns empty string if config is nil or template not found.
|
|
func GetPureAgentSystemPrompt(cfg *config.Config) string {
|
|
if cfg != nil && cfg.PromptTemplates != nil {
|
|
if t := config.DefaultTemplateByMode(cfg.PromptTemplates.AgentSystemPrompt, "pure"); t != nil && t.Content != "" {
|
|
return t.Content
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// GetProgressiveRAGSystemPrompt returns the Progressive RAG Agent system prompt from config templates.
|
|
// The template must be defined in config/prompt_templates/agent_system_prompt.yaml
|
|
// with mode "rag". Returns empty string if config is nil or template not found.
|
|
func GetProgressiveRAGSystemPrompt(cfg *config.Config) string {
|
|
if cfg != nil && cfg.PromptTemplates != nil {
|
|
if t := config.DefaultTemplateByMode(cfg.PromptTemplates.AgentSystemPrompt, "rag"); t != nil && t.Content != "" {
|
|
return t.Content
|
|
}
|
|
}
|
|
return ""
|
|
}
|