// 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 是一条连线。 type Edge struct { Source string `json:"source"` Target string `json:"target"` } // 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)) }