Skip to content

Commit

Permalink
messages beta no streaming (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
madebywelch authored Dec 24, 2023
1 parent adc37b6 commit 4f967e7
Show file tree
Hide file tree
Showing 11 changed files with 364 additions and 14 deletions.
48 changes: 46 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -105,7 +105,7 @@ func main() {
}
```

### Streaming Example Output
### Completion Streaming Example Output

```
There
Expand All @@ -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:
Expand Down
32 changes: 20 additions & 12 deletions pkg/anthropic/events.go
Original file line number Diff line number Diff line change
@@ -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"
)
2 changes: 2 additions & 0 deletions pkg/anthropic/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
75 changes: 75 additions & 0 deletions pkg/anthropic/integration_tests/message_integration_test.go
Original file line number Diff line number Diff line change
@@ -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
55 changes: 55 additions & 0 deletions pkg/anthropic/message.go
Original file line number Diff line number Diff line change
@@ -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
}
99 changes: 99 additions & 0 deletions pkg/anthropic/message_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
20 changes: 20 additions & 0 deletions pkg/anthropic/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
18 changes: 18 additions & 0 deletions pkg/anthropic/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit 4f967e7

Please sign in to comment.