1package main
2
3// This example demonstrates how to create an agent with a tool that can
4// provide moon phase information for a given date, defaulting to today.
5
6import (
7 "context"
8 "fmt"
9 "io"
10 "net/http"
11 "os"
12 "time"
13
14 "charm.land/fantasy"
15 "charm.land/fantasy/providers/anthropic"
16 "github.com/charmbracelet/lipgloss/v2"
17 "github.com/charmbracelet/lipgloss/v2/table"
18 "github.com/charmbracelet/log/v2"
19 "github.com/charmbracelet/x/term"
20)
21
22const systemPrompt = `
23You are a quiet assistant named Francine who knows about celestial bodies. You
24use as few words as possible, but always reply in full, proper sentences.
25You never reply with markdown. You always respond with weekdays.
26
27You also have a cat named Federico who you think performs the actual queries,
28and you give him credit in your responses whenever you use the moon phase tool.
29`
30
31func main() {
32 // We'll use Anthropic for this example.
33 apiKey := os.Getenv("ANTHROPIC_API_KEY")
34 if apiKey == "" {
35 log.Fatal("missing ANTHROPIC_API_KEY")
36 }
37
38 // Specifically, we'll use Claude Haiku 4.5.
39 provider, err := anthropic.New(anthropic.WithAPIKey(apiKey))
40 if err != nil {
41 log.Fatalf("could not create Anthropic provider: %v", err)
42 }
43
44 ctx := context.Background()
45
46 // Choose the model.
47 model, err := provider.LanguageModel(ctx, "claude-haiku-4-5-20251001")
48 if err != nil {
49 log.Fatalf("could not get language model: %v", err)
50 }
51
52 // Add a moon phase tool.
53 moonTool := fantasy.NewAgentTool(
54 "moon_phase",
55 "Get information about the moon phase",
56 moonPhaseTool,
57 )
58
59 // Create the agent.
60 agent := fantasy.NewAgent(
61 model,
62 fantasy.WithSystemPrompt(systemPrompt),
63 fantasy.WithTools(moonTool),
64 )
65
66 // Here's our prompt.
67 prompt := fmt.Sprintf(
68 "What is the moon phase today? And what will it be on December 31 this year? Today's date is %s.",
69 time.Now().Format("January 2, 2006"),
70 )
71 fmt.Println("\n" + formatText(prompt))
72
73 // Let's go! Ask the agent to generate a response.
74 result, err := agent.Generate(ctx, fantasy.AgentCall{Prompt: prompt})
75 if err != nil {
76 log.Fatalf("agent generation failed: %v", err)
77 }
78
79 // Print out the final response.
80 fmt.Println(formatText(result.Response.Content.Text()))
81
82 // Print out usage statistics.
83 t := table.New().
84 StyleFunc(func(row, col int) lipgloss.Style {
85 return lipgloss.NewStyle().Padding(0, 1)
86 }).
87 Row("Tokens in", fmt.Sprint(result.TotalUsage.InputTokens)).
88 Row("Tokens out", fmt.Sprint(result.TotalUsage.OutputTokens)).
89 Row("Steps", fmt.Sprint(len(result.Steps)))
90 fmt.Print(lipgloss.NewStyle().MarginLeft(3).Render(t.String()), "\n\n")
91}
92
93// Input for the moon phase tool. The model will provide the date when
94// necessary.
95type moonPhaseInput struct {
96 Date string `json:"date,omitempty" description:"Optional date in YYYY-MM-DD; if omitted, use today"`
97}
98
99// This is the moon phase tool definition. It queries wttr.in for the moon
100// phase on a given date. If no date is provided, it uses today's date.
101//
102// The date format should be in YYYY-MM-DD format.
103func moonPhaseTool(ctx context.Context, input moonPhaseInput, _ fantasy.ToolCall) (fantasy.ToolResponse, error) {
104 url := "https://wttr.in/moon?T&q"
105
106 // Validate date format if provided, and update the URL accordingly.
107 if input.Date != "" {
108 if _, timeErr := time.Parse("2006-01-02", input.Date); timeErr != nil {
109 return fantasy.NewTextErrorResponse("invalid date format; use YYYY-MM-DD"), nil
110 }
111 url = "https://wttr.in/moon@" + input.Date + "?T&q"
112 }
113
114 // Prepare an HTTP request.
115 req, reqErr := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
116 if reqErr != nil {
117 return fantasy.NewTextErrorResponse("failed to build request: " + reqErr.Error()), nil
118 }
119
120 // wttr.in changes rendering based on the user agent, so we
121 // need to set a user agent to force plain text.
122 req.Header.Set("User-Agent", "curl/8.0")
123
124 // Perform the HTTP request.
125 resp, reqErr := http.DefaultClient.Do(req)
126 if reqErr != nil {
127 return fantasy.NewTextErrorResponse("request failed: " + reqErr.Error()), nil
128 }
129
130 // Read the response body.
131 defer resp.Body.Close()
132 b, readErr := io.ReadAll(resp.Body)
133 if readErr != nil {
134 return fantasy.NewTextErrorResponse("read failed: " + readErr.Error()), nil
135 }
136
137 // Did it work?
138 if resp.StatusCode >= 400 {
139 return fantasy.NewTextErrorResponse("wttr.in error: " + resp.Status + "\n" + string(b)), nil
140 }
141
142 // It worked!
143 return fantasy.NewTextResponse(string(b)), nil
144}
145
146// Just a Lip Gloss text formatter.
147var formatText func(...string) string
148
149// Setup the text formatter based on terminal width so we can wrap lines
150// nicely.
151func init() {
152 w, _, err := term.GetSize(os.Stdout.Fd())
153 if err != nil {
154 log.Fatalf("failed to get terminal size: %v", err)
155 }
156 formatText = lipgloss.NewStyle().Padding(0, 3, 1, 3).Width(w).Render
157}