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
This commit is contained in:
@@ -29,6 +29,7 @@ type Config struct {
|
||||
LLM LLMConfig
|
||||
Security SecurityConfig
|
||||
WebSearch WebSearchConfig
|
||||
Gitea GiteaConfig
|
||||
|
||||
SQLitePath string
|
||||
}
|
||||
@@ -52,11 +53,11 @@ type WebUIConfig struct {
|
||||
}
|
||||
|
||||
type LLMConfig struct {
|
||||
BaseURL string
|
||||
APIKey string
|
||||
Model string
|
||||
FileModel string
|
||||
FilePromptMode string
|
||||
BaseURL string
|
||||
APIKey string
|
||||
Model string
|
||||
FileModel string
|
||||
RouterModel string // 轻量路由模型,用于技能意图路由;为空则仅用关键词匹配
|
||||
}
|
||||
|
||||
type SecurityConfig struct {
|
||||
@@ -70,6 +71,13 @@ type WebSearchConfig struct {
|
||||
APIKey string
|
||||
}
|
||||
|
||||
type GiteaConfig struct {
|
||||
BaseURL string // Gitea 实例地址
|
||||
Token string // Personal Access Token
|
||||
Owner string // 仓库所有者
|
||||
Repo string // 仓库名称
|
||||
}
|
||||
|
||||
func Load() (Config, error) {
|
||||
agentWorkspaceDir := resolveAgentWorkspaceDir()
|
||||
if err := preloadEnvFiles(); err != nil {
|
||||
@@ -106,11 +114,11 @@ func Load() (Config, error) {
|
||||
MaxUploadBytes: int64(intFromEnv("WEBUI_MAX_UPLOAD_MB", 20)) * 1024 * 1024,
|
||||
},
|
||||
LLM: LLMConfig{
|
||||
BaseURL: strings.TrimRight(defaultIfEmpty(os.Getenv("LLM_BASE_URL"), "https://api.openai.com/v1"), "/"),
|
||||
APIKey: strings.TrimSpace(os.Getenv("LLM_API_KEY")),
|
||||
Model: defaultIfEmpty(os.Getenv("LLM_MODEL"), "gpt-4o-mini"),
|
||||
FileModel: defaultIfEmpty(os.Getenv("LLM_FILE_MODEL"), defaultIfEmpty(os.Getenv("LLM_MODEL"), "gpt-4o-mini")),
|
||||
FilePromptMode: normalizeFilePromptMode(defaultIfEmpty(os.Getenv("LLM_FILE_PROMPT_MODE"), "user_content_file_parts")),
|
||||
BaseURL: strings.TrimRight(defaultIfEmpty(os.Getenv("LLM_BASE_URL"), "https://api.openai.com/v1"), "/"),
|
||||
APIKey: strings.TrimSpace(os.Getenv("LLM_API_KEY")),
|
||||
Model: defaultIfEmpty(os.Getenv("LLM_MODEL"), "gpt-4o-mini"),
|
||||
FileModel: defaultIfEmpty(os.Getenv("LLM_FILE_MODEL"), defaultIfEmpty(os.Getenv("LLM_MODEL"), "gpt-4o-mini")),
|
||||
RouterModel: strings.TrimSpace(os.Getenv("LLM_ROUTER_MODEL")),
|
||||
},
|
||||
SQLitePath: defaultIfEmpty(os.Getenv("SQLITE_PATH"), filepath.Join(defaultDataDir, "laodingbot.db")),
|
||||
WebSearch: WebSearchConfig{
|
||||
@@ -122,6 +130,12 @@ func Load() (Config, error) {
|
||||
AllowedCommands: splitCSV(defaultIfEmpty(os.Getenv("ALLOWED_COMMANDS"), "pwd,ls,cat,echo,grep,find,head,tail,go")),
|
||||
WorkDir: defaultIfEmpty(os.Getenv("WORK_DIR"), defaultWorkSubdir),
|
||||
},
|
||||
Gitea: GiteaConfig{
|
||||
BaseURL: strings.TrimRight(strings.TrimSpace(os.Getenv("GITEA_BASE_URL")), "/"),
|
||||
Token: strings.TrimSpace(os.Getenv("GITEA_TOKEN")),
|
||||
Owner: strings.TrimSpace(os.Getenv("GITEA_OWNER")),
|
||||
Repo: strings.TrimSpace(os.Getenv("GITEA_REPO")),
|
||||
},
|
||||
}
|
||||
|
||||
cfg.MessageChannel = strings.ToLower(strings.TrimSpace(cfg.MessageChannel))
|
||||
@@ -178,9 +192,6 @@ func Load() (Config, error) {
|
||||
if cfg.LLM.APIKey == "" {
|
||||
return Config{}, fmt.Errorf("LLM_API_KEY is required")
|
||||
}
|
||||
if cfg.LLM.FilePromptMode != "user_content_file_parts" && cfg.LLM.FilePromptMode != "system_fileid_uri" {
|
||||
return Config{}, fmt.Errorf("LLM_FILE_PROMPT_MODE must be one of: user_content_file_parts, system_fileid_uri")
|
||||
}
|
||||
|
||||
cfg.SoulPath = resolvePathInWorkspace(cfg.SoulPath, agentWorkspaceDir)
|
||||
cfg.SkillsDir = resolvePathInWorkspace(cfg.SkillsDir, agentWorkspaceDir)
|
||||
@@ -417,14 +428,3 @@ func splitCSV(raw string) []string {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeFilePromptMode(v string) string {
|
||||
v = strings.ToLower(strings.TrimSpace(v))
|
||||
if v == "" {
|
||||
return "user_content_file_parts"
|
||||
}
|
||||
if v == "system_fileid" || v == "system_fileid_url" || v == "system_fileid_uri" {
|
||||
return "system_fileid_uri"
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user