Files
sundynix-agentix/sundynix-dispatcher/internal/dsl/compile.go
T
Blizzard 71db0e295f feat: compose.NewGraph 全图编译 — 工具节点在 Eino 图里真实执行
dispatcher 按每个任务的 DSL 动态编译 Eino 图:工具/检索节点按拓扑序作为真实图
节点经 NATS 调 MCP,产出注入模型上下文。不再是固定的 recall→prompt→model。

- dsl: 加 Parse(图结构) + (Flow)Topo(Kahn 拓扑序,环退化声明序) + ToolBinding(tool/
  retriever 节点→工具名+参数)
- eino/compile.go: 逐任务 compileFlow —— START→init(身份+记忆召回)→tool_n(真调 MCP,
  失败降级)→prompt(黑板 RunCtx 组装 system+画像+工具产出+历史+输入)→model→END
- eino/orchestrator: 去掉启动期静态图,Handle 内按 DSL 动态编译;删旧 graph.go/state.go
- 工具节点产出作为参考资料注入 system,模型据此作答
- 验证: 全模块 build✓ + e2e PASS; 真实 DeepSeek 双证——回归(input+agent)→'蓝色';
  工具节点(echo 注入事实)→mcp-go 日志证明图里真调 echo→模型据参考资料答'…Milvus…'

注: 分支/并行节点(compose.Branch/fan-out)暂未编译,是更大 TODO。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 16:45:33 +08:00

169 lines
4.4 KiB
Go
Raw 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 是一条连线。
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_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))
}