From aa574a8cb2084e667d02601f74c249ad249b3b17 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Wed, 10 Jun 2026 16:34:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20DSL=E2=86=92=E5=AF=B9=E8=AF=9D=E7=BC=96?= =?UTF-8?q?=E8=AF=91=20=E2=80=94=20Eino=20=E5=9B=BE=E7=94=A8=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E5=AD=97=E6=AE=B5=E8=80=8C=E9=9D=9E=E6=95=B4=E6=AE=B5?= =?UTF-8?q?=20JSON=20=E5=96=82=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dispatcher 真正解析 DSL 图:input 节点文本=用户消息,agent 节点 system=系统提示词, 不再把整段 DSL JSON 当 prompt 丢给模型。 - dispatcher/internal/dsl: Compile(graph)→Plan{System,Query,Tools} (input.text/agent.prompt→query, agent.system→system, tool.tool→tools, 兜底默认) - eino/graph: recall 调 dsl.Compile,模板加 {system}(Agent 系统提示词+画像注入) - eino/orchestrator: 写回历史落真实 query 而非 DSL 原文 - frontend nodeCatalog: input 节点改 text 字段(用户输入,必填),检查器可编辑 - 验证: 全模块+前端 build✓; 真实 DeepSeek——curl DSL(input '中国首都?')→'北京'; 真实浏览器——加 input 节点输入'NATS是什么'→运行→DeepSeek 简洁正确作答 Co-Authored-By: Claude Opus 4.8 --- .../frontend/src/studio/nodeCatalog.ts | 4 +- sundynix-dispatcher/internal/dsl/compile.go | 89 +++++++++++++++++++ sundynix-dispatcher/internal/eino/graph.go | 19 ++-- .../internal/eino/orchestrator.go | 3 +- 4 files changed, 106 insertions(+), 9 deletions(-) create mode 100644 sundynix-dispatcher/internal/dsl/compile.go diff --git a/sundynix-desktop/frontend/src/studio/nodeCatalog.ts b/sundynix-desktop/frontend/src/studio/nodeCatalog.ts index ebbdf17..f28edac 100644 --- a/sundynix-desktop/frontend/src/studio/nodeCatalog.ts +++ b/sundynix-desktop/frontend/src/studio/nodeCatalog.ts @@ -30,10 +30,10 @@ export const NODE_KINDS: Record = { badge: "bg-amber-100 text-amber-700", desc: "用户提问 / 文件 / 变量", fields: [ + { key: "text", label: "用户输入", type: "textarea", placeholder: "要发送给模型的内容…", required: true }, { key: "source", label: "来源", type: "select", options: ["用户输入", "本地文件", "变量"] }, - { key: "placeholder", label: "占位文案", type: "text", placeholder: "请输入…" }, ], - defaults: { source: "用户输入", placeholder: "请输入…" }, + defaults: { text: "", source: "用户输入" }, }, retriever: { kind: "retriever", diff --git a/sundynix-dispatcher/internal/dsl/compile.go b/sundynix-dispatcher/internal/dsl/compile.go new file mode 100644 index 0000000..d1a5df7 --- /dev/null +++ b/sundynix-dispatcher/internal/dsl/compile.go @@ -0,0 +1,89 @@ +// 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 助手。" + +// Compile 解析 DSL 图,抽取对话计划。无法解析时退化为把原文当输入(兼容旧行为)。 +func Compile(graph json.RawMessage) Plan { + var f Flow + if err := json.Unmarshal(graph, &f); 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} +} + +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)) +} diff --git a/sundynix-dispatcher/internal/eino/graph.go b/sundynix-dispatcher/internal/eino/graph.go index 5ac7102..5450fb2 100644 --- a/sundynix-dispatcher/internal/eino/graph.go +++ b/sundynix-dispatcher/internal/eino/graph.go @@ -7,6 +7,7 @@ import ( "github.com/cloudwego/eino/compose" "github.com/cloudwego/eino/schema" + "github.com/sundynix/sundynix-dispatcher/internal/dsl" "github.com/sundynix/sundynix-dispatcher/internal/llm" "github.com/sundynix/sundynix-shared/contract" ) @@ -27,28 +28,34 @@ func buildGraph(ctx context.Context, pool *llm.Pool, fetch memoryFetcher, fetchH compose.WithGenLocalState(func(context.Context) *AgentState { return &AgentState{} }), ) - // 1) recall:取 user_id/session_id → 召回画像(memory_get)+历史(history_get) → 写 State,输出模板变量。 + // 1) recall:编译 DSL → 取系统提示词/用户输入 → 召回画像+历史 → 写 State,输出模板变量。 if err := g.AddLambdaNode("recall", compose.InvokableLambda( func(ctx context.Context, t *contract.Task) (map[string]any, error) { uid, _ := t.Meta[contract.MetaUserID].(string) sid, _ := t.Meta[contract.MetaSessionID].(string) - profile := fetch(ctx, uid, string(t.Graph)) + plan := dsl.Compile(t.Graph) // DSL→对话编译:抽取 system / query / tools + profile := fetch(ctx, uid, plan.Query) hist := fetchHist(ctx, sid) _ = compose.ProcessState(ctx, func(_ context.Context, s *AgentState) error { - s.UserID, s.SessionID, s.Profile, s.Input = uid, sid, profile, string(t.Graph) + s.UserID, s.SessionID, s.Profile, s.Input = uid, sid, profile, plan.Query return nil }) if profile == "" { profile = "(暂无该用户的偏好记忆)" } - return map[string]any{"profile": profile, "query": string(t.Graph), "history": hist}, nil + return map[string]any{ + "system": plan.System, + "profile": profile, + "query": plan.Query, + "history": hist, + }, nil })); err != nil { return nil, err } - // 2) prompt:画像注入 system,历史用占位符插入,用户输入作为 user message。 + // 2) prompt:Agent 节点系统提示词 + 画像注入 system,历史用占位符,用户输入作为 user message。 tpl := prompt.FromMessages(schema.FString, - schema.SystemMessage("你在与特定用户对话。关于该用户的已知信息:\n{profile}\n请据此个性化作答并保持其偏好。"), + schema.SystemMessage("{system}\n\n关于当前用户的已知信息:\n{profile}\n请据此个性化作答并保持其偏好。"), schema.MessagesPlaceholder("history", true), schema.UserMessage("{query}"), ) diff --git a/sundynix-dispatcher/internal/eino/orchestrator.go b/sundynix-dispatcher/internal/eino/orchestrator.go index 2514d00..8434f42 100644 --- a/sundynix-dispatcher/internal/eino/orchestrator.go +++ b/sundynix-dispatcher/internal/eino/orchestrator.go @@ -13,6 +13,7 @@ import ( "github.com/cloudwego/eino/compose" "github.com/cloudwego/eino/schema" + "github.com/sundynix/sundynix-dispatcher/internal/dsl" "github.com/sundynix/sundynix-dispatcher/internal/harness" "github.com/sundynix/sundynix-dispatcher/internal/llm" "github.com/sundynix/sundynix-shared/contract" @@ -168,7 +169,7 @@ func (o *Orchestrator) memorize(t *contract.Task, answer string) { uid, _ := t.Meta[contract.MetaUserID].(string) sid, _ := t.Meta[contract.MetaSessionID].(string) if sid != "" && o.tools != nil { - o.appendHistory(sid, "user", string(t.Graph)) + o.appendHistory(sid, "user", dsl.Compile(t.Graph).Query) // 落真实用户输入,而非 DSL 原文 o.appendHistory(sid, "assistant", answer) log.Printf("[eino] (writeback) task %s 已落会话历史 session=%s", t.ID, sid) }