From 59e7c2237dea2611213cdebb6558e6f87af6ea9b Mon Sep 17 00:00:00 2001 From: krypticmouse Date: Tue, 16 Sep 2025 16:13:12 -0700 Subject: [PATCH 1/4] Add predictor docs main --- crates/dspy-rs/README.md | 4 +- crates/dspy-rs/examples/01-simple.rs | 11 ++- crates/dspy-rs/src/core/lm/mod.rs | 2 +- crates/dspy-rs/src/core/settings.rs | 20 +++-- docs/docs/building-blocks/predictors.mdx | 104 +++++++++++++++++++++++ docs/docs/getting-started/quickstart.mdx | 65 ++++++-------- 6 files changed, 153 insertions(+), 53 deletions(-) diff --git a/crates/dspy-rs/README.md b/crates/dspy-rs/README.md index 0087dfde..b8540b9f 100644 --- a/crates/dspy-rs/README.md +++ b/crates/dspy-rs/README.md @@ -48,7 +48,7 @@ cargo add dspy-rs Here's a simple example to get you started: ```rust -use dsrs::prelude::*; +use dsrs::*; use anyhow::Result; #[Signature] @@ -67,7 +67,7 @@ async fn main() -> Result<()> { // Configure your LM (Language Model) configure( LM::builder() - .api_key(SecretString::from(std::env::var("OPENAI_API_KEY")?)) + .api_key(std::env::var("OPENAI_API_KEY")?) .build(), ChatAdapter {}, ); diff --git a/crates/dspy-rs/examples/01-simple.rs b/crates/dspy-rs/examples/01-simple.rs index e445b987..55f0d91c 100644 --- a/crates/dspy-rs/examples/01-simple.rs +++ b/crates/dspy-rs/examples/01-simple.rs @@ -73,18 +73,21 @@ impl Module for QARater { #[tokio::main] async fn main() { + let lm = LM::builder() + .api_key(SecretString::from(std::env::var("OPENAI_API_KEY").unwrap())) + .build(); configure( - LM::builder() - .api_key(SecretString::from(std::env::var("OPENAI_API_KEY").unwrap())) - .build(), + &lm, ChatAdapter, ); let example = example! { - "question": "input" => "What is the capital of France?", + "question": "input" => "What is the capital of India?", }; let qa_rater = QARater::builder().build(); let prediction = qa_rater.forward(example).await.unwrap(); println!("{prediction:?}"); + + println!("LM History: {:?}", lm.inspect_history(1)); } diff --git a/crates/dspy-rs/src/core/lm/mod.rs b/crates/dspy-rs/src/core/lm/mod.rs index cb71b2c8..8aa613c5 100644 --- a/crates/dspy-rs/src/core/lm/mod.rs +++ b/crates/dspy-rs/src/core/lm/mod.rs @@ -13,7 +13,7 @@ use async_openai::{Client, config::OpenAIConfig}; use bon::Builder; use secrecy::{ExposeSecretMut, SecretString}; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct LMResponse { pub chat: Chat, pub config: LMConfig, diff --git a/crates/dspy-rs/src/core/settings.rs b/crates/dspy-rs/src/core/settings.rs index d4292fc1..dbd9e6f4 100644 --- a/crates/dspy-rs/src/core/settings.rs +++ b/crates/dspy-rs/src/core/settings.rs @@ -3,21 +3,27 @@ use std::sync::{Arc, LazyLock, RwLock}; use super::LM; use crate::adapter::Adapter; -pub struct Settings { - pub lm: LM, +pub struct Settings<'a> { + pub lm: &'a LM, pub adapter: Arc, } -pub static GLOBAL_SETTINGS: LazyLock>> = +pub static GLOBAL_SETTINGS: LazyLock>>> = LazyLock::new(|| RwLock::new(None)); -pub fn get_lm() -> LM { - GLOBAL_SETTINGS.read().unwrap().as_ref().unwrap().lm.clone() +pub fn get_lm() -> &'static LM { + GLOBAL_SETTINGS + .read() + .unwrap() + .as_ref() + .unwrap() + .lm } -pub fn configure(lm: LM, adapter: impl Adapter + 'static) { +pub fn configure(lm: &LM, adapter: impl Adapter + 'static) { + let static_lm: &'static LM = Box::leak(Box::new(lm)); let settings = Settings { - lm, + lm: static_lm, adapter: Arc::new(adapter), }; *GLOBAL_SETTINGS.write().unwrap() = Some(settings); diff --git a/docs/docs/building-blocks/predictors.mdx b/docs/docs/building-blocks/predictors.mdx index 77cee4a6..d7535173 100644 --- a/docs/docs/building-blocks/predictors.mdx +++ b/docs/docs/building-blocks/predictors.mdx @@ -3,3 +3,107 @@ title: 'Predictors' description: 'Learn how to create and use predictors for LM inference' icon: 'robot' --- + + +## What is a predictor? + +Predictors execute LM calls for a signature and input data. In Rust terms, a `Predict` struct wraps a `MetaSignature` and calls the LM via an `Adapter`. + +- **Purpose:** Execute an LM call for a signature with the provided input data. +- **Rust shape:** `Predict` holds a `Box` and uses the configured `Adapter` + `LM` to run inference. +- **Trait:** Anything that implements the `Predictor` trait can be invoked with `forward`/`forward_with_config`. + +## API surface + +- **`Predictor` trait:** + - `async fn forward(&self, inputs: Example) -> Result` — uses global settings (LM + Adapter). + - `async fn forward_with_config(&self, inputs: Example, lm: &mut LM) -> Result` — supply your own `LM` (uses `ChatAdapter`). +- **`Predict` struct:** concrete predictor that wraps a `MetaSignature`. + +## Minimal usage + +```rust +use dspy_rs::{ + ChatAdapter, Example, LM, LMConfig, Predict, Predictor, Signature, configure, hashmap, +}; + +#[Signature] +struct QA { + /// Use Renaissance-era English to answer the question. + #[input] + question: String, + #[output] + answer: String, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Configure global LM + adapter once + let lm = LM::builder() + .config(LMConfig::builder().model("gpt-4.1-nano".to_string()).build()) + .api_key(std::env::var("OPENAI_API_KEY")?.into()) + .build(); + configure(lm, ChatAdapter::default()); + + // Create predictor for your signature + let predictor = Predict::new(QA::new()); + + // Provide inputs as an Example + let inputs = Example::new( + hashmap! { "question".to_string() => "What is gravity?".into() }, + vec!["question".to_string()], + vec!["answer".to_string()], + ); + + let pred = predictor.forward(inputs).await?; + println!("Answer: {}", pred.get("answer", None).as_str().unwrap()); + Ok(()) +} +``` + +## Inline signatures + +You can also build a predictor from an inline signature: + +```rust +use dspy_rs::{Predict, sign}; + +let predict = Predict::new(sign! { (question: String) -> answer: String }); +``` + +## How it works under the hood + +- `Predict::forward` reads the globally configured `LM` and `Adapter` from `GLOBAL_SETTINGS` and calls the adapter with your signature and inputs. +- `Predict::forward_with_config` lets you supply a mutable `&mut LM` directly and uses `ChatAdapter` for the call. This is helpful for tests or local overrides. + +## Testing and determinism + +In tests, inject a `DummyLM` and call `forward_with_config` to avoid network calls and ensure deterministic outputs. + +```rust +use dspy_rs::{DummyLM, Example, Predict, Predictor, Signature, hashmap}; + +#[Signature] +struct QA { #[input] question: String, #[output] answer: String } + +#[tokio::test] +async fn predicts_locally() -> anyhow::Result<()> { + let predict = Predict::new(QA::new()); + let mut lm = DummyLM::default().into(); // convert into LM + + let inputs = Example::new( + hashmap! { "question".to_string() => "Test?".into() }, + vec!["question".to_string()], + vec!["answer".to_string()], + ); + + let out = predict.forward_with_config(inputs, &mut lm).await?; + assert!(out.get("answer", None).is_string()); + Ok(()) +} +``` + +## Where predictors fit + +- A `Signature` defines the task schema; a `Predictor` executes it. +- You can compose multiple predictors and custom logic into higher-level `Module`s. See the modules guide for composition patterns. diff --git a/docs/docs/getting-started/quickstart.mdx b/docs/docs/getting-started/quickstart.mdx index a1852f5c..233154b5 100644 --- a/docs/docs/getting-started/quickstart.mdx +++ b/docs/docs/getting-started/quickstart.mdx @@ -30,45 +30,40 @@ tokio = "1.47.1" cargo add dspy-rs anyhow serde serde_json tokio ``` -This will create an alias `dsrs` for the `dspy-rs` crate which is the intended way to use it. - We need to install DSRS using the name `dspy-rs` for now, because `dsrs` is an already-published crate. -This may change in the future if the `dsrs` crate name is donated back or becomes available. +This may change in the future if the `dsrs` crate name is donated or becomes available. The first step in DSRs is to configure your Language Model (LM). DSRs supports -any LM supported via the `async-openai` crate. You can define your LM +any OpenAI compatible LM supported via the `async-openai` crate. You can define your LM configuration using the builder pattern as follows. Once the LM instance is created, pass it to the configure function along with -a chat adapter to set the global LM and adapter for your application. +a chat adapter to set the global LM and adapter for your application. An adapter sits on top of the LM and signature and +is responsible for converting signatures to prompts and parse the output fields from the LM response. `ChatAdapter` is the default adapter in DSRs and is responsible for converting -the instructions and the structure from your signature (defined in the next step) -into a prompt that the LM can follow to complete the task. +the signature (defined in the next step) to a list of messages that the `LM` can use to generate the output. ```rust -use dspy_rs::{configure, ChatAdapter, LM, LMConfig}; +use dspy_rs::{configure, ChatAdapter, LM}; use std::env; +use secrecy::SecretString; fn main() -> Result<(), anyhow::Error> { - //Define a config for the LM - let config = LMConfig::builder() - .model("gpt-4.1-nano".to_string()) - .build(); - // Create the LM instance via the builder - let lm = LM::builder() - .config(config) - .api_key(env::var("OPENAI_API_KEY")?.into()) - .build(); - // Configure the global LM and adapter - configure(lm, ChatAdapter::default()); + configure( + // Dec + LM::builder() + .api_key(SecretString::from(std::env::var("OPENAI_API_KEY")?)) + .build(), + ChatAdapter {}, + ); - Ok(()) + ... } ``` @@ -76,11 +71,11 @@ fn main() -> Result<(), anyhow::Error> { -A signature defines the structure of your task: what inputs it takes and what outputs it should produce. Think of it as a schema for your LM call, +A signature in DSRs specifies your task: it describes the instructions, the expected inputs, and the outputs your LM should generate. You can think of it as a schema that guides how your prompt for the LLM call is structured. -You can create your signature in DSRs in one of two ways: using an inline macro, and via an attribute macro. +You can create your signature in DSRs in one of two ways: using an inline signature or via structs. -Let's create a question-answering signature using the inline macro: +Let's create a question-answering signature using the inline signature: ```rust let signature = sign! { @@ -90,11 +85,11 @@ let signature = sign! { The input fields are to the left of the `->` arrow, and the output fields are to the right. Multiple fields can be comma-separated, for e.g., `(question: String, context: String) -> answer: String`. -Alternatively, you can have more control over defining more granular aspects of the signature by defining signature using attribute macro on structs. +Alternatively, you can have more control over defining more granular aspects of the signature by defining them using structs. ```rust #[Signature] -struct QASignature { +struct QA { /// Answer the question concisely. #[input(desc="Question to be answered.")] @@ -106,8 +101,7 @@ struct QASignature { ``` The advantage of the latter approach is that you can add doc comments at the -top of the struct, specifying -important domain information or specific instructions to the LM. +top of the struct, specifying detailed instructions for the task. Additionally, you can also annotate each field with `#[input]` and `#[output]` attributes, useful when you have multiple input and output fields, and when you want to add descriptions to each field. @@ -116,7 +110,11 @@ you want to add descriptions to each field. -A predictor is the simplest module in DSRs. It takes a signature and input data, and orchestrates the LM call to produce a prediction. Let's demonstrate this +LM is what define the configuration of the LLM call being made. It takes a signature and input data and calls the LLM to produce a prediction. + +Let's say you want to generate output + +Let's demonstrate this with an example. Gravity was explained by Isaac Newton in 1687. To make this more interesting, @@ -131,17 +129,6 @@ use dspy_rs::{ }; use std::env; -#[Signature] -struct QA { - /// Use Renaissance-era English to answer the question. - - #[input] - pub question: String, - - #[output] - pub answer: String, -} - #[tokio::main] async fn main() -> Result<(), anyhow::Error> { let config = LMConfig::builder().model("gpt-4.1-nano".to_string()).build(); From 695aad2dd22f4461767b5bd4b1b39f76f70cfeff Mon Sep 17 00:00:00 2001 From: krypticmouse Date: Tue, 16 Sep 2025 18:22:21 -0700 Subject: [PATCH 2/4] revert scripts to og state --- crates/dspy-rs/examples/01-simple.rs | 9 +++------ crates/dspy-rs/src/core/settings.rs | 20 +++++++------------- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/crates/dspy-rs/examples/01-simple.rs b/crates/dspy-rs/examples/01-simple.rs index 55f0d91c..63057f50 100644 --- a/crates/dspy-rs/examples/01-simple.rs +++ b/crates/dspy-rs/examples/01-simple.rs @@ -73,11 +73,10 @@ impl Module for QARater { #[tokio::main] async fn main() { - let lm = LM::builder() - .api_key(SecretString::from(std::env::var("OPENAI_API_KEY").unwrap())) - .build(); configure( - &lm, + LM::builder() + .api_key(SecretString::from(std::env::var("OPENAI_API_KEY").unwrap())) + .build(), ChatAdapter, ); @@ -88,6 +87,4 @@ async fn main() { let qa_rater = QARater::builder().build(); let prediction = qa_rater.forward(example).await.unwrap(); println!("{prediction:?}"); - - println!("LM History: {:?}", lm.inspect_history(1)); } diff --git a/crates/dspy-rs/src/core/settings.rs b/crates/dspy-rs/src/core/settings.rs index dbd9e6f4..d4292fc1 100644 --- a/crates/dspy-rs/src/core/settings.rs +++ b/crates/dspy-rs/src/core/settings.rs @@ -3,27 +3,21 @@ use std::sync::{Arc, LazyLock, RwLock}; use super::LM; use crate::adapter::Adapter; -pub struct Settings<'a> { - pub lm: &'a LM, +pub struct Settings { + pub lm: LM, pub adapter: Arc, } -pub static GLOBAL_SETTINGS: LazyLock>>> = +pub static GLOBAL_SETTINGS: LazyLock>> = LazyLock::new(|| RwLock::new(None)); -pub fn get_lm() -> &'static LM { - GLOBAL_SETTINGS - .read() - .unwrap() - .as_ref() - .unwrap() - .lm +pub fn get_lm() -> LM { + GLOBAL_SETTINGS.read().unwrap().as_ref().unwrap().lm.clone() } -pub fn configure(lm: &LM, adapter: impl Adapter + 'static) { - let static_lm: &'static LM = Box::leak(Box::new(lm)); +pub fn configure(lm: LM, adapter: impl Adapter + 'static) { let settings = Settings { - lm: static_lm, + lm, adapter: Arc::new(adapter), }; *GLOBAL_SETTINGS.write().unwrap() = Some(settings); From 0c0ebca24f55f9eb7d50cb062fdb7895e90803e4 Mon Sep 17 00:00:00 2001 From: krypticmouse Date: Tue, 16 Sep 2025 18:35:21 -0700 Subject: [PATCH 3/4] revert --- crates/dspy-rs/README.md | 2 +- crates/dspy-rs/examples/01-simple.rs | 11 ++++++----- crates/dspy-rs/src/core/lm/mod.rs | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/dspy-rs/README.md b/crates/dspy-rs/README.md index b8540b9f..9519d181 100644 --- a/crates/dspy-rs/README.md +++ b/crates/dspy-rs/README.md @@ -67,7 +67,7 @@ async fn main() -> Result<()> { // Configure your LM (Language Model) configure( LM::builder() - .api_key(std::env::var("OPENAI_API_KEY")?) + .api_key(std::env::var("OPENAI_API_KEY")?.into()) .build(), ChatAdapter {}, ); diff --git a/crates/dspy-rs/examples/01-simple.rs b/crates/dspy-rs/examples/01-simple.rs index 63057f50..512fac1b 100644 --- a/crates/dspy-rs/examples/01-simple.rs +++ b/crates/dspy-rs/examples/01-simple.rs @@ -13,7 +13,6 @@ use dspy_rs::{ ChatAdapter, Example, LM, Module, Predict, Prediction, Predictor, Signature, configure, example, hashmap, prediction, }; -use secrecy::SecretString; #[Signature(cot)] struct QASignature { @@ -72,19 +71,21 @@ impl Module for QARater { } #[tokio::main] -async fn main() { +async fn main() -> Result<()> { configure( LM::builder() - .api_key(SecretString::from(std::env::var("OPENAI_API_KEY").unwrap())) - .build(), + .api_key(std::env::var("OPENAI_API_KEY")?.into()) + .build(), ChatAdapter, ); let example = example! { - "question": "input" => "What is the capital of India?", + "question": "input" => "What is the capital of France?", }; let qa_rater = QARater::builder().build(); let prediction = qa_rater.forward(example).await.unwrap(); println!("{prediction:?}"); + + Ok(()) } diff --git a/crates/dspy-rs/src/core/lm/mod.rs b/crates/dspy-rs/src/core/lm/mod.rs index 8aa613c5..cb71b2c8 100644 --- a/crates/dspy-rs/src/core/lm/mod.rs +++ b/crates/dspy-rs/src/core/lm/mod.rs @@ -13,7 +13,7 @@ use async_openai::{Client, config::OpenAIConfig}; use bon::Builder; use secrecy::{ExposeSecretMut, SecretString}; -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct LMResponse { pub chat: Chat, pub config: LMConfig, From 811d2ca0ffef7c7f2415d25f825273d8e7a85d04 Mon Sep 17 00:00:00 2001 From: krypticmouse Date: Tue, 16 Sep 2025 18:37:06 -0700 Subject: [PATCH 4/4] revert --- crates/dspy-rs/examples/01-simple.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/dspy-rs/examples/01-simple.rs b/crates/dspy-rs/examples/01-simple.rs index 512fac1b..e445b987 100644 --- a/crates/dspy-rs/examples/01-simple.rs +++ b/crates/dspy-rs/examples/01-simple.rs @@ -13,6 +13,7 @@ use dspy_rs::{ ChatAdapter, Example, LM, Module, Predict, Prediction, Predictor, Signature, configure, example, hashmap, prediction, }; +use secrecy::SecretString; #[Signature(cot)] struct QASignature { @@ -71,10 +72,10 @@ impl Module for QARater { } #[tokio::main] -async fn main() -> Result<()> { +async fn main() { configure( LM::builder() - .api_key(std::env::var("OPENAI_API_KEY")?.into()) + .api_key(SecretString::from(std::env::var("OPENAI_API_KEY").unwrap())) .build(), ChatAdapter, ); @@ -86,6 +87,4 @@ async fn main() -> Result<()> { let qa_rater = QARater::builder().build(); let prediction = qa_rater.forward(example).await.unwrap(); println!("{prediction:?}"); - - Ok(()) }