diff --git a/IMPLEMENTATION_GUIDE.md b/IMPLEMENTATION_GUIDE.md new file mode 100644 index 0000000..bb18d21 --- /dev/null +++ b/IMPLEMENTATION_GUIDE.md @@ -0,0 +1,269 @@ +# Multi-Step Function Calling Agent - Detailed Implementation Guide + +## Overview of Changes + +I've implemented a comprehensive multi-step function calling agent that enables the LLM to reason through complex problems by chaining multiple function calls together. Here's what was added: + +## ๐Ÿ—๏ธ Key Components Added + +### 1. `agent.go` - Core Multi-Step Agent +```go +// AgentContext maintains conversation history and function results +type AgentContext struct { + Messages []AgentMessage `json:"messages"` + Results map[string]interface{} `json:"results"` // stored function results + MaxSteps int `json:"max_steps"` // prevents infinite loops +} + +// Agent orchestrates multi-step function calling +type Agent struct { + gopilot *Gopilot + context *AgentContext +} +``` + +### 2. `examples.go` - Demo Functions +```go +// WeatherResponse - Gets weather data for cities +type WeatherResponse struct { + City string `json:"city"` + Temperature int `json:"temperature"` + Condition string `json:"condition"` + Humidity int `json:"humidity"` + WindSpeed int `json:"wind_speed"` +} + +// ClothingResponse - Suggests clothes based on weather +type ClothingResponse struct { + Recommendation string `json:"recommendation"` + Items []string `json:"items"` + Reasoning string `json:"reasoning"` +} +``` + +### 3. `mock.go` - Testing Infrastructure +```go +// MockLLMClient for deterministic testing +type MockLLMClient struct { + responses []string + callCount int + systemPrompt string +} +``` + +## ๐Ÿš€ Usage Examples + +### Single-Step vs Multi-Step Comparison + +**Before (Single-Step):** +```go +// Old way - direct function execution +gp := gopilot.NewGopilot(client) +gp.FunctionRegister(weatherFn) +result, err := gp.FunctionExecute("get-weather", map[string]interface{}{ + "city": "Istanbul", +}) +``` + +**After (Multi-Step Agent):** +```go +// New way - intelligent multi-step reasoning +agent := gopilot.NewAgent(gp, 5) // max 5 steps +result, err := agent.ExecuteMultiStep("Based on today's weather, what should I wear in Istanbul?") + +// Agent automatically: +// Step 1: Calls get-weather("Istanbul") +// Step 2: Uses weather data to call suggest-clothes(weather_data) +// Step 3: Provides final comprehensive answer +``` + +## ๐Ÿ”„ Multi-Step Execution Flow + +Here's a step-by-step simulation of how the agent works: + +### Step-by-Step Process Simulation + +```go +package main + +import ( + "fmt" + "github.com/SadikSunbul/gopilot" +) + +func demonstrateMultiStep() { + // 1. User asks complex question + userInput := "Based on today's weather, what should I wear in Istanbul?" + + // 2. Agent breaks it down into steps + fmt.Println("๐Ÿง  Agent reasoning:") + fmt.Println(" 'This requires weather data first, then clothing suggestions'") + + // 3. Step 1: Get weather + fmt.Println("โš™๏ธ Step 1: Calling get-weather") + weatherResult := map[string]interface{}{ + "city": "Istanbul", + "temperature": 12, + "condition": "cloudy", + "humidity": 75, + "wind_speed": 15, + } + fmt.Printf(" ๐Ÿ“ค Weather result: %+v\n", weatherResult) + + // 4. Step 2: Use weather data for clothing suggestions + fmt.Println("โš™๏ธ Step 2: Calling suggest-clothes with weather data") + clothingResult := map[string]interface{}{ + "recommendation": "Dress in layers", + "items": []string{"light jacket", "long pants", "comfortable shoes"}, + "reasoning": "Temperature is 12ยฐC, which is cool", + } + fmt.Printf(" ๐Ÿ“ค Clothing result: %+v\n", clothingResult) + + // 5. Step 3: Generate final answer + fmt.Println("๐ŸŽฏ Final answer generated combining all data") + finalAnswer := "Based on today's weather in Istanbul (12ยฐC and cloudy), I recommend dressing in layers with a light jacket, long pants, and comfortable shoes." + fmt.Printf(" ๐Ÿค– Answer: %s\n", finalAnswer) +} +``` + +## ๐Ÿ“‹ Complete Working Example + +```go +package main + +import ( + "context" + "fmt" + "log" + + "github.com/SadikSunbul/gopilot" + "github.com/SadikSunbul/gopilot/clients" +) + +func main() { + // Initialize with real Gemini API or mock for testing + var client gopilot.LLMProvider + + apiKey := os.Getenv("GEMINI_API_KEY") + if apiKey != "" { + // Use real Gemini + geminiClient, err := clients.NewGeminiClient(context.Background(), apiKey, "gemini-2.0-flash") + if err != nil { + log.Fatal(err) + } + client = geminiClient + } else { + // Use mock for testing + mockResponses := []string{ + `{"action": "function_call", "agent": "get-weather", "parameters": {"city": "Istanbul"}}`, + `{"action": "function_call", "agent": "suggest-clothes", "parameters": {"weather_data": {"city": "Istanbul", "temperature": 12, "condition": "cloudy", "humidity": 75}}}`, + `{"action": "final_answer", "answer": "Based on 12ยฐC cloudy weather, wear layers with light jacket..."}`, + } + client = gopilot.NewMockLLMClient(mockResponses) + } + + // Create GoPilot instance + gp, err := gopilot.NewGopilot(client) + if err != nil { + log.Fatal(err) + } + + // Register functions + weatherFn := gopilot.CreateWeatherFunction() + clothingFn := gopilot.CreateClothingFunction() + + gp.FunctionRegister(weatherFn) + gp.FunctionRegister(clothingFn) + + // Create multi-step agent + agent := gopilot.NewAgent(gp, 5) // max 5 steps + + // Execute complex reasoning + result, err := agent.ExecuteMultiStep("Based on today's weather, what should I wear in Istanbul?") + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Final Answer: %s\n", result) + + // Inspect the reasoning process + context := agent.GetContext() + fmt.Printf("\nSteps taken: %d\n", context.GetStepCount()) + fmt.Printf("Functions called: %d\n", len(context.Results)) + + // Show each function result + for funcName, result := range context.Results { + fmt.Printf("- %s: %+v\n", funcName, result) + } +} +``` + +## ๐Ÿงช Testing Examples + +```go +func TestMultiStepAgent(t *testing.T) { + // Create deterministic mock responses + mockLLM := gopilot.NewMockLLMClient([]string{ + `{"action": "function_call", "agent": "get-weather", "parameters": {"city": "Istanbul"}}`, + `{"action": "function_call", "agent": "suggest-clothes", "parameters": {"weather_data": {...}}}`, + `{"action": "final_answer", "answer": "Comprehensive clothing recommendation"}`, + }) + + gp, _ := gopilot.NewGopilot(mockLLM) + gp.FunctionRegister(gopilot.CreateWeatherFunction()) + gp.FunctionRegister(gopilot.CreateClothingFunction()) + + agent := gopilot.NewAgent(gp, 5) + + result, err := agent.ExecuteMultiStep("What should I wear in Istanbul?") + + assert.NoError(t, err) + assert.Contains(t, result.(string), "recommendation") + assert.Equal(t, 3, mockLLM.GetCallCount()) // 3 LLM calls made +} +``` + +## ๐Ÿ›ก๏ธ Safety Features + +### Maximum Steps Prevention +```go +// Prevents infinite loops +agent := gopilot.NewAgent(gp, 3) // Will stop after 3 function calls + +// If agent tries to exceed limit: +result, err := agent.ExecuteMultiStep("complex request") +// Returns error: "maximum steps (3) reached without reaching final answer" +``` + +### Context Management +```go +// Access conversation history and results +context := agent.GetContext() +fmt.Printf("Messages: %d\n", len(context.Messages)) +fmt.Printf("Function results: %d\n", len(context.Results)) +fmt.Printf("Steps used: %d/%d\n", context.GetStepCount(), context.MaxSteps) + +// Reset for new conversation +agent.Reset() +``` + +## ๐ŸŽฏ Key Benefits + +1. **Intelligent Chaining**: Functions automatically use results from previous calls +2. **Natural Language**: Complex requests in plain English +3. **Type Safety**: All function parameters are strongly typed +4. **Testing Support**: Mock client for deterministic testing +5. **Safety Controls**: Maximum step limits prevent infinite loops +6. **Backward Compatible**: Existing single-step usage still works + +## ๐Ÿ” Before vs After Comparison + +| Feature | Before (Single-Step) | After (Multi-Step Agent) | +|---------|---------------------|--------------------------| +| **Complexity** | One function per request | Multiple chained functions | +| **Context** | No memory between calls | Maintains conversation history | +| **Intelligence** | Manual parameter mapping | Automatic reasoning and chaining | +| **Use Cases** | Simple, direct tasks | Complex multi-step scenarios | +| **Safety** | Basic error handling | Step limits, comprehensive validation | + +This implementation provides a solid foundation for complex AI-powered automation while maintaining the simplicity and reliability of the existing single-step functionality. \ No newline at end of file diff --git a/README.md b/README.md index f5cc33a..ebc15a8 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ go get github.com/SadikSunbul/gopilot ## Quick Start -Here's a simple example that demonstrates how to use GoPilot: +Here's a simple example that demonstrates how to use GoPilot for **single-step** function calling: ```go package main @@ -130,6 +130,74 @@ func main() { ## Advanced Usage +## Advanced Usage + +### Multi-Step Function Calling Agent + +GoPilot now supports **multi-step reasoning** where the LLM can chain multiple function calls together to solve complex problems. This is perfect for scenarios where you need to gather information from one function and use it in another. + +#### Example: Weather + Clothing Recommendation + +```go +package main + +import ( + "context" + "fmt" + "log" + + "github.com/SadikSunbul/gopilot" + "github.com/SadikSunbul/gopilot/clients" +) + +func main() { + // Initialize Gemini client + client, err := clients.NewGeminiClient(context.Background(), "your-api-key", "gemini-2.0-flash") + if err != nil { + log.Fatal(err) + } + defer client.Close() + + // Create GoPilot instance + gp, err := gopilot.NewGopilot(client) + if err != nil { + log.Fatal(err) + } + + // Register multi-step functions + weatherFn := gopilot.CreateWeatherFunction() + clothingFn := gopilot.CreateClothingFunction() + + gp.FunctionRegister(weatherFn) + gp.FunctionRegister(clothingFn) + + // Create multi-step agent + agent := gopilot.NewAgent(gp, 5) // max 5 steps + + // Multi-step execution + result, err := agent.ExecuteMultiStep("Based on today's weather, what should I wear in Istanbul?") + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Result: %v\n", result) +} +``` + +#### How Multi-Step Works + +1. **User Query**: "Based on today's weather, what should I wear in Istanbul?" +2. **Step 1**: Agent calls `get-weather` function with `{"city": "Istanbul"}` +3. **Step 2**: Agent calls `suggest-clothes` function with weather data from Step 1 +4. **Final Answer**: Agent provides comprehensive clothing recommendation + +#### Agent Features + +- **Context Management**: Maintains conversation history and function results +- **Result Chaining**: Results from previous functions are available to subsequent calls +- **Safety Limits**: Maximum step count prevents infinite loops +- **Dynamic Reasoning**: LLM decides when to stop and provide final answer + ### Complex Parameter Types GoPilot supports nested and complex parameter types: diff --git a/agent.go b/agent.go new file mode 100644 index 0000000..99f9651 --- /dev/null +++ b/agent.go @@ -0,0 +1,277 @@ +package gopilot + +import ( + "encoding/json" + "fmt" + "log" + "strings" + + "github.com/SadikSunbul/gopilot/clients" +) + +// AgentMessage represents a single message in the conversation +type AgentMessage struct { + Role string `json:"role"` // "user", "assistant", "function" + Content string `json:"content"` // the message content + Result interface{} `json:"result,omitempty"` // function execution result +} + +// AgentContext maintains conversation history and function results +type AgentContext struct { + Messages []AgentMessage `json:"messages"` + Results map[string]interface{} `json:"results"` // named function results + MaxSteps int `json:"max_steps"` // maximum number of steps to prevent infinite loops +} + +// NewAgentContext creates a new agent context +func NewAgentContext(maxSteps int) *AgentContext { + if maxSteps <= 0 { + maxSteps = 10 // default maximum steps + } + return &AgentContext{ + Messages: make([]AgentMessage, 0), + Results: make(map[string]interface{}, 0), + MaxSteps: maxSteps, + } +} + +// AddUserMessage adds a user message to the context +func (ctx *AgentContext) AddUserMessage(content string) { + ctx.Messages = append(ctx.Messages, AgentMessage{ + Role: "user", + Content: content, + }) +} + +// AddAssistantMessage adds an assistant message to the context +func (ctx *AgentContext) AddAssistantMessage(content string) { + ctx.Messages = append(ctx.Messages, AgentMessage{ + Role: "assistant", + Content: content, + }) +} + +// AddFunctionResult adds a function execution result to the context +func (ctx *AgentContext) AddFunctionResult(functionName string, result interface{}) { + ctx.Results[functionName] = result + ctx.Messages = append(ctx.Messages, AgentMessage{ + Role: "function", + Content: fmt.Sprintf("Function %s executed", functionName), + Result: result, + }) +} + +// GetStepCount returns the current number of steps (function calls) +func (ctx *AgentContext) GetStepCount() int { + count := 0 + for _, msg := range ctx.Messages { + if msg.Role == "function" { + count++ + } + } + return count +} + +// Agent represents a multi-step function calling agent +type Agent struct { + gopilot *Gopilot + context *AgentContext +} + +// NewAgent creates a new multi-step agent +func NewAgent(gopilot *Gopilot, maxSteps int) *Agent { + agent := &Agent{ + gopilot: gopilot, + context: NewAgentContext(maxSteps), + } + + // Set the multi-step system prompt + agent.setAgentSystemPrompt() + + return agent +} + +// setAgentSystemPrompt configures the system prompt for multi-step reasoning +func (a *Agent) setAgentSystemPrompt() { + agentList := a.gopilot.registry.List() + + // Build function descriptions + var functionDescriptions strings.Builder + for _, fn := range agentList { + functionDescriptions.WriteString(fmt.Sprintf("Function: %s\n", fn.GetName())) + functionDescriptions.WriteString(fmt.Sprintf("Description: %s\n", fn.GetDescription())) + functionDescriptions.WriteString("Parameters:\n") + + for name, param := range fn.GetParameters() { + functionDescriptions.WriteString(formatParameterSchema(name, param, 1)) + } + functionDescriptions.WriteString("\n") + } + + // Default rules for multi-step reasoning + rules := ` +1. Analyze the user's request to determine if multiple functions are needed +2. Execute functions sequentially, using results from previous calls +3. Only provide final_answer when you have sufficient information +4. Use specific data from previous function results in subsequent calls +5. Provide clear reasoning for each action +` + + // Build final prompt + prompt := fmt.Sprintf(agentSystemPrompt, rules, functionDescriptions.String()) + + a.gopilot.llm.SetSystemPrompt(prompt) +} + +// AgentResponse represents the enhanced response from the agent +type AgentResponse struct { + Action string `json:"action"` // "function_call" or "final_answer" + Agent string `json:"agent,omitempty"` // function name (if action is function_call) + Parameters map[string]interface{} `json:"parameters,omitempty"` // function parameters (if action is function_call) + Answer string `json:"answer,omitempty"` // final answer (if action is final_answer) + Reasoning string `json:"reasoning,omitempty"` // explanation of the action +} + +// ExecuteMultiStep executes a multi-step reasoning chain +func (a *Agent) ExecuteMultiStep(userInput string) (interface{}, error) { + // Add initial user message + a.context.AddUserMessage(userInput) + + for a.context.GetStepCount() < a.context.MaxSteps { + // Generate next action from LLM + prompt := a.buildContextualPrompt() + response, err := a.gopilot.llm.Generate(prompt) + if err != nil { + return nil, fmt.Errorf("failed to generate response: %w", err) + } + + // Parse the enhanced response + agentResp, err := a.parseAgentResponse(response) + if err != nil { + return nil, fmt.Errorf("failed to parse agent response: %w", err) + } + + // Log the reasoning + if agentResp.Reasoning != "" { + log.Printf("Agent reasoning: %s", agentResp.Reasoning) + } + + // Handle the action + switch agentResp.Action { + case "final_answer": + a.context.AddAssistantMessage(agentResp.Answer) + return agentResp.Answer, nil + + case "function_call": + if agentResp.Agent == "" { + return nil, fmt.Errorf("function name is required for function_call action") + } + + // Execute the function + result, err := a.gopilot.FunctionExecute(agentResp.Agent, agentResp.Parameters) + if err != nil { + return nil, fmt.Errorf("function execution failed: %w", err) + } + + // Add result to context + a.context.AddFunctionResult(agentResp.Agent, result) + + default: + return nil, fmt.Errorf("unknown action: %s", agentResp.Action) + } + } + + return nil, fmt.Errorf("maximum steps (%d) reached without reaching final answer", a.context.MaxSteps) +} + +// buildContextualPrompt builds a prompt that includes conversation history and previous results +func (a *Agent) buildContextualPrompt() string { + prompt := "Based on the conversation history and previous function results, determine the next action.\n\n" + + // Add conversation history + prompt += "Conversation History:\n" + for _, msg := range a.context.Messages { + switch msg.Role { + case "user": + prompt += fmt.Sprintf("User: %s\n", msg.Content) + case "assistant": + prompt += fmt.Sprintf("Assistant: %s\n", msg.Content) + case "function": + prompt += fmt.Sprintf("Function Result: %s -> %s\n", msg.Content, a.formatResult(msg.Result)) + } + } + + // Add available results + if len(a.context.Results) > 0 { + prompt += "\nAvailable Results:\n" + for name, result := range a.context.Results { + prompt += fmt.Sprintf("- %s: %s\n", name, a.formatResult(result)) + } + } + + prompt += "\nRespond with your next action in the specified JSON format." + return prompt +} + +// formatResult formats a function result for display in prompts +func (a *Agent) formatResult(result interface{}) string { + if result == nil { + return "null" + } + + data, err := json.Marshal(result) + if err != nil { + return fmt.Sprintf("%v", result) + } + return string(data) +} + +// parseAgentResponse parses the LLM response into an AgentResponse +func (a *Agent) parseAgentResponse(response *clients.LLMResponse) (*AgentResponse, error) { + // Handle special case for final_answer + if response.Agent == "final_answer" { + answer, _ := response.Parameters["answer"].(string) + return &AgentResponse{ + Action: "final_answer", + Answer: answer, + }, nil + } + + // Check if this is a regular function call (old format) + if response.Agent != "" { + // Convert old format to new format + agentResp := &AgentResponse{ + Action: "function_call", + Agent: response.Agent, + Parameters: response.Parameters, + } + return agentResp, nil + } + + // For new format responses, the response should contain action directly + // This would happen when the LLM generates the new JSON format + var agentResp AgentResponse + + // Try to unmarshal the entire response as JSON to get the action field + responseBytes, err := json.Marshal(response) + if err != nil { + return nil, err + } + + // Try to parse as AgentResponse + if err := json.Unmarshal(responseBytes, &agentResp); err != nil { + return nil, fmt.Errorf("failed to parse agent response: %w", err) + } + + return &agentResp, nil +} + +// Reset resets the agent context for a new conversation +func (a *Agent) Reset() { + a.context = NewAgentContext(a.context.MaxSteps) +} + +// GetContext returns the current agent context (for debugging/inspection) +func (a *Agent) GetContext() *AgentContext { + return a.context +} \ No newline at end of file diff --git a/agent_test.go b/agent_test.go new file mode 100644 index 0000000..f3309ce --- /dev/null +++ b/agent_test.go @@ -0,0 +1,146 @@ +package gopilot + +import ( + "strings" + "testing" +) + +func TestAgentSingleStep(t *testing.T) { + // Create mock LLM with single response + mockLLM := NewMockLLMClient([]string{ + `{"action": "function_call", "agent": "get-weather", "parameters": {"city": "Istanbul"}, "reasoning": "Need to get weather for Istanbul"}`, + `{"action": "final_answer", "answer": "The weather in Istanbul is 12ยฐC and cloudy with 75% humidity and 15 km/h wind.", "reasoning": "I have the weather information requested"}`, + }) + + // Create gopilot instance + gopilot, err := NewGopilot(mockLLM) + if err != nil { + t.Fatalf("Failed to create gopilot: %v", err) + } + + // Register weather function + weatherFn := CreateWeatherFunction() + if err := gopilot.FunctionRegister(weatherFn); err != nil { + t.Fatalf("Failed to register weather function: %v", err) + } + + // Create agent + agent := NewAgent(gopilot, 5) + + // Test single step execution + result, err := agent.ExecuteMultiStep("What's the weather in Istanbul?") + if err != nil { + t.Fatalf("Failed to execute multi-step: %v", err) + } + + // Verify result is a string (final answer) + resultStr, ok := result.(string) + if !ok { + t.Fatalf("Expected string result, got %T", result) + } + + if resultStr == "" { + t.Fatalf("Expected non-empty result") + } + + // Verify that the weather function was called + if mockLLM.GetCallCount() != 2 { + t.Fatalf("Expected 2 LLM calls, got %d", mockLLM.GetCallCount()) + } +} + +func TestAgentMultiStep(t *testing.T) { + // Create mock LLM with multi-step responses + mockLLM := NewMockLLMClient([]string{ + `{"action": "function_call", "agent": "get-weather", "parameters": {"city": "Istanbul"}, "reasoning": "First need to get weather for Istanbul"}`, + `{"action": "function_call", "agent": "suggest-clothes", "parameters": {"weather_data": {"city": "Istanbul", "temperature": 12, "condition": "cloudy", "humidity": 75, "wind_speed": 15}}, "reasoning": "Now I can suggest clothes based on the weather"}`, + `{"action": "final_answer", "answer": "Based on today's weather in Istanbul (12ยฐC and cloudy), I recommend dressing in layers with a light jacket or sweater, long pants, and comfortable shoes. Since it's cloudy, you might also want to bring a light jacket in case it gets cooler.", "reasoning": "I have both weather and clothing suggestions"}`, + }) + + // Create gopilot instance + gopilot, err := NewGopilot(mockLLM) + if err != nil { + t.Fatalf("Failed to create gopilot: %v", err) + } + + // Register functions + weatherFn := CreateWeatherFunction() + clothingFn := CreateClothingFunction() + + if err := gopilot.FunctionRegister(weatherFn); err != nil { + t.Fatalf("Failed to register weather function: %v", err) + } + + if err := gopilot.FunctionRegister(clothingFn); err != nil { + t.Fatalf("Failed to register clothing function: %v", err) + } + + // Create agent + agent := NewAgent(gopilot, 5) + + // Test multi-step execution + result, err := agent.ExecuteMultiStep("Based on today's weather, what should I wear in Istanbul?") + if err != nil { + t.Fatalf("Failed to execute multi-step: %v", err) + } + + // Verify result is a string (final answer) + resultStr, ok := result.(string) + if !ok { + t.Fatalf("Expected string result, got %T", result) + } + + if resultStr == "" { + t.Fatalf("Expected non-empty result") + } + + // Verify that both functions were called (3 LLM calls total) + if mockLLM.GetCallCount() != 3 { + t.Fatalf("Expected 3 LLM calls, got %d", mockLLM.GetCallCount()) + } + + // Verify context has the expected number of messages + context := agent.GetContext() + if len(context.Messages) < 3 { // user message + 2 function results + t.Fatalf("Expected at least 3 messages in context, got %d", len(context.Messages)) + } + + // Verify that results were stored + if len(context.Results) < 2 { + t.Fatalf("Expected at least 2 function results, got %d", len(context.Results)) + } +} + +func TestAgentMaxSteps(t *testing.T) { + // Create mock LLM that always wants to call a function (infinite loop) + mockLLM := NewMockLLMClient([]string{ + `{"action": "function_call", "agent": "get-weather", "parameters": {"city": "Istanbul"}, "reasoning": "Getting weather"}`, + `{"action": "function_call", "agent": "get-weather", "parameters": {"city": "Istanbul"}, "reasoning": "Getting weather again"}`, + `{"action": "function_call", "agent": "get-weather", "parameters": {"city": "Istanbul"}, "reasoning": "Getting weather yet again"}`, + }) + + // Create gopilot instance + gopilot, err := NewGopilot(mockLLM) + if err != nil { + t.Fatalf("Failed to create gopilot: %v", err) + } + + // Register weather function + weatherFn := CreateWeatherFunction() + if err := gopilot.FunctionRegister(weatherFn); err != nil { + t.Fatalf("Failed to register weather function: %v", err) + } + + // Create agent with only 2 max steps + agent := NewAgent(gopilot, 2) + + // Test that max steps limit is enforced + result, err := agent.ExecuteMultiStep("What's the weather in Istanbul?") + if err == nil { + t.Fatalf("Expected error due to max steps, but got result: %v", result) + } + + if !strings.Contains(err.Error(), "maximum steps") { + t.Fatalf("Expected max steps error, got: %v", err) + } +} \ No newline at end of file diff --git a/examples.go b/examples.go new file mode 100644 index 0000000..99a0bb8 --- /dev/null +++ b/examples.go @@ -0,0 +1,166 @@ +package gopilot + +import ( + "fmt" + "strings" + + "github.com/SadikSunbul/gopilot/pkg/generator" +) + +// WeatherParams represents parameters for getting weather information +type WeatherParams struct { + City string `json:"city" description:"The name of the city to get weather information for" required:"true"` +} + +// WeatherResponse represents weather information +type WeatherResponse struct { + City string `json:"city"` + Temperature int `json:"temperature"` + Condition string `json:"condition"` + Humidity int `json:"humidity"` + WindSpeed int `json:"wind_speed"` +} + +// GetWeather simulates getting weather information for a city +func GetWeather(params WeatherParams) (WeatherResponse, error) { + if params.City == "" { + return WeatherResponse{}, fmt.Errorf("city cannot be empty") + } + + // Simulate weather data based on city + weatherData := map[string]WeatherResponse{ + "istanbul": { + City: "Istanbul", + Temperature: 12, + Condition: "cloudy", + Humidity: 75, + WindSpeed: 15, + }, + "london": { + City: "London", + Temperature: 8, + Condition: "rainy", + Humidity: 85, + WindSpeed: 20, + }, + "tokyo": { + City: "Tokyo", + Temperature: 18, + Condition: "sunny", + Humidity: 60, + WindSpeed: 10, + }, + "new york": { + City: "New York", + Temperature: 5, + Condition: "snowy", + Humidity: 70, + WindSpeed: 25, + }, + } + + cityKey := strings.ToLower(params.City) + if weather, exists := weatherData[cityKey]; exists { + return weather, nil + } + + // Default weather for unknown cities + return WeatherResponse{ + City: params.City, + Temperature: 20, + Condition: "partly cloudy", + Humidity: 65, + WindSpeed: 12, + }, nil +} + +// ClothingParams represents parameters for clothing suggestions +type ClothingParams struct { + WeatherData WeatherResponse `json:"weather_data" description:"Weather information to base clothing suggestions on" required:"true"` +} + +// ClothingResponse represents clothing suggestions +type ClothingResponse struct { + Recommendation string `json:"recommendation"` + Items []string `json:"items"` + Reasoning string `json:"reasoning"` +} + +// SuggestClothes suggests appropriate clothing based on weather data +func SuggestClothes(params ClothingParams) (ClothingResponse, error) { + weather := params.WeatherData + if weather.City == "" { + return ClothingResponse{}, fmt.Errorf("weather data is required") + } + + var recommendation string + var items []string + var reasoning string + + temp := weather.Temperature + condition := strings.ToLower(weather.Condition) + + // Base clothing on temperature + if temp < 0 { + items = append(items, "heavy winter coat", "thermal underwear", "warm boots", "gloves", "hat") + recommendation = "Dress very warmly" + reasoning = fmt.Sprintf("Temperature is %dยฐC, which is freezing", temp) + } else if temp < 10 { + items = append(items, "warm jacket", "long pants", "closed shoes", "scarf") + recommendation = "Dress warmly" + reasoning = fmt.Sprintf("Temperature is %dยฐC, which is cold", temp) + } else if temp < 20 { + items = append(items, "light jacket or sweater", "long pants", "comfortable shoes") + recommendation = "Dress in layers" + reasoning = fmt.Sprintf("Temperature is %dยฐC, which is cool", temp) + } else if temp < 30 { + items = append(items, "light shirt", "comfortable pants", "light shoes") + recommendation = "Dress comfortably" + reasoning = fmt.Sprintf("Temperature is %dยฐC, which is pleasant", temp) + } else { + items = append(items, "light clothing", "shorts", "sandals", "sun hat") + recommendation = "Dress lightly" + reasoning = fmt.Sprintf("Temperature is %dยฐC, which is hot", temp) + } + + // Adjust for weather conditions + if strings.Contains(condition, "rain") { + items = append(items, "waterproof jacket", "umbrella", "waterproof shoes") + reasoning += " and it's rainy" + } else if strings.Contains(condition, "snow") { + items = append(items, "warm waterproof boots", "snow gloves") + reasoning += " and it's snowy" + } else if strings.Contains(condition, "wind") || weather.WindSpeed > 20 { + items = append(items, "windbreaker") + reasoning += " and it's windy" + } else if strings.Contains(condition, "sunny") { + items = append(items, "sunglasses", "sunscreen") + reasoning += " and it's sunny" + } + + return ClothingResponse{ + Recommendation: recommendation, + Items: items, + Reasoning: reasoning, + }, nil +} + +// CreateWeatherFunction creates a weather function for the registry +func CreateWeatherFunction() *Function[WeatherParams, WeatherResponse] { + return &Function[WeatherParams, WeatherResponse]{ + Name: "get-weather", + Description: "Gets current weather information for a specified city", + Parameters: generator.GenerateParameterSchema(WeatherParams{}), + Execute: GetWeather, + } +} + +// CreateClothingFunction creates a clothing suggestion function for the registry +func CreateClothingFunction() *Function[ClothingParams, ClothingResponse] { + return &Function[ClothingParams, ClothingResponse]{ + Name: "suggest-clothes", + Description: "Suggests appropriate clothing based on weather conditions", + Parameters: generator.GenerateParameterSchema(ClothingParams{}), + Execute: SuggestClothes, + } +} \ No newline at end of file diff --git a/examples/demo/demo b/examples/demo/demo new file mode 100755 index 0000000..14d30d0 Binary files /dev/null and b/examples/demo/demo differ diff --git a/examples/demo/go.mod b/examples/demo/go.mod new file mode 100644 index 0000000..238da4c --- /dev/null +++ b/examples/demo/go.mod @@ -0,0 +1,44 @@ +module demo + +go 1.23.1 + +replace github.com/SadikSunbul/gopilot => ../.. + +require github.com/SadikSunbul/gopilot v0.0.0-00010101000000-000000000000 + +require ( + cloud.google.com/go v0.115.0 // indirect + cloud.google.com/go/ai v0.8.0 // indirect + cloud.google.com/go/auth v0.6.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect + cloud.google.com/go/compute/metadata v0.3.0 // indirect + cloud.google.com/go/longrunning v0.5.7 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/generative-ai-go v0.19.0 // indirect + github.com/google/s2a-go v0.1.7 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/googleapis/gax-go/v2 v2.12.5 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect + go.opentelemetry.io/otel v1.26.0 // indirect + go.opentelemetry.io/otel/metric v1.26.0 // indirect + go.opentelemetry.io/otel/trace v1.26.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect + golang.org/x/time v0.5.0 // indirect + google.golang.org/api v0.186.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 // indirect + google.golang.org/grpc v1.64.1 // indirect + google.golang.org/protobuf v1.34.2 // indirect +) diff --git a/examples/demo/go.sum b/examples/demo/go.sum new file mode 100644 index 0000000..e064aa8 --- /dev/null +++ b/examples/demo/go.sum @@ -0,0 +1,166 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14= +cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU= +cloud.google.com/go/ai v0.8.0 h1:rXUEz8Wp2OlrM8r1bfmpF2+VKqc1VJpafE3HgzRnD/w= +cloud.google.com/go/ai v0.8.0/go.mod h1:t3Dfk4cM61sytiggo2UyGsDVW3RF1qGZaUKDrZFyqkE= +cloud.google.com/go/auth v0.6.0 h1:5x+d6b5zdezZ7gmLWD1m/xNjnaQ2YDhmIz/HH3doy1g= +cloud.google.com/go/auth v0.6.0/go.mod h1:b4acV+jLQDyjwm4OXHYjNvRi4jvGBzHWJRtJcy+2P4g= +cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= +cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU= +cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/generative-ai-go v0.19.0 h1:R71szggh8wHMCUlEMsW2A/3T+5LdEIkiaHSYgSpUgdg= +github.com/google/generative-ai-go v0.19.0/go.mod h1:JYolL13VG7j79kM5BtHz4qwONHkeJQzOCkKXnpqtS/E= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA= +github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 h1:A3SayB3rNyt+1S6qpI9mHPkeHTZbD7XILEqWnYZb2l0= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0/go.mod h1:27iA5uvhuRNmalO+iEUdVn5ZMj2qy10Mm+XRIpRmyuU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc= +go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= +go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= +go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30= +go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4= +go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= +go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.186.0 h1:n2OPp+PPXX0Axh4GuSsL5QL8xQCTb2oDwyzPnQvqUug= +google.golang.org/api v0.186.0/go.mod h1:hvRbBmgoje49RV3xqVXrmP6w93n6ehGgIVPYrGtBFFc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 h1:MuYw1wJzT+ZkybKfaOXKp5hJiZDn2iHaXRw0mRYdHSc= +google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4/go.mod h1:px9SlOOZBg1wM1zdnr8jEL4CNGUBZ+ZKYtNPApNQc4c= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 h1:Di6ANFilr+S60a4S61ZM00vLdw0IrQOSMS2/6mrnOU0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= +google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/examples/demo/main.go b/examples/demo/main.go new file mode 100644 index 0000000..2484d23 --- /dev/null +++ b/examples/demo/main.go @@ -0,0 +1,161 @@ +package main + +import ( + "bufio" + "context" + "fmt" + "log" + "os" + "strings" + + "github.com/SadikSunbul/gopilot" + "github.com/SadikSunbul/gopilot/clients" +) + +func main() { + fmt.Println("๐Ÿค– GoPilot Multi-Step Agent Demo") + fmt.Println("=================================") + fmt.Println() + + // Check for API key + apiKey := os.Getenv("GEMINI_API_KEY") + if apiKey == "" { + fmt.Println("โš ๏ธ GEMINI_API_KEY environment variable not set.") + fmt.Println(" This demo will run with a mock client for testing purposes.") + fmt.Println() + runMockDemo() + return + } + + // Initialize real Gemini client + client, err := clients.NewGeminiClient(context.Background(), apiKey, "gemini-2.0-flash") + if err != nil { + log.Fatal("Failed to initialize Gemini client:", err) + } + defer client.Close() + + // Create GoPilot instance + gp, err := gopilot.NewGopilot(client) + if err != nil { + log.Fatal("Failed to initialize gopilot:", err) + } + + // Register example functions + weatherFn := gopilot.CreateWeatherFunction() + clothingFn := gopilot.CreateClothingFunction() + + if err := gp.FunctionRegister(weatherFn); err != nil { + log.Fatal("Failed to register weather function:", err) + } + + if err := gp.FunctionRegister(clothingFn); err != nil { + log.Fatal("Failed to register clothing function:", err) + } + + // Create multi-step agent + agent := gopilot.NewAgent(gp, 5) + + fmt.Println("๐ŸŒŸ Multi-step agent ready! Try these examples:") + fmt.Println(" โ€ข 'What should I wear in Istanbul today?'") + fmt.Println(" โ€ข 'Based on the weather in London, what clothes should I pack?'") + fmt.Println(" โ€ข 'What's the weather like in Tokyo?'") + fmt.Println(" โ€ข Type 'exit' to quit") + fmt.Println() + + // Interactive loop + reader := bufio.NewReader(os.Stdin) + for { + fmt.Print("๐Ÿ‘ค You: ") + input, err := reader.ReadString('\n') + if err != nil { + log.Fatal("Error reading input:", err) + } + + input = strings.TrimSpace(input) + if strings.ToLower(input) == "exit" { + fmt.Println("๐Ÿ‘‹ Goodbye!") + break + } + + if input == "" { + continue + } + + fmt.Println("๐Ÿค” Thinking...") + + // Execute multi-step reasoning + result, err := agent.ExecuteMultiStep(input) + if err != nil { + fmt.Printf("โŒ Error: %v\n\n", err) + continue + } + + fmt.Printf("๐Ÿค– Agent: %v\n\n", result) + + // Reset agent for next conversation + agent.Reset() + } +} + +func runMockDemo() { + fmt.Println("Running with mock LLM client...") + fmt.Println() + + // Create mock responses that simulate the weather + clothing chain + mockResponses := []string{ + `{"agent": "get-weather", "parameters": {"city": "Istanbul"}}`, + `{"agent": "final_answer", "parameters": {"answer": "Based on the weather in Istanbul today (12ยฐC and cloudy with 75% humidity), I recommend dressing in layers. Wear a light jacket or sweater, long pants, and comfortable shoes. Since it's cloudy and humid, you might want to bring a light umbrella just in case of rain."}}`, + } + + mockLLM := gopilot.NewMockLLMClient(mockResponses) + + // Create GoPilot instance with mock + gp, err := gopilot.NewGopilot(mockLLM) + if err != nil { + log.Fatal("Failed to initialize gopilot:", err) + } + + // Register example functions + weatherFn := gopilot.CreateWeatherFunction() + clothingFn := gopilot.CreateClothingFunction() + + if err := gp.FunctionRegister(weatherFn); err != nil { + log.Fatal("Failed to register weather function:", err) + } + + if err := gp.FunctionRegister(clothingFn); err != nil { + log.Fatal("Failed to register clothing function:", err) + } + + // Create multi-step agent + agent := gopilot.NewAgent(gp, 5) + + fmt.Println("๐ŸŒŸ Demo: Multi-step reasoning for clothing recommendation") + fmt.Println() + + userInput := "Based on today's weather, what should I wear in Istanbul?" + fmt.Printf("๐Ÿ‘ค User: %s\n", userInput) + fmt.Println("๐Ÿค” Agent is thinking through multiple steps...") + fmt.Println() + + // Execute multi-step reasoning + result, err := agent.ExecuteMultiStep(userInput) + if err != nil { + log.Fatal("Error:", err) + } + + fmt.Printf("๐Ÿค– Final Answer: %v\n", result) + fmt.Println() + + // Show the step-by-step process + context := agent.GetContext() + fmt.Println("๐Ÿ“ Step-by-step process:") + stepNum := 1 + for _, msg := range context.Messages { + if msg.Role == "function" { + fmt.Printf(" Step %d: %s\n", stepNum, msg.Content) + stepNum++ + } + } + fmt.Printf(" Step %d: Provided final answer\n", stepNum) +} \ No newline at end of file diff --git a/examples/simulation_example.go b/examples/simulation_example.go new file mode 100644 index 0000000..e6e1b66 --- /dev/null +++ b/examples/simulation_example.go @@ -0,0 +1,163 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + + "github.com/SadikSunbul/gopilot" +) + +// SimulationExample demonstrates step-by-step multi-step function calling +func main() { + fmt.Println("๐Ÿš€ GoPilot Multi-Step Agent - Detailed Simulation") + fmt.Println("=" + fmt.Sprintf("%50s", "=")) + fmt.Println() + + // 1. SETUP: Create mock responses that simulate intelligent reasoning + fmt.Println("๐Ÿ“‹ SIMULATION SETUP") + fmt.Println("User Request: 'Based on today's weather, what should I wear in Istanbul?'") + fmt.Println() + + // These responses simulate what a real LLM would generate + mockResponses := []string{ + // Step 1: Agent decides to get weather first + `{ + "action": "function_call", + "agent": "get-weather", + "parameters": {"city": "Istanbul"}, + "reasoning": "I need to get the current weather in Istanbul before I can recommend appropriate clothing" + }`, + // Step 2: Agent uses weather data to suggest clothes + `{ + "action": "function_call", + "agent": "suggest-clothes", + "parameters": { + "weather_data": { + "city": "Istanbul", + "temperature": 12, + "condition": "cloudy", + "humidity": 75, + "wind_speed": 15 + } + }, + "reasoning": "Now that I have the weather data (12ยฐC, cloudy, 75% humidity), I can suggest appropriate clothing" + }`, + // Step 3: Agent provides final comprehensive answer + `{ + "action": "final_answer", + "answer": "Based on today's weather in Istanbul (12ยฐC and cloudy with 75% humidity and 15 km/h wind), I recommend dressing in layers: wear a light jacket or sweater, long pants, and comfortable shoes. Since it's cloudy and humid, you might want to bring a light umbrella as there's a chance of rain. The wind speed of 15 km/h means you should wear something that won't be too loose.", + "reasoning": "I now have complete weather information and clothing suggestions, so I can provide a comprehensive final answer" + }`, + } + + // 2. INITIALIZE: Set up the multi-step agent + fmt.Println("๐Ÿ”ง AGENT INITIALIZATION") + mockLLM := gopilot.NewMockLLMClient(mockResponses) + + gp, err := gopilot.NewGopilot(mockLLM) + if err != nil { + log.Fatal("Failed to initialize gopilot:", err) + } + + // Register the demo functions + weatherFn := gopilot.CreateWeatherFunction() + clothingFn := gopilot.CreateClothingFunction() + + gp.FunctionRegister(weatherFn) + gp.FunctionRegister(clothingFn) + + // Create multi-step agent with max 5 steps + agent := gopilot.NewAgent(gp, 5) + fmt.Println("โœ… Agent created with weather and clothing functions registered") + fmt.Println("โœ… Maximum steps set to 5 to prevent infinite loops") + fmt.Println() + + // 3. EXECUTION: Run the multi-step reasoning + fmt.Println("๐Ÿง  MULTI-STEP REASONING EXECUTION") + fmt.Println("โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€") + + userInput := "Based on today's weather, what should I wear in Istanbul?" + fmt.Printf("๐Ÿ‘ค User: %s\n\n", userInput) + + // Execute and track each step + fmt.Println("๐Ÿค– Agent begins multi-step reasoning...\n") + + result, err := agent.ExecuteMultiStep(userInput) + if err != nil { + log.Fatal("Error:", err) + } + + fmt.Printf("๐ŸŽฏ FINAL RESULT: %s\n\n", result) + + // 4. ANALYSIS: Show the detailed step-by-step breakdown + fmt.Println("๐Ÿ“Š DETAILED STEP ANALYSIS") + fmt.Println("โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€") + + context := agent.GetContext() + stepNum := 1 + + fmt.Printf("Initial Context:\n") + fmt.Printf(" โ€ข Max Steps: %d\n", context.MaxSteps) + fmt.Printf(" โ€ข Messages: %d\n", len(context.Messages)) + fmt.Printf(" โ€ข Function Results: %d\n\n", len(context.Results)) + + // Show each step in detail + for _, msg := range context.Messages { + switch msg.Role { + case "user": + fmt.Printf("๐Ÿ“ Initial Request: %s\n\n", msg.Content) + case "function": + fmt.Printf("โš™๏ธ STEP %d: %s\n", stepNum, msg.Content) + if msg.Result != nil { + resultBytes, _ := json.MarshalIndent(msg.Result, " ", " ") + fmt.Printf(" ๐Ÿ“ค Result: %s\n\n", string(resultBytes)) + } + stepNum++ + case "assistant": + fmt.Printf("๐Ÿค– Final Answer Generated: %s\n\n", msg.Content) + } + } + + // 5. FUNCTION RESULTS: Show all stored results + fmt.Println("๐Ÿ’พ STORED FUNCTION RESULTS") + fmt.Println("โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€") + for funcName, result := range context.Results { + fmt.Printf("๐Ÿ” Function: %s\n", funcName) + resultBytes, _ := json.MarshalIndent(result, " ", " ") + fmt.Printf(" Result: %s\n\n", string(resultBytes)) + } + + // 6. SIMULATION SUMMARY + fmt.Println("๐Ÿ“ˆ SIMULATION SUMMARY") + fmt.Println("โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€") + fmt.Printf("โœ… Total LLM Calls: %d\n", mockLLM.GetCallCount()) + fmt.Printf("โœ… Functions Executed: %d\n", len(context.Results)) + fmt.Printf("โœ… Steps Completed: %d/%d\n", context.GetStepCount(), context.MaxSteps) + fmt.Printf("โœ… Final Answer Provided: Yes\n") + fmt.Println() + + // 7. ARCHITECTURE EXPLANATION + fmt.Println("๐Ÿ—๏ธ ARCHITECTURE OVERVIEW") + fmt.Println("โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€") + fmt.Println("1. ๐Ÿง  Agent: Orchestrates multi-step reasoning") + fmt.Println("2. ๐Ÿ“š AgentContext: Maintains conversation history & results") + fmt.Println("3. ๐ŸŽฏ Function Registry: Available functions (weather, clothing)") + fmt.Println("4. ๐Ÿค– LLM: Makes intelligent decisions about next actions") + fmt.Println("5. ๐Ÿ”„ Execution Loop: Continues until final_answer or max steps") + fmt.Println() + + // 8. EXAMPLE OF CHAINING + fmt.Println("๐Ÿ”— FUNCTION CHAINING EXAMPLE") + fmt.Println("โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€") + fmt.Println("Weather Function Result โ†’ Used as input to โ†’ Clothing Function") + fmt.Println() + fmt.Println("Step 1: get-weather('Istanbul') โ†’") + fmt.Println(" {city: 'Istanbul', temperature: 12, condition: 'cloudy', humidity: 75}") + fmt.Println() + fmt.Println("Step 2: suggest-clothes(weather_data_from_step_1) โ†’") + fmt.Println(" {recommendation: 'Dress in layers', items: ['light jacket', 'long pants']}") + fmt.Println() + fmt.Println("Step 3: final_answer(combining_all_data) โ†’") + fmt.Println(" 'Based on 12ยฐC cloudy weather in Istanbul, wear layers with light jacket...'") +} \ No newline at end of file diff --git a/go.mod b/go.mod index 1b28f74..c46c683 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,6 @@ require ( cloud.google.com/go/compute/metadata v0.3.0 // indirect cloud.google.com/go/longrunning v0.5.7 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/getsentry/sentry-go v0.32.0 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect @@ -30,8 +29,6 @@ require ( go.opentelemetry.io/otel v1.26.0 // indirect go.opentelemetry.io/otel/metric v1.26.0 // indirect go.opentelemetry.io/otel/trace v1.26.0 // indirect - go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.36.0 // indirect golang.org/x/net v0.38.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect diff --git a/go.sum b/go.sum index 7cee1e6..e064aa8 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,6 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/getsentry/sentry-go v0.32.0 h1:YKs+//QmwE3DcYtfKRH8/KyOOF/I6Qnx7qYGNHCGmCY= -github.com/getsentry/sentry-go v0.32.0/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -89,10 +87,6 @@ go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgS go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4= go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= diff --git a/mock.go b/mock.go new file mode 100644 index 0000000..f9c1317 --- /dev/null +++ b/mock.go @@ -0,0 +1,96 @@ +package gopilot + +import ( + "encoding/json" + + "github.com/SadikSunbul/gopilot/clients" +) + +// MockLLMClient implements LLMProvider for testing multi-step scenarios +type MockLLMClient struct { + responses []string + callCount int + systemPrompt string +} + +// NewMockLLMClient creates a new mock LLM client with predefined responses +func NewMockLLMClient(responses []string) *MockLLMClient { + return &MockLLMClient{ + responses: responses, + callCount: 0, + } +} + +// SetResponses updates the mock responses (useful for testing) +func (m *MockLLMClient) SetResponses(responses []string) { + m.responses = responses + m.callCount = 0 +} + +// Generate simulates LLM response generation +func (m *MockLLMClient) Generate(prompt string) (*clients.LLMResponse, error) { + if m.callCount >= len(m.responses) { + // Return unsupported if we've exhausted responses + return &clients.LLMResponse{ + Agent: "unsupported", + Parameters: map[string]interface{}{ + "message": "No more responses available", + }, + }, nil + } + + response := m.responses[m.callCount] + m.callCount++ + + // Parse the mock response as JSON to extract the intended structure + var agentResp map[string]interface{} + if err := json.Unmarshal([]byte(response), &agentResp); err != nil { + return nil, err + } + + // Check if this is a function_call action + if action, ok := agentResp["action"].(string); ok && action == "function_call" { + // Extract agent and parameters for function calls + agent, _ := agentResp["agent"].(string) + parameters, _ := agentResp["parameters"].(map[string]interface{}) + + return &clients.LLMResponse{ + Agent: agent, + Parameters: parameters, + }, nil + } + + // For final_answer, we use a special agent name + if action, ok := agentResp["action"].(string); ok && action == "final_answer" { + answer, _ := agentResp["answer"].(string) + return &clients.LLMResponse{ + Agent: "final_answer", + Parameters: map[string]interface{}{ + "answer": answer, + }, + }, nil + } + + // Fallback: try to parse as standard LLMResponse + var result clients.LLMResponse + if err := json.Unmarshal([]byte(response), &result); err != nil { + return nil, err + } + + return &result, nil +} + +// SetSystemPrompt sets the system prompt (for compatibility) +func (m *MockLLMClient) SetSystemPrompt(systemPrompt string) { + m.systemPrompt = systemPrompt +} + +// GetCallCount returns the number of times Generate has been called +func (m *MockLLMClient) GetCallCount() int { + return m.callCount +} + +// GetSystemPrompt returns the current system prompt +func (m *MockLLMClient) GetSystemPrompt() string { + return m.systemPrompt +} \ No newline at end of file diff --git a/systemPrompt.go b/systemPrompt.go index bd6ffeb..65f27f2 100644 --- a/systemPrompt.go +++ b/systemPrompt.go @@ -45,3 +45,56 @@ Respond ONLY in the following JSON format: "param2": "value2" } }` + +var agentSystemPrompt = `You are an intelligent multi-step reasoning agent for the GoPilot system. +Your role is to analyze complex user requests and break them down into sequential function calls. + +CORE CAPABILITIES: +1. Multi-Step Planning: Break complex requests into sequential function calls +2. Context Management: Use results from previous function calls in subsequent ones +3. Dynamic Reasoning: Adapt your plan based on function results +4. Final Answer Generation: Provide comprehensive answers using all collected information + +DECISION MAKING RULES: +%s + +MULTI-STEP REASONING: +When handling a request: +1. Analyze if the request requires multiple functions +2. Plan the sequence of function calls needed +3. Execute functions one by one, using previous results +4. Provide a final comprehensive answer + +AVAILABLE FUNCTIONS AND PARAMETERS: +%s + +RESPONSE ACTIONS: +You must choose one of two actions: + +1. FUNCTION_CALL: When you need to execute a function +{ + "action": "function_call", + "agent": "function-name", + "parameters": { + "param1": "value1" + }, + "reasoning": "Brief explanation of why this function is needed" +} + +2. FINAL_ANSWER: When you have enough information to provide the final answer +{ + "action": "final_answer", + "answer": "Your comprehensive final answer", + "reasoning": "Brief explanation of how you reached this conclusion" +} + +CONTEXT USAGE: +- Use results from previous function calls in your parameters +- Reference specific data from previous results when appropriate +- Build upon information gathered in previous steps + +IMPORTANT: +- Always include "reasoning" to explain your decision +- Only use "final_answer" when you have sufficient information +- Use previous function results to inform subsequent function calls +- Be concise but comprehensive in your final answers`