1package main
2
3// This example demonstrates how to hook into the various parts of a streaming
4// tool call.
5
6import (
7 "context"
8 "fmt"
9 "math/rand/v2"
10 "os"
11 "strings"
12 "time"
13
14 "charm.land/fantasy"
15 "charm.land/fantasy/providers/kronk"
16)
17
18const modelURL = "Qwen/Qwen3-8B-GGUF/Qwen3-8B-Q8_0.gguf"
19
20const systemPrompt = `
21You are moderately helpful assistant with a new puppy named Chuck. Chuck is
22moody and ranges from very happy to very annoyed. He's pretty happy-go-lucky,
23but new encounters make him pretty uncomfortable.
24
25You despise emojis and never use them. Same with Markdown. Same with em-dashes.
26You prefer "welp" to "well" when starting a sentence (that's just how you were
27raised). You also don't use run-on sentences, including entering a comma where
28there should be a period. You had a decent education and did well in elementary
29school grammar. You grew up in the United States, specifically Kansas City,
30Missouri.
31`
32
33// Input for a tool call. The LLM will look at the struct tags and fill out the
34// values as necessary.
35type dogInteraction struct {
36 OtherDogName string `json:"dogName" description:"Name of the other dog. Just make something up. All the dogs are named after Japanese cars from the 80s."`
37}
38
39// Here's a tool call. In this case it's a set of random barks.
40func letsBark(ctx context.Context, i dogInteraction, _ fantasy.ToolCall) (fantasy.ToolResponse, error) {
41 var r fantasy.ToolResponse
42 if rand.Float64() >= 0.5 {
43 r.Content = randomBarks(1, 3)
44 } else {
45 r.Content = randomBarks(5, 10)
46 }
47 return r, nil
48}
49
50func main() {
51 if err := run(); err != nil {
52 fmt.Printf("\nERROR: %s\n", err)
53 os.Exit(1)
54 }
55}
56
57func run() error {
58 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
59 defer cancel()
60
61 // Create the provider with optional logging.
62 provider, err := kronk.New(
63 kronk.WithName("kronk"),
64 kronk.WithLogger(kronk.FmtLogger),
65 )
66 if err != nil {
67 return fmt.Errorf("unable to create provider: %w", err)
68 }
69
70 // Clean up when done.
71 defer func() {
72 fmt.Println("\nUnloading Kronk")
73 if closer, ok := provider.(interface{ Close(context.Context) error }); ok {
74 if err := closer.Close(context.Background()); err != nil {
75 fmt.Printf("failed to close provider: %v\n", err)
76 }
77 }
78 }()
79
80 // Get a language model by providing the model URL.
81 // The provider will download and initialize the model automatically.
82 model, err := provider.LanguageModel(ctx, modelURL)
83 if err != nil {
84 return fmt.Errorf("unable to get language model: %w", err)
85 }
86
87 // -------------------------------------------------------------------------
88
89 // Let's add a tool to our belt. A tool for dogs.
90 barkTool := fantasy.NewAgentTool(
91 "bark",
92 "Have Chuck express his feelings by barking. A few barks means he's happy and many barks means he's not.",
93 letsBark,
94 )
95
96 // Time to make the agent.
97 agent := fantasy.NewAgent(
98 model,
99 fantasy.WithSystemPrompt(systemPrompt),
100 fantasy.WithTools(barkTool),
101 )
102
103 // Alright, let's setup a streaming request!
104 streamCall := fantasy.AgentStreamCall{
105 // The prompt.
106 Prompt: "what does Chuck say when he is happy",
107
108 // When reasoning starts (Qwen3 models use "thinking" mode).
109 OnReasoningStart: func(id string, content fantasy.ReasoningContent) error {
110 fmt.Print("\n[Thinking: ")
111 return nil
112 },
113
114 // When we receive reasoning content.
115 OnReasoningDelta: func(id, text string) error {
116 // Print reasoning in a subdued way
117 fmt.Print(text)
118 return nil
119 },
120
121 // When reasoning ends.
122 OnReasoningEnd: func(id string, reasoning fantasy.ReasoningContent) error {
123 fmt.Print("]\n\n")
124 return nil
125 },
126
127 // When we receive a chunk of streaming data.
128 OnTextDelta: func(id, text string) error {
129 _, fmtErr := fmt.Print(text)
130 return fmtErr
131 },
132
133 // When tool calls are invoked.
134 OnToolCall: func(toolCall fantasy.ToolCallContent) error {
135 fmt.Printf("\n-> Invoking the %s tool with input %s\n", toolCall.ToolName, toolCall.Input)
136 return nil
137 },
138
139 // When a tool call completes.
140 OnToolResult: func(res fantasy.ToolResultContent) error {
141 text, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentText](res.Result)
142 if !ok {
143 return fmt.Errorf("failed to cast result to text")
144 }
145 _, fmtErr := fmt.Printf("\n-> Using the %s tool: %s", res.ToolName, text.Text)
146 return fmtErr
147 },
148
149 // When a step finishes, such as a tool call or a response from the
150 // LLM.
151 OnStepFinish: func(_ fantasy.StepResult) error {
152 fmt.Print("\n-> Step completed\n")
153 return nil
154 },
155 }
156
157 fmt.Println("Generating...")
158
159 // Finally, let's stream everything!
160 _, err = agent.Stream(ctx, streamCall)
161 if err != nil {
162 fmt.Fprintf(os.Stderr, "Error generating response: %v\n", err)
163 os.Exit(1)
164 }
165
166 return nil
167}
168
169// Return a random number of barks between low and high.
170func randomBarks(low, high int) string {
171 const bark = "ruff"
172 numBarks := low + rand.IntN(high-low+1)
173 var barks strings.Builder
174 for i := range numBarks {
175 if i > 0 {
176 barks.WriteString(" ")
177 }
178 barks.WriteString(bark)
179 }
180 return barks.String()
181}