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
|
|
|
|
package piplan
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"encoding/json"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
|
|
"laodingbot/internal/logger"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// Feature 产品经理视角输出的业务特性。
|
|
|
|
|
|
type Feature struct {
|
|
|
|
|
|
FeatureID string `json:"feature_id"`
|
|
|
|
|
|
Title string `json:"title"`
|
|
|
|
|
|
BenefitHypothesis string `json:"benefit_hypothesis"`
|
|
|
|
|
|
AcceptanceCriteria []string `json:"acceptance_criteria"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Enabler 系统架构师视角输出的技术赋能特性(架构跑道)。
|
|
|
|
|
|
type Enabler struct {
|
|
|
|
|
|
EnablerID string `json:"enabler_id"`
|
|
|
|
|
|
Title string `json:"title"`
|
|
|
|
|
|
ArchitecturalPurpose string `json:"architectural_purpose"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NFRs 非功能性需求。
|
|
|
|
|
|
type NFRs struct {
|
|
|
|
|
|
Performance string `json:"performance"`
|
|
|
|
|
|
Security string `json:"security"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Dependency RTE 梳理的任务依赖关系。
|
|
|
|
|
|
type Dependency struct {
|
|
|
|
|
|
SourceID string `json:"source_id"`
|
|
|
|
|
|
TargetID string `json:"target_id"`
|
|
|
|
|
|
Reason string `json:"reason"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// PIPlanInput publish_pi_plan 工具的完整输入结构。
|
|
|
|
|
|
type PIPlanInput struct {
|
|
|
|
|
|
PIVision string `json:"pi_vision"`
|
|
|
|
|
|
Features []Feature `json:"features"`
|
|
|
|
|
|
Enablers []Enabler `json:"enablers"`
|
|
|
|
|
|
NFRs NFRs `json:"nfrs"`
|
|
|
|
|
|
Dependencies []Dependency `json:"dependencies"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Tool 实现 SAFe PI 规划发布工具。
|
|
|
|
|
|
type Tool struct {
|
|
|
|
|
|
maxOutputChars int
|
|
|
|
|
|
log *logger.Logger
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// New 创建一个新的 publish_pi_plan 工具实例。
|
|
|
|
|
|
func New(maxOutputChars int, log *logger.Logger) *Tool {
|
|
|
|
|
|
if maxOutputChars <= 0 {
|
|
|
|
|
|
maxOutputChars = 20000
|
|
|
|
|
|
}
|
|
|
|
|
|
return &Tool{
|
|
|
|
|
|
maxOutputChars: maxOutputChars,
|
|
|
|
|
|
log: log,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (t *Tool) Name() string { return "publish_pi_plan" }
|
|
|
|
|
|
|
|
|
|
|
|
func (t *Tool) Description() string {
|
|
|
|
|
|
return `当铁三角(PM, 架构师, RTE)完成 PI 规划推演后,调用此工具输出标准化的架构蓝图与任务清单。输入为 JSON,包含 pi_vision, features, enablers, nfrs, dependencies 字段。`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (t *Tool) Call(ctx context.Context, input string) (string, error) {
|
|
|
|
|
|
plan, err := parseInput(input)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", fmt.Errorf("publish_pi_plan: invalid input: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err := validate(plan); err != nil {
|
|
|
|
|
|
return "", fmt.Errorf("publish_pi_plan: validation failed: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if t.log != nil {
|
|
|
|
|
|
t.log.Infof("publish_pi_plan: features=%d enablers=%d deps=%d",
|
|
|
|
|
|
len(plan.Features), len(plan.Enablers), len(plan.Dependencies))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
output := render(plan)
|
|
|
|
|
|
|
2026-03-15 00:32:14 +08:00
|
|
|
|
if len([]rune(output)) > t.maxOutputChars {
|
|
|
|
|
|
output = string([]rune(output)[:t.maxOutputChars])
|
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
|
|
|
|
}
|
|
|
|
|
|
return output, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func parseInput(input string) (*PIPlanInput, error) {
|
|
|
|
|
|
raw := strings.TrimSpace(input)
|
|
|
|
|
|
if raw == "" {
|
|
|
|
|
|
return nil, fmt.Errorf("empty input")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var plan PIPlanInput
|
|
|
|
|
|
if err := json.Unmarshal([]byte(raw), &plan); err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("JSON parse error: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
return &plan, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func validate(p *PIPlanInput) error {
|
|
|
|
|
|
if strings.TrimSpace(p.PIVision) == "" {
|
|
|
|
|
|
return fmt.Errorf("pi_vision is required")
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(p.Features) == 0 {
|
|
|
|
|
|
return fmt.Errorf("at least one feature is required")
|
|
|
|
|
|
}
|
|
|
|
|
|
for i, f := range p.Features {
|
|
|
|
|
|
if strings.TrimSpace(f.FeatureID) == "" {
|
|
|
|
|
|
return fmt.Errorf("features[%d].feature_id is required", i)
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.TrimSpace(f.Title) == "" {
|
|
|
|
|
|
return fmt.Errorf("features[%d].title is required", i)
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.TrimSpace(f.BenefitHypothesis) == "" {
|
|
|
|
|
|
return fmt.Errorf("features[%d].benefit_hypothesis is required", i)
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(f.AcceptanceCriteria) == 0 {
|
|
|
|
|
|
return fmt.Errorf("features[%d].acceptance_criteria requires at least one item", i)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
for i, e := range p.Enablers {
|
|
|
|
|
|
if strings.TrimSpace(e.EnablerID) == "" {
|
|
|
|
|
|
return fmt.Errorf("enablers[%d].enabler_id is required", i)
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.TrimSpace(e.Title) == "" {
|
|
|
|
|
|
return fmt.Errorf("enablers[%d].title is required", i)
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.TrimSpace(e.ArchitecturalPurpose) == "" {
|
|
|
|
|
|
return fmt.Errorf("enablers[%d].architectural_purpose is required", i)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.TrimSpace(p.NFRs.Performance) == "" {
|
|
|
|
|
|
return fmt.Errorf("nfrs.performance is required")
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.TrimSpace(p.NFRs.Security) == "" {
|
|
|
|
|
|
return fmt.Errorf("nfrs.security is required")
|
|
|
|
|
|
}
|
|
|
|
|
|
for i, d := range p.Dependencies {
|
|
|
|
|
|
if strings.TrimSpace(d.SourceID) == "" {
|
|
|
|
|
|
return fmt.Errorf("dependencies[%d].source_id is required", i)
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.TrimSpace(d.TargetID) == "" {
|
|
|
|
|
|
return fmt.Errorf("dependencies[%d].target_id is required", i)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// render 将 PI 规划输入渲染为标准化的 Markdown 架构蓝图与任务清单。
|
|
|
|
|
|
func render(p *PIPlanInput) string {
|
|
|
|
|
|
var b strings.Builder
|
|
|
|
|
|
|
|
|
|
|
|
// ── 标题 ──
|
|
|
|
|
|
b.WriteString("# PI 规划架构蓝图与任务清单\n\n")
|
|
|
|
|
|
|
|
|
|
|
|
// ── 1. PI 愿景 ──
|
|
|
|
|
|
b.WriteString("## 1. PI 愿景\n\n")
|
|
|
|
|
|
b.WriteString(strings.TrimSpace(p.PIVision))
|
|
|
|
|
|
b.WriteString("\n\n")
|
|
|
|
|
|
|
|
|
|
|
|
// ── 2. 业务特性清单 (Features) ──
|
|
|
|
|
|
b.WriteString("## 2. 业务特性清单 (Features)\n\n")
|
|
|
|
|
|
for _, f := range p.Features {
|
|
|
|
|
|
b.WriteString(fmt.Sprintf("### %s — %s\n\n", f.FeatureID, f.Title))
|
|
|
|
|
|
b.WriteString(fmt.Sprintf("**业务价值假设**: %s\n\n", f.BenefitHypothesis))
|
|
|
|
|
|
b.WriteString("**验收标准 (AC)**:\n\n")
|
|
|
|
|
|
for j, ac := range f.AcceptanceCriteria {
|
|
|
|
|
|
b.WriteString(fmt.Sprintf("- [ ] AC-%d: %s\n", j+1, ac))
|
|
|
|
|
|
}
|
|
|
|
|
|
b.WriteString("\n")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 3. 技术赋能特性 (Enablers / 架构跑道) ──
|
|
|
|
|
|
b.WriteString("## 3. 技术赋能特性 (Enablers / 架构跑道)\n\n")
|
|
|
|
|
|
if len(p.Enablers) == 0 {
|
|
|
|
|
|
b.WriteString("_无技术赋能特性。_\n\n")
|
|
|
|
|
|
} else {
|
|
|
|
|
|
b.WriteString("| Enabler ID | 名称 | 架构意图 |\n")
|
|
|
|
|
|
b.WriteString("|------------|------|----------|\n")
|
|
|
|
|
|
for _, e := range p.Enablers {
|
|
|
|
|
|
b.WriteString(fmt.Sprintf("| %s | %s | %s |\n",
|
|
|
|
|
|
e.EnablerID, e.Title, e.ArchitecturalPurpose))
|
|
|
|
|
|
}
|
|
|
|
|
|
b.WriteString("\n")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 4. 非功能性需求 (NFRs) ──
|
|
|
|
|
|
b.WriteString("## 4. 非功能性需求 (NFRs)\n\n")
|
|
|
|
|
|
b.WriteString(fmt.Sprintf("- **性能**: %s\n", p.NFRs.Performance))
|
|
|
|
|
|
b.WriteString(fmt.Sprintf("- **安全与合规**: %s\n", p.NFRs.Security))
|
|
|
|
|
|
b.WriteString("\n")
|
|
|
|
|
|
|
|
|
|
|
|
// ── 5. 依赖关系图 ──
|
|
|
|
|
|
b.WriteString("## 5. 依赖关系\n\n")
|
|
|
|
|
|
if len(p.Dependencies) == 0 {
|
|
|
|
|
|
b.WriteString("_无跨任务依赖。_\n\n")
|
|
|
|
|
|
} else {
|
|
|
|
|
|
b.WriteString("| 前置任务 (Source) | 后续任务 (Target) | 依赖原因 |\n")
|
|
|
|
|
|
b.WriteString("|-------------------|-------------------|----------|\n")
|
|
|
|
|
|
for _, d := range p.Dependencies {
|
|
|
|
|
|
reason := d.Reason
|
|
|
|
|
|
if reason == "" {
|
|
|
|
|
|
reason = "—"
|
|
|
|
|
|
}
|
|
|
|
|
|
b.WriteString(fmt.Sprintf("| %s | %s | %s |\n",
|
|
|
|
|
|
d.SourceID, d.TargetID, reason))
|
|
|
|
|
|
}
|
|
|
|
|
|
b.WriteString("\n")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 6. 建议执行顺序 ──
|
|
|
|
|
|
b.WriteString("## 6. 建议执行顺序\n\n")
|
|
|
|
|
|
order := computeExecutionOrder(p)
|
|
|
|
|
|
for i, id := range order {
|
|
|
|
|
|
b.WriteString(fmt.Sprintf("%d. %s\n", i+1, id))
|
|
|
|
|
|
}
|
|
|
|
|
|
b.WriteString("\n")
|
|
|
|
|
|
|
|
|
|
|
|
// ── 7. 质量门禁检查清单 ──
|
|
|
|
|
|
b.WriteString("## 7. 质量门禁检查清单\n\n")
|
|
|
|
|
|
b.WriteString("### 业务验收测试用例\n\n")
|
|
|
|
|
|
for _, f := range p.Features {
|
|
|
|
|
|
for j, ac := range f.AcceptanceCriteria {
|
|
|
|
|
|
b.WriteString(fmt.Sprintf("- [ ] [%s] AC-%d: %s\n", f.FeatureID, j+1, ac))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
b.WriteString("\n### 非功能性验证\n\n")
|
|
|
|
|
|
b.WriteString(fmt.Sprintf("- [ ] 性能压测: %s\n", p.NFRs.Performance))
|
|
|
|
|
|
b.WriteString(fmt.Sprintf("- [ ] 安全扫描: %s\n", p.NFRs.Security))
|
|
|
|
|
|
b.WriteString("\n")
|
|
|
|
|
|
|
|
|
|
|
|
return b.String()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// computeExecutionOrder 根据依赖关系计算拓扑排序的执行顺序。
|
|
|
|
|
|
// 先排 Enabler,再排 Feature;无依赖的排在前面。
|
|
|
|
|
|
func computeExecutionOrder(p *PIPlanInput) []string {
|
|
|
|
|
|
// 收集所有 ID
|
|
|
|
|
|
allIDs := make([]string, 0, len(p.Enablers)+len(p.Features))
|
|
|
|
|
|
idSet := make(map[string]bool)
|
|
|
|
|
|
for _, e := range p.Enablers {
|
|
|
|
|
|
allIDs = append(allIDs, e.EnablerID)
|
|
|
|
|
|
idSet[e.EnablerID] = true
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, f := range p.Features {
|
|
|
|
|
|
allIDs = append(allIDs, f.FeatureID)
|
|
|
|
|
|
idSet[f.FeatureID] = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 构建入度表和邻接表
|
|
|
|
|
|
inDegree := make(map[string]int)
|
|
|
|
|
|
adj := make(map[string][]string)
|
|
|
|
|
|
for _, id := range allIDs {
|
|
|
|
|
|
inDegree[id] = 0
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, d := range p.Dependencies {
|
|
|
|
|
|
if !idSet[d.SourceID] || !idSet[d.TargetID] {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
adj[d.SourceID] = append(adj[d.SourceID], d.TargetID)
|
|
|
|
|
|
inDegree[d.TargetID]++
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Kahn 拓扑排序
|
|
|
|
|
|
queue := make([]string, 0)
|
|
|
|
|
|
// 先加入度为 0 的 Enabler,再加入度为 0 的 Feature,保持稳定顺序
|
|
|
|
|
|
for _, e := range p.Enablers {
|
|
|
|
|
|
if inDegree[e.EnablerID] == 0 {
|
|
|
|
|
|
queue = append(queue, e.EnablerID)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, f := range p.Features {
|
|
|
|
|
|
if inDegree[f.FeatureID] == 0 {
|
|
|
|
|
|
queue = append(queue, f.FeatureID)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var result []string
|
|
|
|
|
|
for len(queue) > 0 {
|
|
|
|
|
|
curr := queue[0]
|
|
|
|
|
|
queue = queue[1:]
|
|
|
|
|
|
result = append(result, curr)
|
|
|
|
|
|
for _, next := range adj[curr] {
|
|
|
|
|
|
inDegree[next]--
|
|
|
|
|
|
if inDegree[next] == 0 {
|
|
|
|
|
|
queue = append(queue, next)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果存在环,将未排序的节点追加到末尾并标记
|
|
|
|
|
|
if len(result) < len(allIDs) {
|
|
|
|
|
|
for _, id := range allIDs {
|
|
|
|
|
|
if inDegree[id] > 0 {
|
|
|
|
|
|
result = append(result, id+" ⚠️(循环依赖)")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
}
|