main.go

  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}