# Artificial ‒ typed, composable prompt engineering in Rust
Artificial is a **batteries-included toy framework** that demonstrates how to build
strongly-typed, provider-agnostic prompt pipelines in Rust.
The code base is intentionally small—less than 3 k lines spread over multiple
crates—yet it already shows how to:
* Compose reusable prompt *fragments* into high-level templates.
* Target multiple model providers via pluggable back-ends
(currently OpenAI, adding others is trivial).
* Describe the LLM’s **JSON response format in Rust** and have it checked
at compile time with [`schemars`](https://docs.rs/schemars).
* Bubble up provider errors through a single, unified error type.
* Keep all heavy dependencies optional behind feature flags.
If you are curious how the new crop of “AI SDKs” work under the hood—or you
need a lean starting point for your own experiments—this repo is for you.
---
## Table of contents
1. [Crate layout](#crate-layout)
2. [Installation](#installation)
3. [Quick start](#quick-start)
4. [Library tour](#library-tour)
5. [Design goals](#design-goals)
6. [FAQ](#faq)
7. [License](#license)
---
## Crate layout
| **`artificial-core`** | Provider-agnostic traits (`ChatCompletionProvider`, `PromptTemplate`), client, error types |
| **`artificial-prompt`** | String-building helpers (`PromptBuilder`, `PromptChain`) |
| **`artificial-types`** | Shared fragments (`CurrentDateFragment`, `StaticFragment`) and output helpers |
| **`artificial-openai`** | Thin wrapper around *OpenAI /v1* with JSON-Schema function calling |
| **`artificial`** | Glue crate that re-exports everything above for convenience |
Each crate lives under `crates/*` and can be used independently, but most
people will depend on the umbrella crate:
```toml
[dependencies]
artificial = { version = "*", features = ["openai"] }
```
---
## Installation
Artificial is published as a **workspace example**, so you typically work
against the Git repository directly:
```bash
git clone https://github.com/mrcrgl/artificial-rs.git
cd artificial-rs
cargo run -p artificial --example openai_hello_world
```
Requirements:
* **Rust 1.77** or newer (edition 2024)
* An OpenAI API key exported as `OPENAI_API_KEY`
* Internet access for the example back-end
---
## Quick start
Below is a minimal “Hello, JSON” example taken from
[`examples/openai_hello_world.rs`](crates/artificial/examples/openai_hello_world.rs):
```rust
use artificial::{
ArtificialClient,
generic::{GenericMessage, GenericRole},
model::{Model, OpenAiModel},
template::{IntoPrompt, PromptTemplate},
};
use artificial_openai::OpenAiAdapterBuilder;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
struct HelloResponse { greeting: String }
struct HelloPrompt;
impl IntoPrompt for HelloPrompt {
type Message = GenericMessage;
fn into_prompt(self) -> Vec<Self::Message> {
vec![
GenericMessage::new("You are R2-D2.".into(), GenericRole::System),
GenericMessage::new("Say hello!".into(), GenericRole::User),
]
}
}
impl PromptTemplate for HelloPrompt {
type Output = HelloResponse;
const MODEL: Model = Model::OpenAi(OpenAiModel::Gpt4oMini);
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let backend = OpenAiAdapterBuilder::new_from_env().build()?;
let client = ArtificialClient::new(backend);
let response = client.chat_complete(HelloPrompt).await?;
println!("The droid says: {:?}", response);
Ok(())
}
```
Run it:
```bash
cargo run -p artificial --example openai_hello_world
```
The program sends a request with an **inline JSON-Schema** and prints the
deserialised reply.
---
## Library tour
### Prompt fragments
`artificial-types::fragments` contains small building blocks that turn into
`GenericMessage`s—think “Current date”, “Static system instruction”, “Last user
message”. You can combine them via `PromptChain`:
```rust
PromptChain::new()
.with(CurrentDateFragment::new())
.with(StaticFragment::new("You are a helpful bot.", GenericRole::System))
.with(last_user_message)
.build();
```
### Strongly-typed outputs
Define a `struct` (must implement `JsonSchema` + `Deserialize`) and declare it
as `PromptTemplate::Output`. The OpenAI back-end automatically injects the
schema as `response_format = json_schema`.
### Provider back-ends
A back-end only has to implement the single-method trait
```rust
trait ChatCompletionProvider {
type Message;
fn chat_complete<P>(&self, prompt: P) -> Pin<Box<dyn Future<Output = Result<P::Output>> + Send>>
}
```
Because the `PromptTemplate` carries the desired model, the back-end can map it
to the provider’s naming scheme (`gpt-4o-mini`, `gpt-4o`, …).
---
## Design goals
* **Minimal surface area** – the framework should fit in one screenful of
source code, making it easy to fork or copy-paste.
* **Compile-time checks everywhere** – wrong model name? missing `OPENAI_API_KEY`?
Incompatible JSON schema? You find out early.
* **Pluggable transports** – swap out `artificial-openai` for an
Ollama/Anthropic adapter without touching user code.
* **No macros** – generic traits and `impl`s keep the magic transparent.
---
## FAQ
**Why another AI SDK?**
Because many existing SDKs are either *too* heavyweight or hide the inner
workings behind procedural macros. Artificial aims to be the smallest possible
blueprint you can still use in production.
**Does it support streaming completions?**
Not yet. The `ChatCompletionProvider` trait is purposely tiny; adding another method for
streaming is straightforward.
**Is the OpenAI back-end production ready?**
It handles basic JSON-Schema function calling, retries and streaming are
missing. Treat it as a reference implementation.
**How do I add Anthropic or Ollama?**
1. Create `artificial-anthropic` (or similar) crate.
2. Wrap the provider’s HTTP API in a small client struct.
3. Implement `ChatCompletionProvider` for the adapter.
4. Done—`ArtificialClient` works instantly with the new provider.
---
## License
Licensed under **MIT**. See [`LICENSE`](LICENSE) for the full text.