Skip to content

Commit

Permalink
Merge pull request #11 from wladiston/provider-anthropic
Browse files Browse the repository at this point in the history
Add Anthropic as a provider
  • Loading branch information
wladpaiva authored Oct 27, 2023
2 parents f09aa54 + 53d3780 commit f2fabdc
Show file tree
Hide file tree
Showing 16 changed files with 306 additions and 91 deletions.
5 changes: 5 additions & 0 deletions .changeset/five-cheetahs-confess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'aibitat': patch
---

Added Anthropic Provider 🎊
6 changes: 5 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@ BROWSERLESS_TOKEN="your token"

# Get your OpenAI API key from
# https://platform.openai.com/account/api-keys
OPENAI_API_KEY="your key"
OPENAI_API_KEY="your key"

# Get your Anthropic API key from
# https://console.anthropic.com/account/keys
ANTHROPIC_API_KEY="your key"
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ bun run index.ts
| Provider | Status |
| ------------ | ------ |
| OpenAI ||
| Anthropic | 🕝 |
| Anthropic | |
| Cohere | 🕝 |
| Fireworks.ai | 🕝 |
| Hugging Face | 🕝 |
Expand Down Expand Up @@ -232,6 +232,8 @@ The following events are available:

## Contributing

Copy the `.env.example` file to `.env` and add your keys/tokens in there.

To install dependencies:

```bash
Expand Down
Binary file modified bun.lockb
Binary file not shown.
5 changes: 4 additions & 1 deletion examples/latest-news-aibitat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import * as cheerio from 'cheerio'
import {AIbitat} from '../src'
import {cli} from '../src/plugins'

export const aibitat = new AIbitat()
export const aibitat = new AIbitat({
provider: 'anthropic',
model: 'claude-2',
})
.use(cli())
.function({
name: 'aibitat-releases',
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"typescript": "^5.0.0"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.8.1",
"@inquirer/prompts": "^3.2.0",
"chalk": "^5.3.0",
"debug": "^4.3.4",
Expand Down
3 changes: 1 addition & 2 deletions src/aibitat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import OpenAI from 'openai'

import {AIbitat, type AIbitatProps} from './aibitat.ts'
import {RateLimitError} from './error.ts'
import {AIProvider} from './providers/index.ts'
import {type Message} from './types.ts'
import {AIProvider, Message} from './providers/index.ts'

// HACK: Mock the AI provider.
// This is still needed because Bun doesn't support mocking modules yet.
Expand Down
16 changes: 15 additions & 1 deletion src/aibitat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import debug from 'debug'
import {APIError} from './error.ts'
import {
AIProvider,
AnthropicProvider,
OpenAIProvider,
type AnthropicModel,
type OpenAIModel,
} from './providers/index.ts'

Expand All @@ -15,12 +17,22 @@ const log = debug('autogen:chat-aibitat')
* The provider config to use for the AI.
*/
export type ProviderConfig =
// FIX: should only show Openai models when there's no provider
| {
/** The OpenAI API provider */
provider?: 'openai'
/** The model to use with the OpenAI */
model?: OpenAIModel
}
| {
/** The custom AI provider */
provider: 'anthropic'
/**
* The model to use with the Anthropic API.
* @default 'claude-2'
*/
model?: AnthropicModel
}
| {
/** The custom AI provider */
provider: AIProvider<unknown>
Expand Down Expand Up @@ -286,7 +298,7 @@ export class AIbitat {
}
return {
maxRounds: 10,
role: 'Group chat manager.',
role: '',
...config,
}
}
Expand Down Expand Up @@ -824,6 +836,8 @@ ${this.getHistory({to: route.to})
switch (config.provider) {
case 'openai':
return new OpenAIProvider({model: config.model})
case 'anthropic':
return new AnthropicProvider({model: config.model})

default:
throw new Error(
Expand Down
1 change: 0 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export * from './aibitat.ts'
export * from './providers'
export * from './types.ts'
15 changes: 14 additions & 1 deletion src/providers/ai-provider.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
import {FunctionDefinition} from '../aibitat.ts'
import {Message} from '../types.ts'

export type Role = 'system' | 'user' | 'assistant'

export type Message = {
/**
* The contents of the message.
*/
content: string

/**
* The role of the messages author. One of `system`, `user` or `assistant`
*/
role: Role
}

/**
* A service that provides an AI client to create a completion.
Expand Down
39 changes: 39 additions & 0 deletions src/providers/anthropic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {beforeEach, describe, expect, mock, test} from 'bun:test'

import {AuthorizationError} from '../error.ts'
import {Message} from './ai-provider.ts'
import {AnthropicProvider} from './anthropic.ts'

// NOTE: some tests are skipped because it requires a way to mock the http requests.

// // ANTHROPIC - https://docs.anthropic.com/claude/reference/errors-and-rate-limits
// 400 - Invalid request: there was an issue with the format or content of your request.
// 401 - Unauthorized: there's an issue with your API key.
// 403 - Forbidden: your API key does not have permission to use the specified resource.
// 404 - Not found: the requested resource was not found.
// 429 - Your account has hit a rate limit.
// 500 - An unexpected error has occurred internal to Anthropic's systems.
// 529 - Anthropic's API is temporarily overloaded.

const message: Message[] = [
{
content: 'Hello',
role: 'user',
},
]

test('should throw an error when there`s an authorization error', async () => {
const provider = new AnthropicProvider({
options: {
apiKey: 'invalid',
},
})

await expect(provider.create(message)).rejects.toBeInstanceOf(
AuthorizationError,
)
})

test.todo('should throw a generic error when something else happens', () => {})
test.todo('should throw a RateLimitError', () => {})
test.todo('should throw a ServerError', () => {})
216 changes: 216 additions & 0 deletions src/providers/anthropic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import Anthropic, {ClientOptions} from '@anthropic-ai/sdk'
import debug from 'debug'

import {FunctionDefinition} from '../aibitat.ts'
import {
APIError,
AuthorizationError,
RateLimitError,
ServerError,
UnknownError,
} from '../error.ts'
import {AIProvider, Message} from './ai-provider.ts'

const log = debug('autogen:provider:anthropic')

/**
* The model to use for the Anthropic API.
*/
export type AnthropicModel = Anthropic.CompletionCreateParams['model']

/**
* The configuration for the Anthropic provider.
*/
export type AnthropicProviderConfig = {
/**
* The options for the Anthropic client.
* @default {apiKey: process.env.ANTHROPIC_API_KEY}
*/
options?: ClientOptions
/**
* The model to use for the Anthropic API.
* @default 'claude-2'
*/
model?: AnthropicModel
}

/**
* The provider for the OpenAI API.
* By default, the model is set to 'claude-2'.
*/
export class AnthropicProvider extends AIProvider<Anthropic> {
private model: AnthropicModel

constructor(config: AnthropicProviderConfig = {}) {
const {
options = {
apiKey: process.env.ANTHROPIC_API_KEY,
maxRetries: 3,
},
model = 'claude-2',
} = config

const client = new Anthropic(options)

super(client)

this.model = model
}

/**
* Create a completion based on the received messages.
*
* @param messages A list of messages to send to the Anthropic API.
* @returns The completion.
*/
async create(
messages: Message[],
functions?: FunctionDefinition[],
): Promise<string> {
log(`calling 'anthropic.completions.create' with model '${this.model}'`)

// clone messages to avoid mutating the original array
const promptMessages = [...messages]

if (functions) {
const functionPrompt = `<functions>You have been trained to directly call a Javascript function passing a JSON Schema parameter as a response to this chat. This function will return a string that you can use to keep chatting.
Here is a list of functions available to you:
${JSON.stringify(
functions.map(({handler, ...rest}) => rest),
null,
2,
)}
When calling any of those function in order to complete your task, respond only this JSON format. Do not include any other information or any other stuff.
Function call format:
{
function_name: "givenfunctionname",
parameters: {}
}
</functions>`
// add function prompt after the first message
promptMessages.splice(1, 0, {
content: functionPrompt,
role: 'system',
})
}

const prompt = promptMessages
.map(message => {
const {content, role} = message

switch (role) {
case 'system':
return content
? `${Anthropic.HUMAN_PROMPT} <admin>${content}</admin>`
: ''

case 'user':
return `${Anthropic.HUMAN_PROMPT} ${content}`

case 'assistant':
return `${Anthropic.AI_PROMPT} ${content}`

default:
return content
}
})
.filter(Boolean)
.join('\n')
.concat(` ${Anthropic.AI_PROMPT}`)

try {
const response = await this.client.completions.create({
model: this.model,
max_tokens_to_sample: 300,
stream: false,
prompt,
})

const result = response.completion

// Handle function calls if the model returns a function call
if (result.includes('function_name') && functions) {
const functionResponse = await this.callFunction(result, functions)

return await this.create(
[
...messages,
// extend conversation with function response
{
role: 'user',
content: functionResponse,
},
],
functions,
)
}

return result
} catch (error) {
// if (error instanceof Anthropic.BadRequestError) {
// throw new Error(error.message)
// }

if (
error instanceof Anthropic.AuthenticationError ||
error instanceof Anthropic.PermissionDeniedError
) {
throw new AuthorizationError(error.message)
}

// if (error instanceof Anthropic.NotFoundError) {
// throw new Error(error.message)
// }

// if (error instanceof Anthropic.ConflictError) {
// throw new Error(error.message)
// }

// if (error instanceof Anthropic.UnprocessableEntityError) {
// throw new Error(error.message)
// }

if (error instanceof Anthropic.RateLimitError) {
throw new RateLimitError(error.message)
}

if (error instanceof Anthropic.InternalServerError) {
throw new ServerError(error.message)
}

if (error instanceof Anthropic.APIError) {
throw new UnknownError(error.message)
}

throw error
}
}

private callFunction(callJson: string, functions: FunctionDefinition[]) {
let call: object
try {
call = JSON.parse(callJson)
} catch (error) {
return `${callJson}
Invalid JSON: ${(error as Error).message}`
}

const {function_name, parameters} = call as {
function_name: string
parameters: object
}

const functionDefinition = functions.find(
({name}) => name === function_name,
)

if (!functionDefinition) {
return `${callJson} gave me a function not found.`
}

return functionDefinition.handler(parameters)
}
}
Loading

0 comments on commit f2fabdc

Please sign in to comment.