Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement anthropic_sdk_dart, a Dart client for Anthropic API #433

Merged
merged 3 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions packages/anthropic_sdk_dart/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 0.0.1-dev.1

- Bootstrap package.
21 changes: 21 additions & 0 deletions packages/anthropic_sdk_dart/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2023 David Miguel Lozano

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
172 changes: 172 additions & 0 deletions packages/anthropic_sdk_dart/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Anthropic Dart Client

[![tests](https://img.shields.io/github/actions/workflow/status/davidmigloz/langchain_dart/test.yaml?logo=github&label=tests)](https://github.com/davidmigloz/langchain_dart/actions/workflows/test.yaml)
[![anthropic_sdk_dart](https://img.shields.io/pub/v/anthropic_sdk_dart.svg)](https://pub.dev/packages/anthropic_sdk_dart)
[![](https://dcbadge.vercel.app/api/server/x4qbhqecVR?style=flat)](https://discord.gg/x4qbhqecVR)
[![MIT](https://img.shields.io/badge/license-MIT-purple.svg)](https://github.com/davidmigloz/langchain_dart/blob/main/LICENSE)

Unofficial Dart client for [Anthropic](https://docs.anthropic.com/en/api) API (aka Claude API).

## Features

- Fully type-safe, [documented](https://pub.dev/documentation/anthropic_sdk_dart/latest) and tested
- All platforms supported (including streaming on web)
- Custom base URL, headers and query params support (e.g. HTTP proxies)
- Custom HTTP client support (e.g. SOCKS5 proxies or advanced use cases)

**Supported endpoints:**

- Messages (with streaming support)

## Table of contents

- [Usage](#usage)
* [Authentication](#authentication)
* [Messages](#messages)
- [Advance Usage](#advance-usage)
* [Default HTTP client](#default-http-client)
* [Custom HTTP client](#custom-http-client)
* [Using a proxy](#using-a-proxy)
+ [HTTP proxy](#http-proxy)
+ [SOCKS5 proxy](#socks5-proxy)
- [Acknowledgements](#acknowledgements)
- [License](#license)

## Usage

Refer to the [documentation](https://docs.anthropic.com) for more information about the API.

### Authentication

The Anthropic API uses API keys for authentication. Visit the [Anthropic console](https://console.anthropic.com/settings/keys) to retrieve the API key you'll use in your requests.

> **Remember that your API key is a secret!**
> Do not share it with others or expose it in any client-side code (browsers, apps). Production requests must be routed through your own backend server where your API key can be securely loaded from an environment variable or key management service.

```dart
final apiKey = Platform.environment['ANTHROPIC_API_KEY'];
final client = AnthropicClient(apiKey: apiKey);
```

### Messages

Send a structured list of input messages with text and/or image content, and the model will generate the next message in the conversation.

**Create a Message:**

```dart
final res = await client.createMessage(
request: CreateMessageRequest(
model: Model.model(Models.claude3Opus20240229),
maxTokens: 1024,
messages: [
Message(
role: MessageRole.user,
content: 'Hello, Claude',
),
],
),
);
print(res.content.text);
// Hi there! How can I help you today?
```

`Model` is a sealed class that offers two ways to specify the model:
- `Model.modelId('model-id')`: the model ID as string (e.g. `'claude-3-haiku-20240307'`).
- `Model.model(Models.claude3Opus20240229)`: a value from `Models` enum which lists all the available models.

Mind that this list may not be up-to-date. Refer to the [documentation](https://docs.anthropic.com/en/docs/models-overview) for the updated list.

**Streaming messages:**

```dart
final stream = await client.createMessageStream(
request: CreateMessageRequest(
model: Model.model(Models.claude3Opus20240229),
maxTokens: 1024,
messages: [
Message(
role: MessageRole.user,
content: 'Hello, Claude',
),
],
),
);
String text = '';
await for (final res in stream) {
res.map(
messageStart: (e) {},
messageDelta: (e) {},
messageStop: (e) {},
contentBlockStart: (e) {},
contentBlockDelta: (e) {
text += e.delta.text;
},
contentBlockStop: (e) {},
ping: (e) {},
);
}
print(text);
// Hi there! How can I help you today?
```

## Advance Usage

### Default HTTP client

By default, the client uses `https://api.anthropic.com/v1` as the `baseUrl` and the following implementations of `http.Client`:

- Non-web: [`IOClient`](https://pub.dev/documentation/http/latest/io_client/IOClient-class.html)
- Web: [`FetchClient`](https://pub.dev/documentation/fetch_client/latest/fetch_client/FetchClient-class.html) (to support streaming on web)

### Custom HTTP client

You can always provide your own implementation of `http.Client` for further customization:

```dart
final client = AnthropicClient(
apiKey: 'MISTRAL_API_KEY',
client: MyHttpClient(),
);
```

### Using a proxy

#### HTTP proxy

You can use your own HTTP proxy by overriding the `baseUrl` and providing your required `headers`:

```dart
final client = AnthropicClient(
baseUrl: 'https://my-proxy.com',
headers: {
'x-my-proxy-header': 'value',
},
);
```

If you need further customization, you can always provide your own `http.Client`.

#### SOCKS5 proxy

To use a SOCKS5 proxy, you can use the [`socks5_proxy`](https://pub.dev/packages/socks5_proxy) package:

```dart
final baseHttpClient = HttpClient();
SocksTCPClient.assignToHttpClient(baseHttpClient, [
ProxySettings(InternetAddress.loopbackIPv4, 1080),
]);
final httpClient = IOClient(baseClient);

final client = AnthropicClient(
client: httpClient,
);
```

## Acknowledgements

The generation of this client was made possible by the [openapi_spec](https://github.com/tazatechnology/openapi_spec) package.

## License

Anthropic Dart Client is licensed under the [MIT License](https://github.com/davidmigloz/langchain_dart/blob/main/LICENSE).
1 change: 1 addition & 0 deletions packages/anthropic_sdk_dart/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include: ../../analysis_options.yaml
13 changes: 13 additions & 0 deletions packages/anthropic_sdk_dart/build.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
targets:
$default:
builders:
source_gen|combining_builder:
options:
ignore_for_file:
- prefer_final_parameters
- require_trailing_commas
- non_constant_identifier_names
- unnecessary_null_checks
json_serializable:
options:
explicit_to_json: true
7 changes: 7 additions & 0 deletions packages/anthropic_sdk_dart/lib/anthropic_sdk_dart.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/// Dart Client for the Anthropic API (Claude 3 Opus, Sonnet, Haiku, etc.).
library anthropic_sdk_dart;

export 'src/client.dart';
export 'src/extensions.dart';
export 'src/generated/client.dart' show AnthropicClientException;
export 'src/generated/schema/schema.dart';
101 changes: 101 additions & 0 deletions packages/anthropic_sdk_dart/lib/src/client.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// ignore_for_file: use_super_parameters
import 'dart:async';
import 'dart:convert';

import 'package:http/http.dart' as http;

import 'generated/client.dart' as g;
import 'generated/schema/schema.dart';
import 'http_client/http_client.dart';

/// Client for Anthropic API.
///
/// Please see https://docs.anthropic.com/en/api for more details.
class AnthropicClient extends g.AnthropicClient {
/// Create a new Anthropic API client.
///
/// Main configuration options:
/// - `apiKey`: your Anthropic API key. You can find your API key in the
/// [Anthropic console](https://console.anthropic.com/settings/keys).
///
/// Advance configuration options:
/// - `baseUrl`: the base URL to use. Defaults to `https://api.anthropic.com/v1`.
/// You can override this to use a different API URL, or to use a proxy.
/// - `headers`: global headers to send with every request. You can use
/// this to set custom headers, or to override the default headers.
/// - `queryParams`: global query parameters to send with every request. You
/// can use this to set custom query parameters.
/// - `client`: the HTTP client to use. You can set your own HTTP client if
/// you need further customization (e.g. to use a Socks5 proxy).
AnthropicClient({
final String? apiKey,
final String? baseUrl,
final Map<String, String>? headers,
final Map<String, dynamic>? queryParams,
final http.Client? client,
}) : super(
apiKey: apiKey ?? '',
baseUrl: baseUrl,
headers: {
'anthropic-version': '2023-06-01',
...?headers,
},
queryParams: queryParams ?? const {},
client: client ?? createDefaultHttpClient(),
);

// ------------------------------------------
// METHOD: createMessageStream
// ------------------------------------------

/// Create a Message
///
/// Send a structured list of input messages with text and/or image content, and the
/// model will generate the next message in the conversation.
///
/// The Messages API can be used for either single queries or stateless multi-turn
/// conversations.
///
/// `request`: The request parameters for creating a message.
///
/// `POST` `https://api.anthropic.com/v1/messages`
Stream<MessageStreamEvent> createMessageStream({
required final CreateMessageRequest request,
}) async* {
final r = await makeRequestStream(
baseUrl: 'https://api.anthropic.com/v1',
path: '/messages',
method: g.HttpMethod.post,
requestType: 'application/json',
responseType: 'application/json',
body: request.copyWith(stream: true),
headerParams: {
if (apiKey.isNotEmpty) 'x-api-key': apiKey,
},
);
yield* r.stream
.transform(const _AnthropicStreamTransformer()) //
.map(
(final d) => MessageStreamEvent.fromJson(json.decode(d)),
);
}

@override
Future<http.BaseRequest> onRequest(final http.BaseRequest request) {
return onRequestHandler(request);
}
}

class _AnthropicStreamTransformer
extends StreamTransformerBase<List<int>, String> {
const _AnthropicStreamTransformer();

@override
Stream<String> bind(final Stream<List<int>> stream) {
return stream //
.transform(utf8.decoder) //
.transform(const LineSplitter()) //
.where((final i) => i.startsWith('data: '))
.map((final item) => item.substring(6));
}
}
13 changes: 13 additions & 0 deletions packages/anthropic_sdk_dart/lib/src/extensions.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import 'generated/schema/schema.dart';

/// Extension methods for [MessageContent].
extension MessageContentX on MessageContent {
/// Returns the text content of the message.
String get text {
return map(
text: (text) => text.value,
blocks: (blocks) =>
blocks.value.whereType<TextBlock>().map((t) => t.text).join('\n'),
);
}
}
Loading
Loading