2ee16d1f99
按"并存 + 等价回归"策略,对话主流程可跑在 Eino compose.Graph 上: - compose_graph.go:runConversation 按 EINO_COMPOSE 灰度开关分流; runComposeConversation 建图 START→ChatModel→END,Compile→Stream 回流 token; 模型未就绪/编译失败降级回 runAgent。默认关,graph.go 仍是默认且权威。 - compose_callbacks.go:composeTracer 用 utils/callbacks 把 ChatModel/Tool 的 start/end/error 桥到 ExecEvent(可观测归一,不再各处手写 emit)。 - LLM 接口 + Pool 增 ChatModel();fakeLLM 加 cm 字段 + stub Eino 模型。 - 测试:compose 图编译运行 / compose 对话流式 / 开关关→走 runAgent。 顺带修真 bug:SubjectTaskStatus 原 sundynix.tasks.status 落在任务流通配 sundynix.tasks.> 内 → 状态事件被当成"幽灵任务"自我放大(实测污染 2300+ 条)。 挪到 sundynix.status.task + dispatcher 加空任务护栏。 验收:make test-go 全绿;live compose 路径 54 字答复 eval 1.00、默认路径 eval 1.00、幽灵任务 0 复发。剩余 branch/map/render 等节点逐步迁移后 graph.go 退役。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
90 lines
3.0 KiB
Go
90 lines
3.0 KiB
Go
package eino
|
||
|
||
import (
|
||
"context"
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/cloudwego/eino/components/model"
|
||
"github.com/cloudwego/eino/compose"
|
||
"github.com/cloudwego/eino/schema"
|
||
|
||
"github.com/sundynix/sundynix-dispatcher/internal/harness"
|
||
"github.com/sundynix/sundynix-dispatcher/internal/llm"
|
||
)
|
||
|
||
// stubModel 是实现 Eino model.BaseChatModel 的测试桩,固定回一段文本(确定性)。
|
||
type stubModel struct{ reply string }
|
||
|
||
func (s *stubModel) Generate(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.Message, error) {
|
||
return schema.AssistantMessage(s.reply, nil), nil
|
||
}
|
||
|
||
func (s *stubModel) Stream(_ context.Context, _ []*schema.Message, _ ...model.Option) (*schema.StreamReader[*schema.Message], error) {
|
||
sr, sw := schema.Pipe[*schema.Message](1)
|
||
go func() {
|
||
sw.Send(schema.AssistantMessage(s.reply, nil), nil)
|
||
sw.Close()
|
||
}()
|
||
return sr, nil
|
||
}
|
||
|
||
// TestComposeGraphRuns 直接验证 compose.Graph(START→ChatModel→END)能编译并流式产出。
|
||
func TestComposeGraphRuns(t *testing.T) {
|
||
g := compose.NewGraph[[]*schema.Message, *schema.Message]()
|
||
if err := g.AddChatModelNode("model", &stubModel{reply: "你好世界"}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
_ = g.AddEdge(compose.START, "model")
|
||
_ = g.AddEdge("model", compose.END)
|
||
r, err := g.Compile(context.Background())
|
||
if err != nil {
|
||
t.Fatalf("compile: %v", err)
|
||
}
|
||
out, err := r.Invoke(context.Background(), []*schema.Message{schema.UserMessage("hi")})
|
||
if err != nil {
|
||
t.Fatalf("invoke: %v", err)
|
||
}
|
||
if out.Content != "你好世界" {
|
||
t.Fatalf("got %q", out.Content)
|
||
}
|
||
}
|
||
|
||
// TestComposeConversationEquivalent 验证 EINO_COMPOSE=1 时对话主流程走 compose 路径,
|
||
// 把模型输出流式回流到 sink 并累计成稿——与自研 runAgent 行为等价(都逐片转发模型输出)。
|
||
func TestComposeConversationEquivalent(t *testing.T) {
|
||
t.Setenv("EINO_COMPOSE", "1")
|
||
fs := &fakeSink{}
|
||
o := &Orchestrator{
|
||
pool: &fakeLLM{ready: true, cm: &stubModel{reply: "我是 compose 路径的回答"}},
|
||
breaker: harness.NewCircuitBreaker(),
|
||
sink: fs,
|
||
}
|
||
b := &board{query: "你好"}
|
||
tr := &execTracer{} // sink 为 nil → 轨迹发射空操作
|
||
o.runConversation(context.Background(), "task_compose", b, "", tr, "agent")
|
||
|
||
if !strings.Contains(b.answer, "compose 路径") {
|
||
t.Fatalf("成稿未含模型输出: %q", b.answer)
|
||
}
|
||
if !strings.Contains(fs.text(), "compose 路径") {
|
||
t.Fatalf("sink 未收到流式 token: %q", fs.text())
|
||
}
|
||
}
|
||
|
||
// TestComposeDisabledUsesRunAgent 验证开关关闭时仍走自研 runAgent(默认行为不变)。
|
||
func TestComposeDisabledUsesRunAgent(t *testing.T) {
|
||
t.Setenv("EINO_COMPOSE", "0")
|
||
fs := &fakeSink{}
|
||
o := &Orchestrator{
|
||
pool: &fakeLLM{ready: true, stream: func([]llm.ChatMessage) string { return "自研路径回答" }},
|
||
breaker: harness.NewCircuitBreaker(),
|
||
sink: fs,
|
||
}
|
||
b := &board{query: "你好"}
|
||
o.runConversation(context.Background(), "task_legacy", b, "", &execTracer{}, "agent")
|
||
if !strings.Contains(b.answer, "自研路径") {
|
||
t.Fatalf("未走自研路径: %q", b.answer)
|
||
}
|
||
}
|