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