- fix: Compile() defensive map copy, prevents post-compile Graph mutation - fix: correct 'origin'→'destination' in edge validation error message - feat: export ErrNotPaused/ErrAlreadyFinished for errors.Is() support - feat: WithPreNodeHook/WithPostNodeHook now support chaining (multiple hooks) - feat: Compile detects conflicting outgoing edge types on same node - fix: FileCheckpointer atomic write (tmp+rename, crash-safe) - fix: FileCheckpointer threadID path traversal validation - refactor: remove redundant continue in run() loop - docs: add doc.go package-level documentation - test: +7 test cases covering error paths, edge conflicts, hook chaining
160 lines
5.7 KiB
Go
160 lines
5.7 KiB
Go
package main
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"log"
|
||
"time"
|
||
|
||
GopherGraph "github.com/unclesam-ly/GopherGraph"
|
||
)
|
||
|
||
// AgentState 是这个示例工作流的状态
|
||
type AgentState struct {
|
||
Input string
|
||
Draft string
|
||
Reviewed string
|
||
Messages []string // 含引用类型,演示 StateCloner 的必要性
|
||
}
|
||
|
||
// deepClone 演示如何为含切片的状态做安全的深拷贝
|
||
func deepClone(s AgentState) AgentState {
|
||
clone := s
|
||
clone.Messages = append([]string{}, s.Messages...) // 切片深拷贝
|
||
return clone
|
||
}
|
||
|
||
func main() {
|
||
g := GopherGraph.NewGraph[AgentState]()
|
||
|
||
// 节点 1:起草
|
||
g.AddNode("drafter", func(ctx context.Context, s AgentState) (AgentState, error) {
|
||
time.Sleep(80 * time.Millisecond) // 模拟耗时 AI 调用
|
||
s.Draft = fmt.Sprintf("【草稿】%s 的初版内容", s.Input)
|
||
s.Messages = append(s.Messages, "drafter: 完成起草")
|
||
return s, nil
|
||
})
|
||
|
||
// 节点 2:审阅
|
||
g.AddNode("reviewer", func(ctx context.Context, s AgentState) (AgentState, error) {
|
||
time.Sleep(60 * time.Millisecond)
|
||
s.Reviewed = s.Draft + "(已审阅 ✓)"
|
||
s.Messages = append(s.Messages, "reviewer: 审阅完成")
|
||
return s, nil
|
||
})
|
||
|
||
// 并发节点:同时翻译成英文和日文(演示 StateCloner)
|
||
g.AddNode("translate_en", func(ctx context.Context, s AgentState) (AgentState, error) {
|
||
time.Sleep(100 * time.Millisecond)
|
||
s.Messages = append(s.Messages, fmt.Sprintf("translate_en: [EN] %s", s.Reviewed))
|
||
return s, nil
|
||
})
|
||
g.AddNode("translate_ja", func(ctx context.Context, s AgentState) (AgentState, error) {
|
||
time.Sleep(120 * time.Millisecond)
|
||
s.Messages = append(s.Messages, fmt.Sprintf("translate_ja: [JA] %s", s.Reviewed))
|
||
return s, nil
|
||
})
|
||
|
||
// 节点:发布
|
||
g.AddNode("publisher", func(ctx context.Context, s AgentState) (AgentState, error) {
|
||
s.Messages = append(s.Messages, "publisher: 🎉 发布成功")
|
||
return s, nil
|
||
})
|
||
|
||
// 连线
|
||
g.AddEdge("drafter", "reviewer")
|
||
g.AddParallelEdges("reviewer", []string{"translate_en", "translate_ja"}, "publisher",
|
||
func(ctx context.Context, parent AgentState, branches []AgentState) (AgentState, error) {
|
||
// 合并所有分支的 Messages
|
||
for _, b := range branches {
|
||
parent.Messages = append(parent.Messages, b.Messages...)
|
||
}
|
||
parent.Messages = append(parent.Messages, "merger: 翻译分支已合并")
|
||
return parent, nil
|
||
},
|
||
)
|
||
|
||
cg, err := g.Compile()
|
||
if err != nil {
|
||
log.Fatalf("编译失败: %v", err)
|
||
}
|
||
|
||
// ============================================================
|
||
// 使用 Engine 包装器,挂载所有增强能力
|
||
// ============================================================
|
||
var nodeTimings = make(map[string]time.Time)
|
||
|
||
engine := GopherGraph.NewEngine(cg).
|
||
// 1. 注册深拷贝函数,消除并发分支对 Messages 切片的数据竞争
|
||
WithStateCloner(deepClone).
|
||
// 2. 设置最大步数熔断(防御意外死循环)
|
||
WithMaxSteps(50).
|
||
// 3. Pre Hook:记录节点开始时间 + 向控制台推送"流式进度"
|
||
WithPreNodeHook(func(ctx context.Context, name string, s AgentState) {
|
||
nodeTimings[name] = time.Now()
|
||
fmt.Printf(" ⏳ [PRE ] %-15s | 消息数: %d\n", name, len(s.Messages))
|
||
}).
|
||
// 4. Post Hook:计算并打印节点耗时
|
||
WithPostNodeHook(func(ctx context.Context, name string, s AgentState) {
|
||
elapsed := time.Since(nodeTimings[name])
|
||
fmt.Printf(" ✅ [POST] %-15s | 耗时: %v | 消息数: %d\n", name, elapsed.Round(time.Millisecond), len(s.Messages))
|
||
})
|
||
|
||
fmt.Println("🚀 启动工作流...")
|
||
fmt.Println("─────────────────────────────────────────────")
|
||
|
||
totalStart := time.Now()
|
||
thread, err := engine.Start(context.Background(), "drafter", AgentState{
|
||
Input: "GopherGraph 技术文档",
|
||
})
|
||
if err != nil {
|
||
log.Fatalf("运行失败: %v", err)
|
||
}
|
||
|
||
fmt.Println("─────────────────────────────────────────────")
|
||
fmt.Printf("🏁 工作流结束,总耗时: %v\n\n", time.Since(totalStart).Round(time.Millisecond))
|
||
fmt.Println("📋 最终消息日志:")
|
||
for i, msg := range thread.State.Messages {
|
||
fmt.Printf(" %d. %s\n", i+1, msg)
|
||
}
|
||
|
||
// ============================================================
|
||
// 演示流式输出:通过 Context 注入 channel(不需要修改任何 NodeFn)
|
||
// ============================================================
|
||
fmt.Println("\n─────────────────────────────────────────────")
|
||
fmt.Println("📡 演示:通过 Context 注入 channel 实现流式输出")
|
||
fmt.Println("─────────────────────────────────────────────")
|
||
|
||
type streamCtxKey struct{}
|
||
streamCh := make(chan string, 10)
|
||
|
||
// 在 Context 里注入 channel
|
||
streamCtx := context.WithValue(context.Background(), streamCtxKey{}, streamCh)
|
||
|
||
// 启动一个 goroutine 消费流式事件(模拟前端 SSE/WebSocket)
|
||
go func() {
|
||
for msg := range streamCh {
|
||
fmt.Printf(" 📨 [STREAM] %s\n", msg)
|
||
}
|
||
}()
|
||
|
||
// Post Hook 里从 Context 取出 channel,写入流式事件
|
||
streamEngine := GopherGraph.NewEngine(cg).
|
||
WithStateCloner(deepClone).
|
||
WithPostNodeHook(func(ctx context.Context, name string, s AgentState) {
|
||
ch, ok := ctx.Value(streamCtxKey{}).(chan string)
|
||
if ok {
|
||
ch <- fmt.Sprintf("节点 [%s] 执行完毕,当前消息数: %d", name, len(s.Messages))
|
||
}
|
||
})
|
||
|
||
_, err = streamEngine.Start(streamCtx, "drafter", AgentState{Input: "流式演示"})
|
||
if err != nil {
|
||
log.Fatalf("流式演示运行失败: %v", err)
|
||
}
|
||
close(streamCh)
|
||
time.Sleep(50 * time.Millisecond) // 等待消费者 goroutine 打印完毕
|
||
|
||
fmt.Println("\n✨ 示例结束")
|
||
}
|