main.go

  1package main
  2
  3import (
  4	"context"
  5	"fmt"
  6	"os"
  7	"strings"
  8
  9	"github.com/charmbracelet/fantasy/ai"
 10	"github.com/charmbracelet/fantasy/anthropic"
 11)
 12
 13func main() {
 14	// Check for API key
 15	apiKey := os.Getenv("ANTHROPIC_API_KEY")
 16	if apiKey == "" {
 17		fmt.Println("❌ Please set ANTHROPIC_API_KEY environment variable")
 18		fmt.Println("   export ANTHROPIC_API_KEY=your_api_key_here")
 19		os.Exit(1)
 20	}
 21
 22	fmt.Println("🚀 Streaming Agent Example")
 23	fmt.Println("==========================")
 24	fmt.Println()
 25
 26	// Create OpenAI provider and model
 27	provider := anthropic.New(anthropic.WithAPIKey(os.Getenv("ANTHROPIC_API_KEY")))
 28	model, err := provider.LanguageModel("claude-sonnet-4-20250514")
 29	if err != nil {
 30		fmt.Println(err)
 31		return
 32	}
 33
 34	// Define input types for type-safe tools
 35	type WeatherInput struct {
 36		Location string `json:"location" description:"The city and country, e.g. 'London, UK'"`
 37		Unit     string `json:"unit,omitempty" enum:"celsius,fahrenheit" description:"Temperature unit (celsius or fahrenheit)"`
 38	}
 39
 40	type CalculatorInput struct {
 41		Expression string `json:"expression" description:"Mathematical expression to evaluate (e.g., '2 + 2', '10 * 5')"`
 42	}
 43
 44	// Create weather tool using the new type-safe API
 45	weatherTool := ai.NewAgentTool(
 46		"get_weather",
 47		"Get the current weather for a specific location",
 48		func(ctx context.Context, input WeatherInput, _ ai.ToolCall) (ai.ToolResponse, error) {
 49			// Simulate weather lookup with some fake data
 50			location := input.Location
 51			if location == "" {
 52				location = "Unknown"
 53			}
 54
 55			// Default to celsius if not specified
 56			unit := input.Unit
 57			if unit == "" {
 58				unit = "celsius"
 59			}
 60
 61			// Simulate different temperatures for different cities
 62			var temp string
 63			if strings.Contains(strings.ToLower(location), "pristina") {
 64				temp = "15°C"
 65				if unit == "fahrenheit" {
 66					temp = "59°F"
 67				}
 68			} else if strings.Contains(strings.ToLower(location), "london") {
 69				temp = "12°C"
 70				if unit == "fahrenheit" {
 71					temp = "54°F"
 72				}
 73			} else {
 74				temp = "22°C"
 75				if unit == "fahrenheit" {
 76					temp = "72°F"
 77				}
 78			}
 79
 80			weather := fmt.Sprintf("The current weather in %s is %s with partly cloudy skies and light winds.", location, temp)
 81			return ai.NewTextResponse(weather), nil
 82		},
 83	)
 84
 85	// Create calculator tool using the new type-safe API
 86	calculatorTool := ai.NewAgentTool(
 87		"calculate",
 88		"Perform basic mathematical calculations",
 89		func(ctx context.Context, input CalculatorInput, _ ai.ToolCall) (ai.ToolResponse, error) {
 90			// Simple calculator simulation
 91			expr := strings.TrimSpace(input.Expression)
 92			if strings.Contains(expr, "2 + 2") || strings.Contains(expr, "2+2") {
 93				return ai.NewTextResponse("2 + 2 = 4"), nil
 94			} else if strings.Contains(expr, "10 * 5") || strings.Contains(expr, "10*5") {
 95				return ai.NewTextResponse("10 * 5 = 50"), nil
 96			} else if strings.Contains(expr, "15 + 27") || strings.Contains(expr, "15+27") {
 97				return ai.NewTextResponse("15 + 27 = 42"), nil
 98			}
 99			return ai.NewTextResponse("I can calculate simple expressions like '2 + 2', '10 * 5', or '15 + 27'"), nil
100		},
101	)
102
103	// Create agent with tools
104	agent := ai.NewAgent(
105		model,
106		ai.WithSystemPrompt("You are a helpful assistant that can check weather and do calculations. Be concise and friendly."),
107		ai.WithTools(weatherTool, calculatorTool),
108	)
109
110	ctx := context.Background()
111
112	// Demonstrate streaming with comprehensive callbacks
113	fmt.Println("💬 Asking: \"What's the weather in Pristina and what's 2 + 2?\"")
114	fmt.Println()
115
116	// Track streaming events
117	var stepCount int
118	var textBuffer strings.Builder
119	var reasoningBuffer strings.Builder
120
121	// Create streaming call with all callbacks
122	streamCall := ai.AgentStreamCall{
123		Prompt: "What's the weather in Pristina and what's 2 + 2?",
124
125		// Agent-level callbacks
126		OnAgentStart: func() {
127			fmt.Println("🎬 Agent started")
128		},
129		OnAgentFinish: func(result *ai.AgentResult) error {
130			fmt.Printf("🏁 Agent finished with %d steps, total tokens: %d\n", len(result.Steps), result.TotalUsage.TotalTokens)
131			return nil
132		},
133		OnStepStart: func(stepNumber int) error {
134			stepCount++
135			fmt.Printf("📝 Step %d started\n", stepNumber+1)
136			return nil
137		},
138		OnStepFinish: func(stepResult ai.StepResult) error {
139			fmt.Printf("✅ Step completed (reason: %s, tokens: %d)\n", stepResult.FinishReason, stepResult.Usage.TotalTokens)
140			return nil
141		},
142		OnFinish: func(result *ai.AgentResult) {
143			fmt.Printf("🎯 Final result ready with %d steps\n", len(result.Steps))
144		},
145		OnError: func(err error) {
146			fmt.Printf("❌ Error: %v\n", err)
147		},
148
149		// Stream part callbacks
150		OnWarnings: func(warnings []ai.CallWarning) error {
151			for _, warning := range warnings {
152				fmt.Printf("⚠️  Warning: %s\n", warning.Message)
153			}
154			return nil
155		},
156		OnTextStart: func(id string) error {
157			fmt.Print("💭 Assistant: ")
158			return nil
159		},
160		OnTextDelta: func(id, text string) error {
161			fmt.Print(text)
162			textBuffer.WriteString(text)
163			return nil
164		},
165		OnTextEnd: func(id string) error {
166			fmt.Println()
167			return nil
168		},
169		OnReasoningStart: func(id string, _ ai.ReasoningContent) error {
170			fmt.Print("🤔 Thinking: ")
171			return nil
172		},
173		OnReasoningDelta: func(id, text string) error {
174			reasoningBuffer.WriteString(text)
175			return nil
176		},
177		OnReasoningEnd: func(id string, content ai.ReasoningContent) error {
178			if reasoningBuffer.Len() > 0 {
179				fmt.Printf("%s\n", reasoningBuffer.String())
180				reasoningBuffer.Reset()
181			}
182			return nil
183		},
184		OnToolInputStart: func(id, toolName string) error {
185			fmt.Printf("🔧 Calling tool: %s\n", toolName)
186			return nil
187		},
188		OnToolInputDelta: func(id, delta string) error {
189			// Could show tool input being built, but it's often noisy
190			return nil
191		},
192		OnToolInputEnd: func(id string) error {
193			// Tool input complete
194			return nil
195		},
196		OnToolCall: func(toolCall ai.ToolCallContent) error {
197			fmt.Printf("🛠️  Tool call: %s\n", toolCall.ToolName)
198			fmt.Printf("   Input: %s\n", toolCall.Input)
199			return nil
200		},
201		OnToolResult: func(result ai.ToolResultContent) error {
202			fmt.Printf("🎯 Tool result from %s:\n", result.ToolName)
203			switch output := result.Result.(type) {
204			case ai.ToolResultOutputContentText:
205				fmt.Printf("   %s\n", output.Text)
206			case ai.ToolResultOutputContentError:
207				fmt.Printf("   Error: %s\n", output.Error.Error())
208			}
209			return nil
210		},
211		OnSource: func(source ai.SourceContent) error {
212			fmt.Printf("📚 Source: %s (%s)\n", source.Title, source.URL)
213			return nil
214		},
215		OnStreamFinish: func(usage ai.Usage, finishReason ai.FinishReason, providerMetadata ai.ProviderMetadata) error {
216			fmt.Printf("📊 Stream finished (reason: %s, tokens: %d)\n", finishReason, usage.TotalTokens)
217			return nil
218		},
219	}
220
221	// Execute streaming agent
222	result, err := agent.Stream(ctx, streamCall)
223	if err != nil {
224		fmt.Printf("❌ Agent failed: %v\n", err)
225		os.Exit(1)
226	}
227
228	// Display final results
229	fmt.Println()
230	fmt.Println("📋 Final Summary")
231	fmt.Println("================")
232	fmt.Printf("Steps executed: %d\n", len(result.Steps))
233	fmt.Printf("Total tokens used: %d (input: %d, output: %d)\n",
234		result.TotalUsage.TotalTokens,
235		result.TotalUsage.InputTokens,
236		result.TotalUsage.OutputTokens)
237
238	if result.TotalUsage.ReasoningTokens > 0 {
239		fmt.Printf("Reasoning tokens: %d\n", result.TotalUsage.ReasoningTokens)
240	}
241
242	fmt.Printf("Final response: %s\n", result.Response.Content.Text())
243
244	// Show step details
245	fmt.Println()
246	fmt.Println("🔍 Step Details")
247	fmt.Println("===============")
248	for i, step := range result.Steps {
249		fmt.Printf("Step %d:\n", i+1)
250		fmt.Printf("  Finish reason: %s\n", step.FinishReason)
251		fmt.Printf("  Content types: ")
252
253		var contentTypes []string
254		for _, content := range step.Content {
255			contentTypes = append(contentTypes, string(content.GetType()))
256		}
257		fmt.Printf("%s\n", strings.Join(contentTypes, ", "))
258
259		// Show tool calls and results
260		toolCalls := step.Content.ToolCalls()
261		if len(toolCalls) > 0 {
262			fmt.Printf("  Tool calls: ")
263			var toolNames []string
264			for _, tc := range toolCalls {
265				toolNames = append(toolNames, tc.ToolName)
266			}
267			fmt.Printf("%s\n", strings.Join(toolNames, ", "))
268		}
269
270		toolResults := step.Content.ToolResults()
271		if len(toolResults) > 0 {
272			fmt.Printf("  Tool results: %d\n", len(toolResults))
273		}
274
275		fmt.Printf("  Tokens: %d\n", step.Usage.TotalTokens)
276		fmt.Println()
277	}
278
279	fmt.Println("✨ Example completed successfully!")
280}