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