Skip to content

Commit

Permalink
v0.2.0 Release (#1)
Browse files Browse the repository at this point in the history
- Better Markdown support
- Tested tool use with example
- Builder pattern for `Requests`
- Useful conversion shortcuts
- Stream delta enhancements
- CI
- streaming tool example, how to apply deltas
- integrate examples in docs
- More conversion shortcuts making example code much shorter
- More coverage, even of dumb things like accessor methods.
- Verbose Options to print System and Tool use as well as messages.
- More Cow<'static, str> to avoid copies and allow `const` `Message`s and so on. Eventually we might add a lifetime generic so this could also be used for zero-copy, deserialization but the cognitive overhead is not worth it yet.
- Minor bug fixes.
  • Loading branch information
mdegans authored Sep 13, 2024
1 parent abc1c52 commit ce1c0d9
Show file tree
Hide file tree
Showing 18 changed files with 3,303 additions and 233 deletions.
80 changes: 80 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Credit to GitHub Copilot for generating this file
name: Rust CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]

steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools-preview

- name: Cache cargo registry
uses: actions/cache@v2
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-registry-
- name: Cache cargo index
uses: actions/cache@v2
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-index-
- name: Cache cargo build
uses: actions/cache@v2
with:
path: target
key: ${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-build-
- name: Build
run: cargo build --all-features --verbose

- name: Run tests
run: cargo test --all-features --verbose

# This should only happen on push to main. PRs should not upload coverage.
- name: Install llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
if: matrix.os == 'ubuntu-latest' && github.event_name == 'push'

- name: Install nextest
uses: taiki-e/install-action@nextest
if: matrix.os == 'ubuntu-latest' && github.event_name == 'push'

- name: Write API key to api.key
if: matrix.os == 'ubuntu-latest' && github.event_name == 'push'
run: echo ${{ secrets.ANTHROPIC_API_KEY }} > api.key

- name: Collect coverage data (including ignored tests)
if: matrix.os == 'ubuntu-latest' && github.event_name == 'push'
run: cargo llvm-cov nextest --all-features --run-ignored all --lcov --output-path lcov.info

- name: Upload coverage to Codecov
if: matrix.os == 'ubuntu-latest' && github.event_name == 'push'
uses: codecov/codecov-action@v2
with:
files: lcov.info
flags: unittests
name: codecov-umbrella
fail_ci_if_error: true
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/target
Cargo.lock
.vscode
.vscode
cobertura.xml
api.key
lcov.info
16 changes: 15 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "misanthropic"
version = "0.1.4"
version = "0.2.0"
edition = "2021"
authors = ["Michael de Gans <[email protected]>"]
description = "An async, ergonomic, client for Anthropic's Messages API"
Expand Down Expand Up @@ -33,11 +33,16 @@ reqwest = { version = "0.12", features = ["json", "stream"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "1"
# markdown support
pulldown-cmark = { version = "0.12", optional = true }
pulldown-cmark-to-cmark = { version = "17", optional = true }
static_assertions = "1"

[dev-dependencies]
clap = { version = "4", features = ["derive"] }
env_logger = "0.11"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
itertools = "0.13"

[features]
# rustls because I am sick of getting Dependabot alerts for OpenSSL.
Expand All @@ -63,3 +68,12 @@ prompt-caching = ["beta"]
log = ["dep:log"]
# Use rustls instead of the system SSL, such as OpenSSL.
rustls-tls = ["reqwest/rustls-tls"]
# Use `pulldown-cmark` for markdown parsing and `pulldown-cmark-to-cmark` for
# converting to CommonMark.
markdown = ["dep:pulldown-cmark", "dep:pulldown-cmark-to-cmark"]
# Derive PartialEq for all structs and enums.
partialeq = []

[[example]]
name = "strawberry"
required-features = ["markdown"]
40 changes: 14 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
# `misanthropic`

![Build Status](https://github.com/mdegans/misanthropic/actions/workflows/tests.yaml/badge.svg)
[![codecov](https://codecov.io/gh/mdegans/misanthropic/branch/main/graph/badge.svg)](https://codecov.io/gh/mdegans/misanthropic)

Is an unofficial simple, ergonomic, client for the Anthropic Messages API.

## Usage

### Streaming

```rust
// Create a client. `key` will be consumed, zeroized, and stored securely.
// Create a client. The key is encrypted in memory and source string is zeroed.
// When requests are made, the key header is marked as sensitive.
let client = Client::new(key)?;

// Request a stream of events or errors. `json!` can be used, a `Request`, or a
// combination of strings and concrete types like `Model`. All Client request
// methods accept anything serializable for maximum flexibility.
// Request a stream of events or errors. `json!` can be used, the `Request`
// builder pattern (shown in the `Single Message` example below), or anything
// serializable.
let stream = client
// Forces `stream=true` in the request.
.stream(json!({
Expand Down Expand Up @@ -44,31 +48,13 @@ let content: String = stream
### Single Message

```rust
// Create a client. `key` will be consumed and zeroized.
let client = Client::new(key)?;

// Request a single message. The parameters are the same as the streaming
// example above. If a value is `None` it will be omitted from the request.
// This is less flexible than json! but some may prefer it. A Builder pattern
// is not yet available but is planned to reduce the verbosity.
// Many common usage patterns are supported out of the box for building
// `Request`s, such as messages from an iterable of tuples of `Role` and
// `String`.
let message = client
.message(Request {
model: Model::Sonnet35,
messages: vec![Message {
role: Role::User,
content: args.prompt.into(),
}],
max_tokens: 1000.try_into().unwrap(),
metadata: serde_json::Value::Null,
stop_sequences: None,
stream: None,
system: None,
temperature: Some(1.0),
tool_choice: None,
tools: None,
top_k: None,
top_p: None,
})
.message(Request::default().messages([(Role::User, args.prompt)]))
.await?;

println!("{}", message);
Expand All @@ -77,12 +63,14 @@ println!("{}", message);
## Features

- [x] Async but does not _directly_ depend on tokio
- [x] Tool use,
- [x] Streaming responses
- [x] Message responses
- [x] Image support with or without the `image` crate
- [x] Markdown formatting of messages, including images
- [x] Prompt caching support
- [x] Custom request and endpoint support
- [ ] Zero-copy serde - Coming soon!
- [ ] Amazon Bedrock support
- [ ] Vertex AI support

Expand Down
40 changes: 11 additions & 29 deletions examples/neologism.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
//! See `source` for an example of [`Client::message`] using the "neologism
//! creator" prompt. For a streaming example, see the `website_wizard` example.

// Note: This example uses blocking calls for simplicity such as `print`
// `read_to_string`, `stdin().lock()`, and `write`. In a real application, these
// should usually be replaced with async alternatives.

// Note: This example uses blocking calls for simplicity such as `println!()`
// and `stdin().lock()`. In a real application, these should *usually* be
// replaced with async alternatives.
use clap::Parser;
use misanthropic::{
request::{message::Role, Message},
Client, Model, Request,
};
use misanthropic::{request::message::Role, Client, Request};
use std::io::{stdin, BufRead};

/// Invent new words and provide their definitions based on user-provided
Expand Down Expand Up @@ -40,30 +36,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Enter your API key:");
let key = stdin().lock().lines().next().unwrap()?;

// Create a client. `key` will be consumed and zeroized.
// Create a client. The key is encrypted in memory and source string is
// zeroed. When requests are made, the key header is marked as sensitive.
let client = Client::new(key)?;

// Request a completion. `json!` can be used, `Request` or a combination of
// strings and types like `Model`. Client request methods accept anything
// serializable for maximum flexibility.
// Request a completion. `json!` can be used, the `Request` builder pattern,
// or anything serializable. Many common usage patterns are supported out of
// the box for building `Request`s, such as messages from a list of tuples
// of `Role` and `String`.
let message = client
.message(Request {
model: Model::Sonnet35,
messages: vec![Message {
role: Role::User,
content: args.prompt.into(),
}],
max_tokens: 1000.try_into().unwrap(),
metadata: serde_json::Value::Null,
stop_sequences: None,
stream: None,
system: None,
temperature: Some(1.0),
tool_choice: None,
tools: None,
top_k: None,
top_p: None,
})
.message(Request::default().messages([(Role::User, args.prompt)]))
.await?;

println!("{}", message);
Expand Down
Loading

0 comments on commit ce1c0d9

Please sign in to comment.