1bd187874d
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>
170 lines
4.5 KiB
Go
170 lines
4.5 KiB
Go
// 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_search;RAG 接真后改 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))
|
||
}
|