Skip to content
This repository has been archived by the owner on Jan 9, 2025. It is now read-only.

feat(anthropic): add Anthropic component #176

Merged
merged 8 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions ai/anthropic/v0/README.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
title: "Anthropic"
lang: "en-US"
draft: false
description: "Learn about how to set up a VDP Anthropic component https://github.com/instill-ai/instill-core"
---

The Anthropic component is an AI component that allows users to connect the AI models served on the Anthropic Platform.
It can carry out the following tasks:

- [Text Generation Chat](#text-generation-chat)



## Release Stage

`Alpha`



## Configuration

The component configuration is defined and maintained [here](https://github.com/instill-ai/component/blob/main/ai/anthropic/v0/config/definition.json).




## Setup


| Field | Field ID | Type | Note |
| :--- | :--- | :--- | :--- |
| API Key (required) | `api-key` | string | Fill your Anthropic API key. To find your keys, visit the Anthropic console page. |




## Supported Tasks

### Text Generation Chat

Provide text outputs in response to text inputs.


| Input | ID | Type | Description |
| :--- | :--- | :--- | :--- |
| Task ID (required) | `task` | string | `TASK_TEXT_GENERATION_CHAT` |
| Model Name (required) | `model-name` | string | The Anthropic model to be used. |
| Prompt (required) | `prompt` | string | The prompt text |
| System message | `system-message` | string | The system message helps set the behavior of the assistant. For example, you can modify the personality of the assistant or provide specific instructions about how it should behave throughout the conversation. By default, the model’s behavior is using a generic message as "You are a helpful assistant." |
| Extra Parameters | `extra-params` | object | Extra Parameters |
| Prompt Images | `prompt-images` | array[string] | The prompt images (Note: The prompt images will be injected in the order they are provided to the 'prompt' message. Anthropic doesn't support sending images via image-url, use this field instead) |
| Chat history | `chat-history` | array[object] | Incorporate external chat history, specifically previous messages within the conversation. Please note that System Message will be ignored and will not have any effect when this field is populated. Each message should adhere to the format: : \{"role": "The message role, i.e. 'system', 'user' or 'assistant'", "content": "message content"\{. |
| Seed | `seed` | integer | The seed (Note: Not supported by Anthropic Models) |
| Temperature | `temperature` | number | The temperature for sampling |
| Top K | `top-k` | integer | Top k for sampling |
| Max new tokens | `max-new-tokens` | integer | The maximum number of tokens for model to generate |



| Output | ID | Type | Description |
| :--- | :--- | :--- | :--- |
| Text | `text` | string | Model Output |







16 changes: 16 additions & 0 deletions ai/anthropic/v0/assets/anthropic.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
54 changes: 54 additions & 0 deletions ai/anthropic/v0/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package anthropic

import (
"github.com/instill-ai/component/internal/util/httpclient"
"go.uber.org/zap"
"google.golang.org/protobuf/types/known/structpb"
)

type anthropicClient struct {
httpClient *httpclient.Client
}

func newClient(apiKey string, baseURL string, logger *zap.Logger) *anthropicClient {
c := httpclient.New("Anthropic", baseURL,
httpclient.WithLogger(logger),
httpclient.WithEndUserError(new(errBody)),
)
// Anthropic requires an API key to be set in the header "x-api-key" rather than normal "Authorization" header.
c.Header.Set("X-Api-Key", apiKey)
c.Header.Set("anthropic-version", "2023-06-01")

return &anthropicClient{httpClient: c}
}

func (cl *anthropicClient) generateTextChat(request messagesReq) (messagesResp, error) {
resp := messagesResp{}
req := cl.httpClient.R().SetResult(&resp).SetBody(request)
if _, err := req.Post(messagesPath); err != nil {
return resp, err
}
return resp, nil
}

type errBody struct {
Error struct {
Message string `json:"message"`
} `json:"error"`
}

func (e errBody) Message() string {
return e.Error.Message
}

func getBasePath(setup *structpb.Struct) string {
v, ok := setup.GetFields()["base-path"]
if !ok {
return host
}
return v.GetStringValue()
}

func getAPIKey(setup *structpb.Struct) string {
return setup.GetFields()[cfgAPIKey].GetStringValue()
}
220 changes: 220 additions & 0 deletions ai/anthropic/v0/component_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
package anthropic

import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"

qt "github.com/frankban/quicktest"
"go.uber.org/zap"
"google.golang.org/protobuf/types/known/structpb"

"github.com/instill-ai/component/base"
"github.com/instill-ai/component/internal/util/httpclient"
"github.com/instill-ai/x/errmsg"
)

const (
apiKey = "123"
errResp = `
{
"error": {
"message": "Incorrect API key provided."
}
}`
)

func TestComponent_Execute(t *testing.T) {
c := qt.New(t)
ctx := context.Background()

bc := base.Component{Logger: zap.NewNop()}
connector := Init(bc)

testcases := []struct {
name string
task string
path string
contentType string
}{
{
name: "text generation",
task: textGenerationTask,
path: messagesPath,
contentType: httpclient.MIMETypeJSON,
},
}

// TODO we'll likely want to have a test function per task and test at
// least OK, NOK. For now, only errors are tested in order to verify
// end-user messages.
// 2024-06-21 summer intern An-Che: Implemented text generation test case
for _, tc := range testcases {
c.Run("nok - "+tc.name+" 401", func(c *qt.C) {
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Method, qt.Equals, http.MethodPost)
c.Check(r.URL.Path, qt.Equals, tc.path)
c.Check(r.Header.Get("X-Api-Key"), qt.Equals, apiKey)

c.Check(r.Header.Get("Content-Type"), qt.Matches, tc.contentType)

w.Header().Set("Content-Type", httpclient.MIMETypeJSON)
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprintln(w, errResp)
})

anthropicServer := httptest.NewServer(h)
c.Cleanup(anthropicServer.Close)

setup, err := structpb.NewStruct(map[string]any{
"base-path": anthropicServer.URL,
"api-key": apiKey,
})
c.Assert(err, qt.IsNil)

exec, err := connector.CreateExecution(nil, setup, tc.task)
c.Assert(err, qt.IsNil)

pbIn := new(structpb.Struct)
_, err = exec.Execution.Execute(ctx, []*structpb.Struct{pbIn})
c.Check(err, qt.IsNotNil)

want := "Anthropic responded with a 401 status code. Incorrect API key provided."
c.Check(errmsg.Message(err), qt.Equals, want)
})
}
c.Run("nok - unsupported task", func(c *qt.C) {
task := "FOOBAR"

_, err := connector.CreateExecution(nil, nil, task)
c.Check(err, qt.ErrorMatches, "unsupported task")
})
}

func TestComponent_Connection(t *testing.T) {
c := qt.New(t)

bc := base.Component{Logger: zap.NewNop()}
connector := Init(bc)

c.Run("nok - error", func(c *qt.C) {
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Method, qt.Equals, http.MethodGet)

w.Header().Set("Content-Type", httpclient.MIMETypeJSON)
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprintln(w, errResp)
})

anthropicServer := httptest.NewServer(h)
c.Cleanup(anthropicServer.Close)

_, err := structpb.NewStruct(map[string]any{
"base-path": anthropicServer.URL,
})
c.Assert(err, qt.IsNil)
})

c.Run("ok - disconnected", func(c *qt.C) {
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Method, qt.Equals, http.MethodGet)

w.Header().Set("Content-Type", httpclient.MIMETypeJSON)
fmt.Fprintln(w, `{}`)
})

anthropicServer := httptest.NewServer(h)
c.Cleanup(anthropicServer.Close)

_, err := structpb.NewStruct(map[string]any{
"base-path": anthropicServer.URL,
})
c.Assert(err, qt.IsNil)
})

c.Run("ok - connected", func(c *qt.C) {
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Method, qt.Equals, http.MethodGet)

w.Header().Set("Content-Type", httpclient.MIMETypeJSON)
fmt.Fprintln(w, `{"data": [{}]}`)
})

anthropicServer := httptest.NewServer(h)
c.Cleanup(anthropicServer.Close)

setup, err := structpb.NewStruct(map[string]any{
"base-path": anthropicServer.URL,
})
c.Assert(err, qt.IsNil)

err = connector.Test(nil, setup)
c.Check(err, qt.IsNil)
})
}

type MockAnthropicClient struct{}

func (m *MockAnthropicClient) generateTextChat(request messagesReq) (messagesResp, error) {

messageCount := len(request.Messages)
message := fmt.Sprintf("Hi! My name is Claude. (messageCount: %d)", messageCount)
resp := messagesResp{
ID: "msg_013Zva2CMHLNnXjNJJKqJ2EF",
Type: "message",
Role: "assistant",
Content: []content{{Text: message, Type: "text"}},
Model: "claude-3-5-sonnet-20240620",
StopReason: "end_turn",
Usage: usage{InputTokens: 10, OutputTokens: 25},
}

return resp, nil
}

func TestComponent_Generation(t *testing.T) {
c := qt.New(t)
ctx := context.Background()
bc := base.Component{Logger: zap.NewNop()}
connector := Init(bc)

mockHistory := []message{
{Role: "user", Content: []content{{Type: "text", Text: "Answer the following question in traditional chinses"}}},
{Role: "assistant", Content: []content{{Type: "text", Text: "沒問題"}}},
}

tc := struct {
input map[string]any
wantResp messagesOutput
}{
input: map[string]any{"prompt": "Hi! What's your name?", "chat-history": mockHistory},
wantResp: messagesOutput{Text: "Hi! My name is Claude. (messageCount: 3)"},
}

c.Run("ok - generation", func(c *qt.C) {
setup, err := structpb.NewStruct(map[string]any{
"api-key": apiKey,
})
c.Assert(err, qt.IsNil)

e := &execution{
ComponentExecution: base.ComponentExecution{Component: connector, SystemVariables: nil, Setup: setup, Task: textGenerationTask},
client: &MockAnthropicClient{},
}
e.execute = e.generateText
exec := &base.ExecutionWrapper{Execution: e}

pbIn, err := base.ConvertToStructpb(tc.input)
c.Assert(err, qt.IsNil)

got, err := exec.Execution.Execute(ctx, []*structpb.Struct{pbIn})
c.Assert(err, qt.IsNil)

wantJSON, err := json.Marshal(tc.wantResp)
c.Assert(err, qt.IsNil)
c.Check(wantJSON, qt.JSONEquals, got[0].AsMap())
})
}
20 changes: 20 additions & 0 deletions ai/anthropic/v0/config/definition.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"availableTasks": [
"TASK_TEXT_GENERATION_CHAT"
],
"custom": false,
"documentationUrl": "https://www.instill.tech/docs/latest/vdp/ai-connectors/anthropic",
"icon": "assets/anthropic.svg",
"id": "anthropic",
"public": true,
"title": "Anthropic",
"description": "Connect the AI models served on the Anthropic Platform",
"type": "COMPONENT_TYPE_AI",
"uid": "42bdb620-74ad-486a-82da-25e45431b42c",
"vendor": "Anthropic",
"vendorAttributes": {},
"version": "0.1.1",
"sourceUrl": "https://github.com/instill-ai/component/blob/main/ai/anthropic/v0",
"releaseStage": "RELEASE_STAGE_ALPHA"
}

Loading
Loading