main.go

  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}