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}