-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.go
More file actions
105 lines (90 loc) · 3.23 KB
/
main.go
File metadata and controls
105 lines (90 loc) · 3.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
// Package main demonstrates human-in-the-loop (interrupt/resume) in gographgo.
//
// Execution flow:
//
// Start → ask_user → process → End
//
// The "ask_user" node calls NodeInterrupt, which halts execution and surfaces
// a question to the caller. The graph resumes when Invoke is called again on
// the same thread ID with a Command carrying the user's answer.
//
// A Checkpointer is required: it persists state between the two Invoke calls
// so the graph knows where to resume from.
package main
import (
"context"
"fmt"
"log"
"github.com/SkinnyPeteTheGiraffe/gographgo/pkg/checkpoint"
"github.com/SkinnyPeteTheGiraffe/gographgo/pkg/graph"
)
// State carries the question and the user's answer.
type State struct {
Question string
Answer string
Summary string
}
func main() {
// InMemorySaver persists checkpoints in memory. For production use
// pkg/checkpoint/postgres or pkg/checkpoint/sqlite instead.
saver := checkpoint.NewInMemorySaver()
builder := graph.NewStateGraph[State]()
// ask_user pauses execution and surfaces a question.
// On the first run, NodeInterrupt halts the node. On resume the graph
// re-runs the node from the top; NodeInterrupt then returns the provided
// answer instead of halting.
builder.AddNode("ask_user", func(ctx context.Context, s State) (graph.NodeResult, error) {
answer := graph.NodeInterrupt(ctx, graph.Dyn(s.Question))
return graph.NodeWrites(map[string]graph.Dynamic{
"Answer": answer,
}), nil
})
// process uses the captured answer.
builder.AddNode("process", func(ctx context.Context, s State) (graph.NodeResult, error) {
summary := fmt.Sprintf("User answered %q to %q", s.Answer, s.Question)
return graph.NodeWrites(map[string]graph.Dynamic{
"Summary": graph.Dyn(summary),
}), nil
})
builder.AddEdge(graph.Start, "ask_user")
builder.AddEdge("ask_user", "process")
builder.AddEdge("process", graph.End)
compiled, err := builder.Compile(graph.CompileOptions{Checkpointer: saver})
if err != nil {
log.Fatalf("compile: %v", err)
}
// All calls sharing the same ThreadID form one conversation thread.
cfg := graph.Config{
ThreadID: "thread-1",
Checkpointer: saver,
}
ctx := graph.WithConfig(context.Background(), cfg)
// --- First invocation: graph halts at the interrupt ---
initial := State{Question: "What is your favorite language?"}
first, err := compiled.Invoke(ctx, initial)
if err != nil {
log.Fatalf("first invoke: %v", err)
}
if len(first.Interrupts) == 0 {
log.Fatal("expected an interrupt, got none")
}
question := first.Interrupts[0].Value.Value()
fmt.Printf("Graph is waiting for input: %v\n", question)
// --- Resume: supply the user's answer via Command ---
// Command.Resume can be a single value (matched positionally) or a
// map[string]Dynamic keyed by interrupt ID for multi-interrupt graphs.
resumeInput := State{} // state is restored from checkpoint; input is ignored
resumeCtx := graph.WithConfig(ctx, graph.Config{
ThreadID: cfg.ThreadID,
Checkpointer: saver,
Metadata: map[string]any{
graph.ConfigKeyResume: "Go",
},
})
second, err := compiled.Invoke(resumeCtx, resumeInput)
if err != nil {
log.Fatalf("resume invoke: %v", err)
}
final := second.Value.(State)
fmt.Printf("Summary: %s\n", final.Summary)
}