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 model, err := provider.LanguageModel(ctx, "claude-haiku-4-5-20251001")
47 if err != nil {
48 log.Fatalf("could not get language model: %v", err)
49 }
50
51 // Add a moon phase tool.
52 moonTool := fantasy.NewAgentTool(
53 "moon_phase",
54 "Get information about the moon phase",
55 moonPhaseTool,
56 )
57
58 // Create the agent.
59 agent := fantasy.NewAgent(
60 model,
61 fantasy.WithSystemPrompt(systemPrompt),
62 fantasy.WithTools(moonTool),
63 )
64
65 // Here's our prompt.
66 const prompt = "What is the moon phase today? And what will it be on December 31, 2025?"
67 fmt.Println("\n" + formatText(prompt))
68
69 // Let's go! Ask the agent to generate a response.
70 result, err := agent.Generate(context.Background(), fantasy.AgentCall{Prompt: prompt})
71 if err != nil {
72 log.Fatalf("agent generation failed: %v", err)
73 }
74
75 // Print out the final response.
76 fmt.Println(formatText(result.Response.Content.Text()))
77
78 // Print out usage statistics.
79 t := table.New().
80 StyleFunc(func(row, col int) lipgloss.Style {
81 return lipgloss.NewStyle().Padding(0, 1)
82 }).
83 Row("Tokens in", fmt.Sprint(result.TotalUsage.InputTokens)).
84 Row("Tokens out", fmt.Sprint(result.TotalUsage.OutputTokens)).
85 Row("Steps", fmt.Sprint(len(result.Steps)))
86 fmt.Print(lipgloss.NewStyle().MarginLeft(3).Render(t.String()), "\n\n")
87}
88
89type moonPhaseInput struct {
90 Date string `json:"date,omitempty" description:"Optional date in YYYY-MM-DD; if omitted, use today"`
91}
92
93// moonPhaseTool queries wttr.in for the moon phase on a given date. If no
94// date is provided, it uses today's date.
95//
96// The date format should be in YYYY-MM-DD format.
97func moonPhaseTool(ctx context.Context, input moonPhaseInput, _ fantasy.ToolCall) (fantasy.ToolResponse, error) {
98 url := "https://wttr.in/moon?T&q"
99
100 // Validate date format if provided, and update the URL accordingly.
101 if input.Date != "" {
102 if _, timeErr := time.Parse("2006-01-02", input.Date); timeErr != nil {
103 return fantasy.NewTextErrorResponse("invalid date format; use YYYY-MM-DD"), nil
104 }
105 url = "https://wttr.in/moon@" + input.Date + "?T&q"
106 }
107
108 // Prepare an HTTP request.
109 req, reqErr := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
110 if reqErr != nil {
111 return fantasy.NewTextErrorResponse("failed to build request: " + reqErr.Error()), nil
112 }
113
114 // wttr.in changes rendering based on the user agent, so we
115 // need to set a user agent to force plain text.
116 req.Header.Set("User-Agent", "curl/8.0")
117
118 // Perform the HTTP request.
119 resp, reqErr := http.DefaultClient.Do(req)
120 if reqErr != nil {
121 return fantasy.NewTextErrorResponse("request failed: " + reqErr.Error()), nil
122 }
123
124 // Read the response body.
125 defer resp.Body.Close()
126 b, readErr := io.ReadAll(resp.Body)
127 if readErr != nil {
128 return fantasy.NewTextErrorResponse("read failed: " + readErr.Error()), nil
129 }
130
131 // Did it work?
132 if resp.StatusCode >= 400 {
133 return fantasy.NewTextErrorResponse("wttr.in error: " + resp.Status + "\n" + string(b)), nil
134 }
135
136 // It worked!
137 return fantasy.NewTextResponse(string(b)), nil
138}
139
140// Just a Lip Gloss text formatter.
141var formatText func(...string) string
142
143// Setup the text formatter based on terminal width so we can wrap lines
144// nicely.
145func init() {
146 w, _, err := term.GetSize(os.Stdout.Fd())
147 if err != nil {
148 log.Fatalf("failed to get terminal size: %v", err)
149 }
150 formatText = lipgloss.NewStyle().Padding(0, 3, 1, 3).Width(w).Render
151}