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