diff --git a/README.md b/README.md index 447f8fc..8a56d4f 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ func main() { The sky appears blue to us due to the way the atmosphere scatters light from the sun ``` -## Streaming Example +## Completion Streaming Example ```go package main @@ -105,7 +105,7 @@ func main() { } ``` -### Streaming Example Output +### Completion Streaming Example Output ``` There @@ -119,6 +119,50 @@ sky appears ``` +## (BETA) Messages Example + +```go +package main + +import ( + "fmt" + + "github.com/madebywelch/anthropic-go/v2/pkg/anthropic" +) + +func main() { + client, err := anthropic.NewClient("your-api-key") + if err != nil { + panic(err) + } + + // Prepare a message request + request := &anthropic.MessageRequest{ + Model: anthropic.ClaudeV2_1, + MaxTokensToSample: 10, + Messages: []anthropic.MessagePartRequest{{Role: "user", Content: "Hello, Anthropics!"}}, + } + + // Call the Message method + response, err := client.Message(request) + if err != nil { + panic(err) + } + + fmt.Println(response.Content) +} +``` + +### Messages Example Output + +``` +{ID:msg_01W3bZkuMrS3h1ehqTdF84vv Type:message Model:claude-2.1 Role:assistant Content:[{Type:text Text:Hello!}] StopReason:end_turn Stop: StopSequence:} +``` + +## Messages Streaming Example + +### Not yet implemented + ## Contributing Contributions to this project are welcome. To contribute, follow these steps: diff --git a/pkg/anthropic/events.go b/pkg/anthropic/events.go index f42a505..5799063 100644 --- a/pkg/anthropic/events.go +++ b/pkg/anthropic/events.go @@ -1,14 +1,22 @@ package anthropic -// CompletionEvent represents the data related to a completion event. -type CompletionEvent struct { - Completion string // The completion text from the model. -} - -// ErrorEvent represents an error event that occurs during streaming. -type ErrorEvent struct { - Error string // A string description of the error. -} - -// PingEvent is an empty struct representing a ping event. -type PingEvent struct{} +// Common types for different events +type MessageEventType string + +// Define a separate type for completion events +type CompletionEventType string + +const ( + // Constants for message event types + MessageEventTypeMessageStart MessageEventType = "message_start" + MessageEventTypeContentBlockStart MessageEventType = "content_block_start" + MessageEventTypePing MessageEventType = "ping" + MessageEventTypeContentBlockDelta MessageEventType = "content_block_delta" + MessageEventTypeContentBlockStop MessageEventType = "content_block_stop" + MessageEventTypeMessageDelta MessageEventType = "message_delta" + MessageEventTypeMessageStop MessageEventType = "message_stop" + + // Constants for completion event types + CompletionEventTypeCompletion CompletionEventType = "completion" + CompletionEventTypePing CompletionEventType = "ping" +) diff --git a/pkg/anthropic/http.go b/pkg/anthropic/http.go index f561c58..35f8441 100644 --- a/pkg/anthropic/http.go +++ b/pkg/anthropic/http.go @@ -8,6 +8,8 @@ import ( const ( // AnthropicAPIVersion is the version of the Anthropics API that this client is compatible with. AnthropicAPIVersion = "2023-06-01" + // AnthropicAPIMessagesBeta is the beta version of the Anthropics API that enables the messages endpoint. + AnthropicAPIMessagesBeta = "messages-2023-12-15" ) // DoRequest sends an HTTP request and returns the response, handling any non-OK HTTP status codes. diff --git a/pkg/anthropic/integration_tests/message_integration_test.go b/pkg/anthropic/integration_tests/message_integration_test.go new file mode 100644 index 0000000..d52f138 --- /dev/null +++ b/pkg/anthropic/integration_tests/message_integration_test.go @@ -0,0 +1,75 @@ +package integration_tests + +import ( + "os" + "testing" + + "github.com/madebywelch/anthropic-go/v2/pkg/anthropic" +) + +func TestMessageIntegration(t *testing.T) { + // Get the API key from the environment + apiKey := os.Getenv("ANTHROPIC_API_KEY") + if apiKey == "" { + t.Skip("ANTHROPIC_API_KEY environment variable is not set, skipping integration test") + } + + // Create a new client + client, err := anthropic.NewClient(apiKey) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + // Prepare a message request + request := &anthropic.MessageRequest{ + Model: anthropic.ClaudeV2_1, + MaxTokensToSample: 10, + Messages: []anthropic.MessagePartRequest{{Role: "user", Content: "Hello, Anthropics!"}}, + } + + // Call the Message method + response, err := client.Message(request) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + // Basic assertion to check if a message response is returned + if response == nil || len(response.Content) == 0 { + t.Errorf("Expected a message response, got none or empty content") + } + + // Ensure the response contains populated ID + if response.ID == "" { + t.Errorf("Expected a message response with a non-empty ID, got none") + } +} + +func TestMessageErrorHandlingIntegration(t *testing.T) { + // Get the API key from the environment + apiKey := os.Getenv("ANTHROPIC_API_KEY") + if apiKey == "" { + t.Skip("ANTHROPIC_API_KEY environment variable is not set, skipping integration test") + } + + // Create a new client + client, err := anthropic.NewClient(apiKey) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + // Prepare a message request + request := &anthropic.MessageRequest{ + Model: anthropic.ClaudeV2_1, + Messages: []anthropic.MessagePartRequest{{Role: "user", Content: "Hello, Anthropics!"}}, + } + + // Call the Message method expecting an error + _, err = client.Message(request) + // We're expecting an error here because we didn't set the required field MaxTokensToSample + if err == nil { + t.Fatal("Expected an error, got none") + } +} + +// - TODO: TestMessageWithParametersIntegration: to test sending a message with various parameters +// - TODO: TestMessageStreamIntegration: to ensure the function correctly handles streaming requests diff --git a/pkg/anthropic/message.go b/pkg/anthropic/message.go new file mode 100644 index 0000000..481dd17 --- /dev/null +++ b/pkg/anthropic/message.go @@ -0,0 +1,55 @@ +package anthropic + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" +) + +func (c *Client) Message(req *MessageRequest) (*MessageResponse, error) { + if req.Stream { + return nil, fmt.Errorf("cannot use Message with streaming enabled, use MessageStream instead (not yet supported)") + } + + return c.sendMessageRequest(req) +} + +// MessageStream (NOT YET SUPPORTED) returns a channel of StreamResponse objects and a channel of errors. +func (c *Client) MessageStream(req *MessageRequest) (<-chan StreamResponse, <-chan error) { + return nil, nil +} + +func (c *Client) sendMessageRequest(req *MessageRequest) (*MessageResponse, error) { + // Marshal the request to JSON + data, err := json.Marshal(req) + if err != nil { + return nil, fmt.Errorf("error marshalling completion request: %w", err) + } + + // Create the HTTP request + requestURL := fmt.Sprintf("%s/v1/messages", c.baseURL) + request, err := http.NewRequest("POST", requestURL, bytes.NewBuffer(data)) + if err != nil { + return nil, fmt.Errorf("error creating new request: %w", err) + } + request.Header.Set("Content-Type", "application/json") + request.Header.Set("X-Api-Key", c.apiKey) + request.Header.Set("anthropic-beta", AnthropicAPIMessagesBeta) + + // Use the DoRequest method to send the HTTP request + response, err := c.DoRequest(request) + if err != nil { + return nil, fmt.Errorf("error sending completion request: %w", err) + } + defer response.Body.Close() + + // Decode the response body to a MessageResponse object + var messageResponse MessageResponse + err = json.NewDecoder(response.Body).Decode(&messageResponse) + if err != nil { + return nil, fmt.Errorf("error decoding message response: %w", err) + } + + return &messageResponse, nil +} diff --git a/pkg/anthropic/message_test.go b/pkg/anthropic/message_test.go new file mode 100644 index 0000000..f7aeecb --- /dev/null +++ b/pkg/anthropic/message_test.go @@ -0,0 +1,99 @@ +package anthropic + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func TestMessage(t *testing.T) { + // Mock server for successful message response + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + resp := MessageResponse{ + ID: "12345", + Type: "testType", + Model: "testModel", + Role: "user", + Content: []MessagePartResponse{{ + Type: "text", + Text: "Test message", + }}, + } + json.NewEncoder(w).Encode(resp) + })) + defer testServer.Close() + + // Create a new client with the test server's URL + client, err := NewClient("fake-api-key") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + client.baseURL = testServer.URL // Override baseURL to point to the test server + + // Prepare a message request + request := &MessageRequest{ + Model: ClaudeV2_1, + Messages: []MessagePartRequest{{Role: "user", Content: "Hello"}}, + } + + // Call the Message method + response, err := client.Message(request) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + // Check the response + expectedContent := "Test message" + if len(response.Content) == 0 || response.Content[0].Text != expectedContent { + t.Errorf("Expected message %q, got %q", expectedContent, response.Content[0].Text) + } +} + +func TestMessageErrorHandling(t *testing.T) { + // Mock server for error response + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + })) + defer testServer.Close() + + // Create a new client with the test server's URL + client, err := NewClient("fake-api-key") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + client.baseURL = testServer.URL // Override baseURL to point to the test server + + // Prepare a message request + request := &MessageRequest{ + Model: ClaudeV2_1, + Messages: []MessagePartRequest{{Role: "user", Content: "Hello"}}, + } + + // Call the Message method expecting an error + _, err = client.Message(request) + if err == nil { + t.Fatal("Expected an error, got none") + } +} + +func TestMessageStreamNotSupported(t *testing.T) { + // Create client + client, err := NewClient("fake-api-key") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + // Prepare a message request with streaming set to true + request := &MessageRequest{ + Model: ClaudeV2_1, + Messages: []MessagePartRequest{{Role: "user", Content: "Hello"}}, + Stream: true, + } + + // Call the Message method expecting an error + _, err = client.Message(request) + if err == nil { + t.Fatal("Expected an error for streaming not supported, got none") + } +} diff --git a/pkg/anthropic/request.go b/pkg/anthropic/request.go index 42ba7b0..dd93459 100644 --- a/pkg/anthropic/request.go +++ b/pkg/anthropic/request.go @@ -24,3 +24,23 @@ func NewCompletionRequest(prompt string, options ...CompletionOption) *Completio } return request } + +// MessagePartRequest is a subset of the request for the Anthropic API message request. +type MessagePartRequest struct { + Role string `json:"role"` + Content string `json:"content"` +} + +// MessageRequest is the request to the Anthropic API for a message request. +type MessageRequest struct { + Model Model `json:"model"` + Messages []MessagePartRequest `json:"messages"` + MaxTokensToSample int `json:"max_tokens"` + SystemPrompt string `json:"system,omitempty"` // optional + Metadata interface{} `json:"metadata,omitempty"` // optional + StopSequences []string `json:"stop_sequences,omitempty"` // optional + Stream bool `json:"stream,omitempty"` // optional + Temperature float64 `json:"temperature,omitempty"` // optional + TopK int `json:"top_k,omitempty"` // optional + TopP float64 `json:"top_p,omitempty"` // optional +} diff --git a/pkg/anthropic/response.go b/pkg/anthropic/response.go index ca63f8e..fc8a69d 100644 --- a/pkg/anthropic/response.go +++ b/pkg/anthropic/response.go @@ -15,3 +15,21 @@ type StreamResponse struct { Stop string `json:"stop"` LogID string `json:"log_id"` } + +// MessageResponse is a subset of the response from the Anthropic API for a message response. +type MessagePartResponse struct { + Type string `json:"type"` + Text string `json:"text"` +} + +// MessageResponse is the response from the Anthropic API for a message response. +type MessageResponse struct { + ID string `json:"id"` + Type string `json:"type"` + Model string `json:"model"` + Role string `json:"role"` + Content []MessagePartResponse `json:"content"` + StopReason string `json:"stop_reason"` + Stop string `json:"stop"` + StopSequence string `json:"stop_sequence"` +} diff --git a/pkg/examples/regular_completion/example.go b/pkg/examples/completion/regular/example.go similarity index 100% rename from pkg/examples/regular_completion/example.go rename to pkg/examples/completion/regular/example.go diff --git a/pkg/examples/stream_completion/example.go b/pkg/examples/completion/stream/example.go similarity index 100% rename from pkg/examples/stream_completion/example.go rename to pkg/examples/completion/stream/example.go diff --git a/pkg/examples/messages/regular/example.go b/pkg/examples/messages/regular/example.go new file mode 100644 index 0000000..4ce0c1b --- /dev/null +++ b/pkg/examples/messages/regular/example.go @@ -0,0 +1,29 @@ +package main + +import ( + "fmt" + + "github.com/madebywelch/anthropic-go/v2/pkg/anthropic" +) + +func main() { + client, err := anthropic.NewClient("your-api-key") + if err != nil { + panic(err) + } + + // Prepare a message request + request := &anthropic.MessageRequest{ + Model: anthropic.ClaudeV2_1, + MaxTokensToSample: 10, + Messages: []anthropic.MessagePartRequest{{Role: "user", Content: "Hello, Anthropics!"}}, + } + + // Call the Message method + response, err := client.Message(request) + if err != nil { + panic(err) + } + + fmt.Println(response.Content) +}