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