Files
Blizzard 1bd187874d feat(orchestration): Phase2 —— map 真并行 fan-out + branch 真/假边标签精确选路
Phase1 让引擎按图执行后,本轮补上两块:

1) map 并行 fan-out(dispatcher)
   - map 节点:planItems 把主题拆成 3–6 子项 → 复用 report 的 writeSections
     有界并发(4)逐项撰写 → 结构化 sections 存黑板 + 拼进 answer + 流式呈现。
   - 检索节点记下 owner 作用域库名(b.kb),供 map 各项并行检索复用。
   - render 节点优先用 map 产出的多章 sections 渲染,否则整段成稿当单章。

2) branch 真/假边标签(前端 + DSL + dispatcher)
   - TypedNode:分支节点渲染两个出口手柄 真(绿)/假(红),连线各带 sourceHandle。
   - exportDsl / TaskDsl:边导出携带 sourceHandle。
   - dispatcher dsl.Edge 增 SourceHandle;branchNode 优先按 true/false 标签精确
     选路,无标签的旧图退回"出边顺序"约定,向后兼容。

实测(gateway+dispatcher+DeepSeek 真跑):
- map:input→map→render,DeepSeek 拆出 5 章并行撰写(347–512字),trace 见
  section:0..4 并发 + 有界并发(section3 等槽);render 因 mcp-go 不在优雅降级 ✓
- branch 标签:把 true 边故意列第二位,条件真仍走 true 标签的分支A、假走分支B,
  证明按标签而非边序选路 ✓
- 桌面端:分支节点正确渲染 真/假 两手柄,无 console 报错 ✓

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:01:51 +08:00

170 lines
4.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package dsl 把前端导出的 JSON DSL 图编译为可执行的对话计划。
// 当前从图中抽取「系统提示词 / 用户输入 / 工具节点」;后续可演进为
// compose.NewGraph 的完整多节点编译(分支/并行/工具节点逐一映射)。
package dsl
import (
"encoding/json"
"fmt"
"strings"
)
// Node 是 DSL 图的一个节点(与前端 exportDsl 对齐)。
type Node struct {
ID string `json:"id"`
Kind string `json:"kind"`
Label string `json:"label"`
Config map[string]any `json:"config"`
}
// Edge 是一条连线。SourceHandle 标记从 branch 节点引出的边走真/假分支("true"/"false")。
type Edge struct {
Source string `json:"source"`
Target string `json:"target"`
SourceHandle string `json:"sourceHandle"`
}
// Flow 是整张图。
type Flow struct {
Version string `json:"version"`
Nodes []Node `json:"nodes"`
Edges []Edge `json:"edges"`
}
// Plan 是编译后的对话计划。
type Plan struct {
System string // 系统提示词(来自 agent 节点的 system;空则默认)
Query string // 用户输入(来自 input 节点 text / agent 节点 prompt
Tools []string // 图中工具节点绑定的 MCP 工具名(供后续工具编排用)
}
const defaultSystem = "你是 sundynix-agentix 平台的 AI 助手。"
// Parse 把 DSL 原文解析为图结构。
func Parse(graph json.RawMessage) (*Flow, error) {
var f Flow
if err := json.Unmarshal(graph, &f); err != nil {
return nil, err
}
return &f, nil
}
// Topo 返回节点的拓扑序(Kahn);无边/有环时退化为声明顺序。
func (f *Flow) Topo() []Node {
byID := make(map[string]Node, len(f.Nodes))
indeg := make(map[string]int, len(f.Nodes))
adj := make(map[string][]string)
for _, n := range f.Nodes {
byID[n.ID] = n
indeg[n.ID] = 0
}
for _, e := range f.Edges {
if _, ok := byID[e.Source]; !ok {
continue
}
if _, ok := byID[e.Target]; !ok {
continue
}
adj[e.Source] = append(adj[e.Source], e.Target)
indeg[e.Target]++
}
var queue, order []string
for _, n := range f.Nodes { // 按声明序入队,保证确定性
if indeg[n.ID] == 0 {
queue = append(queue, n.ID)
}
}
for len(queue) > 0 {
id := queue[0]
queue = queue[1:]
order = append(order, id)
for _, t := range adj[id] {
indeg[t]--
if indeg[t] == 0 {
queue = append(queue, t)
}
}
}
out := make([]Node, 0, len(f.Nodes))
seen := make(map[string]bool)
for _, id := range order {
out = append(out, byID[id])
seen[id] = true
}
for _, n := range f.Nodes { // 有环时补齐剩余
if !seen[n.ID] {
out = append(out, n)
}
}
return out
}
// Compile 解析 DSL 图,抽取对话计划。无法解析时退化为把原文当输入(兼容旧行为)。
func Compile(graph json.RawMessage) Plan {
f, err := Parse(graph)
if err != nil || len(f.Nodes) == 0 {
return Plan{System: defaultSystem, Query: strings.TrimSpace(string(graph))}
}
var queries, systems, tools []string
for _, n := range f.Nodes {
switch n.Kind {
case "input":
if t := str(n.Config["text"]); t != "" {
queries = append(queries, t)
}
case "agent":
if s := str(n.Config["system"]); s != "" {
systems = append(systems, s)
}
if p := str(n.Config["prompt"]); p != "" { // 单 agent 节点快速测试时直接带 prompt
queries = append(queries, p)
}
case "tool":
if t := str(n.Config["tool"]); t != "" {
tools = append(tools, t)
}
}
}
system := strings.Join(systems, "\n")
if system == "" {
system = defaultSystem
}
query := strings.Join(queries, "\n")
if query == "" {
query = "你好" // 无结构化输入时的兜底,避免给模型发空消息
}
return Plan{System: system, Query: query, Tools: tools}
}
// ToolBinding 从 tool/retriever 节点抽取要调用的 MCP 工具名与参数。
// 非工具型节点返回空工具名(由编译器跳过)。
func ToolBinding(n Node) (tool string, args map[string]any) {
args = map[string]any{}
switch n.Kind {
case "tool":
tool = str(n.Config["tool"])
if raw := str(n.Config["args"]); raw != "" {
_ = json.Unmarshal([]byte(raw), &args) // 前端 args 为 JSON 字符串
}
case "retriever":
// 检索节点暂映射到 wiki_searchRAG 接真后改 memory_search / 真实混合检索。
tool = "wiki_search"
if kb := str(n.Config["kb"]); kb != "" {
args["kb"] = kb
}
}
return tool, args
}
func str(v any) string {
if v == nil {
return ""
}
if s, ok := v.(string); ok {
return strings.TrimSpace(s)
}
return strings.TrimSpace(fmt.Sprint(v))
}