Files
LaodingBot/internal/agent/orchestrator_skill_selection_test.go
Ding, Shuo 8dc5354fa4 feat: implement streaming chat, skill routing, and SAFe PI planning tools
- Add /api/chat/stream endpoint with Server-Sent Events (SSE) for real-time message streaming
  * Implement StreamEvent types (thought, tool_call, tool_result, final, error)
  * Add StreamEventCallback mechanism for event propagation
  * Create StreamChatHandler in webui/bot with proper HTTP headers and flushing

- Implement LLM-based skill router for intelligent capability selection
  * Add optional routerLLM client for semantic routing
  * Implement routeSkillsWithLLM() to match user intent to available skills
  * Add matchSkillsByName() for fuzzy skill matching
  * Update buildUnifiedSystemPrompt() to use routed skills

- Add streaming support to ReAct pipeline
  * Implement runUnifiedReActStream() for streaming thought/action/observation
  * Emit StreamEvent at each ReAct step
  * Support callback error handling in streaming mode

- Integrate three new DevOps tools
  * tools/filedoc: Extract document content from file_id via OpenAI
  * tools/giteaticket: Create Gitea issues from PI plan items with SAFe metadata
  * tools/piplan: Publish PI planning blueprints with dependency tracking

- Add SAFe PI Planning skill
  * Implement PM/SA/RTE (iron triangle) workflow
  * Support for Feature, Enabler, and Dependency definition
  * Automatic task decomposition and Gitea integration

- Create frontend integration documentation
  * Complete SSE protocol specification
  * TypeScript fetch + ReadableStream example
  * LLM-ready refactoring template for other projects

- Simplify file handling
  * Remove legacy file context structures and dual-mode processing
  * Consolidate file operations into UploadAndCacheFiles()
  * Remove FilePromptMode configuration and related complexity

- Update configuration
  * Add Router model support (LLM_ROUTER_MODEL)
  * Add Gitea configuration (BaseURL, Token, Owner, Repo)
  * WebSearch and additional tool infrastructure

Tests: All 22 test packages passing, 8/8 webui tests including 3 new stream tests
2026-03-11 17:58:19 +08:00

96 lines
2.9 KiB
Go

package agent
import (
"runtime"
"strings"
"testing"
"laodingbot/internal/knowledge"
)
func TestBuildQueryTokensIncludesChineseBigrams(t *testing.T) {
tokens := buildQueryTokens("请执行命令并查看文件")
joined := strings.Join(tokens, ",")
if !strings.Contains(joined, "命令") {
t.Fatalf("expected token contains 命令, got: %v", tokens)
}
if !strings.Contains(joined, "文件") {
t.Fatalf("expected token contains 文件, got: %v", tokens)
}
}
func TestSelectRelevantSkillsPrefersMatchingSkill(t *testing.T) {
o := &Orchestrator{
skills: []knowledge.Skill{
{Name: "文件系统查询专家", Content: "适用于目录、文件、路径、命令执行等场景"},
{Name: "天气查询", Content: "用于天气和空气质量查询"},
{Name: "日程助手", Content: "用于日程管理"},
},
}
selected := o.selectRelevantSkills("帮我执行命令查看某个文件", 2)
if len(selected) == 0 {
t.Fatal("expected non-empty selected skills")
}
if selected[0].Name != "文件系统查询专家" {
t.Fatalf("expected top skill 文件系统查询专家, got: %s", selected[0].Name)
}
if len(selected) > 2 {
t.Fatalf("expected at most 2 skills, got: %d", len(selected))
}
}
func TestFormatRuntimeContextForPromptIncludesGOOS(t *testing.T) {
doc := formatRuntimeContextForPrompt()
if !strings.Contains(strings.ToLower(doc), strings.ToLower(runtime.GOOS)) {
t.Fatalf("expected runtime context contains GOOS=%s, got: %s", runtime.GOOS, doc)
}
}
func TestMatchSkillsByNameExact(t *testing.T) {
all := []knowledge.Skill{
{Name: "SAFe PI Planning", Content: "PI规划技能"},
{Name: "文件系统查询专家", Content: "文件查询"},
{Name: "代码生成", Content: "代码生成技能"},
}
matched := matchSkillsByName(all, []string{"SAFe PI Planning"})
if len(matched) != 1 {
t.Fatalf("expected 1 match, got %d", len(matched))
}
if matched[0].Name != "SAFe PI Planning" {
t.Fatalf("expected SAFe PI Planning, got %s", matched[0].Name)
}
}
func TestMatchSkillsByNameFuzzy(t *testing.T) {
all := []knowledge.Skill{
{Name: "SAFe PI Planning", Content: "PI规划技能"},
{Name: "文件系统查询专家", Content: "文件查询"},
}
matched := matchSkillsByName(all, []string{"pi planning", "文件"})
if len(matched) != 2 {
t.Fatalf("expected 2 matches, got %d", len(matched))
}
}
func TestMatchSkillsByNameNoMatch(t *testing.T) {
all := []knowledge.Skill{
{Name: "文件系统查询专家", Content: "文件查询"},
}
matched := matchSkillsByName(all, []string{"不存在的技能"})
if len(matched) != 0 {
t.Fatalf("expected 0 matches, got %d", len(matched))
}
}
func TestMatchSkillsByNameEmpty(t *testing.T) {
matched := matchSkillsByName(nil, []string{"any"})
if len(matched) != 0 {
t.Fatalf("expected 0 matches, got %d", len(matched))
}
matched = matchSkillsByName([]knowledge.Skill{{Name: "test"}}, nil)
if len(matched) != 0 {
t.Fatalf("expected 0 matches, got %d", len(matched))
}
}