diff --git a/edgee/edgee.go b/edgee/edgee.go index 93a6585..c25ad6c 100644 --- a/edgee/edgee.go +++ b/edgee/edgee.go @@ -17,8 +17,128 @@ const ( DefaultBaseURL = "https://api.edgee.ai" // APIEndpoint is the chat completions endpoint APIEndpoint = "/v1/chat/completions" + // DefaultMaxIterations is the default max iterations for the agentic loop + DefaultMaxIterations = 10 ) +// ToolHandler is a function that handles tool execution +type ToolHandler func(args map[string]any) (any, error) + +// ExecutableTool represents a tool with an executable handler +type ExecutableTool struct { + Name string + Description string + Parameters map[string]any + Handler ToolHandler +} + +// NewTool creates a new ExecutableTool with a builder pattern +func NewTool(name, description string) *ExecutableTool { + return &ExecutableTool{ + Name: name, + Description: description, + Parameters: map[string]any{ + "type": "object", + "properties": map[string]any{}, + "required": []string{}, + }, + } +} + +// AddParam adds a parameter to the tool +func (t *ExecutableTool) AddParam(name, paramType, description string, required bool) *ExecutableTool { + props := t.Parameters["properties"].(map[string]any) + props[name] = map[string]any{ + "type": paramType, + "description": description, + } + + if required { + req := t.Parameters["required"].([]string) + t.Parameters["required"] = append(req, name) + } + + return t +} + +// AddEnumParam adds an enum parameter to the tool +func (t *ExecutableTool) AddEnumParam(name string, enumValues []string, description string, required bool) *ExecutableTool { + props := t.Parameters["properties"].(map[string]any) + props[name] = map[string]any{ + "type": "string", + "enum": enumValues, + "description": description, + } + + if required { + req := t.Parameters["required"].([]string) + t.Parameters["required"] = append(req, name) + } + + return t +} + +// WithHandler sets the handler function for the tool +func (t *ExecutableTool) WithHandler(handler ToolHandler) *ExecutableTool { + t.Handler = handler + return t +} + +// ToTool converts ExecutableTool to a Tool for API requests +func (t *ExecutableTool) ToTool() Tool { + desc := t.Description + return Tool{ + Type: "function", + Function: FunctionDefinition{ + Name: t.Name, + Description: &desc, + Parameters: t.Parameters, + }, + } +} + +// SimpleInput represents input with executable tools for automatic tool execution +type SimpleInput struct { + Text string + Tools []*ExecutableTool + MaxIterations int +} + +// NewSimpleInput creates a new SimpleInput with tools +func NewSimpleInput(text string, tools ...*ExecutableTool) *SimpleInput { + return &SimpleInput{ + Text: text, + Tools: tools, + MaxIterations: DefaultMaxIterations, + } +} + +// WithMaxIterations sets the maximum number of agentic loop iterations +func (s *SimpleInput) WithMaxIterations(max int) *SimpleInput { + s.MaxIterations = max + return s +} + +// StreamEventType represents the type of stream event +type StreamEventType string + +const ( + StreamEventChunk StreamEventType = "chunk" + StreamEventToolStart StreamEventType = "tool_start" + StreamEventToolResult StreamEventType = "tool_result" + StreamEventIterationComplete StreamEventType = "iteration_complete" +) + +// StreamEvent represents an event during streaming with auto tool execution +type StreamEvent struct { + Type StreamEventType + Chunk *StreamChunk + ToolCall *ToolCall + ToolName string + Result any + Iteration int +} + // Message represents a chat message type Message struct { Role string `json:"role"` @@ -234,136 +354,358 @@ func NewClient(config any) (*Client, error) { // Send sends a chat completion request with flexible input: // - Pass a string for simple user input -// - Pass an InputObject for full control +// - Pass a *SimpleInput for automatic tool execution (agentic loop) +// - Pass an InputObject for manual tool handling // - Pass a map[string]any with "messages", "tools", "tool_choice" keys -func (c *Client) Send(model string, input any) (response SendResponse, err error) { - req, err := c.buildRequest(model, input, false) - if err != nil { - return +func (c *Client) Send(model string, input any) (SendResponse, error) { + // Check if this is a SimpleInput for auto tool execution + switch v := input.(type) { + case *SimpleInput: + return c.sendWithAutoTools(model, v) + case SimpleInput: + return c.sendWithAutoTools(model, &v) + default: + req, err := c.buildRequest(model, input, false) + if err != nil { + return SendResponse{}, err + } + return c.handleNonStreamingResponse(req) } - response, err = c.handleNonStreamingResponse(req) - return +} + +// sendWithAutoTools implements the agentic loop for automatic tool execution +func (c *Client) sendWithAutoTools(model string, input *SimpleInput) (SendResponse, error) { + // Convert executable tools to API tools + tools := make([]Tool, len(input.Tools)) + toolHandlers := make(map[string]ToolHandler) + for i, t := range input.Tools { + tools[i] = t.ToTool() + toolHandlers[t.Name] = t.Handler + } + + // Build initial messages + messages := []Message{{Role: "user", Content: input.Text}} + + // Accumulate usage across iterations + var totalUsage *Usage + + for iteration := 0; iteration < input.MaxIterations; iteration++ { + // Build and send request + req := &Request{ + Model: model, + Messages: messages, + Tools: tools, + Stream: false, + } + + response, err := c.handleNonStreamingResponse(req) + if err != nil { + return response, err + } + + // Accumulate usage + if response.Usage != nil { + if totalUsage == nil { + totalUsage = &Usage{} + } + totalUsage.PromptTokens += response.Usage.PromptTokens + totalUsage.CompletionTokens += response.Usage.CompletionTokens + totalUsage.TotalTokens += response.Usage.TotalTokens + } + + // Check if model requested tool calls + toolCalls := response.ToolCalls() + if len(toolCalls) == 0 { + // No tool calls - return final response with accumulated usage + response.Usage = totalUsage + return response, nil + } + + // Add assistant message with tool calls + if response.MessageContent() != nil { + messages = append(messages, *response.MessageContent()) + } + + // Execute each tool and add results + for _, toolCall := range toolCalls { + handler, ok := toolHandlers[toolCall.Function.Name] + if !ok { + // Unknown tool - add error result + toolCallID := toolCall.ID + messages = append(messages, Message{ + Role: "tool", + Content: fmt.Sprintf(`{"error": "Unknown tool: %s"}`, toolCall.Function.Name), + ToolCallID: &toolCallID, + }) + continue + } + + // Parse arguments + var args map[string]any + if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil { + toolCallID := toolCall.ID + messages = append(messages, Message{ + Role: "tool", + Content: fmt.Sprintf(`{"error": "Failed to parse arguments: %s"}`, err.Error()), + ToolCallID: &toolCallID, + }) + continue + } + + // Execute handler + result, err := handler(args) + var resultStr string + if err != nil { + resultStr = fmt.Sprintf(`{"error": "Tool execution failed: %s"}`, err.Error()) + } else { + resultBytes, _ := json.Marshal(result) + resultStr = string(resultBytes) + } + + toolCallID := toolCall.ID + messages = append(messages, Message{ + Role: "tool", + Content: resultStr, + ToolCallID: &toolCallID, + }) + } + } + + return SendResponse{}, fmt.Errorf("max tool iterations (%d) reached", input.MaxIterations) } // ChatCompletion sends a non-streaming chat completion request (convenience method) -func (c *Client) ChatCompletion(model string, input any) (response SendResponse, err error) { - response, err = c.Send(model, input) - if err != nil { - return +func (c *Client) ChatCompletion(model string, input any) (SendResponse, error) { + return c.Send(model, input) +} + +// Stream sends a streaming chat completion request with flexible input: +// - Pass a string for simple streaming +// - Pass a *SimpleInput for streaming with automatic tool execution +// - Pass an InputObject or map for manual control +func (c *Client) Stream(model string, input any) (<-chan *StreamEvent, <-chan error) { + // Check if this is a SimpleInput for auto tool execution + switch v := input.(type) { + case *SimpleInput: + return c.streamWithAutoTools(model, v) + case SimpleInput: + return c.streamWithAutoTools(model, &v) + default: + // Regular streaming - wrap chunks in StreamEvent + return c.streamRegular(model, input) } - return } -// Stream sends a streaming chat completion request (convenience method) -func (c *Client) Stream(model string, input any) (<-chan *StreamChunk, <-chan error) { +// streamRegular handles regular streaming without auto tools +func (c *Client) streamRegular(model string, input any) (<-chan *StreamEvent, <-chan error) { + eventChan := make(chan *StreamEvent, 10) + errChan := make(chan error, 1) + req, err := c.buildRequest(model, input, true) if err != nil { - errChan := make(chan error, 1) errChan <- err close(errChan) - chunkChan := make(chan *StreamChunk) - close(chunkChan) - return chunkChan, errChan + close(eventChan) + return eventChan, errChan } - result, err := c.handleStreamingResponse(req) - if err != nil { - errChan := make(chan error, 1) - errChan <- err - close(errChan) - chunkChan := make(chan *StreamChunk) - close(chunkChan) - return chunkChan, errChan - } + go func() { + defer close(eventChan) + defer close(errChan) + + chunkChan, chunkErrChan := c.doStreamRequest(req) + + for { + select { + case chunk, ok := <-chunkChan: + if !ok { + return + } + eventChan <- &StreamEvent{ + Type: StreamEventChunk, + Chunk: chunk, + } + case err := <-chunkErrChan: + if err != nil { + errChan <- err + } + return + } + } + }() - return result.ChunkChan, result.ErrChan + return eventChan, errChan } -func (c *Client) buildRequest(model string, input any, stream bool) (*Request, error) { - req := &Request{ - Model: model, - Stream: stream, - } +// streamWithAutoTools implements streaming with automatic tool execution +func (c *Client) streamWithAutoTools(model string, input *SimpleInput) (<-chan *StreamEvent, <-chan error) { + eventChan := make(chan *StreamEvent, 10) + errChan := make(chan error, 1) - switch v := input.(type) { - case string: - // Simple string input - req.Messages = []Message{{Role: "user", Content: v}} - case InputObject: - req.Messages = v.Messages - req.Tools = v.Tools - req.ToolChoice = v.ToolChoice - case *InputObject: - req.Messages = v.Messages - req.Tools = v.Tools - req.ToolChoice = v.ToolChoice - case map[string]any: - // Map input - if messages, ok := v["messages"]; ok { - msgBytes, err := json.Marshal(messages) - if err != nil { - return nil, fmt.Errorf("failed to marshal messages: %w", err) - } - if err := json.Unmarshal(msgBytes, &req.Messages); err != nil { - return nil, fmt.Errorf("failed to unmarshal messages: %w", err) - } + go func() { + defer close(eventChan) + defer close(errChan) + + // Convert executable tools to API tools + tools := make([]Tool, len(input.Tools)) + toolHandlers := make(map[string]ToolHandler) + for i, t := range input.Tools { + tools[i] = t.ToTool() + toolHandlers[t.Name] = t.Handler } - if tools, ok := v["tools"]; ok { - toolBytes, err := json.Marshal(tools) - if err != nil { - return nil, fmt.Errorf("failed to marshal tools: %w", err) + + // Build initial messages + messages := []Message{{Role: "user", Content: input.Text}} + + for iteration := 0; iteration < input.MaxIterations; iteration++ { + // Build request + req := &Request{ + Model: model, + Messages: messages, + Tools: tools, + Stream: true, } - if err := json.Unmarshal(toolBytes, &req.Tools); err != nil { - return nil, fmt.Errorf("failed to unmarshal tools: %w", err) + + // Stream the response and collect tool calls + var collectedToolCalls []ToolCall + var assistantContent strings.Builder + + chunkChan, chunkErrChan := c.doStreamRequest(req) + + streamLoop: + for { + select { + case chunk, ok := <-chunkChan: + if !ok { + break streamLoop + } + + // Send chunk event + eventChan <- &StreamEvent{ + Type: StreamEventChunk, + Chunk: chunk, + } + + // Collect text content + if text := chunk.Text(); text != "" { + assistantContent.WriteString(text) + } + + // Collect tool calls from delta + if len(chunk.Choices) > 0 && chunk.Choices[0].Delta != nil { + for _, tc := range chunk.Choices[0].Delta.ToolCalls { + // Merge or add tool call + found := false + for i := range collectedToolCalls { + if collectedToolCalls[i].ID == tc.ID || (collectedToolCalls[i].ID == "" && i < len(collectedToolCalls)) { + // Merge arguments + collectedToolCalls[i].Function.Arguments += tc.Function.Arguments + if tc.Function.Name != "" { + collectedToolCalls[i].Function.Name = tc.Function.Name + } + if tc.ID != "" { + collectedToolCalls[i].ID = tc.ID + } + if tc.Type != "" { + collectedToolCalls[i].Type = tc.Type + } + found = true + break + } + } + if !found { + collectedToolCalls = append(collectedToolCalls, tc) + } + } + } + + case err := <-chunkErrChan: + if err != nil { + errChan <- err + return + } + break streamLoop + } } - } - if toolChoice, ok := v["tool_choice"]; ok { - req.ToolChoice = toolChoice - } - default: - return nil, fmt.Errorf("unsupported input type: %T", input) - } - return req, nil -} + // If no tool calls, we're done + if len(collectedToolCalls) == 0 { + return + } -func (c *Client) handleNonStreamingResponse(req *Request) (response SendResponse, err error) { - body, err := json.Marshal(req) - if err != nil { - return response, fmt.Errorf("failed to marshal request: %w", err) - } + // Add assistant message with tool calls + messages = append(messages, Message{ + Role: "assistant", + Content: assistantContent.String(), + ToolCalls: collectedToolCalls, + }) + + // Execute each tool + for _, toolCall := range collectedToolCalls { + // Send tool start event + eventChan <- &StreamEvent{ + Type: StreamEventToolStart, + ToolCall: &toolCall, + } - httpReq, err := http.NewRequest("POST", c.baseURL+APIEndpoint, bytes.NewReader(body)) - if err != nil { - return response, fmt.Errorf("failed to create request: %w", err) - } + handler, ok := toolHandlers[toolCall.Function.Name] + var resultStr string + var result any + + if !ok { + resultStr = fmt.Sprintf(`{"error": "Unknown tool: %s"}`, toolCall.Function.Name) + result = map[string]any{"error": fmt.Sprintf("Unknown tool: %s", toolCall.Function.Name)} + } else { + // Parse arguments + var args map[string]any + if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil { + resultStr = fmt.Sprintf(`{"error": "Failed to parse arguments: %s"}`, err.Error()) + result = map[string]any{"error": err.Error()} + } else { + // Execute handler + var err error + result, err = handler(args) + if err != nil { + resultStr = fmt.Sprintf(`{"error": "Tool execution failed: %s"}`, err.Error()) + result = map[string]any{"error": err.Error()} + } else { + resultBytes, _ := json.Marshal(result) + resultStr = string(resultBytes) + } + } + } - httpReq.Header.Set("Content-Type", "application/json") - httpReq.Header.Set("Authorization", "Bearer "+c.apiKey) + // Send tool result event + eventChan <- &StreamEvent{ + Type: StreamEventToolResult, + ToolName: toolCall.Function.Name, + Result: result, + ToolCall: &toolCall, + } - client := &http.Client{} - resp, err := client.Do(httpReq) - if err != nil { - return response, fmt.Errorf("failed to send request: %w", err) - } - defer resp.Body.Close() + toolCallID := toolCall.ID + messages = append(messages, Message{ + Role: "tool", + Content: resultStr, + ToolCallID: &toolCallID, + }) + } - if resp.StatusCode != http.StatusOK { - bodyBytes, _ := io.ReadAll(resp.Body) - return response, fmt.Errorf("API error %d: %s", resp.StatusCode, string(bodyBytes)) - } + // Send iteration complete event + eventChan <- &StreamEvent{ + Type: StreamEventIterationComplete, + Iteration: iteration + 1, + } + } - if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { - return response, fmt.Errorf("failed to decode response: %w", err) - } + errChan <- fmt.Errorf("max tool iterations (%d) reached", input.MaxIterations) + }() - return + return eventChan, errChan } -func (c *Client) handleStreamingResponse(req *Request) (struct { - ChunkChan <-chan *StreamChunk - ErrChan <-chan error -}, error) { +// doStreamRequest performs the actual streaming HTTP request +func (c *Client) doStreamRequest(req *Request) (<-chan *StreamChunk, <-chan error) { chunkChan := make(chan *StreamChunk, 10) errChan := make(chan error, 1) @@ -434,8 +776,86 @@ func (c *Client) handleStreamingResponse(req *Request) (struct { } }() - return struct { - ChunkChan <-chan *StreamChunk - ErrChan <-chan error - }{ChunkChan: chunkChan, ErrChan: errChan}, nil + return chunkChan, errChan +} + +func (c *Client) buildRequest(model string, input any, stream bool) (*Request, error) { + req := &Request{ + Model: model, + Stream: stream, + } + + switch v := input.(type) { + case string: + // Simple string input + req.Messages = []Message{{Role: "user", Content: v}} + case InputObject: + req.Messages = v.Messages + req.Tools = v.Tools + req.ToolChoice = v.ToolChoice + case *InputObject: + req.Messages = v.Messages + req.Tools = v.Tools + req.ToolChoice = v.ToolChoice + case map[string]any: + // Map input + if messages, ok := v["messages"]; ok { + msgBytes, err := json.Marshal(messages) + if err != nil { + return nil, fmt.Errorf("failed to marshal messages: %w", err) + } + if err := json.Unmarshal(msgBytes, &req.Messages); err != nil { + return nil, fmt.Errorf("failed to unmarshal messages: %w", err) + } + } + if tools, ok := v["tools"]; ok { + toolBytes, err := json.Marshal(tools) + if err != nil { + return nil, fmt.Errorf("failed to marshal tools: %w", err) + } + if err := json.Unmarshal(toolBytes, &req.Tools); err != nil { + return nil, fmt.Errorf("failed to unmarshal tools: %w", err) + } + } + if toolChoice, ok := v["tool_choice"]; ok { + req.ToolChoice = toolChoice + } + default: + return nil, fmt.Errorf("unsupported input type: %T", input) + } + + return req, nil +} + +func (c *Client) handleNonStreamingResponse(req *Request) (response SendResponse, err error) { + body, err := json.Marshal(req) + if err != nil { + return response, fmt.Errorf("failed to marshal request: %w", err) + } + + httpReq, err := http.NewRequest("POST", c.baseURL+APIEndpoint, bytes.NewReader(body)) + if err != nil { + return response, fmt.Errorf("failed to create request: %w", err) + } + + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("Authorization", "Bearer "+c.apiKey) + + client := &http.Client{} + resp, err := client.Do(httpReq) + if err != nil { + return response, fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + bodyBytes, _ := io.ReadAll(resp.Body) + return response, fmt.Errorf("API error %d: %s", resp.StatusCode, string(bodyBytes)) + } + + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return response, fmt.Errorf("failed to decode response: %w", err) + } + + return } diff --git a/edgee/edgee_test.go b/edgee/edgee_test.go index 68317af..20605cb 100644 --- a/edgee/edgee_test.go +++ b/edgee/edgee_test.go @@ -890,11 +890,13 @@ func TestClient_Stream(t *testing.T) { BaseURL: server.URL, }) - chunkChan, errChan := client.Stream("gpt-4", "Hello") + eventChan, errChan := client.Stream("gpt-4", "Hello") chunks := []*StreamChunk{} - for chunk := range chunkChan { - chunks = append(chunks, chunk) + for event := range eventChan { + if event.Type == StreamEventChunk && event.Chunk != nil { + chunks = append(chunks, event.Chunk) + } } // Check for errors @@ -945,11 +947,13 @@ func TestClient_Stream(t *testing.T) { }, } - chunkChan, errChan := client.Stream("gpt-4", input) + eventChan, errChan := client.Stream("gpt-4", input) chunks := []*StreamChunk{} - for chunk := range chunkChan { - chunks = append(chunks, chunk) + for event := range eventChan { + if event.Type == StreamEventChunk && event.Chunk != nil { + chunks = append(chunks, event.Chunk) + } } // Check for errors @@ -981,7 +985,7 @@ func TestClient_Stream(t *testing.T) { BaseURL: server.URL, }) - chunkChan, errChan := client.Stream("gpt-4", "Hello") + eventChan, errChan := client.Stream("gpt-4", "Hello") // Wait for error err := <-errChan @@ -993,9 +997,9 @@ func TestClient_Stream(t *testing.T) { } // Channel should be closed - _, ok := <-chunkChan + _, ok := <-eventChan if ok { - t.Error("Expected chunk channel to be closed") + t.Error("Expected event channel to be closed") } }) @@ -1013,11 +1017,13 @@ func TestClient_Stream(t *testing.T) { BaseURL: server.URL, }) - chunkChan, errChan := client.Stream("gpt-4", "Hello") + eventChan, errChan := client.Stream("gpt-4", "Hello") chunks := []*StreamChunk{} - for chunk := range chunkChan { - chunks = append(chunks, chunk) + for event := range eventChan { + if event.Type == StreamEventChunk && event.Chunk != nil { + chunks = append(chunks, event.Chunk) + } } // Check for errors diff --git a/example/main.go b/example/main.go index fc56773..aa7d614 100644 --- a/example/main.go +++ b/example/main.go @@ -1,3 +1,8 @@ +// Example usage of Edgee Gateway SDK +// +// This example demonstrates various ways to use the SDK. +// For more focused examples, see the examples/ directory. + package main import ( @@ -8,15 +13,15 @@ import ( ) func main() { - // Create client with API key from environment variable - client, err := edgee.NewClient(nil) + // Create client with API key + client, err := edgee.NewClient("your-api-key") if err != nil { log.Fatalf("Failed to create client: %v", err) } // Test 1: Simple string input fmt.Println("Test 1: Simple string input") - response1, err := client.ChatCompletion("mistral/mistral-small-latest", "What is the capital of France?") + response1, err := client.Send("devstral2", "What is the capital of France?") if err != nil { log.Printf("Error: %v\n", err) } else { @@ -29,7 +34,7 @@ func main() { // Test 2: Full input object with messages fmt.Println("Test 2: Full input object with messages") - response2, err := client.ChatCompletion("mistral/mistral-small-latest", map[string]interface{}{ + response2, err := client.Send("devstral2", map[string]interface{}{ "messages": []map[string]string{ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Say hello!"}, @@ -42,61 +47,49 @@ func main() { } fmt.Println() - // Test 3: With tools - fmt.Println("Test 3: With tools") - response3, err := client.ChatCompletion("gpt-4o", map[string]interface{}{ - "messages": []map[string]string{ - {"role": "user", "content": "What is the weather in Paris?"}, - }, - "tools": []map[string]interface{}{ - { - "type": "function", - "function": map[string]interface{}{ - "name": "get_weather", - "description": "Get the current weather for a location", - "parameters": map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "location": map[string]string{ - "type": "string", - "description": "City name", - }, - }, - "required": []string{"location"}, - }, - }, - }, - }, - "tool_choice": "auto", - }) + // Test 3: Auto tool execution + fmt.Println("Test 3: Auto tool execution") + weatherTool := edgee.NewTool("get_weather", "Get the current weather for a location"). + AddParam("location", "string", "City name", true). + WithHandler(func(args map[string]any) (any, error) { + location, _ := args["location"].(string) + fmt.Printf(" [Tool called: get_weather(%s)]\n", location) + return map[string]any{ + "location": location, + "temperature": 22, + "condition": "sunny", + }, nil + }) + + input := edgee.NewSimpleInput("What's the weather in Paris?", weatherTool) + response3, err := client.Send("devstral2", input) if err != nil { log.Printf("Error: %v\n", err) } else { fmt.Printf("Content: %s\n", response3.Text()) - if toolCalls := response3.ToolCalls(); len(toolCalls) > 0 { - fmt.Printf("Tool calls: %+v\n", toolCalls) - } } fmt.Println() // Test 4: Streaming fmt.Println("Test 4: Streaming") - chunkChan, errChan := client.Stream("mistral/mistral-small-latest", "What is Go?") + eventChan, errChan := client.Stream("devstral2", "What is Go?") for { select { - case chunk, ok := <-chunkChan: + case event, ok := <-eventChan: if !ok { fmt.Println() return } - if text := chunk.Text(); text != "" { - fmt.Print(text) + if event.Type == edgee.StreamEventChunk && event.Chunk != nil { + if text := event.Chunk.Text(); text != "" { + fmt.Print(text) + } } case err := <-errChan: if err != nil { log.Printf("Error: %v\n", err) - return } + return } } } diff --git a/examples/simple/main.go b/examples/simple/main.go new file mode 100644 index 0000000..76862fe --- /dev/null +++ b/examples/simple/main.go @@ -0,0 +1,45 @@ +// Simple example demonstrating basic usage of the Edgee SDK +// +// This example shows how to: +// - Create an Edgee client with your API key +// - Send a simple text prompt to a model +// - Get the response text +// +// Run with: go run examples/simple/main.go + +package main + +import ( + "fmt" + "log" + + "github.com/edgee-cloud/go-sdk/edgee" +) + +func main() { + // Create the Edgee client with your API key + client, err := edgee.NewClient("your-api-key") + if err != nil { + log.Fatalf("Failed to create client: %v", err) + } + + // Send a simple text prompt to the model + response, err := client.Send("devstral2", "What is the capital of France?") + if err != nil { + log.Fatalf("Error: %v", err) + } + + // Print the response text + fmt.Printf("Response: %s\n", response.Text()) + + // You can also access metadata about the response + fmt.Printf("Model used: %s\n", response.Model) + + if response.Usage != nil { + fmt.Printf("Tokens: %d prompt + %d completion = %d total\n", + response.Usage.PromptTokens, + response.Usage.CompletionTokens, + response.Usage.TotalTokens, + ) + } +} diff --git a/examples/stream_tools/main.go b/examples/stream_tools/main.go new file mode 100644 index 0000000..51b4693 --- /dev/null +++ b/examples/stream_tools/main.go @@ -0,0 +1,92 @@ +// Streaming with automatic tool execution example +// +// This example shows how to: +// - Combine streaming with automatic tool execution +// - Receive real-time events for chunks, tool calls, and results +// - Display the response progressively while tools are being executed +// +// This is useful when you want to show progress to the user while +// tools are being executed in the background. +// +// Run with: go run examples/stream_tools/main.go + +package main + +import ( + "fmt" + "log" + + "github.com/edgee-cloud/go-sdk/edgee" +) + +func main() { + // Create the Edgee client + client, err := edgee.NewClient("your-api-key") + if err != nil { + log.Fatalf("Failed to create client: %v", err) + } + + // Define a tool using the NewTool builder + weatherTool := edgee.NewTool("get_weather", "Get the current weather for a location"). + AddParam("location", "string", "The city name", true). + WithHandler(func(args map[string]any) (any, error) { + location, _ := args["location"].(string) + + // Simulate an API call + return map[string]any{ + "location": location, + "temperature": 22, + "unit": "celsius", + "condition": "sunny", + }, nil + }) + + fmt.Println("Streaming request with auto tool execution...") + fmt.Println() + + // Create SimpleInput for auto tool execution + input := edgee.NewSimpleInput("What's the weather in Paris?", weatherTool) + + // Start a streaming request with tools + eventChan, errChan := client.Stream("devstral2", input) + + // Process events as they arrive + for { + select { + case event, ok := <-eventChan: + if !ok { + // Channel closed, we're done + fmt.Println("\n\nDone!") + return + } + + switch event.Type { + case edgee.StreamEventChunk: + // Text chunks from the model + if event.Chunk != nil { + if text := event.Chunk.Text(); text != "" { + fmt.Print(text) + } + } + + case edgee.StreamEventToolStart: + // A tool is about to be executed + fmt.Printf("\n[Calling tool: %s]\n", event.ToolCall.Function.Name) + + case edgee.StreamEventToolResult: + // A tool has finished executing + fmt.Printf("[Tool result: %s returned %v]\n", event.ToolName, event.Result) + + case edgee.StreamEventIterationComplete: + // An iteration of the agentic loop is complete + fmt.Printf("[Iteration %d complete]\n", event.Iteration) + } + + case err := <-errChan: + if err != nil { + log.Fatalf("Error: %v", err) + } + return + } + } +} diff --git a/examples/streaming/main.go b/examples/streaming/main.go new file mode 100644 index 0000000..1bce036 --- /dev/null +++ b/examples/streaming/main.go @@ -0,0 +1,62 @@ +// Streaming example demonstrating real-time response processing +// +// This example shows how to: +// - Stream responses from a model in real-time +// - Process chunks as they arrive +// - Display text progressively (like a typing effect) +// +// Run with: go run examples/streaming/main.go + +package main + +import ( + "fmt" + "log" + + "github.com/edgee-cloud/go-sdk/edgee" +) + +func main() { + // Create the Edgee client + client, err := edgee.NewClient("your-api-key") + if err != nil { + log.Fatalf("Failed to create client: %v", err) + } + + fmt.Println("Asking the model to count from 1 to 10...") + fmt.Println() + + // Start a streaming request + eventChan, errChan := client.Stream("devstral2", "Count from 1 to 10") + + // Process each event as it arrives + for { + select { + case event, ok := <-eventChan: + if !ok { + // Channel closed, we're done + fmt.Println("\n\n[Stream finished]") + return + } + + // For regular streaming, we only get chunk events + if event.Type == edgee.StreamEventChunk && event.Chunk != nil { + // Print each text chunk as it arrives (no newline, for typing effect) + if text := event.Chunk.Text(); text != "" { + fmt.Print(text) + } + + // Check if the stream is complete + if reason := event.Chunk.FinishReason(); reason != "" { + fmt.Printf("\n\n[Finish reason: %s]", reason) + } + } + + case err := <-errChan: + if err != nil { + log.Fatalf("Error during streaming: %v", err) + } + return + } + } +} diff --git a/examples/tools_auto/main.go b/examples/tools_auto/main.go new file mode 100644 index 0000000..5fc2e66 --- /dev/null +++ b/examples/tools_auto/main.go @@ -0,0 +1,68 @@ +// Auto tool execution example +// +// This example shows how to: +// - Define tools using the NewTool builder +// - Let the SDK automatically execute tools when the model calls them +// - Get the final response after all tool calls are processed +// +// The SDK handles the agentic loop automatically: when the model requests +// a tool call, the SDK executes your handler and sends the result back +// to the model until a final response is generated. +// +// Run with: go run examples/tools_auto/main.go + +package main + +import ( + "fmt" + "log" + + "github.com/edgee-cloud/go-sdk/edgee" +) + +func main() { + // Create the Edgee client + client, err := edgee.NewClient("your-api-key") + if err != nil { + log.Fatalf("Failed to create client: %v", err) + } + + // Define a tool using the NewTool builder + // The builder creates an ExecutableTool with: + // - name: the function name the model will call + // - description: helps the model understand when to use this tool + // - parameters: defined using AddParam/AddEnumParam + // - handler: function that executes when the tool is called + weatherTool := edgee.NewTool("get_weather", "Get the current weather for a location"). + AddParam("location", "string", "The city name", true). + WithHandler(func(args map[string]any) (any, error) { + // This handler is called automatically when the model uses this tool + location, _ := args["location"].(string) + + // In a real app, you would call an actual weather API here + fmt.Printf("[Tool executed: get_weather for %s]\n", location) + + return map[string]any{ + "location": location, + "temperature": 22, + "unit": "celsius", + "condition": "sunny", + }, nil + }) + + // Create a SimpleInput with your prompt and tools + // The SDK will automatically handle tool execution + input := edgee.NewSimpleInput("What's the weather in Paris?", weatherTool) + + fmt.Println("Sending request with auto tool execution...") + fmt.Println() + + // Send the request - the SDK handles the agentic loop automatically + response, err := client.Send("devstral2", input) + if err != nil { + log.Fatalf("Error: %v", err) + } + + // Print the final response (after all tools have been executed) + fmt.Printf("\nFinal response: %s\n", response.Text()) +}