From 3f213455462c5e1d5d17babda77fee5a6f4c69e1 Mon Sep 17 00:00:00 2001 From: nullkey Date: Wed, 4 Feb 2026 03:40:09 +0800 Subject: [PATCH] feat: adapt preloaded skills to smart-reasoning agent --- cmd/skills-demo/docker_sandbox_test_runner.go | 164 -------- cmd/skills-demo/docker_test.go | 165 -------- cmd/skills-demo/main.go | 237 ----------- docker/Dockerfile.app | 1 + docs/agent-skills.md | 305 +++++++------- frontend/src/api/agent/index.ts | 5 + frontend/src/api/skill/index.ts | 12 + frontend/src/i18n/locales/en-US.ts | 13 + frontend/src/i18n/locales/ko-KR.ts | 15 + frontend/src/i18n/locales/ru-RU.ts | 17 +- frontend/src/i18n/locales/zh-CN.ts | 13 + frontend/src/views/agent/AgentEditorModal.vue | 205 ++++++++++ internal/agent/prompts.go | 23 +- internal/agent/skills/loader.go | 7 +- internal/agent/skills/manager.go | 3 +- internal/agent/skills/skill.go | 4 +- internal/agent/tools/skill_execute.go | 7 +- internal/application/service/session.go | 63 +++ internal/application/service/skill_service.go | 135 +++++++ internal/container/container.go | 2 + internal/handler/skill_handler.go | 64 +++ internal/router/router.go | 11 + internal/types/custom_agent.go | 12 +- internal/types/interfaces/skill.go | 16 + skills/preloaded/citation-generator/SKILL.md | 58 +++ skills/preloaded/data-processor/SKILL.md | 154 +++++++ .../data-processor/scripts/analyze.py | 250 ++++++++++++ .../data-processor/scripts/extract_info.py | 199 ++++++++++ .../scripts/format_converter.py | 206 ++++++++++ skills/preloaded/doc-coauthoring/SKILL.md | 375 ++++++++++++++++++ skills/preloaded/document-analyzer/SKILL.md | 80 ++++ skills/preloaded/summary-generator/SKILL.md | 92 +++++ 32 files changed, 2194 insertions(+), 719 deletions(-) delete mode 100644 cmd/skills-demo/docker_sandbox_test_runner.go delete mode 100644 cmd/skills-demo/docker_test.go delete mode 100644 cmd/skills-demo/main.go create mode 100644 frontend/src/api/skill/index.ts create mode 100644 internal/application/service/skill_service.go create mode 100644 internal/handler/skill_handler.go create mode 100644 internal/types/interfaces/skill.go create mode 100644 skills/preloaded/citation-generator/SKILL.md create mode 100644 skills/preloaded/data-processor/SKILL.md create mode 100644 skills/preloaded/data-processor/scripts/analyze.py create mode 100644 skills/preloaded/data-processor/scripts/extract_info.py create mode 100644 skills/preloaded/data-processor/scripts/format_converter.py create mode 100644 skills/preloaded/doc-coauthoring/SKILL.md create mode 100644 skills/preloaded/document-analyzer/SKILL.md create mode 100644 skills/preloaded/summary-generator/SKILL.md diff --git a/cmd/skills-demo/docker_sandbox_test_runner.go b/cmd/skills-demo/docker_sandbox_test_runner.go deleted file mode 100644 index 8c238741..00000000 --- a/cmd/skills-demo/docker_sandbox_test_runner.go +++ /dev/null @@ -1,164 +0,0 @@ -// +build ignore - -// Docker sandbox test program -// Usage: go run cmd/skills-demo/docker_test.go -package main - -import ( - "context" - "fmt" - "os" - "path/filepath" - "runtime" - "strings" - "time" - - "github.com/Tencent/WeKnora/internal/sandbox" -) - -func main() { - fmt.Println("=" + strings.Repeat("=", 70)) - fmt.Println(" Docker Sandbox Test Suite") - fmt.Println("=" + strings.Repeat("=", 70)) - fmt.Println() - - ctx := context.Background() - - // Get the path to examples/skills - _, filename, _, ok := runtime.Caller(0) - if !ok { - fmt.Println("Error: Failed to get current file path") - os.Exit(1) - } - scriptsDir := filepath.Join(filepath.Dir(filename), "..", "..", "examples", "skills", "pdf-processing", "scripts") - - // Create Docker sandbox - config := sandbox.DefaultConfig() - config.Type = sandbox.SandboxTypeDocker - config.FallbackEnabled = false - config.DockerImage = "python:3.11-slim" - - mgr, err := sandbox.NewManager(config) - if err != nil { - fmt.Printf("❌ Failed to create sandbox manager: %v\n", err) - os.Exit(1) - } - - fmt.Printf("✅ Sandbox Type: %s\n\n", mgr.GetType()) - - // Test 1: Basic script execution - fmt.Println("Test 1: Basic Script Execution") - fmt.Println("-" + strings.Repeat("-", 50)) - runTest(ctx, mgr, &sandbox.ExecuteConfig{ - Script: filepath.Join(scriptsDir, "analyze_form.py"), - Args: []string{"sample.pdf"}, - }) - - // Test 2: Script with different arguments - fmt.Println("\nTest 2: Script with Different Arguments") - fmt.Println("-" + strings.Repeat("-", 50)) - runTest(ctx, mgr, &sandbox.ExecuteConfig{ - Script: filepath.Join(scriptsDir, "extract_text.py"), - Args: []string{"document.pdf", "--page", "1"}, - }) - - // Test 3: Environment variables - fmt.Println("\nTest 3: Environment Variables") - fmt.Println("-" + strings.Repeat("-", 50)) - runTest(ctx, mgr, &sandbox.ExecuteConfig{ - Script: filepath.Join(scriptsDir, "analyze_form.py"), - Args: []string{"test.pdf"}, - Env: map[string]string{ - "DEBUG": "true", - "LOG_LEVEL": "verbose", - }, - }) - - // Test 4: Network isolation (default: no network) - fmt.Println("\nTest 4: Network Isolation (network disabled)") - fmt.Println("-" + strings.Repeat("-", 50)) - runTest(ctx, mgr, &sandbox.ExecuteConfig{ - Script: filepath.Join(scriptsDir, "analyze_form.py"), - Args: []string{"test.pdf"}, - AllowNetwork: false, - }) - - // Test 5: Memory limits - fmt.Println("\nTest 5: Memory Limits (128MB)") - fmt.Println("-" + strings.Repeat("-", 50)) - runTest(ctx, mgr, &sandbox.ExecuteConfig{ - Script: filepath.Join(scriptsDir, "analyze_form.py"), - Args: []string{"test.pdf"}, - MemoryLimit: 128 * 1024 * 1024, // 128MB - }) - - // Test 6: CPU limits - fmt.Println("\nTest 6: CPU Limits (0.5 cores)") - fmt.Println("-" + strings.Repeat("-", 50)) - runTest(ctx, mgr, &sandbox.ExecuteConfig{ - Script: filepath.Join(scriptsDir, "analyze_form.py"), - Args: []string{"test.pdf"}, - CPULimit: 0.5, - }) - - // Test 7: Read-only filesystem - fmt.Println("\nTest 7: Read-only Root Filesystem") - fmt.Println("-" + strings.Repeat("-", 50)) - runTest(ctx, mgr, &sandbox.ExecuteConfig{ - Script: filepath.Join(scriptsDir, "analyze_form.py"), - Args: []string{"test.pdf"}, - ReadOnlyRootfs: true, - }) - - // Test 8: Custom timeout - fmt.Println("\nTest 8: Custom Timeout (10s)") - fmt.Println("-" + strings.Repeat("-", 50)) - runTest(ctx, mgr, &sandbox.ExecuteConfig{ - Script: filepath.Join(scriptsDir, "analyze_form.py"), - Args: []string{"test.pdf"}, - Timeout: 10 * time.Second, - }) - - fmt.Println("\n" + strings.Repeat("=", 71)) - fmt.Println(" All Tests Completed!") - fmt.Println(strings.Repeat("=", 71)) -} - -func runTest(ctx context.Context, mgr sandbox.Manager, config *sandbox.ExecuteConfig) { - startTime := time.Now() - result, err := mgr.Execute(ctx, config) - totalTime := time.Since(startTime) - - if err != nil { - fmt.Printf("❌ Error: %v\n", err) - return - } - - fmt.Printf(" Script: %s\n", filepath.Base(config.Script)) - fmt.Printf(" Args: %v\n", config.Args) - fmt.Printf(" Exit Code: %d\n", result.ExitCode) - fmt.Printf(" Duration: %v (total: %v)\n", result.Duration, totalTime) - fmt.Printf(" Success: %v\n", result.IsSuccess()) - - if result.Stdout != "" { - // Show just the first 3 lines of output - lines := strings.Split(result.Stdout, "\n") - preview := strings.Join(lines[:min(3, len(lines))], "\n") - fmt.Printf(" Output (preview):\n %s\n", strings.ReplaceAll(preview, "\n", "\n ")) - } - - if result.Stderr != "" { - fmt.Printf(" Stderr: %s\n", strings.TrimSpace(result.Stderr)) - } - - if result.Error != "" { - fmt.Printf(" Error: %s\n", result.Error) - } -} - -func min(a, b int) int { - if a < b { - return a - } - return b -} diff --git a/cmd/skills-demo/docker_test.go b/cmd/skills-demo/docker_test.go deleted file mode 100644 index ef6f8061..00000000 --- a/cmd/skills-demo/docker_test.go +++ /dev/null @@ -1,165 +0,0 @@ -//go:build ignore -// +build ignore - -// Docker sandbox test program -// Usage: go run cmd/skills-demo/docker_test.go -package main - -import ( - "context" - "fmt" - "os" - "path/filepath" - "runtime" - "strings" - "time" - - "github.com/Tencent/WeKnora/internal/sandbox" -) - -func main() { - fmt.Println("=" + strings.Repeat("=", 70)) - fmt.Println(" Docker Sandbox Test Suite") - fmt.Println("=" + strings.Repeat("=", 70)) - fmt.Println() - - ctx := context.Background() - - // Get the path to examples/skills - _, filename, _, ok := runtime.Caller(0) - if !ok { - fmt.Println("Error: Failed to get current file path") - os.Exit(1) - } - scriptsDir := filepath.Join(filepath.Dir(filename), "..", "..", "examples", "skills", "pdf-processing", "scripts") - - // Create Docker sandbox - config := sandbox.DefaultConfig() - config.Type = sandbox.SandboxTypeDocker - config.FallbackEnabled = false - config.DockerImage = "python:3.11-slim" - - mgr, err := sandbox.NewManager(config) - if err != nil { - fmt.Printf("❌ Failed to create sandbox manager: %v\n", err) - os.Exit(1) - } - - fmt.Printf("✅ Sandbox Type: %s\n\n", mgr.GetType()) - - // Test 1: Basic script execution - fmt.Println("Test 1: Basic Script Execution") - fmt.Println("-" + strings.Repeat("-", 50)) - runTest(ctx, mgr, &sandbox.ExecuteConfig{ - Script: filepath.Join(scriptsDir, "analyze_form.py"), - Args: []string{"sample.pdf"}, - }) - - // Test 2: Script with different arguments - fmt.Println("\nTest 2: Script with Different Arguments") - fmt.Println("-" + strings.Repeat("-", 50)) - runTest(ctx, mgr, &sandbox.ExecuteConfig{ - Script: filepath.Join(scriptsDir, "extract_text.py"), - Args: []string{"document.pdf", "--page", "1"}, - }) - - // Test 3: Environment variables - fmt.Println("\nTest 3: Environment Variables") - fmt.Println("-" + strings.Repeat("-", 50)) - runTest(ctx, mgr, &sandbox.ExecuteConfig{ - Script: filepath.Join(scriptsDir, "analyze_form.py"), - Args: []string{"test.pdf"}, - Env: map[string]string{ - "DEBUG": "true", - "LOG_LEVEL": "verbose", - }, - }) - - // Test 4: Network isolation (default: no network) - fmt.Println("\nTest 4: Network Isolation (network disabled)") - fmt.Println("-" + strings.Repeat("-", 50)) - runTest(ctx, mgr, &sandbox.ExecuteConfig{ - Script: filepath.Join(scriptsDir, "analyze_form.py"), - Args: []string{"test.pdf"}, - AllowNetwork: false, - }) - - // Test 5: Memory limits - fmt.Println("\nTest 5: Memory Limits (128MB)") - fmt.Println("-" + strings.Repeat("-", 50)) - runTest(ctx, mgr, &sandbox.ExecuteConfig{ - Script: filepath.Join(scriptsDir, "analyze_form.py"), - Args: []string{"test.pdf"}, - MemoryLimit: 128 * 1024 * 1024, // 128MB - }) - - // Test 6: CPU limits - fmt.Println("\nTest 6: CPU Limits (0.5 cores)") - fmt.Println("-" + strings.Repeat("-", 50)) - runTest(ctx, mgr, &sandbox.ExecuteConfig{ - Script: filepath.Join(scriptsDir, "analyze_form.py"), - Args: []string{"test.pdf"}, - CPULimit: 0.5, - }) - - // Test 7: Read-only filesystem - fmt.Println("\nTest 7: Read-only Root Filesystem") - fmt.Println("-" + strings.Repeat("-", 50)) - runTest(ctx, mgr, &sandbox.ExecuteConfig{ - Script: filepath.Join(scriptsDir, "analyze_form.py"), - Args: []string{"test.pdf"}, - ReadOnlyRootfs: true, - }) - - // Test 8: Custom timeout - fmt.Println("\nTest 8: Custom Timeout (10s)") - fmt.Println("-" + strings.Repeat("-", 50)) - runTest(ctx, mgr, &sandbox.ExecuteConfig{ - Script: filepath.Join(scriptsDir, "analyze_form.py"), - Args: []string{"test.pdf"}, - Timeout: 10 * time.Second, - }) - - fmt.Println("\n" + strings.Repeat("=", 71)) - fmt.Println(" All Tests Completed!") - fmt.Println(strings.Repeat("=", 71)) -} - -func runTest(ctx context.Context, mgr sandbox.Manager, config *sandbox.ExecuteConfig) { - startTime := time.Now() - result, err := mgr.Execute(ctx, config) - totalTime := time.Since(startTime) - - if err != nil { - fmt.Printf("❌ Error: %v\n", err) - return - } - - fmt.Printf(" Script: %s\n", filepath.Base(config.Script)) - fmt.Printf(" Args: %v\n", config.Args) - fmt.Printf(" Exit Code: %d\n", result.ExitCode) - fmt.Printf(" Duration: %v (total: %v)\n", result.Duration, totalTime) - fmt.Printf(" Success: %v\n", result.IsSuccess()) - - if result.Stdout != "" { - // Show just the first 3 lines of output - lines := strings.Split(result.Stdout, "\n") - preview := strings.Join(lines[:min(3, len(lines))], "\n") - fmt.Printf(" Output (preview):\n %s\n", strings.ReplaceAll(preview, "\n", "\n ")) - } - - if result.Stderr != "" { - fmt.Printf(" Stderr: %s\n", strings.TrimSpace(result.Stderr)) - } - - if result.Error != "" { - fmt.Printf(" Error: %s\n", result.Error) - } -} - -func min(a, b int) int { - if a < b { - return a - } - return b -} diff --git a/cmd/skills-demo/main.go b/cmd/skills-demo/main.go deleted file mode 100644 index 0a6c7967..00000000 --- a/cmd/skills-demo/main.go +++ /dev/null @@ -1,237 +0,0 @@ -// Package main provides a demo program to test the Agent Skills functionality. -// This simulates the agent workflow without requiring a full server setup. -// -// Usage: -// -// go run cmd/skills-demo/main.go [sandbox-type] -// -// sandbox-type can be: local, docker (default: local) -package main - -import ( - "context" - "encoding/json" - "fmt" - "os" - "path/filepath" - "runtime" - "strings" - - "github.com/Tencent/WeKnora/internal/agent/skills" - "github.com/Tencent/WeKnora/internal/sandbox" -) - -func main() { - fmt.Println("=" + strings.Repeat("=", 70)) - fmt.Println(" Agent Skills Demo - Progressive Disclosure in Action") - fmt.Println("=" + strings.Repeat("=", 70)) - fmt.Println() - - ctx := context.Background() - - // Parse sandbox type from command line - sandboxType := "local" - if len(os.Args) > 1 { - sandboxType = os.Args[1] - } - - // Get the path to examples/skills - _, filename, _, ok := runtime.Caller(0) - if !ok { - fmt.Println("Error: Failed to get current file path") - os.Exit(1) - } - skillsDir := filepath.Join(filepath.Dir(filename), "..", "..", "examples", "skills") - - fmt.Printf("📁 Skills directory: %s\n\n", skillsDir) - - // ======================================== - // Step 1: Initialize Sandbox Manager - // ======================================== - fmt.Println("Step 1: Initialize Sandbox Manager") - fmt.Println("-" + strings.Repeat("-", 50)) - fmt.Printf(" Requested sandbox type: %s\n", sandboxType) - - sandboxMgr, err := sandbox.NewManagerFromType(sandboxType, false) // Disable fallback to test specific mode - if err != nil { - fmt.Printf("Error creating sandbox: %v\n", err) - os.Exit(1) - } - fmt.Printf("✅ Sandbox initialized (type: %s)\n\n", sandboxMgr.GetType()) - - // ======================================== - // Step 2: Initialize Skills Manager - // ======================================== - fmt.Println("Step 2: Initialize Skills Manager") - fmt.Println("-" + strings.Repeat("-", 50)) - - skillsConfig := &skills.ManagerConfig{ - SkillDirs: []string{skillsDir}, - AllowedSkills: []string{}, // Allow all - Enabled: true, - } - - skillsManager := skills.NewManager(skillsConfig, sandboxMgr) - if err := skillsManager.Initialize(ctx); err != nil { - fmt.Printf("Error initializing skills: %v\n", err) - os.Exit(1) - } - - metadata := skillsManager.GetAllMetadata() - fmt.Printf("✅ Discovered %d skills\n\n", len(metadata)) - - // ======================================== - // Step 3: Show Level 1 - Metadata (System Prompt) - // ======================================== - fmt.Println("Step 3: Level 1 - Skill Metadata (injected into System Prompt)") - fmt.Println("-" + strings.Repeat("-", 50)) - - // Simulate what gets injected into system prompt - fmt.Println("\n### Available Skills\n") - fmt.Println("The following skills are available. When a user request matches a skill's description,") - fmt.Println("use the `read_skill` tool to load its full instructions before proceeding.\n") - for i, m := range metadata { - fmt.Printf("%d. **%s**: %s\n", i+1, m.Name, m.Description) - } - fmt.Println("\nUse `read_skill` with the skill name to load detailed instructions when needed.") - fmt.Println("Use `execute_skill_script` to run utility scripts bundled with a skill.") - fmt.Println() - - // ======================================== - // Step 4: Simulate Agent Tool Calls - // ======================================== - fmt.Println("Step 4: Simulate Agent Tool Calls") - fmt.Println("-" + strings.Repeat("-", 50)) - - // Scenario: User asks "Help me extract text from a PDF" - fmt.Println("\n🤖 Scenario: User asks 'Help me extract text from a PDF'") - fmt.Println(" Agent recognizes this matches 'pdf-processing' skill") - fmt.Println() - - // ======================================== - // Step 5: Level 2 - Load Skill Instructions - // ======================================== - fmt.Println("Step 5: Level 2 - Agent calls read_skill tool") - fmt.Println("-" + strings.Repeat("-", 50)) - - // Simulate tool call: read_skill(skill_name="pdf-processing") - skill, err := skillsManager.LoadSkill(ctx, "pdf-processing") - if err != nil { - fmt.Printf("Error loading skill: %v\n", err) - } else { - fmt.Printf("✅ Tool: read_skill(skill_name=\"pdf-processing\")\n") - fmt.Printf(" Success: true\n") - fmt.Printf(" Skill: %s\n", skill.Name) - fmt.Printf(" Description: %s\n", skill.Description) - fmt.Printf(" Instructions preview (first 500 chars):\n") - instructions := skill.Instructions - if len(instructions) > 500 { - instructions = instructions[:500] + "\n... (truncated)" - } - for _, line := range strings.Split(instructions, "\n") { - fmt.Printf(" │ %s\n", line) - } - } - fmt.Println() - - // ======================================== - // Step 6: Level 3 - Load Additional Resource - // ======================================== - fmt.Println("Step 6: Level 3 - Agent reads additional file (FORMS.md)") - fmt.Println("-" + strings.Repeat("-", 50)) - - // Simulate tool call: read_skill(skill_name="pdf-processing", file_path="FORMS.md") - formsContent, err := skillsManager.ReadSkillFile(ctx, "pdf-processing", "FORMS.md") - if err != nil { - fmt.Printf("Error reading FORMS.md: %v\n", err) - } else { - fmt.Printf("✅ Tool: read_skill(skill_name=\"pdf-processing\", file_path=\"FORMS.md\")\n") - fmt.Printf(" Success: true\n") - fmt.Printf(" Content length: %d characters\n", len(formsContent)) - // Show first few lines - lines := strings.Split(formsContent, "\n") - fmt.Printf(" Preview (first 10 lines):\n") - for i, line := range lines { - if i >= 10 { - fmt.Printf(" │ ... (truncated)\n") - break - } - fmt.Printf(" │ %s\n", line) - } - } - fmt.Println() - - // ======================================== - // Step 7: Execute Skill Script - // ======================================== - fmt.Println("Step 7: Agent executes skill script in sandbox") - fmt.Println("-" + strings.Repeat("-", 50)) - - // Simulate tool call: execute_skill_script(skill_name="pdf-processing", script_path="scripts/analyze_form.py", args=["test.pdf"]) - args := []string{"test.pdf"} - argsJSON, _ := json.Marshal(args) - fmt.Printf("✅ Tool: execute_skill_script\n") - fmt.Printf(" skill_name: \"pdf-processing\"\n") - fmt.Printf(" script_path: \"scripts/analyze_form.py\"\n") - fmt.Printf(" args: %s\n", string(argsJSON)) - - result, err := skillsManager.ExecuteScript(ctx, "pdf-processing", "scripts/analyze_form.py", args) - if err != nil { - fmt.Printf(" Error: %v\n", err) - } else { - fmt.Printf(" Exit Code: %d\n", result.ExitCode) - fmt.Printf(" Duration: %v\n", result.Duration) - fmt.Printf(" Output:\n") - for _, line := range strings.Split(result.Stdout, "\n") { - fmt.Printf(" │ %s\n", line) - } - if result.Stderr != "" { - fmt.Printf(" Stderr:\n") - for _, line := range strings.Split(result.Stderr, "\n") { - fmt.Printf(" │ %s\n", line) - } - } - } - fmt.Println() - - // ======================================== - // Step 8: List all files in skill - // ======================================== - fmt.Println("Step 8: List all available files in skill") - fmt.Println("-" + strings.Repeat("-", 50)) - - files, err := skillsManager.ListSkillFiles(ctx, "pdf-processing") - if err != nil { - fmt.Printf("Error listing files: %v\n", err) - } else { - fmt.Printf("✅ Files in pdf-processing skill:\n") - for _, f := range files { - isScript := skills.IsScript(f) - scriptTag := "" - if isScript { - scriptTag = " [executable]" - } - fmt.Printf(" - %s%s\n", f, scriptTag) - } - } - fmt.Println() - - // ======================================== - // Summary - // ======================================== - fmt.Println("=" + strings.Repeat("=", 70)) - fmt.Println(" Summary: Progressive Disclosure Flow") - fmt.Println("=" + strings.Repeat("=", 70)) - fmt.Println() - fmt.Println(" Level 1 (Metadata) : ~100 tokens/skill in system prompt") - fmt.Println(" → Agent knows which skills exist") - fmt.Println() - fmt.Println(" Level 2 (Instructions) : Loaded on-demand via read_skill") - fmt.Println(" → Agent learns skill instructions") - fmt.Println() - fmt.Println(" Level 3 (Resources) : Additional files loaded as needed") - fmt.Println(" → Agent accesses reference docs & scripts") - fmt.Println() - fmt.Println(" 🎉 Demo completed successfully!") - fmt.Println() -} diff --git a/docker/Dockerfile.app b/docker/Dockerfile.app index 01803fbf..1cb219ea 100644 --- a/docker/Dockerfile.app +++ b/docker/Dockerfile.app @@ -86,6 +86,7 @@ COPY --from=builder /app/config ./config COPY --from=builder /app/scripts ./scripts COPY --from=builder /app/migrations ./migrations COPY --from=builder /app/dataset/samples ./dataset/samples +COPY --from=builder /app/skills/preloaded ./skills/preloaded COPY --from=builder /root/.duckdb /home/appuser/.duckdb COPY --from=builder /app/WeKnora . diff --git a/docs/agent-skills.md b/docs/agent-skills.md index daee0749..9b89a26f 100644 --- a/docs/agent-skills.md +++ b/docs/agent-skills.md @@ -3,6 +3,7 @@ ## 概述 Agent Skills 是一种让 Agent 通过阅读"使用说明书"来学习新能力的扩展机制。与传统的硬编码工具不同,Skills 通过注入到 System Prompt 来扩展 Agent 的能力,遵循 **Progressive Disclosure(渐进式披露)** 的设计理念。 +目前仅支持带**智能推理**能力的智能体使用。前端可在智能体的编辑页面找到相关配置 ### 核心特性 @@ -83,32 +84,15 @@ with pdfplumber.open("document.pdf") as pdf: print(text) ``` -## Available Operations - -1. **Text Extraction**: Extract text content from PDF pages -2. **Table Extraction**: Extract tabular data from PDFs -... -``` - -### 元数据验证规则 +## 元数据验证规则 | 字段 | 要求 | |------|------| -| `name` | 1-50 字符,仅允许 `a-z`, `0-9`, `-`, `_`,不能是保留词 | +| `name` | 1-50 字符,仅允许汉字、英文字母、数字,不能是保留词 | | `description` | 1-500 字符,描述技能用途和触发条件 | **保留词**:`system`, `default`, `internal`, `core`, `base`, `root`, `admin` -### 最佳实践 - -**name 命名**: -- ✅ `pdf-processing`, `code_review`, `api-client` -- ❌ `PDF Processing`, `my skill`, `system` - -**description 编写**: -- 清晰描述技能的功能 -- 包含触发条件(如 "when working with PDF files") -- 避免过于模糊的描述 ## 配置 @@ -202,96 +186,174 @@ Skills 功能通过两个工具与 Agent 交互: - Ruby (`.rb`) - Go (`.go`) +## 预加载技能(Preloaded Skills) + +系统内置了以下 5 个预加载技能,用于增强知识库问答和文档处理能力: + +### 1. citation-generator - 引用生成器 + +**用途**:自动生成规范引用格式 + +**触发场景**: +- 需要生成参考文献 +- 标注知识库内容出处 +- 要求提供引用信息 + +**核心能力**: +| 功能 | 说明 | +|------|------| +| 来源标注 | 为回答中使用的每个知识点标注来源 | +| 格式化引用 | 支持 APA、MLA、Chicago、简化格式 | +| 参考文献列表 | 在回答末尾生成完整的参考文献列表 | + +**简化引用格式示例**: +``` +根据公司政策[员工手册2024.pdf, 第15页],年假申请需提前... +``` + +--- + +### 2. data-processor - 数据处理器 + +**用途**:数据处理与分析 + +**触发场景**: +- "分析这些数据"、"统计一下"、"计算总数/平均值" +- "转换为 JSON/CSV 格式" +- "提取关键信息"、"整理成表格" +- "生成报告"、"数据汇总" + +**核心能力**: +| 功能 | 说明 | +|------|------| +| 数据分析 | 对检索到的文档数据进行统计分析 | +| 格式转换 | JSON/CSV/Markdown 等格式相互转换 | +| 数据提取 | 从非结构化文本中提取结构化信息 | +| 报告生成 | 生成数据分析报告和摘要 | + +**可用脚本**: +- `scripts/analyze.py` - 数据分析脚本 +- `scripts/format_converter.py` - 格式转换脚本 +- `scripts/extract_info.py` - 信息提取脚本 + +**脚本使用示例**: +```bash +# 数据分析 +echo '{"items": [1, 2, 3, 4, 5]}' | python scripts/analyze.py + +# 格式转换(JSON 转 CSV) +echo '[{"name": "A", "value": 1}]' | python scripts/format_converter.py --to csv + +# 信息提取 +echo "2024年销售额为100万元" | python scripts/extract_info.py +``` + +--- + +### 3. doc-coauthoring - 文档协作 (源于Claude官方Skill) + +**用途**:引导用户完成结构化文档创作 + +**触发场景**: +- 编写文档:"write a doc"、"draft a proposal"、"create a spec" +- 文档类型:PRD、设计文档、决策文档、RFC + +**工作流程**: + +``` +Stage 1: 上下文收集 (Context Gathering) + ↓ +Stage 2: 细化与结构 (Refinement & Structure) + ↓ +Stage 3: 读者测试 (Reader Testing) +``` + +**三阶段说明**: +| 阶段 | 目标 | 关键活动 | +|------|------|----------| +| Stage 1 | 缩小用户与 Claude 之间的信息差 | 元信息提问、上下文收集、澄清问题 | +| Stage 2 | 逐节构建文档 | 头脑风暴、筛选整理、迭代修改 | +| Stage 3 | 测试文档对读者的效果 | 预测读者问题、子代理测试、修复盲点 | + +--- + +### 4. document-analyzer - 文档分析器 + +**用途**:深度分析文档结构和内容 + +**触发场景**: +- 分析文档结构 +- 提取关键信息 +- 识别文档类型 +- 进行内容质量评估 + +**核心能力**: +| 功能 | 说明 | +|------|------| +| 结构分析 | 识别文档的章节层级、组织架构 | +| 关键信息提取 | 提取核心论点、关键数据、重要结论 | +| 文档类型识别 | 判断文档类型(报告、手册、论文、合同等) | +| 内容质量评估 | 评估文档的完整性、一致性、可读性 | + +**分析流程**: +1. **文档概览** - 获取文档基本信息 +2. **结构分析** - 识别标题层级、章节组织 +3. **内容提取** - 提取核心主题、关键论点、支撑数据 +4. **质量评估** - 评估完整性、一致性、清晰度 + +--- + +### 5. summary-generator - 摘要生成器 + +**用途**:生成多层级文档摘要 + +**触发场景**: +- 生成内容摘要 +- 提取要点 +- 创建不同长度的文档概述 + +**摘要层级**: +| 层级 | 名称 | 长度 | 说明 | +|------|------|------|------| +| Level 1 | 一句话摘要 | ~20字 | 核心内容概括 | +| Level 2 | 要点摘要 | 3-5个要点 | **默认层级**,提取最重要的要点 | +| Level 3 | 段落摘要 | 100-200字 | 1-2段文字全面概括 | +| Level 4 | 详细摘要 | 章节式 | 按文档结构分章节摘要 | + +**摘要原则**: +- 准确性:摘要必须准确反映原文内容 +- 完整性:不遗漏重要信息 +- 简洁性:用最少的文字表达核心内容 +- 客观性:不添加个人观点 + +--- + +### 技能目录结构 + +预加载技能位于 `skills/preloaded/` 目录下: + +``` +skills/preloaded/ +├── citation-generator/ +│ └── SKILL.md +├── data-processor/ +│ ├── SKILL.md +│ └── scripts/ +│ ├── analyze.py +│ ├── format_converter.py +│ └── extract_info.py +├── doc-coauthoring/ +│ └── SKILL.md +├── document-analyzer/ +│ └── SKILL.md +└── summary-generator/ + └── SKILL.md +``` + ## 创建自定义 Skill -### 第一步:创建目录结构 +暂时不支持用户自主创建自定义 Skill -```bash -mkdir -p my-skills/code-review -cd my-skills/code-review -``` - -### 第二步:编写 SKILL.md - -```markdown ---- -name: code-review -description: Review code for best practices, security issues, and performance. Use when the user asks to review, analyze, or improve code quality. ---- - -# Code Review Skill - -This skill helps analyze code for quality and security issues. - -## How to Use - -When reviewing code: - -1. Check for common security vulnerabilities -2. Identify performance bottlenecks -3. Suggest best practice improvements - -## Security Checklist - -- [ ] SQL Injection prevention -- [ ] XSS protection -- [ ] Input validation -- [ ] Authentication checks - -## Performance Tips - -- Avoid N+1 queries -- Use appropriate data structures -- Consider caching strategies -``` - -### 第三步:添加辅助脚本(可选) - -创建 `scripts/lint.py`: - -```python -#!/usr/bin/env python3 -"""Simple code linter for demonstration.""" -import sys -import json - -def lint_code(filepath): - issues = [] - with open(filepath) as f: - for i, line in enumerate(f, 1): - if len(line) > 120: - issues.append({ - "line": i, - "issue": "Line too long", - "severity": "warning" - }) - if "eval(" in line: - issues.append({ - "line": i, - "issue": "Avoid using eval()", - "severity": "error" - }) - return issues - -if __name__ == "__main__": - if len(sys.argv) < 2: - print("Usage: lint.py ") - sys.exit(1) - - result = lint_code(sys.argv[1]) - print(json.dumps(result, indent=2)) -``` - -### 第四步:配置 Agent - -将 Skill 目录添加到 Agent 配置: - -```json -{ - "skills_enabled": true, - "skill_dirs": ["/path/to/my-skills"] -} -``` ## 沙箱安全机制 @@ -450,33 +512,4 @@ go run ./cmd/skills-demo/main.go - `skill name too long`: 名称超过 50 字符 - `skill name contains invalid characters`: 包含非法字符 - `skill name is reserved`: 使用了保留词 -- `skill description too long`: 描述超过 500 字符 - -## 运行 Demo - -```bash -cd /path/to/WeKnora -go run ./cmd/skills-demo/main.go -``` - -输出示例: - -``` -======================================================================= - Agent Skills Demo - Progressive Disclosure in Action -======================================================================= - -📁 Skills directory: /path/to/WeKnora/examples/skills - -Step 1: Initialize Sandbox Manager ---------------------------------------------------- -✅ Sandbox initialized (type: local) - -Step 2: Initialize Skills Manager ---------------------------------------------------- -✅ Discovered 1 skills - -... - -🎉 Demo completed successfully! -``` +- `skill description too long`: 描述超过 500 字符 \ No newline at end of file diff --git a/frontend/src/api/agent/index.ts b/frontend/src/api/agent/index.ts index 70797386..66d428d3 100644 --- a/frontend/src/api/agent/index.ts +++ b/frontend/src/api/agent/index.ts @@ -21,6 +21,11 @@ export interface CustomAgentConfig { mcp_selection_mode?: 'all' | 'selected' | 'none'; mcp_services?: string[]; // 选择的MCP服务ID列表 + // ===== Skills设置(仅Agent模式)===== + // Skills选择模式:all=全部预装, selected=指定, none=不使用 + skills_selection_mode?: 'all' | 'selected' | 'none'; + selected_skills?: string[]; // 选择的Skill名称列表 + // ===== 知识库设置 ===== // 知识库选择模式:all=全部知识库, selected=指定知识库, none=不使用知识库 kb_selection_mode?: 'all' | 'selected' | 'none'; diff --git a/frontend/src/api/skill/index.ts b/frontend/src/api/skill/index.ts new file mode 100644 index 00000000..958b91ad --- /dev/null +++ b/frontend/src/api/skill/index.ts @@ -0,0 +1,12 @@ +import { get } from "../../utils/request"; + +// Skill信息 +export interface SkillInfo { + name: string; + description: string; +} + +// 获取预装Skills列表 +export function listSkills() { + return get<{ data: SkillInfo[] }>('/api/v1/skills'); +} diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index 9d03f3fe..1c18e015 100755 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -366,6 +366,19 @@ export default { fallbackResponsePlaceholder: 'Sorry, I cannot answer this question.', fallbackPrompt: 'Fallback Prompt', fallbackPromptPlaceholder: 'Leave empty to use default prompt', + // Skills Config + skillsConfig: 'Skills', + skillsConfigDesc: 'Configure preloaded Skills available to the Agent for specialized domain knowledge and workflows', + skillsSelection: 'Skills Selection', + skillsSelectionDesc: 'Select the scope of Skills available to the Agent', + skillsAll: 'All', + skillsSelected: 'Selected', + skillsNone: 'Disabled', + selectSkills: 'Select Skills', + selectSkillsDesc: 'Choose which Skills to enable', + noSkillsAvailable: 'No preloaded Skills available', + skillsInfoTitle: 'What are Skills?', + skillsInfoContent: 'Skills are preloaded professional knowledge modules that provide domain-specific instructions, workflows, and tool support for the Agent. When enabled, the Agent will automatically load relevant knowledge when needed.', }, selector: { title: 'Select Agent', diff --git a/frontend/src/i18n/locales/ko-KR.ts b/frontend/src/i18n/locales/ko-KR.ts index 0e2f1e69..1633a771 100755 --- a/frontend/src/i18n/locales/ko-KR.ts +++ b/frontend/src/i18n/locales/ko-KR.ts @@ -307,6 +307,21 @@ export default { webSearchFound: "{count}개의 웹 검색 결과 발견", argumentsLabel: "파라미터", toolFallback: "도구", + editor: { + // Skills 설정 + skillsConfig: "스킬 Skills", + skillsConfigDesc: "Agent가 사용할 수 있는 사전 설치된 Skills를 구성하여 전문 영역 지식과 워크플로를 제공합니다", + skillsSelection: "Skills 선택", + skillsSelectionDesc: "Agent가 사용할 수 있는 Skills 범위 선택", + skillsAll: "전체", + skillsSelected: "지정", + skillsNone: "비활성화", + selectSkills: "Skills 선택", + selectSkillsDesc: "활성화할 Skills 선택", + noSkillsAvailable: "사전 설치된 Skills가 없습니다", + skillsInfoTitle: "Skills란 무엇인가요?", + skillsInfoContent: "Skills는 Agent에 특정 도메인의 지침, 워크플로 및 도구 지원을 제공하는 사전 설치된 전문 지식 모듈입니다. 활성화되면 Agent는 필요할 때 자동으로 관련 지식을 로드합니다.", + }, }, settings: { title: "설정", diff --git a/frontend/src/i18n/locales/ru-RU.ts b/frontend/src/i18n/locales/ru-RU.ts index 343d8377..2e871618 100755 --- a/frontend/src/i18n/locales/ru-RU.ts +++ b/frontend/src/i18n/locales/ru-RU.ts @@ -205,7 +205,22 @@ export default { updatePlan: 'Обновить план', webSearchFound: 'Найдено {count} результатов веб‑поиска', argumentsLabel: 'Параметры', - toolFallback: 'Инструмент' + toolFallback: 'Инструмент', + editor: { + // Skills Config + skillsConfig: 'Skills', + skillsConfigDesc: 'Настройка предустановленных Skills для агента, предоставляющих специализированные знания и рабочие процессы', + skillsSelection: 'Выбор Skills', + skillsSelectionDesc: 'Выберите диапазон Skills, доступных агенту', + skillsAll: 'Все', + skillsSelected: 'Выбранные', + skillsNone: 'Отключено', + selectSkills: 'Выбрать Skills', + selectSkillsDesc: 'Выберите Skills для активации', + noSkillsAvailable: 'Нет доступных предустановленных Skills', + skillsInfoTitle: 'Что такое Skills?', + skillsInfoContent: 'Skills — это предустановленные модули профессиональных знаний, которые предоставляют агенту инструкции, рабочие процессы и инструменты для конкретных областей. При активации агент автоматически загружает соответствующие знания по мере необходимости.', + }, }, settings: { title: 'Настройки', diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index 30bcf1d3..d2dec34c 100755 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -896,6 +896,19 @@ export default { fallbackResponsePlaceholder: "抱歉,我无法回答这个问题。", fallbackPrompt: "兜底提示词", fallbackPromptPlaceholder: "留空使用系统默认提示词", + // Skills 配置 + skillsConfig: "技能 Skills", + skillsConfigDesc: "配置 Agent 可以使用的预装 Skills,提供专业领域知识和工作流程", + skillsSelection: "Skills 选择", + skillsSelectionDesc: "选择 Agent 可以使用的 Skills 范围", + skillsAll: "全部", + skillsSelected: "指定", + skillsNone: "禁用", + selectSkills: "选择 Skills", + selectSkillsDesc: "选择要启用的 Skills", + noSkillsAvailable: "暂无预装 Skills", + skillsInfoTitle: "什么是 Skills?", + skillsInfoContent: "Skills 是预装的专业知识模块,可以为 Agent 提供特定领域的指令、工作流程和工具支持。启用 Skills 后,Agent 会在需要时自动加载相关知识。", }, selector: { title: "选择智能体", diff --git a/frontend/src/views/agent/AgentEditorModal.vue b/frontend/src/views/agent/AgentEditorModal.vue index 73876818..2e8d1093 100644 --- a/frontend/src/views/agent/AgentEditorModal.vue +++ b/frontend/src/views/agent/AgentEditorModal.vue @@ -574,6 +574,70 @@ + +
+
+

{{ $t('agent.editor.skillsConfig') }}

+

{{ $t('agent.editor.skillsConfigDesc') }}

+
+ +
+ +
+
+ +

{{ $t('agent.editor.skillsSelectionDesc') }}

+
+
+ + {{ $t('agent.editor.skillsAll') }} + {{ $t('agent.editor.skillsSelected') }} + {{ $t('agent.editor.skillsNone') }} + +
+
+ + +
+
+ +

{{ $t('agent.editor.selectSkillsDesc') }}

+
+
+ + +
+ {{ skill.name }} + {{ skill.description }} +
+
+
+
+
+ + +
+
+

{{ $t('agent.editor.noSkillsAvailable') }}

+
+
+ + +
+ +
+

{{ $t('agent.editor.skillsInfoTitle') }}

+

{{ $t('agent.editor.skillsInfoContent') }}

+
+
+
+
+
@@ -976,6 +1040,7 @@ import { createAgent, updateAgent, getPlaceholders, type CustomAgent, type Place import { listModels, type ModelConfig } from '@/api/model'; import { listKnowledgeBases } from '@/api/knowledge-base'; import { listMCPServices, type MCPService } from '@/api/mcp-service'; +import { listSkills, type SkillInfo } from '@/api/skill'; import { getAgentConfig, getConversationConfig } from '@/api/system'; import { useUIStore } from '@/stores/ui'; import AgentAvatar from '@/components/AgentAvatar.vue'; @@ -1003,6 +1068,7 @@ const saving = ref(false); const allModels = ref([]); const kbOptions = ref<{ label: string; value: string; type?: 'document' | 'faq'; count?: number }[]>([]); const mcpOptions = ref<{ label: string; value: string }[]>([]); +const skillOptions = ref<{ name: string; description: string }[]>([]); // 系统默认配置(用于内置智能体显示默认提示词) const defaultAgentSystemPrompt = ref(''); // Agent 模式的默认系统提示词(来自 agent-config) @@ -1033,6 +1099,9 @@ const kbSelectionMode = ref<'all' | 'selected' | 'none'>('none'); // MCP 服务选择模式:all=全部, selected=指定, none=不使用 const mcpSelectionMode = ref<'all' | 'selected' | 'none'>('none'); +// Skills 选择模式:all=全部, selected=指定, none=不使用 +const skillsSelectionMode = ref<'all' | 'selected' | 'none'>('none'); + // 可用工具列表 (与后台 definitions.go 保持一致) const allTools = [ { value: 'thinking', label: '思考', description: '动态和反思性的问题解决思考工具', requiresKB: false }, @@ -1167,6 +1236,10 @@ const navItems = computed(() => { if (isAgentMode.value) { items.push({ key: 'tools', icon: 'tools', label: t('agent.editor.toolsConfig') || '工具配置' }); } + // Agent模式才显示Skills配置 + if (isAgentMode.value) { + items.push({ key: 'skills', icon: 'lightbulb', label: t('agent.editor.skillsConfig') }); + } // 有知识库能力时才显示检索策略 if (hasKnowledgeBase.value) { items.push({ key: 'retrieval', icon: 'search', label: t('agent.editor.retrievalStrategy') || '检索策略' }); @@ -1203,6 +1276,9 @@ const defaultFormData = { // MCP 服务设置 mcp_selection_mode: 'none' as 'all' | 'selected' | 'none', mcp_services: [] as string[], + // Skills 设置 + skills_selection_mode: 'none' as 'all' | 'selected' | 'none', + selected_skills: [] as string[], // 知识库设置 kb_selection_mode: 'none' as 'all' | 'selected' | 'none', knowledge_bases: [] as string[], @@ -1296,6 +1372,7 @@ watch(() => props.visible, async (val) => { if (!agentData.config.knowledge_bases) agentData.config.knowledge_bases = []; if (!agentData.config.allowed_tools) agentData.config.allowed_tools = []; if (!agentData.config.mcp_services) agentData.config.mcp_services = []; + if (!agentData.config.selected_skills) agentData.config.selected_skills = []; if (!agentData.config.supported_file_types) agentData.config.supported_file_types = []; // 兼容旧数据:如果没有 agent_mode 字段,根据 allowed_tools 推断 @@ -1310,6 +1387,7 @@ watch(() => props.visible, async (val) => { // 初始化知识库选择模式 initKbSelectionMode(); initMcpSelectionMode(); + initSkillsSelectionMode(); // 初始化完成后重置标志 nextTick(() => { isInitializing.value = false; @@ -1336,6 +1414,7 @@ watch(() => props.visible, async (val) => { formData.value = newFormData; kbSelectionMode.value = 'none'; mcpSelectionMode.value = 'none'; + skillsSelectionMode.value = 'none'; } } }); @@ -1366,6 +1445,19 @@ const initMcpSelectionMode = () => { } }; +// 初始化 Skills 选择模式 +const initSkillsSelectionMode = () => { + if (formData.value.config.skills_selection_mode) { + // 如果有保存的模式,直接使用 + skillsSelectionMode.value = formData.value.config.skills_selection_mode; + } else if (formData.value.config.selected_skills?.length > 0) { + // 有指定 Skills + skillsSelectionMode.value = 'selected'; + } else { + skillsSelectionMode.value = 'none'; + } +}; + // 内置智能体:填入系统默认值 const fillBuiltinAgentDefaults = () => { const config = formData.value.config; @@ -1427,6 +1519,19 @@ watch(mcpSelectionMode, (mode) => { // selected 模式保持 mcp_services 不变 }); +// 监听 Skills 选择模式变化 +watch(skillsSelectionMode, (mode) => { + formData.value.config.skills_selection_mode = mode; + if (mode === 'none') { + // 不使用 Skills,清空相关配置 + formData.value.config.selected_skills = []; + } else if (mode === 'all') { + // 全部 Skills,清空指定列表 + formData.value.config.selected_skills = []; + } + // selected 模式保持 selected_skills 不变 +}); + // 监听模式变化,自动调整配置 watch(agentMode, (val) => { if (val === 'smart-reasoning') { @@ -1541,6 +1646,16 @@ const loadDependencies = async () => { console.warn('Failed to load MCP services', e); } + // 加载预装 Skills 列表 + try { + const skillsRes = await listSkills(); + if (skillsRes.data && skillsRes.data.length > 0) { + skillOptions.value = skillsRes.data; + } + } catch (e) { + console.warn('Failed to load skills', e); + } + // 加载占位符定义(从统一 API) try { const placeholdersRes = await getPlaceholders(); @@ -2921,6 +3036,96 @@ const handleSave = async () => { } } +// Skills 选择样式 +.skills-checkbox-group { + display: grid; + grid-template-columns: 1fr; + gap: 12px; + width: 100%; +} + +.skill-checkbox-item { + display: flex; + align-items: flex-start; + padding: 12px 16px; + background: #fafafa; + border-radius: 8px; + border: 1px solid #e5e7eb; + transition: all 0.2s ease; + + &:hover { + border-color: #07c05f; + background: #f0faf5; + } + + :deep(.t-checkbox__input) { + margin-top: 2px; + } + + :deep(.t-checkbox__label) { + flex: 1; + } +} + +.skill-item-content { + display: flex; + flex-direction: column; + gap: 4px; +} + +.skill-name { + font-size: 14px; + font-weight: 500; + color: #333; +} + +.skill-desc { + font-size: 12px; + color: #666; + line-height: 1.5; +} + +.skill-info-box { + display: flex; + gap: 12px; + padding: 16px; + background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); + border-radius: 8px; + border: 1px solid #bae6fd; + margin-top: 16px; + + .info-icon { + font-size: 20px; + color: #0ea5e9; + flex-shrink: 0; + margin-top: 2px; + } + + .info-content { + flex: 1; + + p { + margin: 0; + font-size: 13px; + color: #475569; + line-height: 1.6; + + &:first-child { + margin-bottom: 4px; + } + + strong { + color: #0369a1; + } + } + } +} + +.empty-hint { + color: #999; + font-style: italic; +} + // Checkbox 选中样式 :deep(.t-checkbox) { &.t-is-checked { diff --git a/internal/agent/prompts.go b/internal/agent/prompts.go index e8733c56..86f4ecb8 100644 --- a/internal/agent/prompts.go +++ b/internal/agent/prompts.go @@ -202,16 +202,27 @@ func formatSkillsMetadata(skillsMetadata []*skills.SkillMetadata) string { } var builder strings.Builder - builder.WriteString("\n### Available Skills\n\n") - builder.WriteString("The following skills are available. When a user request matches a skill's description, ") - builder.WriteString("use the `read_skill` tool to load its full instructions before proceeding.\n\n") + 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**: %s\n", i+1, skill.Name, skill.Description)) + builder.WriteString(fmt.Sprintf("%d. **%s**\n", i+1, skill.Name)) + builder.WriteString(fmt.Sprintf(" %s\n\n", skill.Description)) } - builder.WriteString("\nUse `read_skill` with the skill name to load detailed instructions when needed.\n") - builder.WriteString("Use `execute_skill_script` to run utility scripts bundled with a skill.\n") + 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") return builder.String() } diff --git a/internal/agent/skills/loader.go b/internal/agent/skills/loader.go index 7a6c1483..6abc0c5a 100644 --- a/internal/agent/skills/loader.go +++ b/internal/agent/skills/loader.go @@ -236,7 +236,7 @@ func (l *Loader) LoadSkillFile(skillName, relativePath string) (*SkillFile, erro return &SkillFile{ Name: relativePath, - Path: fullPath, + Path: absFilePath, // Use absolute path for sandbox execution Content: string(content), IsScript: IsScript(relativePath), }, nil @@ -287,7 +287,7 @@ func (l *Loader) GetSkillByName(name string) (*Skill, bool) { return skill, ok } -// GetSkillBasePath returns the base path for a skill +// GetSkillBasePath returns the base path for a skill (always absolute) func (l *Loader) GetSkillBasePath(skillName string) (string, error) { skill, ok := l.discoveredSkills[skillName] if !ok { @@ -297,7 +297,8 @@ func (l *Loader) GetSkillBasePath(skillName string) (string, error) { return "", fmt.Errorf("skill not found: %s", skillName) } } - return skill.BasePath, nil + // Return absolute path for consistent sandbox execution + return filepath.Abs(skill.BasePath) } // Reload clears the cache and rediscovers all skills diff --git a/internal/agent/skills/manager.go b/internal/agent/skills/manager.go index d879d5ab..1557bc52 100644 --- a/internal/agent/skills/manager.go +++ b/internal/agent/skills/manager.go @@ -172,7 +172,7 @@ func (m *Manager) ListSkillFiles(ctx context.Context, skillName string) ([]strin } // ExecuteScript executes a script from a skill in the sandbox -func (m *Manager) ExecuteScript(ctx context.Context, skillName, scriptPath string, args []string) (*sandbox.ExecuteResult, error) { +func (m *Manager) ExecuteScript(ctx context.Context, skillName, scriptPath string, args []string, stdin string) (*sandbox.ExecuteResult, error) { if !m.enabled { return nil, fmt.Errorf("skills are not enabled") } @@ -207,6 +207,7 @@ func (m *Manager) ExecuteScript(ctx context.Context, skillName, scriptPath strin Script: file.Path, Args: args, WorkDir: basePath, + Stdin: stdin, } // Execute in sandbox diff --git a/internal/agent/skills/skill.go b/internal/agent/skills/skill.go index 65fa299a..e8dcb902 100644 --- a/internal/agent/skills/skill.go +++ b/internal/agent/skills/skill.go @@ -23,8 +23,8 @@ const ( // Reserved words that cannot be used in skill names var reservedWords = []string{"anthropic", "claude"} -// namePattern validates skill names: lowercase letters, numbers, and hyphens only -var namePattern = regexp.MustCompile(`^[a-z0-9-]+$`) +// namePattern validates skill names: unicode letters, numbers only +var namePattern = regexp.MustCompile(`^[\p{L}\p{N}-]+$`) // xmlTagPattern detects XML tags in content var xmlTagPattern = regexp.MustCompile(`<[^>]+>`) diff --git a/internal/agent/tools/skill_execute.go b/internal/agent/tools/skill_execute.go index 743b1262..d7b1e459 100644 --- a/internal/agent/tools/skill_execute.go +++ b/internal/agent/tools/skill_execute.go @@ -44,6 +44,7 @@ type ExecuteSkillScriptInput struct { SkillName string `json:"skill_name" jsonschema:"Name of the skill containing the script"` ScriptPath string `json:"script_path" jsonschema:"Relative path to the script within the skill directory (e.g. scripts/analyze.py)"` Args []string `json:"args,omitempty" jsonschema:"Optional command-line arguments to pass to the script"` + Input string `json:"input,omitempty" jsonschema:"Optional input data to pass to the script via stdin"` } // ExecuteSkillScriptTool allows the agent to execute skill scripts in a sandbox @@ -98,10 +99,10 @@ func (t *ExecuteSkillScriptTool) Execute(ctx context.Context, args json.RawMessa } // Execute the script in sandbox - logger.Infof(ctx, "[Tool][ExecuteSkillScript] Executing script: %s/%s with args: %v", - input.SkillName, input.ScriptPath, input.Args) + logger.Infof(ctx, "[Tool][ExecuteSkillScript] Executing script: %s/%s with args: %v, input length: %d", + input.SkillName, input.ScriptPath, input.Args, len(input.Input)) - result, err := t.skillManager.ExecuteScript(ctx, input.SkillName, input.ScriptPath, input.Args) + result, err := t.skillManager.ExecuteScript(ctx, input.SkillName, input.ScriptPath, input.Args, input.Input) if err != nil { logger.Errorf(ctx, "[Tool][ExecuteSkillScript] Script execution failed: %v", err) return &types.ToolResult{ diff --git a/internal/application/service/session.go b/internal/application/service/session.go index c5362638..f7cdc682 100644 --- a/internal/application/service/session.go +++ b/internal/application/service/session.go @@ -843,6 +843,66 @@ func (s *sessionService) resolveKnowledgeBasesFromAgent( } } +// configureSkillsFromAgent configures skills settings in AgentConfig based on CustomAgentConfig +// Returns the skill directories and allowed skills based on the selection mode: +// - "all": uses all preloaded skills +// - "selected": uses the explicitly selected skills +// - "none" or "": skills are disabled +func (s *sessionService) configureSkillsFromAgent( + ctx context.Context, + agentConfig *types.AgentConfig, + customAgent *types.CustomAgent, +) { + if customAgent == nil { + return + } + + switch customAgent.Config.SkillsSelectionMode { + case "all": + // Enable all preloaded skills + agentConfig.SkillsEnabled = true + agentConfig.SkillDirs = []string{DefaultPreloadedSkillsDir} + agentConfig.AllowedSkills = nil // Empty means all skills allowed + logger.Infof(ctx, "SkillsSelectionMode=all: enabled all preloaded skills") + case "selected": + // Enable only selected skills + if len(customAgent.Config.SelectedSkills) > 0 { + agentConfig.SkillsEnabled = true + agentConfig.SkillDirs = []string{DefaultPreloadedSkillsDir} + agentConfig.AllowedSkills = customAgent.Config.SelectedSkills + logger.Infof(ctx, "SkillsSelectionMode=selected: enabled %d selected skills: %v", + len(customAgent.Config.SelectedSkills), customAgent.Config.SelectedSkills) + } else { + agentConfig.SkillsEnabled = false + logger.Infof(ctx, "SkillsSelectionMode=selected but no skills selected: skills disabled") + } + case "none", "": + // Skills disabled + agentConfig.SkillsEnabled = false + logger.Infof(ctx, "SkillsSelectionMode=%s: skills disabled", customAgent.Config.SkillsSelectionMode) + default: + // Unknown mode, disable skills + agentConfig.SkillsEnabled = false + logger.Warnf(ctx, "Unknown SkillsSelectionMode=%s: skills disabled", customAgent.Config.SkillsSelectionMode) + } + + // Configure sandbox mode for skill script execution + if agentConfig.SkillsEnabled { + // Set sandbox mode (default to "docker" when skills are enabled) + agentConfig.SandboxMode = customAgent.Config.SandboxMode + if agentConfig.SandboxMode == "" { + agentConfig.SandboxMode = "docker" // Default to docker for security + } + // Set sandbox timeout (default to 60 seconds) + agentConfig.SandboxTimeout = customAgent.Config.SandboxTimeout + if agentConfig.SandboxTimeout == 0 { + agentConfig.SandboxTimeout = 60 + } + logger.Infof(ctx, "Sandbox configured: mode=%s, timeout=%ds", + agentConfig.SandboxMode, agentConfig.SandboxTimeout) + } +} + // buildSearchTargets computes the unified search targets from knowledgeBaseIDs and knowledgeIDs // This is called once at the request entry point to avoid repeated queries later in the pipeline // Logic: @@ -1130,6 +1190,9 @@ func (s *sessionService) AgentQA( RetrieveKBOnlyWhenMentioned: customAgent.Config.RetrieveKBOnlyWhenMentioned, } + // Configure skills based on CustomAgentConfig + s.configureSkillsFromAgent(ctx, agentConfig, customAgent) + // Resolve knowledge bases: request-level @ mentions take priority over agent config // If RetrieveKBOnlyWhenMentioned is enabled and no @ mentions, don't use KB at all hasExplicitMention := len(knowledgeBaseIDs) > 0 || len(knowledgeIDs) > 0 diff --git a/internal/application/service/skill_service.go b/internal/application/service/skill_service.go new file mode 100644 index 00000000..a463f1a1 --- /dev/null +++ b/internal/application/service/skill_service.go @@ -0,0 +1,135 @@ +package service + +import ( + "context" + "fmt" + "os" + "path/filepath" + "sync" + + "github.com/Tencent/WeKnora/internal/agent/skills" + "github.com/Tencent/WeKnora/internal/logger" + "github.com/Tencent/WeKnora/internal/types/interfaces" +) + +// DefaultPreloadedSkillsDir is the default directory for preloaded skills +const DefaultPreloadedSkillsDir = "skills/preloaded" + +// skillService implements SkillService interface +type skillService struct { + loader *skills.Loader + preloadedDir string + mu sync.RWMutex + initialized bool +} + +// NewSkillService creates a new skill service +func NewSkillService() interfaces.SkillService { + // Determine the preloaded skills directory + preloadedDir := getPreloadedSkillsDir() + + return &skillService{ + preloadedDir: preloadedDir, + initialized: false, + } +} + +// getPreloadedSkillsDir returns the path to the preloaded skills directory +func getPreloadedSkillsDir() string { + // Check if SKILLS_DIR environment variable is set + if dir := os.Getenv("WEKNORA_SKILLS_DIR"); dir != "" { + return dir + } + + // Try to find the skills directory relative to the executable + execPath, err := os.Executable() + if err == nil { + execDir := filepath.Dir(execPath) + skillsDir := filepath.Join(execDir, DefaultPreloadedSkillsDir) + if _, err := os.Stat(skillsDir); err == nil { + return skillsDir + } + } + + // Try current working directory + cwd, err := os.Getwd() + if err == nil { + skillsDir := filepath.Join(cwd, DefaultPreloadedSkillsDir) + if _, err := os.Stat(skillsDir); err == nil { + return skillsDir + } + } + + // Default to relative path (will be created if needed) + return DefaultPreloadedSkillsDir +} + +// ensureInitialized initializes the loader if not already done +func (s *skillService) ensureInitialized(ctx context.Context) error { + s.mu.Lock() + defer s.mu.Unlock() + + if s.initialized { + return nil + } + + // Check if preloaded directory exists + if _, err := os.Stat(s.preloadedDir); os.IsNotExist(err) { + logger.Warnf(ctx, "Preloaded skills directory does not exist: %s", s.preloadedDir) + // Create the directory to avoid repeated warnings + if err := os.MkdirAll(s.preloadedDir, 0755); err != nil { + logger.Warnf(ctx, "Failed to create preloaded skills directory: %v", err) + } + } + + // Create loader with preloaded directory + s.loader = skills.NewLoader([]string{s.preloadedDir}) + s.initialized = true + + logger.Infof(ctx, "Skill service initialized with preloaded directory: %s", s.preloadedDir) + + return nil +} + +// ListPreloadedSkills returns metadata for all preloaded skills +func (s *skillService) ListPreloadedSkills(ctx context.Context) ([]*skills.SkillMetadata, error) { + if err := s.ensureInitialized(ctx); err != nil { + return nil, fmt.Errorf("failed to initialize skill service: %w", err) + } + + s.mu.RLock() + defer s.mu.RUnlock() + + metadata, err := s.loader.DiscoverSkills() + if err != nil { + logger.Errorf(ctx, "Failed to discover preloaded skills: %v", err) + return nil, fmt.Errorf("failed to discover skills: %w", err) + } + + logger.Infof(ctx, "Discovered %d preloaded skills", len(metadata)) + + return metadata, nil +} + +// GetSkillByName retrieves a skill by its name +func (s *skillService) GetSkillByName(ctx context.Context, name string) (*skills.Skill, error) { + if err := s.ensureInitialized(ctx); err != nil { + return nil, fmt.Errorf("failed to initialize skill service: %w", err) + } + + s.mu.RLock() + defer s.mu.RUnlock() + + skill, err := s.loader.LoadSkillInstructions(name) + if err != nil { + logger.Errorf(ctx, "Failed to load skill %s: %v", name, err) + return nil, fmt.Errorf("failed to load skill: %w", err) + } + + return skill, nil +} + +// GetPreloadedDir returns the configured preloaded skills directory +func (s *skillService) GetPreloadedDir() string { + return s.preloadedDir +} diff --git a/internal/container/container.go b/internal/container/container.go index cb5e3647..e79b798b 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -197,6 +197,8 @@ func BuildContainer(container *dig.Container) *dig.Container { must(container.Provide(handler.NewMCPServiceHandler)) must(container.Provide(handler.NewWebSearchHandler)) must(container.Provide(handler.NewCustomAgentHandler)) + must(container.Provide(service.NewSkillService)) + must(container.Provide(handler.NewSkillHandler)) logger.Debugf(ctx, "[Container] HTTP handlers registered") // Router configuration diff --git a/internal/handler/skill_handler.go b/internal/handler/skill_handler.go new file mode 100644 index 00000000..338c03ee --- /dev/null +++ b/internal/handler/skill_handler.go @@ -0,0 +1,64 @@ +package handler + +import ( + "net/http" + + "github.com/Tencent/WeKnora/internal/errors" + "github.com/Tencent/WeKnora/internal/logger" + "github.com/Tencent/WeKnora/internal/types/interfaces" + "github.com/gin-gonic/gin" +) + +// SkillHandler handles skill-related HTTP requests +type SkillHandler struct { + skillService interfaces.SkillService +} + +// NewSkillHandler creates a new skill handler +func NewSkillHandler(skillService interfaces.SkillService) *SkillHandler { + return &SkillHandler{ + skillService: skillService, + } +} + +// SkillInfoResponse represents the skill info returned to frontend +type SkillInfoResponse struct { + Name string `json:"name"` + Description string `json:"description"` +} + +// ListSkills godoc +// @Summary 获取预装Skills列表 +// @Description 获取所有预装的Agent Skills元数据 +// @Tags Skills +// @Accept json +// @Produce json +// @Success 200 {object} map[string]interface{} "Skills列表" +// @Failure 500 {object} errors.AppError "服务器错误" +// @Security Bearer +// @Security ApiKeyAuth +// @Router /skills [get] +func (h *SkillHandler) ListSkills(c *gin.Context) { + ctx := c.Request.Context() + + skillsMetadata, err := h.skillService.ListPreloadedSkills(ctx) + if err != nil { + logger.ErrorWithFields(ctx, err, nil) + c.Error(errors.NewInternalServerError("Failed to list skills: " + err.Error())) + return + } + + // Convert to response format + var response []SkillInfoResponse + for _, meta := range skillsMetadata { + response = append(response, SkillInfoResponse{ + Name: meta.Name, + Description: meta.Description, + }) + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "data": response, + }) +} diff --git a/internal/router/router.go b/internal/router/router.go index e44584b0..209b1609 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -48,6 +48,7 @@ type RouterParams struct { FAQHandler *handler.FAQHandler TagHandler *handler.TagHandler CustomAgentHandler *handler.CustomAgentHandler + SkillHandler *handler.SkillHandler } // NewRouter 创建新的路由 @@ -112,6 +113,7 @@ func NewRouter(params RouterParams) *gin.Engine { RegisterMCPServiceRoutes(v1, params.MCPServiceHandler) RegisterWebSearchRoutes(v1, params.WebSearchHandler) RegisterCustomAgentRoutes(v1, params.CustomAgentHandler) + RegisterSkillRoutes(v1, params.SkillHandler) } return r @@ -439,3 +441,12 @@ func RegisterCustomAgentRoutes(r *gin.RouterGroup, agentHandler *handler.CustomA agents.POST("/:id/copy", agentHandler.CopyAgent) } } + +// RegisterSkillRoutes registers skill routes +func RegisterSkillRoutes(r *gin.RouterGroup, skillHandler *handler.SkillHandler) { + skills := r.Group("/skills") + { + // List all preloaded skills + skills.GET("", skillHandler.ListSkills) + } +} diff --git a/internal/types/custom_agent.go b/internal/types/custom_agent.go index 3785ddd2..934a77de 100644 --- a/internal/types/custom_agent.go +++ b/internal/types/custom_agent.go @@ -94,6 +94,16 @@ type CustomAgentConfig struct { // Selected MCP service IDs (only used when MCPSelectionMode is "selected") MCPServices []string `yaml:"mcp_services" json:"mcp_services"` + // ===== Skills Settings (only for smart-reasoning mode) ===== + // Skills selection mode: "all" = all preloaded skills, "selected" = specific skills, "none" = no skills + SkillsSelectionMode string `yaml:"skills_selection_mode" json:"skills_selection_mode"` + // Selected skill names (only used when SkillsSelectionMode is "selected") + SelectedSkills []string `yaml:"selected_skills" json:"selected_skills"` + // Sandbox mode for skill script execution: "docker", "local", "disabled" (default: "docker" when skills enabled) + SandboxMode string `yaml:"sandbox_mode" json:"sandbox_mode"` + // Script execution timeout in seconds (default: 60) + SandboxTimeout int `yaml:"sandbox_timeout" json:"sandbox_timeout"` + // ===== Knowledge Base Settings ===== // Knowledge base selection mode: "all" = all KBs, "selected" = specific KBs, "none" = no KB KBSelectionMode string `yaml:"kb_selection_mode" json:"kb_selection_mode"` @@ -322,7 +332,7 @@ func GetBuiltinDataAnalystAgent(tenantID uint64) *CustomAgent { IsBuiltin: true, TenantID: tenantID, Config: CustomAgentConfig{ - AgentMode: AgentModeSmartReasoning, + AgentMode: AgentModeSmartReasoning, SystemPrompt: `### Role You are WeKnora Data Analyst, an intelligent data analysis assistant powered by DuckDB. You specialize in analyzing structured data from CSV and Excel files using SQL queries. diff --git a/internal/types/interfaces/skill.go b/internal/types/interfaces/skill.go new file mode 100644 index 00000000..16b3896d --- /dev/null +++ b/internal/types/interfaces/skill.go @@ -0,0 +1,16 @@ +package interfaces + +import ( + "context" + + "github.com/Tencent/WeKnora/internal/agent/skills" +) + +// SkillService defines the interface for skill business logic +type SkillService interface { + // ListPreloadedSkills returns metadata for all preloaded skills + ListPreloadedSkills(ctx context.Context) ([]*skills.SkillMetadata, error) + + // GetSkillByName retrieves a skill by its name + GetSkillByName(ctx context.Context, name string) (*skills.Skill, error) +} diff --git a/skills/preloaded/citation-generator/SKILL.md b/skills/preloaded/citation-generator/SKILL.md new file mode 100644 index 00000000..58c9b736 --- /dev/null +++ b/skills/preloaded/citation-generator/SKILL.md @@ -0,0 +1,58 @@ +--- +name: 引用生成器 +description: 自动生成规范引用格式。当用户需要生成参考文献、引用来源、标注知识库内容出处、或要求提供引用信息时使用此技能。 +--- + +# Citation Generator + +为知识库检索结果生成规范的引用格式。 + +## 核心能力 + +1. **来源标注**: 为回答中使用的每个知识点标注来源 +2. **格式化引用**: 支持多种引用格式(APA、MLA、Chicago、简化格式) +3. **参考文献列表**: 在回答末尾生成完整的参考文献列表 + +## 引用格式 + +### 简化格式(默认) + +对于知识库内容,使用以下格式: +``` +[文档名称, 第X页/段落X] +``` + +示例: +``` +根据公司政策[员工手册2024.pdf, 第15页],年假申请需提前... +``` + +### APA 格式 + +``` +作者. (年份). 标题. 来源. +``` + +### 参考文献列表格式 + +在回答末尾,使用以下格式列出所有引用: + +``` +--- +**参考文献** + +1. [1] 文档A - 第X章/第Y页 +2. [2] 文档B - 第Z段 +``` + +## 使用指南 + +1. **检索内容时**: 记录每个检索结果的来源信息(文档名、页码、分块ID) +2. **引用时**: 在使用知识点后立即标注来源 +3. **汇总时**: 在回答末尾列出完整参考文献 + +## 注意事项 + +- 如果检索结果未提供页码,使用分块或段落编号 +- 对于同一文档的多次引用,可合并为一条 +- 引用应准确对应原文内容,不可虚构来源 diff --git a/skills/preloaded/data-processor/SKILL.md b/skills/preloaded/data-processor/SKILL.md new file mode 100644 index 00000000..f5c50f1c --- /dev/null +++ b/skills/preloaded/data-processor/SKILL.md @@ -0,0 +1,154 @@ +--- +name: 数据处理器 +description: 数据处理与分析技能。当用户需要对知识库检索结果进行数据分析、统计计算、格式转换、数据提取或生成报告时使用此技能。支持 Python 脚本执行进行高级数据处理。 +--- + +# Data Processor + +企业级知识库数据处理与分析技能,用于处理 RAG 检索结果和执行数据分析任务。 + +## 核心能力 + +1. **数据分析**: 对检索到的文档数据进行统计分析 +2. **格式转换**: JSON/CSV/Markdown 等格式相互转换 +3. **数据提取**: 从非结构化文本中提取结构化信息 +4. **报告生成**: 生成数据分析报告和摘要 + +## 使用场景 + +当用户请求涉及以下内容时,使用此技能: +- "分析这些数据"、"统计一下"、"计算总数/平均值" +- "转换为 JSON/CSV 格式" +- "提取关键信息"、"整理成表格" +- "生成报告"、"数据汇总" + +## 可用脚本 + +### 1. analyze.py - 数据分析脚本 + +分析输入的 JSON 数据,生成统计报告。 + +**用法**: +```bash +# 通过 stdin 传入 JSON 数据 +echo '{"items": [1, 2, 3, 4, 5]}' | python scripts/analyze.py + +# 或传入文件路径 +python scripts/analyze.py --file data.json +``` + +**输入格式**: +```json +{ + "items": [数据项数组], + "query": "可选的查询描述" +} +``` + +**输出**: JSON 格式的统计结果,包含计数、求和、平均值等。 + +### 2. format_converter.py - 格式转换脚本 + +在 JSON、CSV、Markdown 表格之间转换数据。 + +**用法**: +```bash +# JSON 转 CSV +echo '[{"name": "A", "value": 1}]' | python scripts/format_converter.py --to csv + +# JSON 转 Markdown 表格 +echo '[{"name": "A", "value": 1}]' | python scripts/format_converter.py --to markdown + +# CSV 转 JSON +echo 'name,value\nA,1' | python scripts/format_converter.py --from csv --to json +``` + +### 3. extract_info.py - 信息提取脚本 + +从文本中提取结构化信息(数字、日期、关键词等)。 + +**用法**: +```bash +echo "2024年销售额为100万元,同比增长15%" | python scripts/extract_info.py +``` + +**输出**: +```json +{ + "numbers": ["100", "15"], + "dates": ["2024年"], + "percentages": ["15%"], + "amounts": ["100万元"] +} +``` + +## 处理流程 + +### 分析 RAG 检索结果 + +当需要分析知识库检索结果时: + +1. 收集检索到的文档片段 +2. 提取关键数据点 +3. 使用 `analyze.py` 进行统计 +4. 整理并呈现分析结果 + +**示例**: +``` +用户: "帮我统计知识库中提到的所有产品销售数据" + +步骤: +1. 使用 knowledge_search 检索相关文档 +2. 整理数据为 JSON 格式 +3. 调用 execute_skill_script: + - skill_name: "data-processor" + - script_path: "scripts/analyze.py" + - 通过 stdin 传入数据 +4. 解析输出并生成报告 +``` + +### 数据格式转换 + +当用户需要特定格式输出时: + +1. 整理数据为标准 JSON 格式 +2. 使用 `format_converter.py` 转换 +3. 返回目标格式结果 + +## 最佳实践 + +1. **数据预处理**: 调用脚本前,确保数据格式正确 +2. **错误处理**: 检查脚本执行结果,处理异常情况 +3. **结果验证**: 验证输出结果的合理性 +4. **渐进处理**: 大数据量时分批处理 + +## 输出格式 + +分析结果示例: +```markdown +## 数据分析报告 + +### 基本统计 +- 数据条数: 50 +- 数值总和: 1,234,567 +- 平均值: 24,691.34 +- 最大值: 99,999 +- 最小值: 100 + +### 分布情况 +| 区间 | 数量 | 占比 | +|------|------|------| +| 0-1000 | 10 | 20% | +| 1000-10000 | 25 | 50% | +| >10000 | 15 | 30% | + +### 结论 +根据数据分析,XXX... +``` + +## 注意事项 + +- 脚本在 Docker 沙箱中执行,确保安全隔离 +- 执行超时默认为 60 秒 +- 输入数据大小有限制,大文件请分批处理 +- 脚本输出为 JSON 格式,便于后续处理 diff --git a/skills/preloaded/data-processor/scripts/analyze.py b/skills/preloaded/data-processor/scripts/analyze.py new file mode 100644 index 00000000..b736a113 --- /dev/null +++ b/skills/preloaded/data-processor/scripts/analyze.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +""" +数据分析脚本 - 用于分析 RAG 检索结果和知识库数据 + +支持功能: +- 基本统计(计数、求和、平均值、最大/最小值) +- 数值分布分析 +- 文本统计(词频、字符数) + +用法: + # 通过 stdin 传入 JSON 数据 + echo '{"items": [1, 2, 3, 4, 5]}' | python analyze.py + + # 通过参数传入文件 + python analyze.py --file data.json + + # 指定分析类型 + echo '{"items": [1, 2, 3]}' | python analyze.py --type numeric +""" + +import sys +import json +import argparse +from collections import Counter + + +def analyze_numeric(data: list) -> dict: + """分析数值数据""" + if not data: + return {"error": "空数据集"} + + # 过滤出数值 + numbers = [x for x in data if isinstance(x, (int, float))] + if not numbers: + return {"error": "无有效数值数据"} + + numbers.sort() + n = len(numbers) + + result = { + "count": n, + "sum": sum(numbers), + "mean": sum(numbers) / n, + "min": min(numbers), + "max": max(numbers), + "median": numbers[n // 2] if n % 2 == 1 else (numbers[n // 2 - 1] + numbers[n // 2]) / 2, + } + + # 计算标准差 + mean = result["mean"] + variance = sum((x - mean) ** 2 for x in numbers) / n + result["std_dev"] = variance ** 0.5 + + # 分布统计 + if n >= 5: + result["quartiles"] = { + "q1": numbers[n // 4], + "q2": result["median"], + "q3": numbers[3 * n // 4] + } + + return result + + +def analyze_text(data: list) -> dict: + """分析文本数据""" + if not data: + return {"error": "空数据集"} + + texts = [str(x) for x in data if x] + + # 基本统计 + total_chars = sum(len(t) for t in texts) + total_words = sum(len(t.split()) for t in texts) + + # 词频统计(简单分词) + all_words = [] + for text in texts: + words = text.split() + all_words.extend(w.strip('.,!?;:""\'()[]{}') for w in words if w.strip()) + + word_freq = Counter(all_words) + + result = { + "count": len(texts), + "total_chars": total_chars, + "total_words": total_words, + "avg_chars_per_item": total_chars / len(texts) if texts else 0, + "avg_words_per_item": total_words / len(texts) if texts else 0, + "top_words": dict(word_freq.most_common(10)), + "unique_words": len(word_freq) + } + + return result + + +def analyze_mixed(data: list) -> dict: + """分析混合数据""" + if not data: + return {"error": "空数据集"} + + # 类型统计 + type_counts = Counter(type(x).__name__ for x in data) + + result = { + "total_items": len(data), + "type_distribution": dict(type_counts), + } + + # 分别分析数值和文本 + numbers = [x for x in data if isinstance(x, (int, float))] + texts = [x for x in data if isinstance(x, str)] + + if numbers: + result["numeric_analysis"] = analyze_numeric(numbers) + if texts: + result["text_analysis"] = analyze_text(texts) + + return result + + +def analyze_dict_list(data: list) -> dict: + """分析字典列表(如数据库查询结果)""" + if not data: + return {"error": "空数据集"} + + if not all(isinstance(x, dict) for x in data): + return {"error": "数据格式不正确,需要字典列表"} + + result = { + "record_count": len(data), + "fields": {}, + } + + # 获取所有字段 + all_keys = set() + for item in data: + all_keys.update(item.keys()) + + # 分析每个字段 + for key in all_keys: + values = [item.get(key) for item in data if key in item] + + # 判断字段类型 + non_null_values = [v for v in values if v is not None] + if not non_null_values: + result["fields"][key] = {"type": "all_null", "null_count": len(values)} + continue + + sample = non_null_values[0] + if isinstance(sample, (int, float)): + field_analysis = analyze_numeric(non_null_values) + field_analysis["type"] = "numeric" + elif isinstance(sample, str): + field_analysis = analyze_text(non_null_values) + field_analysis["type"] = "text" + else: + field_analysis = {"type": type(sample).__name__, "count": len(non_null_values)} + + field_analysis["null_count"] = len(values) - len(non_null_values) + result["fields"][key] = field_analysis + + return result + + +def main(): + parser = argparse.ArgumentParser(description="数据分析工具") + parser.add_argument("--file", "-f", help="输入文件路径") + parser.add_argument("--type", "-t", choices=["numeric", "text", "mixed", "auto"], + default="auto", help="分析类型") + parser.add_argument("--pretty", "-p", action="store_true", help="格式化输出") + args = parser.parse_args() + + # 读取输入 + try: + if args.file: + with open(args.file, 'r', encoding='utf-8') as f: + raw_data = f.read() + else: + raw_data = sys.stdin.read() + + if not raw_data.strip(): + print(json.dumps({"error": "空输入"})) + return + + data = json.loads(raw_data) + except json.JSONDecodeError as e: + print(json.dumps({"error": f"JSON 解析错误: {str(e)}"})) + return + except FileNotFoundError: + print(json.dumps({"error": f"文件未找到: {args.file}"})) + return + except Exception as e: + print(json.dumps({"error": f"读取错误: {str(e)}"})) + return + + # 提取数据 + items = None + if isinstance(data, dict): + if "items" in data: + items = data["items"] + elif "data" in data: + items = data["data"] + elif "results" in data: + items = data["results"] + else: + # 假设整个 dict 是单条记录,包装成列表 + items = [data] + elif isinstance(data, list): + items = data + else: + print(json.dumps({"error": "不支持的数据格式,需要列表或包含 items/data/results 的字典"})) + return + + # 根据类型分析 + if args.type == "auto": + # 自动检测 + if items and all(isinstance(x, dict) for x in items): + result = analyze_dict_list(items) + elif items and all(isinstance(x, (int, float)) for x in items): + result = analyze_numeric(items) + elif items and all(isinstance(x, str) for x in items): + result = analyze_text(items) + else: + result = analyze_mixed(items) + elif args.type == "numeric": + result = analyze_numeric(items) + elif args.type == "text": + result = analyze_text(items) + else: + result = analyze_mixed(items) + + # 添加元信息 + output = { + "success": True, + "analysis": result, + "metadata": { + "input_type": type(data).__name__, + "item_count": len(items) if items else 0, + "analysis_type": args.type + } + } + + # 输出 + indent = 2 if args.pretty else None + print(json.dumps(output, ensure_ascii=False, indent=indent)) + + +if __name__ == "__main__": + main() diff --git a/skills/preloaded/data-processor/scripts/extract_info.py b/skills/preloaded/data-processor/scripts/extract_info.py new file mode 100644 index 00000000..73c6a1b5 --- /dev/null +++ b/skills/preloaded/data-processor/scripts/extract_info.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +""" +信息提取脚本 - 从文本中提取结构化信息 + +提取内容: +- 数字 +- 日期 +- 百分比 +- 金额 +- 邮箱 +- URL +- 电话号码 + +用法: + echo "2024年销售额为100万元,同比增长15%" | python extract_info.py + + # 指定提取类型 + echo "联系我: test@example.com 或 13800138000" | python extract_info.py --types email,phone +""" + +import sys +import json +import argparse +import re + + +def extract_numbers(text: str) -> list: + """提取数字""" + # 匹配整数和小数 + pattern = r'-?\d+(?:\.\d+)?' + numbers = re.findall(pattern, text) + # 转换为数值 + result = [] + for n in numbers: + try: + if '.' in n: + result.append(float(n)) + else: + result.append(int(n)) + except ValueError: + result.append(n) + return result + + +def extract_dates(text: str) -> list: + """提取日期""" + patterns = [ + r'\d{4}[-/年]\d{1,2}[-/月]\d{1,2}[日]?', # 2024-01-01 或 2024年1月1日 + r'\d{4}[-/年]\d{1,2}[月]?', # 2024-01 或 2024年1月 + r'\d{4}年', # 2024年 + r'\d{1,2}[-/月]\d{1,2}[日]?', # 01-01 或 1月1日 + ] + + dates = [] + for pattern in patterns: + matches = re.findall(pattern, text) + dates.extend(matches) + + return list(set(dates)) + + +def extract_percentages(text: str) -> list: + """提取百分比""" + pattern = r'-?\d+(?:\.\d+)?%' + return re.findall(pattern, text) + + +def extract_amounts(text: str) -> list: + """提取金额""" + patterns = [ + r'[¥$€£]\s*\d+(?:,\d{3})*(?:\.\d+)?', # ¥100.00 + r'\d+(?:,\d{3})*(?:\.\d+)?\s*[元万亿美金]', # 100万元 + r'\d+(?:\.\d+)?[百千万亿]+[元]?', # 100万 + ] + + amounts = [] + for pattern in patterns: + matches = re.findall(pattern, text) + amounts.extend(matches) + + return list(set(amounts)) + + +def extract_emails(text: str) -> list: + """提取邮箱""" + pattern = r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' + return re.findall(pattern, text) + + +def extract_urls(text: str) -> list: + """提取 URL""" + pattern = r'https?://[^\s<>"{}|\\^`\[\]]+' + return re.findall(pattern, text) + + +def extract_phones(text: str) -> list: + """提取电话号码""" + patterns = [ + r'1[3-9]\d{9}', # 手机号 + r'\d{3,4}[-\s]?\d{7,8}', # 固话 + r'\+\d{1,3}[-\s]?\d{10,12}', # 国际号码 + ] + + phones = [] + for pattern in patterns: + matches = re.findall(pattern, text) + phones.extend(matches) + + return list(set(phones)) + + +def extract_keywords(text: str, min_len: int = 2) -> list: + """提取关键词(中文和英文)""" + # 中文关键词 + chinese_pattern = r'[\u4e00-\u9fa5]{2,}' + chinese_words = re.findall(chinese_pattern, text) + + # 英文关键词 + english_pattern = r'[a-zA-Z]{3,}' + english_words = re.findall(english_pattern, text) + + # 统计词频 + from collections import Counter + words = chinese_words + [w.lower() for w in english_words] + + # 过滤停用词 + stopwords = {'的', '是', '在', '了', '和', '与', '或', '为', '有', '这', '那', '等', + 'the', 'is', 'are', 'was', 'were', 'and', 'or', 'for', 'with', 'this'} + words = [w for w in words if w not in stopwords and len(w) >= min_len] + + word_freq = Counter(words) + return [{"word": w, "count": c} for w, c in word_freq.most_common(20)] + + +def main(): + parser = argparse.ArgumentParser(description="信息提取工具") + parser.add_argument("--types", "-t", + help="要提取的类型,逗号分隔 (numbers,dates,percentages,amounts,emails,urls,phones,keywords)") + parser.add_argument("--pretty", "-p", action="store_true", help="格式化输出") + args = parser.parse_args() + + # 读取输入 + try: + text = sys.stdin.read() + if not text.strip(): + print(json.dumps({"error": "空输入"})) + return + except Exception as e: + print(json.dumps({"error": f"读取错误: {str(e)}"})) + return + + # 确定要提取的类型 + all_types = ["numbers", "dates", "percentages", "amounts", "emails", "urls", "phones", "keywords"] + if args.types: + extract_types = [t.strip().lower() for t in args.types.split(",")] + else: + extract_types = all_types + + # 提取信息 + result = { + "success": True, + "text_length": len(text), + "extracted": {} + } + + extractors = { + "numbers": extract_numbers, + "dates": extract_dates, + "percentages": extract_percentages, + "amounts": extract_amounts, + "emails": extract_emails, + "urls": extract_urls, + "phones": extract_phones, + "keywords": extract_keywords, + } + + for ext_type in extract_types: + if ext_type in extractors: + try: + extracted = extractors[ext_type](text) + if extracted: + result["extracted"][ext_type] = extracted + except Exception as e: + result["extracted"][ext_type] = {"error": str(e)} + + # 统计 + result["summary"] = { + "total_extractions": sum(len(v) if isinstance(v, list) else 0 + for v in result["extracted"].values()), + "types_found": list(result["extracted"].keys()) + } + + # 输出 + indent = 2 if args.pretty else None + print(json.dumps(result, ensure_ascii=False, indent=indent)) + + +if __name__ == "__main__": + main() diff --git a/skills/preloaded/data-processor/scripts/format_converter.py b/skills/preloaded/data-processor/scripts/format_converter.py new file mode 100644 index 00000000..0fed05a6 --- /dev/null +++ b/skills/preloaded/data-processor/scripts/format_converter.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +""" +格式转换脚本 - JSON/CSV/Markdown 相互转换 + +用法: + # JSON 转 CSV + echo '[{"name": "A", "value": 1}]' | python format_converter.py --to csv + + # JSON 转 Markdown 表格 + echo '[{"name": "A", "value": 1}]' | python format_converter.py --to markdown + + # CSV 转 JSON + cat data.csv | python format_converter.py --from csv --to json +""" + +import sys +import json +import argparse +import csv +import io + + +def json_to_csv(data: list) -> str: + """将 JSON 列表转换为 CSV""" + if not data: + return "" + + if not all(isinstance(x, dict) for x in data): + raise ValueError("JSON 数据必须是字典列表") + + # 获取所有字段 + fieldnames = [] + for item in data: + for key in item.keys(): + if key not in fieldnames: + fieldnames.append(key) + + output = io.StringIO() + writer = csv.DictWriter(output, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(data) + + return output.getvalue() + + +def csv_to_json(csv_text: str) -> list: + """将 CSV 转换为 JSON 列表""" + reader = csv.DictReader(io.StringIO(csv_text)) + return list(reader) + + +def json_to_markdown(data: list) -> str: + """将 JSON 列表转换为 Markdown 表格""" + if not data: + return "" + + if not all(isinstance(x, dict) for x in data): + raise ValueError("JSON 数据必须是字典列表") + + # 获取所有字段 + fieldnames = [] + for item in data: + for key in item.keys(): + if key not in fieldnames: + fieldnames.append(key) + + # 构建表头 + lines = [] + lines.append("| " + " | ".join(fieldnames) + " |") + lines.append("| " + " | ".join(["---"] * len(fieldnames)) + " |") + + # 构建数据行 + for item in data: + row = [] + for field in fieldnames: + value = item.get(field, "") + # 转义 Markdown 特殊字符 + str_value = str(value) if value is not None else "" + str_value = str_value.replace("|", "\\|") + row.append(str_value) + lines.append("| " + " | ".join(row) + " |") + + return "\n".join(lines) + + +def markdown_to_json(md_text: str) -> list: + """将 Markdown 表格转换为 JSON 列表""" + lines = [line.strip() for line in md_text.strip().split("\n") if line.strip()] + + if len(lines) < 2: + raise ValueError("无效的 Markdown 表格") + + # 解析表头 + header_line = lines[0] + if not header_line.startswith("|"): + raise ValueError("无效的 Markdown 表格格式") + + headers = [h.strip() for h in header_line.strip("|").split("|")] + + # 跳过分隔行 + data_lines = lines[2:] if len(lines) > 2 else [] + + # 解析数据 + result = [] + for line in data_lines: + if not line.startswith("|"): + continue + values = [v.strip() for v in line.strip("|").split("|")] + item = {} + for i, header in enumerate(headers): + if i < len(values): + item[header] = values[i] + result.append(item) + + return result + + +def detect_format(text: str) -> str: + """自动检测输入格式""" + text = text.strip() + + if text.startswith("[") or text.startswith("{"): + return "json" + elif text.startswith("|"): + return "markdown" + elif "," in text.split("\n")[0]: + return "csv" + else: + return "unknown" + + +def main(): + parser = argparse.ArgumentParser(description="数据格式转换工具") + parser.add_argument("--from", "-f", dest="from_format", + choices=["json", "csv", "markdown", "auto"], + default="auto", help="输入格式") + parser.add_argument("--to", "-t", dest="to_format", + choices=["json", "csv", "markdown"], + required=True, help="输出格式") + parser.add_argument("--pretty", "-p", action="store_true", help="格式化输出") + args = parser.parse_args() + + # 读取输入 + try: + raw_input = sys.stdin.read() + if not raw_input.strip(): + print(json.dumps({"error": "空输入"})) + return + except Exception as e: + print(json.dumps({"error": f"读取错误: {str(e)}"})) + return + + # 检测输入格式 + from_format = args.from_format + if from_format == "auto": + from_format = detect_format(raw_input) + if from_format == "unknown": + print(json.dumps({"error": "无法自动检测输入格式"})) + return + + # 转换为中间格式(JSON 列表) + try: + if from_format == "json": + data = json.loads(raw_input) + if isinstance(data, dict): + # 尝试提取列表 + if "items" in data: + data = data["items"] + elif "data" in data: + data = data["data"] + else: + data = [data] + if not isinstance(data, list): + data = [data] + elif from_format == "csv": + data = csv_to_json(raw_input) + elif from_format == "markdown": + data = markdown_to_json(raw_input) + else: + print(json.dumps({"error": f"不支持的输入格式: {from_format}"})) + return + except Exception as e: + print(json.dumps({"error": f"解析输入失败: {str(e)}"})) + return + + # 转换为目标格式 + try: + if args.to_format == "json": + indent = 2 if args.pretty else None + output = json.dumps(data, ensure_ascii=False, indent=indent) + elif args.to_format == "csv": + output = json_to_csv(data) + elif args.to_format == "markdown": + output = json_to_markdown(data) + else: + print(json.dumps({"error": f"不支持的输出格式: {args.to_format}"})) + return + + print(output) + except Exception as e: + print(json.dumps({"error": f"转换失败: {str(e)}"})) + return + + +if __name__ == "__main__": + main() diff --git a/skills/preloaded/doc-coauthoring/SKILL.md b/skills/preloaded/doc-coauthoring/SKILL.md new file mode 100644 index 00000000..24ca6e83 --- /dev/null +++ b/skills/preloaded/doc-coauthoring/SKILL.md @@ -0,0 +1,375 @@ +--- +name: 文档协作 +description: 引导用户通过结构化的文档共同编写工作流程。当用户想撰写文档、提案、技术规范、决策文档或类似结构化内容时使用。该工作流程帮助用户高效传递上下文,通过迭代优化内容,并验证文档对读者有效。当用户提到写文档、创建提案、起草规范或类似文档任务时触发。 +--- + +# Doc Co-Authoring Workflow + +This skill provides a structured workflow for guiding users through collaborative document creation. Act as an active guide, walking users through three stages: Context Gathering, Refinement & Structure, and Reader Testing. + +## When to Offer This Workflow + +**Trigger conditions:** +- User mentions writing documentation: "write a doc", "draft a proposal", "create a spec", "write up" +- User mentions specific doc types: "PRD", "design doc", "decision doc", "RFC" +- User seems to be starting a substantial writing task + +**Initial offer:** +Offer the user a structured workflow for co-authoring the document. Explain the three stages: + +1. **Context Gathering**: User provides all relevant context while Claude asks clarifying questions +2. **Refinement & Structure**: Iteratively build each section through brainstorming and editing +3. **Reader Testing**: Test the doc with a fresh Claude (no context) to catch blind spots before others read it + +Explain that this approach helps ensure the doc works well when others read it (including when they paste it into Claude). Ask if they want to try this workflow or prefer to work freeform. + +If user declines, work freeform. If user accepts, proceed to Stage 1. + +## Stage 1: Context Gathering + +**Goal:** Close the gap between what the user knows and what Claude knows, enabling smart guidance later. + +### Initial Questions + +Start by asking the user for meta-context about the document: + +1. What type of document is this? (e.g., technical spec, decision doc, proposal) +2. Who's the primary audience? +3. What's the desired impact when someone reads this? +4. Is there a template or specific format to follow? +5. Any other constraints or context to know? + +Inform them they can answer in shorthand or dump information however works best for them. + +**If user provides a template or mentions a doc type:** +- Ask if they have a template document to share +- If they provide a link to a shared document, use the appropriate integration to fetch it +- If they provide a file, read it + +**If user mentions editing an existing shared document:** +- Use the appropriate integration to read the current state +- Check for images without alt-text +- If images exist without alt-text, explain that when others use Claude to understand the doc, Claude won't be able to see them. Ask if they want alt-text generated. If so, request they paste each image into chat for descriptive alt-text generation. + +### Info Dumping + +Once initial questions are answered, encourage the user to dump all the context they have. Request information such as: +- Background on the project/problem +- Related team discussions or shared documents +- Why alternative solutions aren't being used +- Organizational context (team dynamics, past incidents, politics) +- Timeline pressures or constraints +- Technical architecture or dependencies +- Stakeholder concerns + +Advise them not to worry about organizing it - just get it all out. Offer multiple ways to provide context: +- Info dump stream-of-consciousness +- Point to team channels or threads to read +- Link to shared documents + +**If integrations are available** (e.g., Slack, Teams, Google Drive, SharePoint, or other MCP servers), mention that these can be used to pull in context directly. + +**If no integrations are detected and in Claude.ai or Claude app:** Suggest they can enable connectors in their Claude settings to allow pulling context from messaging apps and document storage directly. + +Inform them clarifying questions will be asked once they've done their initial dump. + +**During context gathering:** + +- If user mentions team channels or shared documents: + - If integrations available: Inform them the content will be read now, then use the appropriate integration + - If integrations not available: Explain lack of access. Suggest they enable connectors in Claude settings, or paste the relevant content directly. + +- If user mentions entities/projects that are unknown: + - Ask if connected tools should be searched to learn more + - Wait for user confirmation before searching + +- As user provides context, track what's being learned and what's still unclear + +**Asking clarifying questions:** + +When user signals they've done their initial dump (or after substantial context provided), ask clarifying questions to ensure understanding: + +Generate 5-10 numbered questions based on gaps in the context. + +Inform them they can use shorthand to answer (e.g., "1: yes, 2: see #channel, 3: no because backwards compat"), link to more docs, point to channels to read, or just keep info-dumping. Whatever's most efficient for them. + +**Exit condition:** +Sufficient context has been gathered when questions show understanding - when edge cases and trade-offs can be asked about without needing basics explained. + +**Transition:** +Ask if there's any more context they want to provide at this stage, or if it's time to move on to drafting the document. + +If user wants to add more, let them. When ready, proceed to Stage 2. + +## Stage 2: Refinement & Structure + +**Goal:** Build the document section by section through brainstorming, curation, and iterative refinement. + +**Instructions to user:** +Explain that the document will be built section by section. For each section: +1. Clarifying questions will be asked about what to include +2. 5-20 options will be brainstormed +3. User will indicate what to keep/remove/combine +4. The section will be drafted +5. It will be refined through surgical edits + +Start with whichever section has the most unknowns (usually the core decision/proposal), then work through the rest. + +**Section ordering:** + +If the document structure is clear: +Ask which section they'd like to start with. + +Suggest starting with whichever section has the most unknowns. For decision docs, that's usually the core proposal. For specs, it's typically the technical approach. Summary sections are best left for last. + +If user doesn't know what sections they need: +Based on the type of document and template, suggest 3-5 sections appropriate for the doc type. + +Ask if this structure works, or if they want to adjust it. + +**Once structure is agreed:** + +Create the initial document structure with placeholder text for all sections. + +**If access to artifacts is available:** +Use `create_file` to create an artifact. This gives both Claude and the user a scaffold to work from. + +Inform them that the initial structure with placeholders for all sections will be created. + +Create artifact with all section headers and brief placeholder text like "[To be written]" or "[Content here]". + +Provide the scaffold link and indicate it's time to fill in each section. + +**If no access to artifacts:** +Create a markdown file in the working directory. Name it appropriately (e.g., `decision-doc.md`, `technical-spec.md`). + +Inform them that the initial structure with placeholders for all sections will be created. + +Create file with all section headers and placeholder text. + +Confirm the filename has been created and indicate it's time to fill in each section. + +**For each section:** + +### Step 1: Clarifying Questions + +Announce work will begin on the [SECTION NAME] section. Ask 5-10 clarifying questions about what should be included: + +Generate 5-10 specific questions based on context and section purpose. + +Inform them they can answer in shorthand or just indicate what's important to cover. + +### Step 2: Brainstorming + +For the [SECTION NAME] section, brainstorm [5-20] things that might be included, depending on the section's complexity. Look for: +- Context shared that might have been forgotten +- Angles or considerations not yet mentioned + +Generate 5-20 numbered options based on section complexity. At the end, offer to brainstorm more if they want additional options. + +### Step 3: Curation + +Ask which points should be kept, removed, or combined. Request brief justifications to help learn priorities for the next sections. + +Provide examples: +- "Keep 1,4,7,9" +- "Remove 3 (duplicates 1)" +- "Remove 6 (audience already knows this)" +- "Combine 11 and 12" + +**If user gives freeform feedback** (e.g., "looks good" or "I like most of it but...") instead of numbered selections, extract their preferences and proceed. Parse what they want kept/removed/changed and apply it. + +### Step 4: Gap Check + +Based on what they've selected, ask if there's anything important missing for the [SECTION NAME] section. + +### Step 5: Drafting + +Use `str_replace` to replace the placeholder text for this section with the actual drafted content. + +Announce the [SECTION NAME] section will be drafted now based on what they've selected. + +**If using artifacts:** +After drafting, provide a link to the artifact. + +Ask them to read through it and indicate what to change. Note that being specific helps learning for the next sections. + +**If using a file (no artifacts):** +After drafting, confirm completion. + +Inform them the [SECTION NAME] section has been drafted in [filename]. Ask them to read through it and indicate what to change. Note that being specific helps learning for the next sections. + +**Key instruction for user (include when drafting the first section):** +Provide a note: Instead of editing the doc directly, ask them to indicate what to change. This helps learning of their style for future sections. For example: "Remove the X bullet - already covered by Y" or "Make the third paragraph more concise". + +### Step 6: Iterative Refinement + +As user provides feedback: +- Use `str_replace` to make edits (never reprint the whole doc) +- **If using artifacts:** Provide link to artifact after each edit +- **If using files:** Just confirm edits are complete +- If user edits doc directly and asks to read it: mentally note the changes they made and keep them in mind for future sections (this shows their preferences) + +**Continue iterating** until user is satisfied with the section. + +### Quality Checking + +After 3 consecutive iterations with no substantial changes, ask if anything can be removed without losing important information. + +When section is done, confirm [SECTION NAME] is complete. Ask if ready to move to the next section. + +**Repeat for all sections.** + +### Near Completion + +As approaching completion (80%+ of sections done), announce intention to re-read the entire document and check for: +- Flow and consistency across sections +- Redundancy or contradictions +- Anything that feels like "slop" or generic filler +- Whether every sentence carries weight + +Read entire document and provide feedback. + +**When all sections are drafted and refined:** +Announce all sections are drafted. Indicate intention to review the complete document one more time. + +Review for overall coherence, flow, completeness. + +Provide any final suggestions. + +Ask if ready to move to Reader Testing, or if they want to refine anything else. + +## Stage 3: Reader Testing + +**Goal:** Test the document with a fresh Claude (no context bleed) to verify it works for readers. + +**Instructions to user:** +Explain that testing will now occur to see if the document actually works for readers. This catches blind spots - things that make sense to the authors but might confuse others. + +### Testing Approach + +**If access to sub-agents is available (e.g., in Claude Code):** + +Perform the testing directly without user involvement. + +### Step 1: Predict Reader Questions + +Announce intention to predict what questions readers might ask when trying to discover this document. + +Generate 5-10 questions that readers would realistically ask. + +### Step 2: Test with Sub-Agent + +Announce that these questions will be tested with a fresh Claude instance (no context from this conversation). + +For each question, invoke a sub-agent with just the document content and the question. + +Summarize what Reader Claude got right/wrong for each question. + +### Step 3: Run Additional Checks + +Announce additional checks will be performed. + +Invoke sub-agent to check for ambiguity, false assumptions, contradictions. + +Summarize any issues found. + +### Step 4: Report and Fix + +If issues found: +Report that Reader Claude struggled with specific issues. + +List the specific issues. + +Indicate intention to fix these gaps. + +Loop back to refinement for problematic sections. + +--- + +**If no access to sub-agents (e.g., claude.ai web interface):** + +The user will need to do the testing manually. + +### Step 1: Predict Reader Questions + +Ask what questions people might ask when trying to discover this document. What would they type into Claude.ai? + +Generate 5-10 questions that readers would realistically ask. + +### Step 2: Setup Testing + +Provide testing instructions: +1. Open a fresh Claude conversation: https://claude.ai +2. Paste or share the document content (if using a shared doc platform with connectors enabled, provide the link) +3. Ask Reader Claude the generated questions + +For each question, instruct Reader Claude to provide: +- The answer +- Whether anything was ambiguous or unclear +- What knowledge/context the doc assumes is already known + +Check if Reader Claude gives correct answers or misinterprets anything. + +### Step 3: Additional Checks + +Also ask Reader Claude: +- "What in this doc might be ambiguous or unclear to readers?" +- "What knowledge or context does this doc assume readers already have?" +- "Are there any internal contradictions or inconsistencies?" + +### Step 4: Iterate Based on Results + +Ask what Reader Claude got wrong or struggled with. Indicate intention to fix those gaps. + +Loop back to refinement for any problematic sections. + +--- + +### Exit Condition (Both Approaches) + +When Reader Claude consistently answers questions correctly and doesn't surface new gaps or ambiguities, the doc is ready. + +## Final Review + +When Reader Testing passes: +Announce the doc has passed Reader Claude testing. Before completion: + +1. Recommend they do a final read-through themselves - they own this document and are responsible for its quality +2. Suggest double-checking any facts, links, or technical details +3. Ask them to verify it achieves the impact they wanted + +Ask if they want one more review, or if the work is done. + +**If user wants final review, provide it. Otherwise:** +Announce document completion. Provide a few final tips: +- Consider linking this conversation in an appendix so readers can see how the doc was developed +- Use appendices to provide depth without bloating the main doc +- Update the doc as feedback is received from real readers + +## Tips for Effective Guidance + +**Tone:** +- Be direct and procedural +- Explain rationale briefly when it affects user behavior +- Don't try to "sell" the approach - just execute it + +**Handling Deviations:** +- If user wants to skip a stage: Ask if they want to skip this and write freeform +- If user seems frustrated: Acknowledge this is taking longer than expected. Suggest ways to move faster +- Always give user agency to adjust the process + +**Context Management:** +- Throughout, if context is missing on something mentioned, proactively ask +- Don't let gaps accumulate - address them as they come up + +**Artifact Management:** +- Use `create_file` for drafting full sections +- Use `str_replace` for all edits +- Provide artifact link after every change +- Never use artifacts for brainstorming lists - that's just conversation + +**Quality over Speed:** +- Don't rush through stages +- Each iteration should make meaningful improvements +- The goal is a document that actually works for readers diff --git a/skills/preloaded/document-analyzer/SKILL.md b/skills/preloaded/document-analyzer/SKILL.md new file mode 100644 index 00000000..b7e60eb7 --- /dev/null +++ b/skills/preloaded/document-analyzer/SKILL.md @@ -0,0 +1,80 @@ +--- +name: 文档分析器 +description: 深度分析文档结构和内容。当用户需要分析文档结构、提取关键信息、识别文档类型、进行内容质量评估、或理解文档组织方式时使用此技能。 +--- + +# Document Analyzer + +对知识库中的文档进行深度结构分析和内容理解。 + +## 核心能力 + +1. **结构分析**: 识别文档的章节层级、组织架构 +2. **关键信息提取**: 提取核心论点、关键数据、重要结论 +3. **文档类型识别**: 判断文档类型(报告、手册、论文、合同等) +4. **内容质量评估**: 评估文档的完整性、一致性、可读性 + +## 分析流程 + +### 1. 文档概览 + +首先获取文档的整体信息: +- 文档名称和类型 +- 总页数/分块数 +- 创建/更新时间 +- 主要章节/标题 + +### 2. 结构分析 + +识别并描述: +- 标题层级结构 +- 章节组织方式 +- 逻辑流程(时间顺序、因果关系、并列结构) + +### 3. 内容提取 + +重点关注: +- **核心主题**: 文档的中心议题 +- **关键论点**: 主要观点和论述 +- **支撑数据**: 重要的数据、统计、事实 +- **结论建议**: 文档的结论或建议 + +### 4. 质量评估 + +评估维度: +- 完整性:是否涵盖必要内容 +- 一致性:前后是否逻辑一致 +- 清晰度:表达是否清晰易懂 + +## 输出格式 + +```markdown +## 文档分析报告 + +### 基本信息 +- 文档名称:XXX +- 文档类型:XXX +- 结构层级:X级 + +### 文档结构 +1. 第一章:XXX + 1.1 ... + 1.2 ... +2. 第二章:XXX + +### 核心内容 +- 主题:XXX +- 关键论点: + 1. ... + 2. ... +- 重要数据:XXX + +### 分析结论 +XXX +``` + +## 注意事项 + +- 保持客观中立,忠于原文 +- 区分事实陈述和观点表达 +- 标注信息来源位置 diff --git a/skills/preloaded/summary-generator/SKILL.md b/skills/preloaded/summary-generator/SKILL.md new file mode 100644 index 00000000..d29ffcbc --- /dev/null +++ b/skills/preloaded/summary-generator/SKILL.md @@ -0,0 +1,92 @@ +--- +name: 摘要生成器 +description: 生成多层级文档摘要。当用户需要生成内容摘要、提取要点、创建不同长度的文档概述、或快速了解文档主要内容时使用此技能。 +--- + +# Summary Generator + +为知识库文档生成多层次、高质量的摘要。 + +## 核心能力 + +1. **多层级摘要**: 支持一句话摘要、段落摘要、详细摘要 +2. **要点提取**: 提取文档的核心要点列表 +3. **结构化总结**: 按章节/主题组织的结构化摘要 + +## 摘要层级 + +### Level 1: 一句话摘要(约20字) + +用一句话概括文档的核心内容和价值: +``` +本文档详细说明了XX产品的使用方法和常见问题解决方案。 +``` + +### Level 2: 要点摘要(3-5个要点) + +提取文档最重要的3-5个要点: +``` +**核心要点** +1. 要点一:XXX +2. 要点二:XXX +3. 要点三:XXX +``` + +### Level 3: 段落摘要(100-200字) + +用1-2段文字全面概括文档内容: +``` +本文档主要介绍了...首先...其次...最后... +``` + +### Level 4: 详细摘要(章节式) + +按文档结构生成分章节摘要: +``` +**第一章 概述** +XXX... + +**第二章 详细说明** +XXX... + +**第三章 总结与建议** +XXX... +``` + +## 摘要原则 + +1. **准确性**: 摘要必须准确反映原文内容 +2. **完整性**: 不遗漏重要信息 +3. **简洁性**: 用最少的文字表达核心内容 +4. **客观性**: 不添加个人观点 + +## 默认行为 + +- 未指定层级时,默认生成 Level 2(要点摘要) +- 根据文档长度自动调整摘要详细程度 +- 保留关键数据和结论 + +## 使用示例 + +用户请求:"帮我总结这篇文档" + +响应格式: +``` +**文档摘要** + +本文档关于 [主题]。 + +**核心要点** +1. [要点1] +2. [要点2] +3. [要点3] + +**详细内容** +[简要展开说明...] +``` + +## 注意事项 + +- 数字、日期等关键信息需保留原值 +- 专业术语保持原有表述 +- 如有多个文档,分别生成摘要后再做整合