2026-03-08 22:38:29 +08:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
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))
|
|
|
|
|
}
|
|
|
|
|
}
|