diff --git a/CURRENT_PLAN.md b/CURRENT_PLAN.md
index 33eafe2b..d7173215 100644
--- a/CURRENT_PLAN.md
+++ b/CURRENT_PLAN.md
@@ -1,3 +1,5 @@
+> **Superseded** by [`docs/plans/2026-05-08-dsrs-crate-split-design.md`](docs/plans/2026-05-08-dsrs-crate-split-design.md) and its implementation plan. Retained for historical context.
+
> Status Update (2026-02-08): **Superseded historical plan**.
>
> Phase 1 (Bridge Root Excision) is now the active baseline: legacy bridge crates are removed from the workspace.
diff --git a/CURRENT_SPEC.md b/CURRENT_SPEC.md
index 4f390c5c..0650b792 100644
--- a/CURRENT_SPEC.md
+++ b/CURRENT_SPEC.md
@@ -4,6 +4,8 @@
**Status:** Draft
**Last Updated:** 2026-01-08
+> **Superseded** by [`docs/plans/2026-05-08-dsrs-crate-split-design.md`](docs/plans/2026-05-08-dsrs-crate-split-design.md) and its implementation plan for runtime crate topology. Retained for historical context.
+
> Status Update (2026-02-08):
> Legacy bridge crates are removed from the workspace.
> Current typed and optimizer contracts remain unchanged in Phase 1.
diff --git a/Cargo.lock b/Cargo.lock
index 11e8b33b..468f751c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -135,6 +135,12 @@ version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
+[[package]]
+name = "arrayref"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
+
[[package]]
name = "arrayvec"
version = "0.5.2"
@@ -580,6 +586,20 @@ version = "2.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
+[[package]]
+name = "blake3"
+version = "1.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce"
+dependencies = [
+ "arrayref",
+ "arrayvec 0.7.6",
+ "cc",
+ "cfg-if",
+ "constant_time_eq",
+ "cpufeatures 0.3.0",
+]
+
[[package]]
name = "block-buffer"
version = "0.10.4"
@@ -614,6 +634,16 @@ dependencies = [
"syn 2.0.106",
]
+[[package]]
+name = "borsh"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+]
+
[[package]]
name = "brotli"
version = "8.0.2"
@@ -728,7 +758,10 @@ checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
dependencies = [
"android-tzdata",
"iana-time-zone",
+ "js-sys",
"num-traits",
+ "serde",
+ "wasm-bindgen",
"windows-link 0.1.3",
]
@@ -901,6 +934,12 @@ dependencies = [
"tiny-keccak",
]
+[[package]]
+name = "constant_time_eq"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
+
[[package]]
name = "convert_case"
version = "0.6.0"
@@ -956,6 +995,15 @@ dependencies = [
"libc",
]
+[[package]]
+name = "cpufeatures"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "crc32fast"
version = "1.5.0"
@@ -1208,36 +1256,183 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]]
-name = "dspy-rs"
-version = "0.7.3"
+name = "dsrs-cache"
+version = "0.0.0"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "dsrs-core",
+ "foyer",
+ "serde",
+ "serde_json",
+ "tempfile",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "dsrs-core"
+version = "0.0.0"
dependencies = [
"anyhow",
- "arrow",
"async-trait",
"bamltype",
"bon",
- "csv",
"dsrs_macros",
- "enum_dispatch",
"facet",
- "foyer",
"futures",
- "hf-hub",
"indexmap",
"kdam",
- "minijinja",
+ "rig-core",
+ "rstest 0.25.0",
+ "schemars",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.17",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "dsrs-data"
+version = "0.0.0"
+dependencies = [
+ "anyhow",
+ "arrow",
+ "bamltype",
+ "bon",
+ "csv",
+ "dsrs-core",
+ "dsrs-evaluate",
+ "dsrs-predict",
+ "dsrs_macros",
+ "facet",
+ "hf-hub",
"parquet",
- "rand 0.8.5",
"rayon",
"regex",
"reqwest 0.13.2",
+ "rstest 0.25.0",
+ "serde",
+ "serde_json",
+ "tempfile",
+ "thiserror 2.0.17",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "dsrs-evaluate"
+version = "0.0.0"
+dependencies = [
+ "anyhow",
+ "bamltype",
+ "dsrs-core",
+ "dsrs-data",
+ "dsrs-lm",
+ "dsrs-predict",
+ "dsrs-trace",
+ "dsrs_macros",
+ "serde",
+ "serde_json",
+ "tokio",
+]
+
+[[package]]
+name = "dsrs-gepa"
+version = "0.0.0"
+dependencies = [
+ "anyhow",
+ "bon",
+ "dsrs-core",
+ "dsrs-evaluate",
+ "dsrs-lm",
+ "dsrs-predict",
+ "dsrs-trace",
+ "dsrs_macros",
+ "facet",
+ "rand 0.8.5",
+ "serde",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "dsrs-leaven"
+version = "0.0.0"
+dependencies = [
+ "anyhow",
+ "dsrs-core",
+ "dsrs-evaluate",
+ "dsrs-predict",
+ "dsrs_macros",
+ "leaven-core",
+ "leaven-engine",
+ "leaven-evidence",
+ "leaven-surface",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.17",
+]
+
+[[package]]
+name = "dsrs-lm"
+version = "0.0.0"
+dependencies = [
+ "anyhow",
+ "bamltype",
+ "bon",
+ "dsrs-cache",
+ "dsrs-core",
+ "dsrs-predict",
+ "dsrs-trace",
+ "dsrs_macros",
+ "enum_dispatch",
+ "facet",
+ "indexmap",
+ "minijinja",
+ "regex",
+ "reqwest 0.13.2",
"rig-core",
"rstest 0.25.0",
- "schemars",
"serde",
"serde_json",
"temp-env",
- "tempfile",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "dsrs-predict"
+version = "0.0.0"
+dependencies = [
+ "anyhow",
+ "bamltype",
+ "bon",
+ "dsrs-core",
+ "dsrs-lm",
+ "dsrs-trace",
+ "dsrs_macros",
+ "facet",
+ "rig-core",
+ "rstest 0.25.0",
+ "serde",
+ "serde_json",
+ "temp-env",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "dsrs-trace"
+version = "0.0.0"
+dependencies = [
+ "anyhow",
+ "bon",
+ "dsrs-core",
+ "dsrs-lm",
+ "dsrs-predict",
+ "serde_json",
"thiserror 2.0.17",
"tokio",
"tracing",
@@ -1248,13 +1443,18 @@ dependencies = [
name = "dsrs_macros"
version = "0.7.2"
dependencies = [
- "dspy-rs",
+ "bamltype",
+ "dsrs-core",
+ "facet",
"minijinja",
"proc-macro-crate",
"proc-macro2",
"quote",
+ "rstest 0.25.0",
+ "schemars",
"serde_json",
"syn 2.0.106",
+ "tempfile",
"trybuild",
]
@@ -1884,6 +2084,12 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
[[package]]
name = "hf-hub"
version = "0.4.3"
@@ -2401,6 +2607,83 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+[[package]]
+name = "leaven-core"
+version = "0.0.0"
+dependencies = [
+ "chrono",
+ "leaven-kernel",
+ "serde",
+ "smol_str",
+]
+
+[[package]]
+name = "leaven-engine"
+version = "0.0.0"
+dependencies = [
+ "futures",
+ "indexmap",
+ "leaven-core",
+ "leaven-kernel",
+ "leaven-store",
+ "leaven-surface",
+ "leaven-workspace",
+ "thiserror 2.0.17",
+]
+
+[[package]]
+name = "leaven-evidence"
+version = "0.0.0"
+dependencies = [
+ "leaven-core",
+ "leaven-kernel",
+ "ordered-float 4.6.0",
+ "thiserror 2.0.17",
+]
+
+[[package]]
+name = "leaven-kernel"
+version = "0.0.0"
+dependencies = [
+ "blake3",
+ "chrono",
+ "hex",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.17",
+ "uuid",
+]
+
+[[package]]
+name = "leaven-store"
+version = "0.0.0"
+dependencies = [
+ "bytes",
+ "leaven-core",
+ "leaven-kernel",
+ "thiserror 2.0.17",
+]
+
+[[package]]
+name = "leaven-surface"
+version = "0.0.0"
+dependencies = [
+ "leaven-core",
+ "leaven-kernel",
+ "thiserror 2.0.17",
+]
+
+[[package]]
+name = "leaven-workspace"
+version = "0.0.0"
+dependencies = [
+ "futures",
+ "leaven-kernel",
+ "parking_lot",
+ "serde",
+ "thiserror 2.0.17",
+]
+
[[package]]
name = "lexical-core"
version = "1.0.5"
@@ -2956,6 +3239,17 @@ dependencies = [
"num-traits",
]
+[[package]]
+name = "ordered-float"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951"
+dependencies = [
+ "num-traits",
+ "rand 0.8.5",
+ "serde",
+]
+
[[package]]
name = "ordered-float"
version = "5.1.0"
@@ -3338,6 +3632,7 @@ dependencies = [
"libc",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
+ "serde",
]
[[package]]
@@ -3377,6 +3672,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.16",
+ "serde",
]
[[package]]
@@ -4025,7 +4321,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
- "cpufeatures",
+ "cpufeatures 0.2.17",
"digest",
]
@@ -4083,6 +4379,16 @@ version = "2.0.0-alpha.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef784004ca8777809dcdad6ac37629f0a97caee4c685fcea805278d81dd8b857"
+[[package]]
+name = "smol_str"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9676b89cd56310a87b93dec47b11af744f34d5fc9f367b829474eec0a891350d"
+dependencies = [
+ "borsh",
+ "serde",
+]
+
[[package]]
name = "snap"
version = "1.1.1"
@@ -4454,7 +4760,6 @@ dependencies = [
"bytes",
"libc",
"mio",
- "parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
diff --git a/README.md b/README.md
index 47cfd12f..0a61caa6 100644
--- a/README.md
+++ b/README.md
@@ -2,54 +2,52 @@
# DSRs
-A high-performance DSPy rewrite in Rust for building LM-powered applications
+A high-performance Rust runtime for building typed LM-powered applications
[](LICENSE)
[](https://www.rust-lang.org)
-[](https://crates.io/crates/dspy-rs)
-[](https://docs.rs/dspy-rs)
+[](#crates)
+[](https://dsrs.herumbshandilya.com)
[](#)
-[Documentation](https://dsrs.herumbshandilya.com) • [API Reference](https://docs.rs/dspy-rs) • [Examples](crates/dspy-rs/examples/) • [Issues](https://github.com/krypticmouse/dsrs/issues) • [Discord](https://discord.com/invite/ZAEGgxjPUe)
+[Documentation](https://dsrs.herumbshandilya.com) • [Crates](#crates) • [Examples](crates/dsrs-predict/examples/) • [Issues](https://github.com/krypticmouse/dsrs/issues) • [Discord](https://discord.com/invite/ZAEGgxjPUe)
---
-## 🚀 Overview
+## Overview
-**DSRs** (DSPy Rust) is a ground-up rewrite of the [DSPy framework](https://github.com/stanfordnlp/dspy) in Rust, designed for building robust, high-performance applications powered by Language Models. Unlike a simple port, DSRs leverages Rust's type system, memory safety, and concurrency features to provide a more efficient and reliable foundation for LM applications.
+**DSRs** is a ground-up Rust runtime for building robust, high-performance applications powered by language models. It uses Rust's type system, memory safety, and concurrency features to provide a reliable foundation for typed LM pipelines.
-## 📦 Installation
+## Installation
-Add DSRs to your `Cargo.toml`:
+Depend on the crates you use:
```toml
[dependencies]
-# Option 1: Use the shorter alias (recommended)
-dsrs = { package = "dspy-rs", version = "0.7.3" }
-
-# Option 2: Use the full name
-dspy-rs = "0.7.3"
+dsrs-core = "0.7"
+dsrs-lm = "0.7"
+dsrs-predict = "0.7"
+dsrs-trace = "0.7"
```
Or use cargo:
```bash
-# Option 1: Add with alias (recommended)
-cargo add dsrs --package dspy-rs
-
-# Option 2: Add with full name
-cargo add dspy-rs
+cargo add dsrs-core dsrs-lm dsrs-predict dsrs-trace
```
-## 🔧 Quick Start
+## Quick Start
Here's a simple example to get you started:
```rust
use anyhow::Result;
-use dspy_rs::{configure, init_tracing, ChatAdapter, LM, Predict, Signature};
+use dsrs_lm::{configure, ChatAdapter, LM};
+use dsrs_macros::Signature;
+use dsrs_predict::Predict;
+use dsrs_trace::init_tracing;
#[derive(Signature, Clone)]
struct SentimentAnalyzer {
@@ -98,19 +96,22 @@ Result:
Answer: "Positive"
```
-## 🏗️ Architecture
+## Crates
-DSRs follows a modular architecture with clear separation of concerns:
+DSRs is split into layer-aligned crates. There is no facade crate; depend on the leaf crates directly.
-```
-dsrs/
-├── core/ # Core abstractions (LM, Module, Signature)
-├── adapter/ # LM provider adapters (OpenAI, etc.)
-├── data/ # Data structures (Example, Prediction)
-├── predictors/ # Built-in predictors (Predict, Chain, etc.)
-├── evaluate/ # Evaluation framework and metrics
-└── macros/ # Derive macros for signatures
-```
+| Crate | Purpose |
+|-------|---------|
+| `dsrs-core` | Signatures, modules, schema, errors, typed data, and abstract bridge traits. |
+| `dsrs-lm` | LM client, client registry, usage accounting, and `ChatAdapter`. |
+| `dsrs-predict` | `Predict`, `ChainOfThought`, and ReAct predictors. |
+| `dsrs-evaluate` | Evaluation framework, typed metrics, and feedback helpers. |
+| `dsrs-gepa` | GEPA optimizer. |
+| `dsrs-data` | DataLoader with feature-gated CSV, Parquet, and Hugging Face support. |
+| `dsrs-trace` | Execution graph recording and tracing helpers. |
+| `dsrs-cache` | Foyer-backed LM cache. |
+| `dsrs-leaven` | Leaven integration scaffold. |
+| `dsrs-macros` | Derive macros for signatures and field metadata. |
### Core Components
@@ -203,45 +204,12 @@ println!("Average score: {}", score);
#### 6. **Optimization** - Optimize your Modules
-DSRs provides two powerful optimizers:
-
-**COPRO (Collaborative Prompt Optimization)**
-```rust
-#[derive(Builder, facet::Facet)]
-#[facet(crate = facet)]
-pub struct MyModule {
- predictor: Predict,
-}
-
-// Create and configure the optimizer
-let optimizer = COPRO::builder()
- .breadth(10) // Number of candidates per iteration
- .depth(3) // Number of refinement iterations
- .build();
-
-// Prepare training data
-let train_examples = load_training_data();
-let metric = ExactMatchMetric;
+DSRs keeps GEPA as the active optimizer crate while the Leaven integration is being built out. COPRO and MIPROv2 were deleted with the crate split.
-// Compile optimizes the module in-place
-let mut module = MyModule::new();
-optimizer.compile(&mut module, train_examples, &metric).await?;
-```
-
-**MIPROv2 (Multi-prompt Instruction Proposal Optimizer v2)** - Advanced optimizer using LLMs
```rust
-// MIPROv2 uses a 3-stage process:
-// 1. Generate execution traces
-// 2. LLM generates candidate prompts with best practices
-// 3. Evaluate and select the best prompt
-
-let optimizer = MIPROv2::builder()
- .num_candidates(10) // Number of candidate prompts to generate
- .num_trials(20) // Number of evaluation trials
- .minibatch_size(25) // Examples per evaluation
- .temperature(1.0) // Temperature for prompt generation
- .build();
+use dsrs_gepa::GEPAOptimizer;
+let optimizer = GEPAOptimizer::builder().build();
optimizer.compile(&mut module, train_examples, &metric).await?;
```
@@ -253,7 +221,8 @@ Default behavior is:
- Missing signature-required fields return an error with row + field context.
```rust
-use dspy_rs::{DataLoader, Signature, TypedLoadOptions};
+use dsrs_data::{DataLoader, TypedLoadOptions};
+use dsrs_macros::Signature;
#[derive(Signature, Clone, Debug)]
struct QA {
@@ -280,7 +249,7 @@ let trainset = DataLoader::load_csv_with::(
true,
TypedLoadOptions::default(),
|row| {
- Ok(dspy_rs::Example::new(
+ Ok(dsrs_core::Example::new(
QAInput {
question: row.get::("prompt")?,
},
@@ -297,7 +266,7 @@ Migration note:
- `save_json` / `save_csv` were removed from `DataLoader`.
- Use typed `load_*` / `load_*_with` APIs.
-See `examples/08-optimize-mipro.rs` for a complete example (requires `parquet` feature).
+See the `dsrs-data` crate tests and examples for complete loader coverage.
**Component Discovery:**
```rust
@@ -375,7 +344,7 @@ cargo run --example 01-simple
### Chain of Thought (CoT) Reasoning
```rust
-use dspy_rs::ChainOfThought;
+use dsrs_predict::ChainOfThought;
// ChainOfThought wraps any signature, adding a `reasoning` field
let cot = ChainOfThought::::new();
@@ -393,27 +362,7 @@ DSRs includes a tracing system that captures the dataflow through modules as a D
See `examples/12-tracing.rs` for a complete example.
-### Optimizer Comparison
-
-| Feature | COPRO | MIPROv2 | GEPA |
-|---------|-------|---------|------|
-| **Approach** | Iterative refinement | LLM-guided generation | Evolutionary search with textual feedback |
-| **Complexity** | Simple | Advanced | Advanced |
-| **Best For** | Quick optimization | Best results | Complex tasks with subtle failure modes |
-| **Training Data** | Uses scores | Uses traces & descriptions | Uses rich textual feedback |
-| **Prompting Tips** | No | Yes (15+ best practices) | No |
-| **Program Understanding** | Basic | LLM-generated descriptions | LLM-judge feedback |
-| **Few-shot Examples** | No | Yes (auto-selected) | No |
-
-**When to use COPRO:**
-- Fast iteration needed
-- Simple tasks
-- Limited compute budget
-
-**When to use MIPROv2:**
-- Best possible results needed
-- Complex reasoning tasks
-- Have good training data (15+ examples recommended)
+### GEPA
**When to use GEPA:**
- Tasks where score alone doesn't explain what went wrong
@@ -462,13 +411,12 @@ This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENS
- Inspired by the original [DSPy](https://github.com/stanfordnlp/dspy) framework
- Built with the amazing Rust ecosystem
- Special thanks to the DSPy community for the discussion and ideas
-- MIPROv2 implementation
## 🔗 Resources
- [Documentation](https://dsrs.herumbshandilya.com)
-- [API Reference](https://docs.rs/dspy-rs)
-- [Examples](crates/dspy-rs/examples/)
+- [Crates](#crates)
+- [Examples](crates/dsrs-predict/examples/)
- [GitHub Issues](https://github.com/krypticmouse/dsrs/issues)
- [Discord Community](https://discord.com/invite/ZAEGgxjPUe)
- [Original DSPy Paper](https://arxiv.org/abs/2310.03714)
diff --git a/crates/bamltype-derive/src/lib.rs b/crates/bamltype-derive/src/lib.rs
index a34963ed..8254e7db 100644
--- a/crates/bamltype-derive/src/lib.rs
+++ b/crates/bamltype-derive/src/lib.rs
@@ -168,13 +168,9 @@ fn resolve_runtime_crate() -> syn::Result {
return Ok(path);
}
- if let Some(dspy_path) = find_crate_path("dspy-rs") {
- return Ok(syn::parse_quote!(#dspy_path::__macro_support::bamltype));
- }
-
Err(syn::Error::new(
Span::call_site(),
- "could not resolve bamltype runtime crate; expected dependency on `bamltype` or `dspy-rs`",
+ "could not resolve bamltype runtime crate; expected dependency on `bamltype`",
))
}
diff --git a/crates/bamltype/AGENTS.md b/crates/bamltype/AGENTS.md
index f9a680c8..c95dc9a2 100644
--- a/crates/bamltype/AGENTS.md
+++ b/crates/bamltype/AGENTS.md
@@ -3,7 +3,7 @@
## Baseline Commands
1. `cargo test -p bamltype --tests`
-2. `cargo test -p dspy-rs --test typed_integration --test test_typed_alias --test test_typed_prompt_format`
+2. `cargo test -p bamltype && cargo test -p dsrs_macros --test test_bamltype_attr_contract --test test_field_macro`
## Test Layers
diff --git a/crates/dspy-rs/Cargo.toml b/crates/dspy-rs/Cargo.toml
deleted file mode 100644
index 94f32610..00000000
--- a/crates/dspy-rs/Cargo.toml
+++ /dev/null
@@ -1,56 +0,0 @@
-[package]
-name = "dspy-rs"
-authors = ["Herumb Shandilya "]
-version = "0.7.3"
-edition = "2024"
-description = "A DSPy rewrite(not port) to Rust."
-readme = "../../README.md"
-documentation = "https://dsrs.herumbshandilya.com"
-homepage = "https://dsrs.herumbshandilya.com"
-repository = "https://github.com/krypticmouse/DSRs"
-license = "Apache-2.0"
-exclude = [
- "docs/*",
-]
-
-[dependencies]
-futures = "0.3.31"
-indexmap = "2.10.0"
-rayon = "1.10.0"
-rstest = "0.25.0"
-schemars = "1.0.4"
-serde = { version = "1.0.219", features = ["derive"] }
-serde_json = { version = "1.0.140", features = ["preserve_order"] }
-tokio = { version = "1.46.1", features = ["full"] }
-async-trait = "0.1.83"
-anyhow = "1.0.99"
-bon = "3.7.0"
-bamltype = { path = "../bamltype" }
-# Keep this direct pin in sync with workspace [patch.crates-io] for self-sufficient external path consumers.
-facet = { git = "https://github.com/darinkishore/facet", rev = "cc8613c97cd1ec03e63659db34a947989b45c8a5", default-features = false, features = ["std"] }
-thiserror = "2.0.17"
-dsrs_macros = { version = "0.7.2", path = "../dsrs-macros" }
-csv = { version = "1.3.1" }
-hf-hub = { version = "0.4.3", features = ["tokio"] }
-parquet = { version = "56.1.0" }
-arrow = { version = "56.1.0" }
-regex = "1.11.2"
-reqwest = { version = "0.13", features = ["blocking"] }
-kdam = "0.6.3"
-rand = "0.8.5"
-foyer = { version = "0.20.0", features = ["serde"]}
-tempfile = "3.23.0"
-rig-core = { git = "https://github.com/0xPlaygrounds/rig", rev="aee3b8bf6576ce41c9ac1dd82520752a65fa0127" }
-enum_dispatch = "0.3.13"
-tracing = "0.1.44"
-tracing-subscriber = { version = "0.3.22", features = ["env-filter", "fmt"] }
-minijinja = { git = "https://github.com/boundaryml/minijinja.git", branch = "main", default-features = false, features = ["builtins", "serde"] }
-
-[package.metadata.cargo-machete]
-ignored = ["rig-core"]
-
-[features]
-default = []
-
-[dev-dependencies]
-temp-env = { version = "0.3.6", features = ["async_closure"] }
diff --git a/crates/dspy-rs/examples/02-module-iteration-and-updation.rs b/crates/dspy-rs/examples/02-module-iteration-and-updation.rs
deleted file mode 100644
index d0d09893..00000000
--- a/crates/dspy-rs/examples/02-module-iteration-and-updation.rs
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
-Script to optimize a module via the typed optimizer API.
-
-Run with:
-```
-cargo run --example 02-module-iteration-and-updation
-```
-*/
-
-use anyhow::Result;
-use bon::Builder;
-use dspy_rs::{
- COPRO, ChatAdapter, Example, LM, MetricOutcome, Module, Optimizer, Predict, PredictError,
- Predicted, Signature, TypedMetric, average_score, configure, evaluate_trainset, init_tracing,
-};
-
-#[derive(Signature, Clone, Debug)]
-struct QA {
- #[input]
- question: String,
-
- #[output]
- answer: String,
-}
-
-#[derive(Builder, facet::Facet)]
-#[facet(crate = facet)]
-struct QAModule {
- #[builder(default = Predict::::builder().instruction("Answer clearly.").build())]
- answerer: Predict,
-}
-
-impl Module for QAModule {
- type Input = QAInput;
- type Output = QAOutput;
-
- async fn forward(&self, input: QAInput) -> Result, PredictError> {
- self.answerer.call(input).await
- }
-}
-
-struct ExactMatch;
-
-impl TypedMetric for ExactMatch {
- async fn evaluate(
- &self,
- example: &Example,
- prediction: &Predicted,
- ) -> Result {
- let expected = example.output.answer.trim().to_lowercase();
- let actual = prediction.answer.trim().to_lowercase();
- Ok(MetricOutcome::score((expected == actual) as u8 as f32))
- }
-}
-
-fn trainset() -> Vec> {
- vec![
- Example::new(
- QAInput {
- question: "What is 2+2?".to_string(),
- },
- QAOutput {
- answer: "4".to_string(),
- },
- ),
- Example::new(
- QAInput {
- question: "Capital of France?".to_string(),
- },
- QAOutput {
- answer: "Paris".to_string(),
- },
- ),
- ]
-}
-
-#[tokio::main]
-async fn main() -> Result<()> {
- init_tracing()?;
-
- configure(
- LM::builder()
- .model("openai:gpt-4o-mini".to_string())
- .build()
- .await?,
- ChatAdapter,
- );
-
- let metric = ExactMatch;
- let mut module = QAModule::builder().build();
- let trainset = trainset();
-
- let baseline = average_score(&evaluate_trainset(&module, &trainset, &metric).await?);
- println!("baseline score: {baseline:.3}");
-
- let optimizer = COPRO::builder().breadth(4).depth(1).build();
- optimizer
- .compile(&mut module, trainset.clone(), &metric)
- .await?;
-
- let optimized = average_score(&evaluate_trainset(&module, &trainset, &metric).await?);
- println!("optimized score: {optimized:.3}");
-
- Ok(())
-}
diff --git a/crates/dspy-rs/examples/04-optimize-hotpotqa.rs b/crates/dspy-rs/examples/04-optimize-hotpotqa.rs
deleted file mode 100644
index 0907db86..00000000
--- a/crates/dspy-rs/examples/04-optimize-hotpotqa.rs
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
-Script to optimize a typed QA module for a HotpotQA subset with COPRO.
-
-Run with:
-```
-cargo run --example 04-optimize-hotpotqa --features dataloaders
-```
-*/
-
-use anyhow::Result;
-use bon::Builder;
-use dspy_rs::{
- COPRO, ChatAdapter, DataLoader, Example, LM, MetricOutcome, Module, Optimizer, Predict,
- PredictError, Predicted, Signature, TypedLoadOptions, TypedMetric, average_score, configure,
- evaluate_trainset, init_tracing,
-};
-
-#[derive(Signature, Clone, Debug)]
-struct QA {
- /// Concisely answer the question, but be accurate.
-
- #[input]
- question: String,
-
- #[output(desc = "Answer in less than 5 words.")]
- answer: String,
-}
-
-#[derive(Builder, facet::Facet)]
-#[facet(crate = facet)]
-struct QAModule {
- #[builder(default = Predict::::builder().instruction("Answer clearly and briefly.").build())]
- answerer: Predict,
-}
-
-impl Module for QAModule {
- type Input = QAInput;
- type Output = QAOutput;
-
- async fn forward(&self, input: QAInput) -> Result, PredictError> {
- self.answerer.call(input).await
- }
-}
-
-struct ExactMatchMetric;
-
-impl TypedMetric for ExactMatchMetric {
- async fn evaluate(
- &self,
- example: &Example,
- prediction: &Predicted,
- ) -> Result {
- let expected = example.output.answer.trim().to_lowercase();
- let actual = prediction.answer.trim().to_lowercase();
- Ok(MetricOutcome::score((expected == actual) as u8 as f32))
- }
-}
-
-#[tokio::main]
-async fn main() -> Result<()> {
- init_tracing()?;
-
- configure(
- LM::builder()
- .model("openai:gpt-4o-mini".to_string())
- .build()
- .await?,
- ChatAdapter,
- );
-
- let examples = DataLoader::load_hf::(
- "hotpotqa/hotpot_qa",
- "fullwiki",
- "validation",
- true,
- TypedLoadOptions::default(),
- )?[..10]
- .to_vec();
-
- let metric = ExactMatchMetric;
- let mut module = QAModule::builder().build();
-
- let baseline = average_score(&evaluate_trainset(&module, &examples, &metric).await?);
- println!("baseline score: {baseline:.3}");
-
- let optimizer = COPRO::builder().breadth(10).depth(1).build();
- optimizer
- .compile(&mut module, examples.clone(), &metric)
- .await?;
-
- let optimized = average_score(&evaluate_trainset(&module, &examples, &metric).await?);
- println!("optimized score: {optimized:.3}");
-
- Ok(())
-}
diff --git a/crates/dspy-rs/examples/08-optimize-mipro.rs b/crates/dspy-rs/examples/08-optimize-mipro.rs
deleted file mode 100644
index 6fab8439..00000000
--- a/crates/dspy-rs/examples/08-optimize-mipro.rs
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
-Example: optimize a typed QA module using MIPROv2.
-
-Run with:
-```
-cargo run --example 08-optimize-mipro --features dataloaders
-```
-*/
-
-use anyhow::Result;
-use bon::Builder;
-use dspy_rs::{
- ChatAdapter, DataLoader, Example, LM, MIPROv2, MetricOutcome, Module, Optimizer, Predict,
- PredictError, Predicted, Signature, TypedLoadOptions, TypedMetric, average_score, configure,
- evaluate_trainset, init_tracing,
-};
-
-#[derive(Signature, Clone, Debug)]
-struct QuestionAnswering {
- /// Answer the question accurately and concisely.
-
- #[input]
- question: String,
-
- #[output]
- answer: String,
-}
-
-#[derive(Builder, facet::Facet)]
-#[facet(crate = facet)]
-struct SimpleQA {
- #[builder(default = Predict::::builder().instruction("Answer clearly.").build())]
- answerer: Predict,
-}
-
-impl Module for SimpleQA {
- type Input = QuestionAnsweringInput;
- type Output = QuestionAnsweringOutput;
-
- async fn forward(
- &self,
- input: QuestionAnsweringInput,
- ) -> Result, PredictError> {
- self.answerer.call(input).await
- }
-}
-
-struct ExactMatchMetric;
-
-impl TypedMetric for ExactMatchMetric {
- async fn evaluate(
- &self,
- example: &Example,
- prediction: &Predicted,
- ) -> Result {
- let expected = example.output.answer.trim().to_lowercase();
- let actual = prediction.answer.trim().to_lowercase();
-
- let score = if expected == actual {
- 1.0
- } else if expected.contains(&actual) || actual.contains(&expected) {
- 0.5
- } else {
- 0.0
- };
-
- Ok(MetricOutcome::score(score))
- }
-}
-
-#[tokio::main]
-async fn main() -> Result<()> {
- init_tracing()?;
-
- println!("=== MIPROv2 Optimizer Example ===\n");
-
- configure(LM::default(), ChatAdapter);
-
- println!("Loading training data from HuggingFace...");
- let train_examples = DataLoader::load_hf::(
- "hotpotqa/hotpot_qa",
- "fullwiki",
- "validation",
- true,
- TypedLoadOptions::default(),
- )?;
-
- let train_subset = train_examples[..15].to_vec();
- println!("Using {} training examples\n", train_subset.len());
-
- let metric = ExactMatchMetric;
- let mut qa_module = SimpleQA::builder().build();
-
- println!("Evaluating baseline performance...");
- let baseline_score =
- average_score(&evaluate_trainset(&qa_module, &train_subset[..5], &metric).await?);
- println!("Baseline score: {:.3}\n", baseline_score);
-
- let optimizer = MIPROv2::builder()
- .num_candidates(8)
- .num_trials(15)
- .minibatch_size(10)
- .build();
-
- println!("Starting MIPROv2 optimization...");
- optimizer
- .compile(&mut qa_module, train_subset.clone(), &metric)
- .await?;
-
- println!("Evaluating optimized performance...");
- let optimized_score =
- average_score(&evaluate_trainset(&qa_module, &train_subset[..5], &metric).await?);
- println!("Optimized score: {:.3}", optimized_score);
-
- let improvement = ((optimized_score - baseline_score) / baseline_score.max(1e-6)) * 100.0;
- println!(
- "\nImprovement: {:.1}% ({:.3} -> {:.3})",
- improvement, baseline_score, optimized_score
- );
-
- let result = qa_module
- .call(QuestionAnsweringInput {
- question: "What is the capital of France?".to_string(),
- })
- .await?
- .into_inner();
- println!("Question: What is the capital of France?");
- println!("Answer: {}", result.answer);
-
- Ok(())
-}
diff --git a/crates/dspy-rs/examples/94-smoke-slice5-optimizer-interface.rs b/crates/dspy-rs/examples/94-smoke-slice5-optimizer-interface.rs
deleted file mode 100644
index 97d7fec1..00000000
--- a/crates/dspy-rs/examples/94-smoke-slice5-optimizer-interface.rs
+++ /dev/null
@@ -1,73 +0,0 @@
-use anyhow::{Result, bail};
-use dspy_rs::{
- COPRO, ChainOfThought, ChatAdapter, Example, LM, MetricOutcome, Optimizer, Predicted,
- Signature, TypedMetric, WithReasoning, configure,
-};
-
-#[derive(Signature, Clone, Debug, facet::Facet)]
-#[facet(crate = facet)]
-struct SmokeSig {
- #[input]
- prompt: String,
-
- #[output]
- answer: String,
-}
-
-struct SmokeMetric;
-
-impl TypedMetric> for SmokeMetric {
- async fn evaluate(
- &self,
- _example: &Example,
- prediction: &Predicted>,
- ) -> Result {
- let answer = prediction.answer.to_ascii_lowercase();
- Ok(MetricOutcome::score(
- (answer.contains("smoke") || answer.contains("ok")) as u8 as f32,
- ))
- }
-}
-
-#[tokio::main]
-async fn main() -> Result<()> {
- // Smoke Label: Slice 5 Optimizer Interface
- configure(
- LM::builder()
- .model("openai:gpt-5.2".to_string())
- .build()
- .await?,
- ChatAdapter,
- );
-
- let mut module = ChainOfThought::::new();
- let trainset = vec![Example::new(
- SmokeSigInput {
- prompt: "Return exactly smoke-ok.".to_string(),
- },
- SmokeSigOutput {
- answer: "smoke-ok".to_string(),
- },
- )];
-
- let optimizer = COPRO::builder().breadth(4).depth(1).build();
- optimizer
- .compile(&mut module, trainset, &SmokeMetric)
- .await?;
-
- let output = module
- .call(SmokeSigInput {
- prompt: "Return exactly smoke-ok.".to_string(),
- })
- .await?
- .into_inner();
-
- println!("reasoning: {}", output.reasoning);
- println!("answer: {}", output.answer);
-
- if output.answer.trim().is_empty() {
- bail!("unexpected empty answer");
- }
-
- Ok(())
-}
diff --git a/crates/dspy-rs/src/core/mod.rs b/crates/dspy-rs/src/core/mod.rs
deleted file mode 100644
index 64bf2cb8..00000000
--- a/crates/dspy-rs/src/core/mod.rs
+++ /dev/null
@@ -1,43 +0,0 @@
-//! The foundational abstractions everything else is built on.
-//!
-//! A [`Signature`] declares what you want the LM to do — input fields, output fields,
-//! and an instruction. [`SignatureSchema`] is the Facet-derived metadata for those fields,
-//! cached once per type and shared by the adapter and optimizer. [`Module`] is the trait
-//! every prompting strategy implements — it's deliberately narrow (`forward` takes an
-//! input, returns a predicted output) so that strategies are interchangeable.
-//!
-//! [`Predicted`] wraps a typed output with [`CallMetadata`] (raw response text, token
-//! usage, per-field parse results). The error hierarchy — [`PredictError`], [`ParseError`],
-//! [`LmError`] — distinguishes LM failures from parse failures so callers can handle
-//! retries differently. [`LM`] is the language model client itself.
-//!
-//! Optimizer leaf discovery is internal (`visit_named_predictors_mut`) and currently
-//! traverses struct fields plus `Option`, `Vec`, `HashMap`, and `Box`.
-//! `Rc`/`Arc` wrappers that contain `Predict` leaves are rejected with explicit
-//! container errors.
-//!
-//! Most users import these through the crate root (`use dspy_rs::*`). Module authors
-//! who need fine-grained prompt control also use [`SignatureSchema`] and the adapter
-//! building blocks directly.
-
-pub(crate) mod dyn_predictor;
-mod errors;
-pub mod lm;
-pub mod module;
-mod module_ext;
-mod predicted;
-mod schema;
-pub mod settings;
-pub mod signature;
-pub mod specials;
-
-pub(crate) use dyn_predictor::*;
-pub use errors::{ConversionError, ErrorClass, JsonishError, LmError, ParseError, PredictError};
-pub use lm::*;
-pub use module::*;
-pub use module_ext::*;
-pub use predicted::{CallMetadata, ConstraintResult, FieldMeta, Predicted};
-pub use schema::{FieldMetadataSpec, FieldPath, FieldSchema, InputRenderSpec, SignatureSchema};
-pub use settings::*;
-pub use signature::*;
-pub use specials::*;
diff --git a/crates/dspy-rs/src/evaluate/mod.rs b/crates/dspy-rs/src/evaluate/mod.rs
deleted file mode 100644
index 410eb298..00000000
--- a/crates/dspy-rs/src/evaluate/mod.rs
+++ /dev/null
@@ -1,22 +0,0 @@
-//! Evaluation and metrics for measuring module performance.
-//!
-//! The evaluation loop is simple: run the module on each training example, score the
-//! result with a [`TypedMetric`], collect [`MetricOutcome`]s. Optimizers use this
-//! internally, but you can also call [`evaluate_trainset`] directly to benchmark
-//! your module before and after optimization.
-//!
-//! Two kinds of metrics:
-//! - **Score-only** — return [`MetricOutcome::score()`] with a `f32`. Enough for
-//! [`COPRO`](crate::COPRO) and [`MIPROv2`](crate::MIPROv2).
-//! - **Score + feedback** — return [`MetricOutcome::with_feedback()`] with a
-//! [`FeedbackMetric`]. Required by [`GEPA`](crate::GEPA), which uses the textual
-//! feedback to guide evolutionary search.
-
-pub mod evaluator;
-pub mod feedback;
-pub mod feedback_helpers;
-pub mod metrics;
-
-pub use evaluator::*;
-pub use feedback::*;
-pub use feedback_helpers::*;
diff --git a/crates/dspy-rs/src/lib.rs b/crates/dspy-rs/src/lib.rs
deleted file mode 100644
index c8e5e2af..00000000
--- a/crates/dspy-rs/src/lib.rs
+++ /dev/null
@@ -1,300 +0,0 @@
-//! Typed prompt engineering and LM program optimization.
-//!
-//! DSRs is a Rust port of [DSPy](https://github.com/stanfordnlp/dspy): you declare what
-//! you want the LM to produce (a [`Signature`]), pick a prompting strategy (a [`Module`]
-//! like [`Predict`] or [`ChainOfThought`]), and let an [`Optimizer`] tune the program's
-//! instructions and demos on your training data. The type system enforces correctness
-//! at every layer — field types, strategy swaps, and augmentation composition are all
-//! compile-time checked.
-//!
-//! # The mental model
-//!
-//! Three concepts, three layers:
-//!
-//! | Layer | Concept | Key types | Who |
-//! |-------|---------|-----------|-----|
-//! | **Signatures** | "Given these inputs, produce these outputs" | [`Signature`], `#[derive(Signature)]` | Everyone |
-//! | **Modules** | Prompting strategies that implement a signature | [`Module`], [`Predict`], [`ChainOfThought`] | Everyone |
-//! | **Optimization** | Auto-tuning instructions and demos | [`Optimizer`], [`COPRO`], [`GEPA`], [`MIPROv2`] | When you need better results |
-//!
-//! A [`Predict`] is the leaf — the only thing that actually calls the LM. Every other
-//! module ([`ChainOfThought`], custom pipelines) delegates to one or more `Predict` leaves.
-//! Optimizers discover these leaves automatically via Facet reflection and mutate their
-//! instructions and few-shot demos.
-//!
-//! # Quick start
-//!
-//! ```no_run
-//! use dspy_rs::*;
-//!
-//! #[derive(Signature, Clone, Debug)]
-//! /// Answer questions accurately and concisely.
-//! struct QA {
-//! #[input] question: String,
-//! #[output] answer: String,
-//! }
-//!
-//! # async fn example() -> Result<(), PredictError> {
-//! // 1. Configure the LM
-//! let lm = LM::builder()
-//! .model("openai:gpt-4o-mini".to_string())
-//! .build()
-//! .await
-//! .unwrap();
-//! dspy_rs::configure(lm, ChatAdapter);
-//!
-//! // 2. Pick a strategy
-//! let cot = ChainOfThought::::new();
-//!
-//! // 3. Call it
-//! let result = cot.call(QAInput { question: "What is 2+2?".into() }).await?;
-//! println!("{}", result.reasoning); // chain-of-thought text
-//! println!("{}", result.answer); // the actual answer, via Deref
-//! # Ok(())
-//! # }
-//! ```
-//!
-//! `ChainOfThought` returns [`Predicted>`](Predicted), not
-//! `Predicted`. You access `.reasoning` directly and `.answer` through auto-deref
-//! ([`WithReasoning`] derefs to `O`). This pattern holds for all augmentations — the
-//! compiler tells you what changed when you swap strategies.
-//!
-//! # What doesn't work (yet)
-//!
-//! - **No dynamic graph / structural optimization.** The type-erased `ProgramGraph`,
-//! `DynModule`, `StrategyFactory` layer was prototyped and intentionally removed.
-//! Everything here is statically typed, which is both the strength and the constraint.
-//! - **MIPRO is instruction-only.** It should also mutate demos per-predictor based on
-//! trace data — Python DSPy does this — but it doesn't yet (`TODO(trace-demos)`).
-//! - **No `ReAct`, `BestOfN`, `Refine`, or other advanced modules** beyond `ChainOfThought`.
-//! The module trait and augmentation system are designed for them, but nobody's built
-//! them yet.
-//! - **`CallMetadata` is not extensible.** Modules can't attach custom metadata (e.g.
-//! "which attempt won in BestOfN"). This should probably be a trait with associated
-//! types, but it isn't.
-//! - **Container traversal is partial.** The optimizer walker handles `Option`, `Vec`,
-//! `HashMap`, and `Box`. `Rc`/`Arc` containing `Predict` leaves return
-//! explicit container errors (not silent skips), and `Predict` discovery requires
-//! a valid shape-local accessor payload (`TODO(dsrs-shared-ptr-policy)`).
-//!
-//! # Crate organization
-//!
-//! - [`adapter`] — Prompt formatting and LM response parsing ([`ChatAdapter`])
-//! - [`core`] — [`Module`] trait, [`Signature`] trait, [`SignatureSchema`], error types,
-//! LM client, [`Predicted`] and [`CallMetadata`]
-//! - [`predictors`] — [`Predict`] (the leaf module) and typed [`Example`]
-//! - [`modules`] — [`ChainOfThought`] and augmentation types
-//! - [`evaluate`] — [`TypedMetric`] trait, [`evaluate_trainset`], scoring utilities
-//! - [`optimizer`] — [`Optimizer`] trait, [`COPRO`], [`GEPA`], [`MIPROv2`]
-//! - [`data`] — [`DataLoader`] for JSON/CSV/Parquet/HuggingFace datasets
-//! - [`trace`] — Execution graph recording for debugging
-//! - [`utils`] — Response caching
-
-// TODO(dsrs-facet-lint-scope): remove this crate-level allow once Facet's generated
-// extension-attr dispatch no longer triggers rust-lang/rust#52234 on in-crate usage.
-#![allow(macro_expanded_macro_exports_accessed_by_absolute_paths)]
-
-extern crate self as dspy_rs;
-
-pub mod adapter;
-pub mod augmentation;
-pub mod core;
-pub mod data;
-pub mod evaluate;
-pub mod modules;
-pub mod optimizer;
-pub mod predictors;
-pub mod trace;
-pub mod utils;
-
-pub use adapter::chat::*;
-pub use augmentation::*;
-pub use core::*;
-pub use data::dataloader::*;
-pub(crate) use data::example::Example as RawExample;
-pub use data::prediction::*;
-pub use data::serialize::*;
-pub use data::utils::*;
-pub use evaluate::*;
-pub use modules::*;
-pub use optimizer::*;
-pub use predictors::*;
-pub use utils::*;
-
-pub use bamltype::BamlConvertError;
-pub use bamltype::BamlType; // attribute macro
-pub use bamltype::Shape;
-pub use bamltype::baml_types::{
- BamlValue, Constraint, ConstraintLevel, ResponseCheck, StreamingMode, TypeIR,
-};
-pub use bamltype::internal_baml_jinja::types::{OutputFormatContent, RenderOptions};
-pub use bamltype::jsonish::deserializer::deserialize_flags::Flag;
-pub use dsrs_macros::*;
-pub use facet::Facet;
-
-/// Pre-built signature for use in doc examples. Not part of the public API.
-#[doc(hidden)]
-pub mod doctest {
- #[derive(crate::Signature, Clone, Debug)]
- /// Answer questions accurately and concisely.
- pub struct QA {
- #[input]
- pub question: String,
- #[output]
- pub answer: String,
- }
-}
-
-#[doc(hidden)]
-pub mod __macro_support {
- pub use anyhow;
- pub use bamltype;
- pub use indexmap;
- pub use schemars;
- pub use serde;
- pub use serde_json;
-}
-
-#[macro_export]
-macro_rules! field {
- // Example Usage: field! {
- // input["Description"] => question: String
- // }
- //
- // Example Output:
- //
- // {
- // "question": {
- // "type": "String",
- // "desc": "Description",
- // "schema": ""
- // },
- // ...
- // }
-
- // Pattern for field definitions with descriptions
- { $($field_type:ident[$desc:literal] => $field_name:ident : $field_ty:ty),* $(,)? } => {{
- use $crate::__macro_support::serde_json::json;
-
- let mut result = $crate::__macro_support::serde_json::Map::new();
-
- $(
- let type_str = stringify!($field_ty);
- let schema = {
- let schema = $crate::__macro_support::schemars::schema_for!($field_ty);
- let schema_json = $crate::__macro_support::serde_json::to_value(schema).unwrap();
- // Extract just the properties if it's an object schema
- if let Some(obj) = schema_json.as_object() {
- if obj.contains_key("properties") {
- schema_json["properties"].clone()
- } else {
- "".to_string().into()
- }
- } else {
- "".to_string().into()
- }
- };
- result.insert(
- stringify!($field_name).to_string(),
- json!({
- "type": type_str,
- "desc": $desc,
- "schema": schema,
- "__dsrs_field_type": stringify!($field_type)
- })
- );
- )*
-
- $crate::__macro_support::serde_json::Value::Object(result)
- }};
-
- // Pattern for field definitions without descriptions
- { $($field_type:ident => $field_name:ident : $field_ty:ty),* $(,)? } => {{
- use $crate::__macro_support::serde_json::json;
-
- let mut result = $crate::__macro_support::serde_json::Map::new();
-
- $(
- let type_str = stringify!($field_ty);
- let schema = {
- let schema = $crate::__macro_support::schemars::schema_for!($field_ty);
- let schema_json = $crate::__macro_support::serde_json::to_value(schema).unwrap();
- // Extract just the properties if it's an object schema
- if let Some(obj) = schema_json.as_object() {
- if obj.contains_key("properties") {
- schema_json["properties"].clone()
- } else {
- "".to_string().into()
- }
- } else {
- "".to_string().into()
- }
- };
- result.insert(
- stringify!($field_name).to_string(),
- json!({
- "type": type_str,
- "desc": "",
- "schema": schema,
- "__dsrs_field_type": stringify!($field_type)
- })
- );
- )*
-
- $crate::__macro_support::serde_json::Value::Object(result)
- }};
-}
-
-#[macro_export]
-macro_rules! sign {
- // Example Usage: signature! {
- // question: String, random: bool -> answer: String
- // }
- //
- // Example Output:
- //
- // #[derive(Signature, Clone)]
- // struct InlineSignature {
- // #[input]
- // question: String,
- // #[input]
- // random: bool,
- // #[output]
- // answer: String,
- // }
- //
- // Predict::::new()
-
- // Pattern: input fields -> output fields
- { ($($input_name:ident : $input_type:ty),* $(,)?) -> $($output_name:ident : $output_type:ty),* $(,)? } => {{
- #[derive($crate::Signature, Clone)]
- struct __InlineSignature {
- $(
- #[input]
- $input_name: $input_type,
- )*
- $(
- #[output]
- $output_name: $output_type,
- )*
- }
-
- $crate::Predict::<__InlineSignature>::new()
- }};
-}
-
-/// Source:
-/// Author:
-/// License: MIT
-/// Description: This macro creates a HashMap from a list of key-value pairs.
-/// Reason for Reuse: Want to avoid adding a dependency for a simple macro.
-#[macro_export]
-macro_rules! hashmap {
- () => {
- ::std::collections::HashMap::new()
- };
-
- ($($key:expr => $value:expr),+ $(,)?) => {
- ::std::collections::HashMap::from([ $(($key, $value)),* ])
- };
-}
diff --git a/crates/dspy-rs/src/optimizer/copro.rs b/crates/dspy-rs/src/optimizer/copro.rs
deleted file mode 100644
index 736f0f87..00000000
--- a/crates/dspy-rs/src/optimizer/copro.rs
+++ /dev/null
@@ -1,303 +0,0 @@
-use anyhow::{Result, anyhow};
-use bon::Builder;
-
-use crate::core::DynPredictor;
-use crate::evaluate::{TypedMetric, average_score};
-use crate::optimizer::{
- Optimizer, evaluate_module_with_metric, predictor_names, with_named_predictor,
-};
-use crate::predictors::Example;
-use crate::{Facet, Module, Signature};
-
-/// Breadth-first instruction optimizer.
-///
-/// COPRO (Collaborative Prompt Optimization) generates `breadth` candidate instructions
-/// per predictor, evaluates each on the trainset, keeps the best, then repeats for
-/// `depth` rounds. Simple and predictable — good for quick iteration when you want
-/// better instructions without complex search.
-///
-/// Does not use feedback from the metric — only the numerical score matters. If you
-/// have rich textual feedback, use [`GEPA`](crate::GEPA) instead.
-///
-/// # Hyperparameters
-///
-/// - **`breadth`** (default: 10) — candidates per round per predictor. Higher = more
-/// exploration but proportionally more LM calls. Must be > 1.
-/// - **`depth`** (default: 3) — optimization rounds. Each round refines the previous
-/// best instruction. Diminishing returns beyond ~5.
-/// - **`init_temperature`** (default: 1.4) — **currently unused.** Reserved for LM-generated
-/// candidate diversity. Setting this has no effect.
-/// - **`prompt_model`** — optional separate LM for generating candidate instructions.
-/// Falls back to the global LM if unset.
-///
-/// # Cost
-///
-/// Total LM calls ≈ `breadth × depth × num_predictors × trainset_size`. For a module
-/// with 2 predictors, breadth=10, depth=3, and 50 training examples: ~3000 calls.
-///
-/// ```ignore
-/// let copro = COPRO::builder().breadth(10).depth(3).build();
-/// copro.compile(&mut module, trainset, &metric).await?;
-/// ```
-#[derive(Builder)]
-pub struct COPRO {
- /// Candidate instructions generated per round (must be > 1).
- #[builder(default = 10)]
- pub breadth: usize,
- /// Optimization rounds — each refines the previous best.
- #[builder(default = 3)]
- pub depth: usize,
- /// **Currently unused.** Reserved for controlling LM-generated candidate diversity.
- /// Setting this has no effect.
- #[builder(default = 1.4)]
- pub init_temperature: f32,
- /// Whether to track per-round statistics.
- #[builder(default = false)]
- pub track_stats: bool,
- /// Optional separate LM for generating candidate instructions.
- pub prompt_model: Option,
-}
-
-impl COPRO {
- fn current_instruction(module: &mut M, predictor_name: &str) -> Result
- where
- M: for<'a> Facet<'a>,
- {
- with_named_predictor(module, predictor_name, |predictor| {
- Ok(predictor.instruction())
- })
- }
-
- fn set_instruction(module: &mut M, predictor_name: &str, instruction: String) -> Result<()>
- where
- M: for<'a> Facet<'a>,
- {
- with_named_predictor(module, predictor_name, |predictor| {
- predictor.set_instruction(instruction);
- Ok(())
- })
- }
-
- async fn score_candidate(
- &self,
- module: &mut M,
- predictor_name: &str,
- candidate_instruction: &str,
- trainset: &[Example],
- metric: &MT,
- ) -> Result
- where
- S: Signature,
- S::Input: Clone,
- M: Module + for<'a> Facet<'a>,
- MT: TypedMetric,
- {
- let original_state = with_named_predictor(module, predictor_name, |predictor| {
- Ok(predictor.dump_state())
- })?;
-
- Self::set_instruction(module, predictor_name, candidate_instruction.to_string())?;
- let evaluation = evaluate_module_with_metric(&*module, trainset, metric).await;
-
- match evaluation {
- Ok(outcomes) => {
- with_named_predictor(module, predictor_name, |predictor| {
- predictor.load_state(original_state.clone())
- })?;
- Ok(average_score(&outcomes))
- }
- Err(eval_err) => {
- if let Err(restore_err) =
- with_named_predictor(module, predictor_name, |predictor| {
- predictor.load_state(original_state)
- })
- {
- return Err(anyhow!(
- "candidate evaluation failed: {eval_err}; failed to restore predictor state: {restore_err}"
- ));
- }
- Err(eval_err)
- }
- }
- }
-
- fn candidate_instructions(
- &self,
- base_instruction: &str,
- predictor: &dyn DynPredictor,
- depth: usize,
- ) -> Vec {
- let mut candidates = Vec::with_capacity(self.breadth.max(1));
- candidates.push(base_instruction.to_string());
-
- let output_hint = predictor
- .schema()
- .output_fields()
- .last()
- .map(|field| field.lm_name)
- .unwrap_or("output");
-
- for idx in 0..self.breadth.saturating_sub(1) {
- candidates.push(format!(
- "{base_instruction}\n\nOptimization hint (d{} c{}): Be explicit and concise for `{}`.",
- depth + 1,
- idx + 1,
- output_hint,
- ));
- }
-
- candidates
- }
-}
-
-impl Optimizer for COPRO {
- type Report = ();
-
- async fn compile(
- &self,
- module: &mut M,
- trainset: Vec>,
- metric: &MT,
- ) -> Result
- where
- S: Signature,
- S::Input: Clone,
- M: Module + for<'a> Facet<'a>,
- MT: TypedMetric,
- {
- if self.breadth <= 1 {
- return Err(anyhow!("breadth must be greater than 1"));
- }
-
- let predictor_names = predictor_names(module)?;
-
- if predictor_names.is_empty() {
- return Err(anyhow!("no optimizable predictors found"));
- }
-
- for depth in 0..self.depth {
- for predictor_name in &predictor_names {
- let base_instruction = Self::current_instruction(module, predictor_name)?;
-
- let candidates = with_named_predictor(module, predictor_name, |predictor| {
- Ok(self.candidate_instructions(&base_instruction, predictor, depth))
- })?;
-
- let mut best_instruction = base_instruction.clone();
- let mut best_score = f32::MIN;
-
- for candidate in candidates {
- let score = self
- .score_candidate::(
- module,
- predictor_name,
- &candidate,
- &trainset,
- metric,
- )
- .await?;
- if score > best_score {
- best_score = score;
- best_instruction = candidate;
- }
- }
-
- Self::set_instruction(module, predictor_name, best_instruction)?;
- }
- }
-
- Ok(())
- }
-}
-
-#[cfg(test)]
-mod tests {
- use anyhow::{Result, anyhow};
-
- use super::*;
- use crate::evaluate::{MetricOutcome, TypedMetric};
- use crate::{CallMetadata, Predict, PredictError, Predicted, Signature};
-
- #[derive(Signature, Clone, Debug)]
- struct CoproStateSig {
- #[input]
- prompt: String,
-
- #[output]
- answer: String,
- }
-
- #[derive(facet::Facet)]
- #[facet(crate = facet)]
- struct CoproStateModule {
- predictor: Predict,
- }
-
- impl Module for CoproStateModule {
- type Input = CoproStateSigInput;
- type Output = CoproStateSigOutput;
-
- async fn forward(
- &self,
- input: CoproStateSigInput,
- ) -> Result, PredictError> {
- Ok(Predicted::new(
- CoproStateSigOutput {
- answer: input.prompt,
- },
- CallMetadata::default(),
- ))
- }
- }
-
- struct AlwaysFailMetric;
-
- impl TypedMetric for AlwaysFailMetric {
- async fn evaluate(
- &self,
- _example: &Example,
- _prediction: &Predicted,
- ) -> Result {
- Err(anyhow!("metric failure"))
- }
- }
-
- fn trainset() -> Vec> {
- vec![Example::new(
- CoproStateSigInput {
- prompt: "one".to_string(),
- },
- CoproStateSigOutput {
- answer: "one".to_string(),
- },
- )]
- }
-
- #[tokio::test]
- async fn score_candidate_restores_state_when_metric_errors() {
- let optimizer = COPRO::builder().breadth(2).depth(1).build();
- let mut module = CoproStateModule {
- predictor: Predict::::builder()
- .instruction("seed-instruction")
- .build(),
- };
-
- let err = optimizer
- .score_candidate::(
- &mut module,
- "predictor",
- "candidate instruction",
- &trainset(),
- &AlwaysFailMetric,
- )
- .await
- .expect_err("candidate scoring should propagate metric failure");
- assert!(err.to_string().contains("metric failure"));
-
- let instruction = with_named_predictor(&mut module, "predictor", |predictor| {
- Ok(predictor.instruction())
- })
- .expect("predictor lookup should succeed");
- assert_eq!(instruction, "seed-instruction");
- }
-}
diff --git a/crates/dspy-rs/src/optimizer/mipro.rs b/crates/dspy-rs/src/optimizer/mipro.rs
deleted file mode 100644
index 6f2b4136..00000000
--- a/crates/dspy-rs/src/optimizer/mipro.rs
+++ /dev/null
@@ -1,509 +0,0 @@
-use anyhow::{Result, anyhow};
-use bon::Builder;
-
-use crate::evaluate::{TypedMetric, average_score};
-use crate::optimizer::{
- Optimizer, evaluate_module_with_metric, predictor_names, with_named_predictor,
-};
-use crate::predictors::Example;
-use crate::{BamlType, BamlValue, Facet, Module, Signature, SignatureSchema};
-
-/// A single program execution trace: input, outputs, and score.
-///
-/// Used internally by [`MIPROv2`] to collect execution data that informs
-/// candidate instruction generation. Traces with higher scores guide the
-/// optimizer toward better instructions.
-#[derive(Clone, Debug)]
-pub struct Trace {
- pub input: S::Input,
- pub outputs: BamlValue,
- pub score: Option,
-}
-
-impl Trace {
- pub fn new(input: S::Input, outputs: BamlValue, score: Option) -> Self {
- Self {
- input,
- outputs,
- score,
- }
- }
-
- pub fn format_for_prompt(&self) -> String {
- let mut result = String::new();
- result.push_str("Input:\n");
-
- result.push_str(&format!(" {}\n", self.input.to_baml_value()));
-
- result.push_str("Output:\n");
- result.push_str(&format!(" {}\n", self.outputs));
-
- if let Some(score) = self.score {
- result.push_str(&format!("Score: {:.3}\n", score));
- }
-
- result
- }
-}
-
-/// An instruction candidate with its evaluated score.
-///
-/// Generated by [`MIPROv2`]'s candidate generation step, then scored by
-/// evaluating the module with this instruction on a minibatch.
-#[derive(Clone, Debug)]
-pub struct PromptCandidate {
- pub instruction: String,
- pub score: f32,
-}
-
-impl PromptCandidate {
- pub fn new(instruction: String) -> Self {
- Self {
- instruction,
- score: 0.0,
- }
- }
-
- pub fn with_score(mut self, score: f32) -> Self {
- self.score = score;
- self
- }
-}
-
-/// Library of general prompting best practices used to seed candidate generation.
-///
-/// These tips are appended to candidate instructions during [`MIPROv2`] optimization
-/// to introduce diversity. Each candidate gets a different tip from the rotation.
-pub struct PromptingTips {
- pub tips: Vec,
-}
-
-impl PromptingTips {
- pub fn default_tips() -> Self {
- Self {
- tips: vec![
- "Use clear and specific language".to_string(),
- "Provide context about the task domain".to_string(),
- "Specify the desired output format".to_string(),
- "Use chain-of-thought reasoning for complex tasks".to_string(),
- "Include few-shot examples when helpful".to_string(),
- "Break down complex instructions into steps".to_string(),
- "Use role-playing (e.g., 'You are an expert...') when appropriate".to_string(),
- "Specify constraints and edge cases".to_string(),
- "Request explanations or reasoning when needed".to_string(),
- "Use structured output formats (JSON, lists, etc.) when applicable".to_string(),
- "Consider the model's strengths and limitations".to_string(),
- "Be explicit about what to avoid or exclude".to_string(),
- "Use positive framing (what to do vs. what not to do)".to_string(),
- "Provide examples of both correct and incorrect outputs when useful".to_string(),
- "Use delimiters or markers to separate different sections".to_string(),
- ],
- }
- }
-
- pub fn format_for_prompt(&self) -> String {
- self.tips
- .iter()
- .enumerate()
- .map(|(i, tip)| format!("{}. {}", i + 1, tip))
- .collect::>()
- .join("\n")
- }
-}
-
-/// Trace-guided instruction optimizer.
-///
-/// MIPROv2 (Multi-prompt Instruction PRoposal Optimizer v2) works in three phases:
-///
-/// 1. **Trace collection** — runs the module on the trainset to collect execution
-/// traces with scores
-/// 2. **Candidate generation** — uses the traces and prompting tips to generate
-/// `num_candidates` instruction variants per predictor
-/// 3. **Trial evaluation** — evaluates up to `num_trials` candidates on a minibatch,
-/// keeps the best
-///
-/// Unlike [`GEPA`](crate::GEPA), MIPROv2 does not require feedback — only numerical scores.
-/// Unlike [`COPRO`](crate::COPRO), it uses execution traces to inform candidate generation
-/// rather than
-/// blind search.
-///
-/// # What it doesn't do
-///
-/// MIPRO only optimizes instructions, not demos. Per-predictor demo mutation from
-/// trace data is the next step — Python DSPy does this and it matters. The
-/// `TODO(trace-demos)` markers in the source track this gap.
-///
-/// # Hyperparameters
-///
-/// - **`num_candidates`** (default: 10) — instruction variants generated per predictor.
-/// - **`num_trials`** (default: 20) — maximum candidates evaluated per predictor.
-/// If `num_trials` < `num_candidates`, only the first `num_trials` are evaluated.
-/// - **`minibatch_size`** (default: 25) — examples per candidate evaluation.
-///
-/// # Cost
-///
-/// Roughly `num_predictors × (trainset_size + num_trials × minibatch_size)` LM calls.
-///
-/// ```ignore
-/// let mipro = MIPROv2::builder()
-/// .num_candidates(10)
-/// .num_trials(20)
-/// .build();
-/// mipro.compile(&mut module, trainset, &metric).await?;
-/// ```
-#[derive(Builder)]
-pub struct MIPROv2 {
- /// Instruction variants generated per predictor.
- #[builder(default = 10)]
- pub num_candidates: usize,
-
- /// Maximum candidates evaluated per predictor.
- #[builder(default = 20)]
- pub num_trials: usize,
-
- /// Examples per candidate evaluation.
- #[builder(default = 25)]
- pub minibatch_size: usize,
-}
-
-impl MIPROv2 {
- async fn generate_traces(
- &self,
- module: &M,
- examples: &[Example],
- metric: &MT,
- ) -> Result>>
- where
- S: Signature,
- S::Input: Clone,
- M: Module,
- MT: TypedMetric,
- {
- let mut traces = Vec::with_capacity(examples.len());
- for example in examples {
- let input = example.input.clone();
- let predicted = module.call(input).await.map_err(|err| anyhow!("{err}"))?;
- let outcome = metric.evaluate(example, &predicted).await?;
- let (output, _) = predicted.into_parts();
- traces.push(Trace::new(
- example.input.clone(),
- output.to_baml_value(),
- Some(outcome.score),
- ));
- }
-
- Ok(traces)
- }
-
- pub fn select_best_traces<'a, S: Signature>(
- &self,
- traces: &'a [Trace],
- num_select: usize,
- ) -> Vec<&'a Trace> {
- let mut scored_traces: Vec<_> = traces.iter().filter(|t| t.score.is_some()).collect();
-
- scored_traces.sort_by(|a, b| {
- b.score
- .partial_cmp(&a.score)
- .unwrap_or(std::cmp::Ordering::Equal)
- });
-
- scored_traces.into_iter().take(num_select).collect()
- }
-
- fn generate_candidate_instructions(
- &self,
- program_description: &str,
- traces: &[Trace],
- num_candidates: usize,
- ) -> Vec {
- let tips = PromptingTips::default_tips();
- let score_hint = traces.iter().filter_map(|t| t.score).fold(0.0f32, f32::max);
-
- (0..num_candidates)
- .map(|idx| {
- let tip = &tips.tips[idx % tips.tips.len()];
- format!(
- "{program_description}\n\nOptimization candidate {}:\n- {}\n- Target score >= {:.3}",
- idx + 1,
- tip,
- score_hint
- )
- })
- .collect()
- }
-
- pub fn create_prompt_candidates(&self, instructions: Vec) -> Vec {
- instructions.into_iter().map(PromptCandidate::new).collect()
- }
-
- async fn evaluate_candidate(
- &self,
- module: &mut M,
- candidate: &PromptCandidate,
- eval_examples: &[Example],
- predictor_name: &str,
- metric: &MT,
- ) -> Result
- where
- S: Signature,
- S::Input: Clone,
- M: Module + for<'a> Facet<'a>,
- MT: TypedMetric,
- {
- let original_state = with_named_predictor(module, predictor_name, |predictor| {
- Ok(predictor.dump_state())
- })?;
-
- with_named_predictor(module, predictor_name, |predictor| {
- predictor.set_instruction(candidate.instruction.clone());
- // TODO(trace-demos): derive per-predictor demos from successful traces.
- // MIPRO is intentionally instruction-only in this release.
- Ok(())
- })?;
-
- let minibatch_end = eval_examples.len().min(self.minibatch_size);
- let minibatch = &eval_examples[..minibatch_end];
- let evaluation = evaluate_module_with_metric(&*module, minibatch, metric).await;
-
- match evaluation {
- Ok(outcomes) => {
- with_named_predictor(module, predictor_name, |predictor| {
- predictor.load_state(original_state.clone())
- })?;
- Ok(average_score(&outcomes))
- }
- Err(eval_err) => {
- if let Err(restore_err) =
- with_named_predictor(module, predictor_name, |predictor| {
- predictor.load_state(original_state)
- })
- {
- return Err(anyhow!(
- "candidate evaluation failed: {eval_err}; failed to restore predictor state: {restore_err}"
- ));
- }
- Err(eval_err)
- }
- }
- }
-
- async fn evaluate_and_select_best(
- &self,
- module: &mut M,
- candidates: Vec,
- eval_examples: &[Example],
- predictor_name: &str,
- metric: &MT,
- ) -> Result
- where
- S: Signature,
- S::Input: Clone,
- M: Module + for<'a> Facet<'a>,
- MT: TypedMetric,
- {
- let mut evaluated = Vec::new();
-
- let num_trials = self.num_trials.max(1);
- for candidate in candidates.into_iter().take(num_trials) {
- let score = self
- .evaluate_candidate::(
- module,
- &candidate,
- eval_examples,
- predictor_name,
- metric,
- )
- .await?;
- evaluated.push(candidate.with_score(score));
- }
-
- evaluated
- .into_iter()
- .max_by(|a, b| {
- a.score
- .partial_cmp(&b.score)
- .unwrap_or(std::cmp::Ordering::Equal)
- })
- .ok_or_else(|| anyhow!("no candidates to evaluate"))
- }
-
- pub fn format_schema_fields(&self, signature: &SignatureSchema) -> String {
- let mut result = String::new();
-
- result.push_str("Input Fields:\n");
- for field in signature.input_fields() {
- let desc = if field.docs.is_empty() {
- "No description"
- } else {
- field.docs.as_str()
- };
- result.push_str(&format!(" - {}: {}\n", field.lm_name, desc));
- }
-
- result.push_str("\nOutput Fields:\n");
- for field in signature.output_fields() {
- let desc = if field.docs.is_empty() {
- "No description"
- } else {
- field.docs.as_str()
- };
- result.push_str(&format!(" - {}: {}\n", field.lm_name, desc));
- }
-
- result
- }
-}
-
-impl Optimizer for MIPROv2 {
- type Report = ();
-
- async fn compile(
- &self,
- module: &mut M,
- trainset: Vec>,
- metric: &MT,
- ) -> Result
- where
- S: Signature,
- S::Input: Clone,
- M: Module + for<'a> Facet<'a>,
- MT: TypedMetric,
- {
- let predictor_names = predictor_names(module)?;
-
- if predictor_names.is_empty() {
- return Err(anyhow!("no optimizable predictors found"));
- }
-
- for predictor_name in predictor_names {
- let signature_desc = {
- with_named_predictor(module, &predictor_name, |predictor| {
- Ok(self.format_schema_fields(predictor.schema()))
- })?
- };
-
- let traces = self
- .generate_traces::(module, &trainset, metric)
- .await?;
- let instructions =
- self.generate_candidate_instructions(&signature_desc, &traces, self.num_candidates);
- let candidates = self.create_prompt_candidates(instructions);
- let best_candidate = self
- .evaluate_and_select_best::(
- module,
- candidates,
- &trainset,
- &predictor_name,
- metric,
- )
- .await?;
-
- with_named_predictor(module, &predictor_name, |predictor| {
- predictor.set_instruction(best_candidate.instruction.clone());
- // TODO(trace-demos): apply per-predictor demos derived from traces.
- // MIPRO is intentionally instruction-only in this release.
- Ok(())
- })?;
- }
-
- Ok(())
- }
-}
-
-#[cfg(test)]
-mod tests {
- use anyhow::{Result, anyhow};
-
- use super::*;
- use crate::evaluate::{MetricOutcome, TypedMetric};
- use crate::{CallMetadata, Predict, PredictError, Predicted, Signature};
-
- #[derive(Signature, Clone, Debug)]
- struct MiproStateSig {
- #[input]
- prompt: String,
-
- #[output]
- answer: String,
- }
-
- #[derive(facet::Facet)]
- #[facet(crate = facet)]
- struct MiproStateModule {
- predictor: Predict,
- }
-
- impl Module for MiproStateModule {
- type Input = MiproStateSigInput;
- type Output = MiproStateSigOutput;
-
- async fn forward(
- &self,
- input: MiproStateSigInput,
- ) -> Result, PredictError> {
- Ok(Predicted::new(
- MiproStateSigOutput {
- answer: input.prompt,
- },
- CallMetadata::default(),
- ))
- }
- }
-
- struct AlwaysFailMetric;
-
- impl TypedMetric for AlwaysFailMetric {
- async fn evaluate(
- &self,
- _example: &Example,
- _prediction: &Predicted,
- ) -> Result {
- Err(anyhow!("metric failure"))
- }
- }
-
- fn trainset() -> Vec> {
- vec![Example::new(
- MiproStateSigInput {
- prompt: "one".to_string(),
- },
- MiproStateSigOutput {
- answer: "one".to_string(),
- },
- )]
- }
-
- #[tokio::test]
- async fn evaluate_candidate_restores_state_when_metric_errors() {
- let optimizer = MIPROv2::builder()
- .num_candidates(2)
- .num_trials(1)
- .minibatch_size(1)
- .build();
- let mut module = MiproStateModule {
- predictor: Predict::::builder()
- .instruction("seed-instruction")
- .build(),
- };
- let candidate = PromptCandidate::new("candidate instruction".to_string());
-
- let err = optimizer
- .evaluate_candidate::(
- &mut module,
- &candidate,
- &trainset(),
- "predictor",
- &AlwaysFailMetric,
- )
- .await
- .expect_err("candidate evaluation should propagate metric failure");
- assert!(err.to_string().contains("metric failure"));
-
- let instruction = with_named_predictor(&mut module, "predictor", |predictor| {
- Ok(predictor.instruction())
- })
- .expect("predictor lookup should succeed");
- assert_eq!(instruction, "seed-instruction");
- }
-}
diff --git a/crates/dspy-rs/src/predictors/mod.rs b/crates/dspy-rs/src/predictors/mod.rs
deleted file mode 100644
index 691e5c76..00000000
--- a/crates/dspy-rs/src/predictors/mod.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-pub mod predict;
-
-pub use predict::*;
diff --git a/crates/dspy-rs/src/trace/mod.rs b/crates/dspy-rs/src/trace/mod.rs
deleted file mode 100644
index ff12a365..00000000
--- a/crates/dspy-rs/src/trace/mod.rs
+++ /dev/null
@@ -1,24 +0,0 @@
-//! Execution graph recording for debugging and inspection.
-//!
-//! Wrap a module call in [`trace()`] to capture a DAG of every [`Predict`](crate::Predict)
-//! invocation, with inputs and outputs at each node. The trace is scoped — only calls
-//! within the closure are recorded. The resulting [`Graph`] can be inspected or replayed
-//! via the [`Executor`].
-//!
-//! ```ignore
-//! let (result, graph) = dspy_rs::trace::trace(|| module.call(input)).await;
-//! println!("{} nodes recorded", graph.nodes.len());
-//! ```
-//!
-//! This is a debugging tool, not a performance tool. The `Mutex` inside the
-//! trace scope adds synchronization overhead. Don't trace in production hot paths.
-
-pub mod context;
-pub mod dag;
-pub mod executor;
-pub mod value;
-
-pub use context::*;
-pub use dag::*;
-pub use executor::*;
-pub use value::*;
diff --git a/crates/dspy-rs/src/trace/value.rs b/crates/dspy-rs/src/trace/value.rs
deleted file mode 100644
index f7cf9590..00000000
--- a/crates/dspy-rs/src/trace/value.rs
+++ /dev/null
@@ -1,34 +0,0 @@
-use serde::Serialize;
-use serde_json::Value;
-
-#[derive(Clone, Debug, Serialize)]
-pub struct TrackedValue {
- pub value: Value,
- #[serde(skip)]
- pub source: Option<(usize, String)>, // (node_id, key)
-}
-
-impl TrackedValue {
- pub fn new(value: Value, source: Option<(usize, String)>) -> Self {
- Self { value, source }
- }
-}
-
-pub trait IntoTracked {
- fn into_tracked(self) -> TrackedValue;
-}
-
-impl IntoTracked for TrackedValue {
- fn into_tracked(self) -> Self {
- self
- }
-}
-
-impl> IntoTracked for T {
- fn into_tracked(self) -> TrackedValue {
- TrackedValue {
- value: self.into(),
- source: None,
- }
- }
-}
diff --git a/crates/dspy-rs/src/utils/mod.rs b/crates/dspy-rs/src/utils/mod.rs
deleted file mode 100644
index 9462711b..00000000
--- a/crates/dspy-rs/src/utils/mod.rs
+++ /dev/null
@@ -1,16 +0,0 @@
-//! LM response caching.
-//!
-//! The [`ResponseCache`] provides a hybrid memory + disk cache backed by
-//! [foyer](https://docs.rs/foyer). It also maintains a sliding window of recent
-//! entries for [`LM::inspect_history`](crate::LM::inspect_history).
-//!
-//! Caching is per-LM-instance and keyed on the full prompt content. Cache entries
-//! are not shared across LM instances.
-
-pub mod cache;
-pub mod serde_utils;
-pub mod telemetry;
-
-pub use cache::{Cache, CacheEntry, ResponseCache};
-pub use serde_utils::get_iter_from_value;
-pub use telemetry::{TelemetryInitError, init_tracing, truncate};
diff --git a/crates/dspy-rs/src/utils/serde_utils.rs b/crates/dspy-rs/src/utils/serde_utils.rs
deleted file mode 100644
index 9bda5a78..00000000
--- a/crates/dspy-rs/src/utils/serde_utils.rs
+++ /dev/null
@@ -1,11 +0,0 @@
-pub fn get_iter_from_value(
- value: &serde_json::Value,
-) -> impl Iterator- {
- value
- .as_object()
- .unwrap()
- .iter()
- .map(|(k, v)| (k.to_string(), v.clone()))
- .collect::>()
- .into_iter()
-}
diff --git a/crates/dspy-rs/tests/test_miprov2.rs b/crates/dspy-rs/tests/test_miprov2.rs
deleted file mode 100644
index 79a5435b..00000000
--- a/crates/dspy-rs/tests/test_miprov2.rs
+++ /dev/null
@@ -1,140 +0,0 @@
-use dspy_rs::{BamlValue, MIPROv2, PromptCandidate, PromptingTips, Signature, Trace};
-use rstest::*;
-
-#[derive(Signature, Clone, Debug)]
-struct TestSignature {
- #[input]
- question: String,
-
- #[output]
- answer: String,
-}
-
-fn input(question: &str) -> TestSignatureInput {
- TestSignatureInput {
- question: question.to_string(),
- }
-}
-
-#[rstest]
-fn test_trace_formatting() {
- let trace = Trace::::new(
- input("What is 2+2?"),
- BamlValue::String("4".to_string()),
- Some(1.0),
- );
- let formatted = trace.format_for_prompt();
-
- assert!(formatted.contains("question"));
- assert!(formatted.contains("What is 2+2?"));
- assert!(formatted.contains("4"));
- assert!(formatted.contains("Score: 1.000"));
-}
-
-#[rstest]
-fn test_trace_formatting_without_score() {
- let trace = Trace::::new(
- input("input"),
- BamlValue::String("result".to_string()),
- None,
- );
- let formatted = trace.format_for_prompt();
-
- assert!(formatted.contains("Input:"));
- assert!(formatted.contains("Output:"));
- assert!(!formatted.contains("Score:"));
-}
-
-#[rstest]
-fn test_prompting_tips_default() {
- let tips = PromptingTips::default_tips();
-
- assert!(!tips.tips.is_empty());
- assert!(tips.tips.len() >= 15);
-}
-
-#[rstest]
-fn test_prompting_tips_formatting() {
- let tips = PromptingTips::default_tips();
- let formatted = tips.format_for_prompt();
-
- assert!(formatted.contains("1."));
- assert!(formatted.contains("\n"));
-}
-
-#[rstest]
-fn test_prompt_candidate_creation() {
- let candidate = PromptCandidate::new("Test instruction".to_string());
-
- assert_eq!(candidate.instruction, "Test instruction");
- assert_eq!(candidate.score, 0.0);
-}
-
-#[rstest]
-fn test_prompt_candidate_with_score() {
- let candidate = PromptCandidate::new("test".to_string()).with_score(0.85);
- assert_eq!(candidate.score, 0.85);
-}
-
-#[rstest]
-fn test_miprov2_default_configuration() {
- let optimizer = MIPROv2::builder().build();
-
- assert_eq!(optimizer.num_candidates, 10);
- assert_eq!(optimizer.num_trials, 20);
- assert_eq!(optimizer.minibatch_size, 25);
-}
-
-#[rstest]
-fn test_select_best_traces_descending_order() {
- let optimizer = MIPROv2::builder().build();
-
- let traces = vec![
- Trace::::new(input("a"), BamlValue::String("a".to_string()), Some(0.1)),
- Trace::::new(input("b"), BamlValue::String("b".to_string()), Some(0.5)),
- Trace::::new(input("c"), BamlValue::String("c".to_string()), Some(0.3)),
- ];
-
- let best = optimizer.select_best_traces(&traces, 2);
- assert_eq!(best.len(), 2);
- assert_eq!(best[0].score, Some(0.5));
- assert_eq!(best[1].score, Some(0.3));
-}
-
-#[rstest]
-fn test_select_best_traces_ignores_none_scores() {
- let optimizer = MIPROv2::builder().build();
-
- let traces = vec![
- Trace::::new(input("a"), BamlValue::String("a".to_string()), None),
- Trace::::new(input("b"), BamlValue::String("b".to_string()), Some(0.8)),
- ];
-
- let best = optimizer.select_best_traces(&traces, 2);
- assert_eq!(best.len(), 1);
- assert_eq!(best[0].score, Some(0.8));
-}
-
-#[rstest]
-fn test_create_prompt_candidates_uses_all_instructions() {
- let optimizer = MIPROv2::builder().build();
- let candidates = optimizer.create_prompt_candidates(vec![
- "instruction-1".to_string(),
- "instruction-2".to_string(),
- ]);
-
- assert_eq!(candidates.len(), 2);
- assert_eq!(candidates[0].instruction, "instruction-1");
- assert_eq!(candidates[1].instruction, "instruction-2");
-}
-
-#[rstest]
-fn test_format_schema_fields_reads_typed_schema() {
- let optimizer = MIPROv2::builder().build();
- let rendered = optimizer.format_schema_fields(TestSignature::schema());
-
- assert!(rendered.contains("Input Fields:"));
- assert!(rendered.contains("question"));
- assert!(rendered.contains("Output Fields:"));
- assert!(rendered.contains("answer"));
-}
diff --git a/crates/dspy-rs/tests/test_optimizer_named_parameters_integration.rs b/crates/dspy-rs/tests/test_optimizer_named_parameters_integration.rs
deleted file mode 100644
index 8d4abdcd..00000000
--- a/crates/dspy-rs/tests/test_optimizer_named_parameters_integration.rs
+++ /dev/null
@@ -1,86 +0,0 @@
-use anyhow::Result;
-use dspy_rs::{
- COPRO, CallMetadata, Example, MetricOutcome, Module, Optimizer, Predict, PredictError,
- Predicted, Signature, TypedMetric,
-};
-
-#[derive(Signature, Clone, Debug)]
-struct OptimizerSig {
- #[input]
- prompt: String,
-
- #[output]
- answer: String,
-}
-
-#[derive(facet::Facet)]
-#[facet(crate = facet)]
-struct InstructionEchoModule {
- predictor: Predict,
-}
-
-impl Module for InstructionEchoModule {
- type Input = OptimizerSigInput;
- type Output = OptimizerSigOutput;
-
- async fn forward(
- &self,
- input: OptimizerSigInput,
- ) -> Result, PredictError> {
- let _ = &self.predictor;
- Ok(Predicted::new(
- OptimizerSigOutput {
- answer: input.prompt,
- },
- CallMetadata::default(),
- ))
- }
-}
-
-struct InstructionLengthMetric;
-
-impl TypedMetric for InstructionLengthMetric {
- async fn evaluate(
- &self,
- _example: &Example,
- prediction: &Predicted,
- ) -> Result {
- Ok(MetricOutcome::score(prediction.answer.len() as f32))
- }
-}
-
-fn trainset() -> Vec> {
- vec![
- Example::new(
- OptimizerSigInput {
- prompt: "one".to_string(),
- },
- OptimizerSigOutput {
- answer: "one".to_string(),
- },
- ),
- Example::new(
- OptimizerSigInput {
- prompt: "two".to_string(),
- },
- OptimizerSigOutput {
- answer: "two".to_string(),
- },
- ),
- ]
-}
-
-#[tokio::test]
-async fn optimizer_compile_succeeds_without_public_named_parameter_access() {
- let mut module = InstructionEchoModule {
- predictor: Predict::::builder()
- .instruction("seed")
- .build(),
- };
-
- let optimizer = COPRO::builder().breadth(4).depth(1).build();
- optimizer
- .compile::(&mut module, trainset(), &InstructionLengthMetric)
- .await
- .expect("COPRO compile should succeed with internal predictor discovery");
-}
diff --git a/crates/dspy-rs/tests/test_optimizer_typed_metric.rs b/crates/dspy-rs/tests/test_optimizer_typed_metric.rs
deleted file mode 100644
index c05a590d..00000000
--- a/crates/dspy-rs/tests/test_optimizer_typed_metric.rs
+++ /dev/null
@@ -1,194 +0,0 @@
-use anyhow::{Result, anyhow};
-use dspy_rs::{
- COPRO, CallMetadata, Example, MIPROv2, MetricOutcome, Module, Optimizer, Predict, PredictError,
- Predicted, Signature, TypedMetric,
-};
-use std::collections::HashSet;
-use std::sync::{Arc, Mutex};
-
-#[derive(Signature, Clone, Debug)]
-struct OptimizerSig {
- #[input]
- prompt: String,
-
- #[output]
- answer: String,
-}
-
-#[derive(facet::Facet)]
-#[facet(crate = facet)]
-struct InstructionEchoModule {
- predictor: Predict,
-}
-
-impl Module for InstructionEchoModule {
- type Input = OptimizerSigInput;
- type Output = OptimizerSigOutput;
-
- async fn forward(
- &self,
- input: OptimizerSigInput,
- ) -> Result, PredictError> {
- let _ = &self.predictor;
- Ok(Predicted::new(
- OptimizerSigOutput {
- answer: input.prompt,
- },
- CallMetadata::default(),
- ))
- }
-}
-
-struct RecordingMetric {
- seen_answers: Arc>>,
-}
-
-impl TypedMetric for RecordingMetric {
- async fn evaluate(
- &self,
- example: &Example,
- prediction: &Predicted,
- ) -> Result {
- self.seen_answers
- .lock()
- .expect("metric lock should not be poisoned")
- .push(prediction.answer.clone());
-
- let score = (prediction.answer == example.input.prompt) as u8 as f32;
- Ok(MetricOutcome::score(score))
- }
-}
-
-struct FailingMetric;
-
-impl TypedMetric for FailingMetric {
- async fn evaluate(
- &self,
- _example: &Example,
- _prediction: &Predicted,
- ) -> Result {
- Err(anyhow!("metric failure"))
- }
-}
-
-fn trainset() -> Vec> {
- vec![
- Example::new(
- OptimizerSigInput {
- prompt: "one".to_string(),
- },
- OptimizerSigOutput {
- answer: "one".to_string(),
- },
- ),
- Example::new(
- OptimizerSigInput {
- prompt: "two".to_string(),
- },
- OptimizerSigOutput {
- answer: "two".to_string(),
- },
- ),
- ]
-}
-
-#[tokio::test]
-async fn copro_compile_uses_typed_metric_predictions() {
- let seen_answers = Arc::new(Mutex::new(Vec::new()));
- let metric = RecordingMetric {
- seen_answers: Arc::clone(&seen_answers),
- };
-
- let mut module = InstructionEchoModule {
- predictor: Predict::::builder()
- .instruction("seed")
- .build(),
- };
-
- let optimizer = COPRO::builder().breadth(3).depth(1).build();
- optimizer
- .compile::(&mut module, trainset(), &metric)
- .await
- .expect("COPRO compile should succeed on typed metric");
-
- let seen = seen_answers
- .lock()
- .expect("metric lock should not be poisoned");
- assert!(!seen.is_empty(), "metric should receive typed predictions");
- let expected_prompts = HashSet::from(["one".to_string(), "two".to_string()]);
- assert!(seen.iter().all(|answer| expected_prompts.contains(answer)));
- assert!(seen.iter().any(|answer| answer == "one"));
- assert!(seen.iter().any(|answer| answer == "two"));
-}
-
-#[tokio::test]
-async fn mipro_compile_uses_typed_metric_predictions() {
- let seen_answers = Arc::new(Mutex::new(Vec::new()));
- let metric = RecordingMetric {
- seen_answers: Arc::clone(&seen_answers),
- };
-
- let mut module = InstructionEchoModule {
- predictor: Predict::::builder()
- .instruction("seed")
- .build(),
- };
-
- let optimizer = MIPROv2::builder()
- .num_candidates(4)
- .num_trials(2)
- .minibatch_size(2)
- .build();
-
- optimizer
- .compile::(&mut module, trainset(), &metric)
- .await
- .expect("MIPRO compile should succeed on typed metric");
-
- let seen = seen_answers
- .lock()
- .expect("metric lock should not be poisoned");
- assert!(!seen.is_empty(), "metric should receive typed predictions");
- let expected_prompts = HashSet::from(["one".to_string(), "two".to_string()]);
- assert!(seen.iter().all(|answer| expected_prompts.contains(answer)));
- assert!(seen.iter().any(|answer| answer == "one"));
- assert!(seen.iter().any(|answer| answer == "two"));
-}
-
-#[tokio::test]
-async fn copro_compile_propagates_metric_errors() {
- let mut module = InstructionEchoModule {
- predictor: Predict::::builder()
- .instruction("seed")
- .build(),
- };
- let optimizer = COPRO::builder().breadth(3).depth(1).build();
-
- let err = optimizer
- .compile::(&mut module, trainset(), &FailingMetric)
- .await
- .expect_err("COPRO should propagate typed metric errors");
-
- assert!(err.to_string().contains("metric failure"));
-}
-
-#[tokio::test]
-async fn mipro_compile_propagates_metric_errors() {
- let mut module = InstructionEchoModule {
- predictor: Predict::::builder()
- .instruction("seed")
- .build(),
- };
- let optimizer = MIPROv2::builder()
- .num_candidates(4)
- .num_trials(2)
- .minibatch_size(2)
- .build();
-
- let err = optimizer
- .compile::(&mut module, trainset(), &FailingMetric)
- .await
- .expect_err("MIPRO should propagate typed metric errors");
-
- assert!(err.to_string().contains("metric failure"));
-}
diff --git a/crates/dsrs-cache/Cargo.toml b/crates/dsrs-cache/Cargo.toml
new file mode 100644
index 00000000..d90e1377
--- /dev/null
+++ b/crates/dsrs-cache/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "dsrs-cache"
+version = "0.0.0"
+edition = "2024"
+rust-version = "1.85"
+license = "Apache-2.0"
+repository = "https://github.com/krypticmouse/DSRs"
+description = "DSRs response cache support."
+
+[dependencies]
+anyhow = "1.0.99"
+async-trait = "0.1.83"
+dsrs-core = { path = "../dsrs-core" }
+foyer = { version = "0.20.0", features = ["serde"] }
+serde = { version = "1.0.219", features = ["derive"] }
+serde_json = { version = "1.0.140", features = ["preserve_order"] }
+tempfile = "3.23.0"
+tokio = { version = "1.46.1", features = ["sync"] }
+tracing = "0.1.44"
+
+[dev-dependencies]
+tokio = { version = "1.46.1", features = ["macros", "rt", "sync"] }
diff --git a/crates/dspy-rs/src/utils/cache.rs b/crates/dsrs-cache/src/cache.rs
similarity index 71%
rename from crates/dspy-rs/src/utils/cache.rs
rename to crates/dsrs-cache/src/cache.rs
index 866c8cf0..efc8cab2 100644
--- a/crates/dspy-rs/src/utils/cache.rs
+++ b/crates/dsrs-cache/src/cache.rs
@@ -7,7 +7,7 @@ use tempfile;
use tokio::sync::mpsc;
use tracing::{debug, trace, warn};
-use crate::{Prediction, RawExample};
+use dsrs_core::{Prediction, RawExample};
type CacheKey = Vec<(String, Value)>;
@@ -132,3 +132,62 @@ impl Cache for ResponseCache {
Ok(self.history_window[..actual_n].to_vec())
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use dsrs_core::{LmUsage, hashmap};
+
+ fn raw_key(question: &str) -> RawExample {
+ RawExample::new(
+ hashmap! {
+ "question".to_string() => question.into(),
+ },
+ vec!["question".to_string()],
+ vec![],
+ )
+ }
+
+ fn prediction(answer: &str) -> Prediction {
+ Prediction::new(
+ hashmap! {
+ "answer".to_string() => answer.into(),
+ },
+ LmUsage::default(),
+ )
+ }
+
+ #[tokio::test]
+ async fn insert_get_and_history_round_trip_cached_prediction() {
+ let mut cache = ResponseCache::new().await;
+ let key = raw_key("capital?");
+ assert!(cache.get(key.clone()).await.unwrap().is_none());
+
+ let (tx, rx) = mpsc::channel(1);
+ let entry = CacheEntry {
+ prompt: "prompt".to_string(),
+ prediction: prediction("Paris"),
+ };
+ tx.send(entry.clone()).await.unwrap();
+ drop(tx);
+
+ cache.insert(key.clone(), rx).await.unwrap();
+
+ let cached = cache.get(key).await.unwrap().unwrap();
+ assert_eq!(cached.get("answer", None), "Paris");
+ let history = cache.get_history(10).await.unwrap();
+ assert_eq!(history.len(), 1);
+ assert_eq!(history[0].prompt, entry.prompt);
+ }
+
+ #[tokio::test]
+ async fn insert_with_closed_channel_is_noop() {
+ let mut cache = ResponseCache::new().await;
+ let (_tx, rx) = mpsc::channel(1);
+ drop(_tx);
+
+ cache.insert(raw_key("missing"), rx).await.unwrap();
+
+ assert!(cache.get_history(1).await.unwrap().is_empty());
+ }
+}
diff --git a/crates/dsrs-cache/src/lib.rs b/crates/dsrs-cache/src/lib.rs
new file mode 100644
index 00000000..8fee7dec
--- /dev/null
+++ b/crates/dsrs-cache/src/lib.rs
@@ -0,0 +1,5 @@
+//! LM response caching.
+
+pub mod cache;
+
+pub use cache::{Cache, CacheEntry, ResponseCache};
diff --git a/crates/dsrs-core/Cargo.toml b/crates/dsrs-core/Cargo.toml
new file mode 100644
index 00000000..dc6b28bf
--- /dev/null
+++ b/crates/dsrs-core/Cargo.toml
@@ -0,0 +1,29 @@
+[package]
+name = "dsrs-core"
+version = "0.0.0"
+edition = "2024"
+rust-version = "1.85"
+license = "Apache-2.0"
+repository = "https://github.com/krypticmouse/DSRs"
+description = "DSRs core: signature, module, schema, and abstract bridge traits."
+
+[dependencies]
+anyhow = "1.0.99"
+async-trait = "0.1.83"
+bamltype = { path = "../bamltype" }
+bon = "3.7.0"
+dsrs_macros = { version = "0.7.2", path = "../dsrs-macros" }
+facet = { git = "https://github.com/darinkishore/facet", rev = "cc8613c97cd1ec03e63659db34a947989b45c8a5", default-features = false, features = ["std"] }
+futures = "0.3.31"
+indexmap = "2.10.0"
+kdam = "0.6.3"
+rig-core = { git = "https://github.com/0xPlaygrounds/rig", rev = "aee3b8bf6576ce41c9ac1dd82520752a65fa0127" }
+schemars = "1.0.4"
+serde = { version = "1.0.219", features = ["derive"] }
+serde_json = { version = "1.0.140", features = ["preserve_order"] }
+thiserror = "2.0.17"
+tracing = "0.1.44"
+
+[dev-dependencies]
+rstest = "0.25.0"
+tokio = { version = "1.46.1", features = ["macros", "rt", "time"] }
diff --git a/crates/dspy-rs/src/augmentation.rs b/crates/dsrs-core/src/augmentation.rs
similarity index 98%
rename from crates/dspy-rs/src/augmentation.rs
rename to crates/dsrs-core/src/augmentation.rs
index 96e37861..5637d40f 100644
--- a/crates/dspy-rs/src/augmentation.rs
+++ b/crates/dsrs-core/src/augmentation.rs
@@ -13,8 +13,8 @@ use facet::Facet;
///
/// Usually derived:
///
-/// ```
-/// use dspy_rs::*;
+/// ```ignore
+/// use dsrs_core::*;
///
/// #[derive(Augmentation, Clone, Debug)]
/// #[augment(output, prepend)]
diff --git a/crates/dsrs-core/src/demo.rs b/crates/dsrs-core/src/demo.rs
new file mode 100644
index 00000000..16afaf17
--- /dev/null
+++ b/crates/dsrs-core/src/demo.rs
@@ -0,0 +1,15 @@
+use crate::Signature;
+
+/// A typed input/output pair for few-shot prompting.
+#[derive(Clone, Debug, facet::Facet)]
+#[facet(crate = facet)]
+pub struct Example {
+ pub input: S::Input,
+ pub output: S::Output,
+}
+
+impl Example
{
+ pub fn new(input: S::Input, output: S::Output) -> Self {
+ Self { input, output }
+ }
+}
diff --git a/crates/dspy-rs/src/core/dyn_predictor.rs b/crates/dsrs-core/src/dyn_predictor.rs
similarity index 90%
rename from crates/dspy-rs/src/core/dyn_predictor.rs
rename to crates/dsrs-core/src/dyn_predictor.rs
index d5a436a8..c7b00b7d 100644
--- a/crates/dspy-rs/src/core/dyn_predictor.rs
+++ b/crates/dsrs-core/src/dyn_predictor.rs
@@ -5,8 +5,7 @@ use anyhow::Result;
use bamltype::facet_reflect::Peek;
use facet::{ConstTypeId, Def, Facet, KnownPointer, Shape, Type, UserType};
-use crate::SignatureSchema;
-use crate::data::example::Example as RawExample;
+use crate::{RawExample, SignatureSchema};
/// Type-erased optimizer handle to a [`crate::Predict`] leaf.
///
@@ -17,7 +16,7 @@ use crate::data::example::Example as RawExample;
///
/// Normal users never touch this — you pass your module to `optimizer.compile()`
/// and it uses `DynPredictor` internally.
-pub(crate) trait DynPredictor: Send + Sync {
+pub trait DynPredictor: Send + Sync {
/// Returns the [`SignatureSchema`] for this predictor's signature.
fn schema(&self) -> &SignatureSchema;
@@ -55,7 +54,7 @@ pub(crate) trait DynPredictor: Send + Sync {
/// Used by [`DynPredictor::dump_state`]/[`DynPredictor::load_state`] for
/// saving and restoring optimized parameters.
#[derive(Clone, Debug, Default)]
-pub(crate) struct PredictState {
+pub struct PredictState {
/// The demos as type-erased examples.
pub demos: Vec,
/// The instruction override, if any.
@@ -67,7 +66,7 @@ type VisitMutFn =
#[derive(Clone, Copy, Debug, facet::Facet)]
#[facet(opaque)]
-pub(crate) struct PredictAccessorFns {
+pub struct PredictAccessorFns {
pub visit_mut: VisitMutFn,
}
@@ -81,7 +80,7 @@ impl Eq for PredictAccessorFns {}
facet::define_attr_grammar! {
ns "dsrs";
- crate_path $crate::core::dyn_predictor;
+ crate_path $crate::dyn_predictor;
pub enum Attr {
PredictAccessor(Option<&'static PredictAccessorFns>),
@@ -90,7 +89,7 @@ facet::define_attr_grammar! {
/// Error from [`visit_named_predictors_mut`] when the Facet walker encounters an unsupported structure.
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
-pub(crate) enum NamedParametersError {
+pub enum NamedParametersError {
/// A `Predict` leaf was found inside an unsupported container (`Rc`, `Arc`, etc.).
#[error("container `{ty}` at `{path}` contains a parameter leaf")]
Container { path: String, ty: &'static str },
@@ -117,7 +116,7 @@ pub(crate) enum NamedParametersError {
name = "dsrs.visit_named_predictors_mut",
skip(module, visitor)
)]
-pub(crate) fn visit_named_predictors_mut(
+pub fn visit_named_predictors_mut(
module: &mut M,
mut visitor: F,
) -> std::result::Result<(), NamedParametersError>
@@ -335,7 +334,7 @@ fn resolve_predict_leaf(shape: &'static Shape) -> PredictLeafResolution {
}
fn is_predict_shape_identity(shape: &'static Shape) -> bool {
- shape.type_identifier == "Predict" && shape.module_path == Some("dspy_rs::predictors::predict")
+ shape.type_identifier == "Predict" && shape.module_path == Some("dsrs_predict::predict")
}
fn push_field(path: &str, field: &str) -> String {
@@ -397,45 +396,7 @@ fn pointer_name(pointer: Option) -> &'static str {
mod tests {
use super::*;
use crate as dsrs;
- use crate::Signature;
- use crate::predictors::Predict as RealPredict;
use std::ops::ControlFlow;
- use std::rc::Rc;
- use std::sync::Arc;
-
- #[derive(Signature, Clone, Debug)]
- struct DummySig {
- #[input]
- value: String,
-
- #[output]
- done: bool,
- }
-
- #[derive(facet::Facet)]
- #[facet(crate = facet)]
- struct SharedPointerModule {
- rc_predictor: Rc>,
- arc_predictor: Arc>,
- }
-
- #[test]
- fn named_parameters_rejects_shared_pointers() {
- let mut module = SharedPointerModule {
- rc_predictor: Rc::new(RealPredict::::new()),
- arc_predictor: Arc::new(RealPredict::::new()),
- };
-
- match visit_named_predictors_mut(&mut module, |_path, _predictor| ControlFlow::Continue(()))
- {
- Err(NamedParametersError::Container { path, ty }) => {
- assert_eq!(path, "rc_predictor");
- assert_eq!(ty, "Rc");
- }
- Ok(_) => panic!("walk unexpectedly succeeded"),
- Err(other) => panic!("unexpected error: {other:?}"),
- }
- }
#[derive(facet::Facet)]
#[facet(crate = facet, dsrs::predict_accessor)]
@@ -519,11 +480,6 @@ mod tests {
}
}
- #[test]
- fn real_predict_shape_has_strict_identity_marker() {
- assert!(is_predict_shape_identity(RealPredict::::SHAPE));
- }
-
#[derive(facet::Facet)]
#[facet(crate = facet)]
struct Predict;
diff --git a/crates/dspy-rs/src/core/errors.rs b/crates/dsrs-core/src/errors.rs
similarity index 100%
rename from crates/dspy-rs/src/core/errors.rs
rename to crates/dsrs-core/src/errors.rs
diff --git a/crates/dspy-rs/src/data/example.rs b/crates/dsrs-core/src/example.rs
similarity index 100%
rename from crates/dspy-rs/src/data/example.rs
rename to crates/dsrs-core/src/example.rs
diff --git a/crates/dsrs-core/src/lib.rs b/crates/dsrs-core/src/lib.rs
new file mode 100644
index 00000000..857e567b
--- /dev/null
+++ b/crates/dsrs-core/src/lib.rs
@@ -0,0 +1,149 @@
+//! Core typed substrate for DSRs.
+
+// TODO(dsrs-facet-lint-scope): remove this crate-level allow once Facet's generated
+// extension-attr dispatch no longer triggers rust-lang/rust#52234 on in-crate usage.
+#![allow(macro_expanded_macro_exports_accessed_by_absolute_paths)]
+
+pub mod augmentation;
+mod demo;
+pub mod dyn_predictor;
+mod errors;
+mod example;
+mod module;
+mod module_ext;
+mod predicted;
+mod prediction;
+mod schema;
+mod signature;
+mod specials;
+mod usage;
+
+pub use augmentation::*;
+pub use demo::*;
+pub use dyn_predictor::*;
+pub use errors::{ConversionError, ErrorClass, JsonishError, LmError, ParseError, PredictError};
+pub use example::Example as RawExample;
+pub use module::*;
+pub use module_ext::*;
+pub use predicted::{CallMetadata, ConstraintResult, FieldMeta, Predicted};
+pub use prediction::*;
+pub use schema::{FieldMetadataSpec, FieldPath, FieldSchema, InputRenderSpec, SignatureSchema};
+pub use signature::*;
+pub use specials::*;
+pub use usage::*;
+
+pub use bamltype::BamlConvertError;
+pub use bamltype::BamlType;
+pub use bamltype::Shape;
+pub use bamltype::baml_types::{
+ BamlValue, Constraint, ConstraintLevel, ResponseCheck, StreamingMode, TypeIR,
+};
+pub use bamltype::internal_baml_jinja::types::{OutputFormatContent, RenderOptions};
+pub use bamltype::jsonish::deserializer::deserialize_flags::Flag;
+pub use dsrs_macros::*;
+pub use facet::Facet;
+
+#[doc(hidden)]
+pub mod __macro_support {
+ pub use anyhow;
+ pub use bamltype;
+ pub use indexmap;
+ pub use schemars;
+ pub use serde;
+ pub use serde_json;
+}
+
+#[macro_export]
+macro_rules! field {
+ { $($field_type:ident[$desc:literal] => $field_name:ident : $field_ty:ty),* $(,)? } => {{
+ use $crate::__macro_support::serde_json::json;
+
+ let mut result = $crate::__macro_support::serde_json::Map::new();
+
+ $(
+ let type_str = stringify!($field_ty);
+ let schema = {
+ let schema = $crate::__macro_support::schemars::schema_for!($field_ty);
+ let schema_json = $crate::__macro_support::serde_json::to_value(schema).unwrap();
+ if let Some(obj) = schema_json.as_object() {
+ if obj.contains_key("properties") {
+ schema_json["properties"].clone()
+ } else {
+ "".to_string().into()
+ }
+ } else {
+ "".to_string().into()
+ }
+ };
+ result.insert(
+ stringify!($field_name).to_string(),
+ json!({
+ "type": type_str,
+ "desc": $desc,
+ "schema": schema,
+ "__dsrs_field_type": stringify!($field_type)
+ })
+ );
+ )*
+
+ $crate::__macro_support::serde_json::Value::Object(result)
+ }};
+
+ { $($field_type:ident => $field_name:ident : $field_ty:ty),* $(,)? } => {{
+ use $crate::__macro_support::serde_json::json;
+
+ let mut result = $crate::__macro_support::serde_json::Map::new();
+
+ $(
+ let type_str = stringify!($field_ty);
+ let schema = {
+ let schema = $crate::__macro_support::schemars::schema_for!($field_ty);
+ let schema_json = $crate::__macro_support::serde_json::to_value(schema).unwrap();
+ if let Some(obj) = schema_json.as_object() {
+ if obj.contains_key("properties") {
+ schema_json["properties"].clone()
+ } else {
+ "".to_string().into()
+ }
+ } else {
+ "".to_string().into()
+ }
+ };
+ result.insert(
+ stringify!($field_name).to_string(),
+ json!({
+ "type": type_str,
+ "desc": "",
+ "schema": schema,
+ "__dsrs_field_type": stringify!($field_type)
+ })
+ );
+ )*
+
+ $crate::__macro_support::serde_json::Value::Object(result)
+ }};
+}
+
+#[macro_export]
+macro_rules! hashmap {
+ () => {
+ ::std::collections::HashMap::new()
+ };
+
+ ($($key:expr => $value:expr),+ $(,)?) => {
+ ::std::collections::HashMap::from([ $(($key, $value)),* ])
+ };
+}
+
+#[derive(Clone, Debug, serde::Serialize)]
+pub struct TrackedValue {
+ pub value: serde_json::Value,
+ #[serde(skip)]
+ pub source: Option<(usize, String)>,
+}
+
+impl TrackedValue {
+ pub fn new(value: serde_json::Value, source: Option<(usize, String)>) -> Self {
+ Self { value, source }
+ }
+}
diff --git a/crates/dspy-rs/src/core/module.rs b/crates/dsrs-core/src/module.rs
similarity index 99%
rename from crates/dspy-rs/src/core/module.rs
rename to crates/dsrs-core/src/module.rs
index 234998e3..5c08418e 100644
--- a/crates/dspy-rs/src/core/module.rs
+++ b/crates/dsrs-core/src/module.rs
@@ -87,10 +87,9 @@ pub trait Module: Send + Sync {
///
/// Shows a progress bar on stderr. Use [`forward_all_with_progress`] to disable it.
///
-/// ```no_run
+/// ```ignore
/// # async fn example() -> Result<(), Box> {
-/// use dspy_rs::*;
-/// use dspy_rs::doctest::*;
+/// use dsrs_core::*;
///
/// let predict = Predict::::new();
/// let inputs = vec![
diff --git a/crates/dspy-rs/src/core/module_ext.rs b/crates/dsrs-core/src/module_ext.rs
similarity index 100%
rename from crates/dspy-rs/src/core/module_ext.rs
rename to crates/dsrs-core/src/module_ext.rs
diff --git a/crates/dspy-rs/src/core/predicted.rs b/crates/dsrs-core/src/predicted.rs
similarity index 98%
rename from crates/dspy-rs/src/core/predicted.rs
rename to crates/dsrs-core/src/predicted.rs
index c40940c2..2dd5c67d 100644
--- a/crates/dspy-rs/src/core/predicted.rs
+++ b/crates/dsrs-core/src/predicted.rs
@@ -37,7 +37,7 @@ pub struct ConstraintResult {
/// all live here.
///
/// ```
-/// use dspy_rs::CallMetadata;
+/// use dsrs_core::CallMetadata;
///
/// let meta = CallMetadata::default();
/// assert_eq!(meta.lm_usage.total_tokens, 0);
@@ -151,7 +151,7 @@ impl CallMetadata {
/// limitation.
///
/// ```
-/// use dspy_rs::{Predicted, CallMetadata};
+/// use dsrs_core::{Predicted, CallMetadata};
///
/// #[derive(Debug)]
/// struct QAOutput { answer: String }
diff --git a/crates/dspy-rs/src/data/prediction.rs b/crates/dsrs-core/src/prediction.rs
similarity index 95%
rename from crates/dspy-rs/src/data/prediction.rs
rename to crates/dsrs-core/src/prediction.rs
index 62180db4..5e7501fe 100644
--- a/crates/dspy-rs/src/data/prediction.rs
+++ b/crates/dsrs-core/src/prediction.rs
@@ -39,9 +39,9 @@ impl Prediction {
.clone()
}
- pub fn get_tracked(&self, key: &str) -> crate::trace::TrackedValue {
+ pub fn get_tracked(&self, key: &str) -> crate::TrackedValue {
let val = self.get(key, None);
- crate::trace::TrackedValue {
+ crate::TrackedValue {
value: val,
source: self.node_id.map(|id| (id, key.to_string())),
}
diff --git a/crates/dspy-rs/src/core/schema.rs b/crates/dsrs-core/src/schema.rs
similarity index 100%
rename from crates/dspy-rs/src/core/schema.rs
rename to crates/dsrs-core/src/schema.rs
diff --git a/crates/dspy-rs/src/core/signature.rs b/crates/dsrs-core/src/signature.rs
similarity index 98%
rename from crates/dspy-rs/src/core/signature.rs
rename to crates/dsrs-core/src/signature.rs
index 616917a9..b44acc0d 100644
--- a/crates/dspy-rs/src/core/signature.rs
+++ b/crates/dsrs-core/src/signature.rs
@@ -28,9 +28,8 @@ pub enum ConstraintKind {
/// following this instruction." You define it, the system handles prompt formatting,
/// response parsing, and type checking.
///
-/// ```
-/// use dspy_rs::*;
-/// use dspy_rs::doctest::*;
+/// ```ignore
+/// use dsrs_core::*;
///
/// // The derive generates QAInput { question } and QAOutput { answer }
/// let _input = QAInput { question: "What is 2+2?".into() };
diff --git a/crates/dspy-rs/src/core/specials.rs b/crates/dsrs-core/src/specials.rs
similarity index 100%
rename from crates/dspy-rs/src/core/specials.rs
rename to crates/dsrs-core/src/specials.rs
diff --git a/crates/dspy-rs/src/core/lm/usage.rs b/crates/dsrs-core/src/usage.rs
similarity index 100%
rename from crates/dspy-rs/src/core/lm/usage.rs
rename to crates/dsrs-core/src/usage.rs
diff --git a/crates/dspy-rs/tests/test_call_outcome.rs b/crates/dsrs-core/tests/test_call_outcome.rs
similarity index 99%
rename from crates/dspy-rs/tests/test_call_outcome.rs
rename to crates/dsrs-core/tests/test_call_outcome.rs
index ee8183c1..effb2e81 100644
--- a/crates/dspy-rs/tests/test_call_outcome.rs
+++ b/crates/dsrs-core/tests/test_call_outcome.rs
@@ -1,4 +1,4 @@
-use dspy_rs::{
+use dsrs_core::{
CallMetadata, ConstraintResult, FieldMeta, LmUsage, ParseError, PredictError, Predicted,
};
use indexmap::IndexMap;
diff --git a/crates/dspy-rs/tests/test_module_ext.rs b/crates/dsrs-core/tests/test_module_ext.rs
similarity index 93%
rename from crates/dspy-rs/tests/test_module_ext.rs
rename to crates/dsrs-core/tests/test_module_ext.rs
index c7bb1c16..1bea0e97 100644
--- a/crates/dspy-rs/tests/test_module_ext.rs
+++ b/crates/dsrs-core/tests/test_module_ext.rs
@@ -1,4 +1,4 @@
-use dspy_rs::{BamlType, CallMetadata, Module, ModuleExt, ParseError, PredictError, Predicted};
+use dsrs_core::{BamlType, CallMetadata, Module, ModuleExt, ParseError, PredictError, Predicted};
struct MaybeFails;
@@ -22,7 +22,7 @@ impl Module for MaybeFails {
let input_value = input.value;
let metadata = CallMetadata::new(
format!("raw:{input_value}"),
- dspy_rs::LmUsage::default(),
+ dsrs_core::LmUsage::default(),
Vec::new(),
Vec::new(),
Some(input_value.max(0) as usize),
@@ -36,7 +36,7 @@ impl Module for MaybeFails {
raw_response: format!("raw:{input_value}"),
},
raw_response: format!("raw:{input_value}"),
- lm_usage: dspy_rs::LmUsage::default(),
+ lm_usage: dsrs_core::LmUsage::default(),
})
} else {
Ok(Predicted::new(
@@ -65,7 +65,7 @@ fn transform_int_payload(value: IntPayload) -> Result
raw_response: "transform".to_string(),
},
raw_response: "transform".to_string(),
- lm_usage: dspy_rs::LmUsage::default(),
+ lm_usage: dsrs_core::LmUsage::default(),
})
}
}
diff --git a/crates/dspy-rs/tests/test_module_forward_all.rs b/crates/dsrs-core/tests/test_module_forward_all.rs
similarity index 94%
rename from crates/dspy-rs/tests/test_module_forward_all.rs
rename to crates/dsrs-core/tests/test_module_forward_all.rs
index a2376455..63495bf2 100644
--- a/crates/dspy-rs/tests/test_module_forward_all.rs
+++ b/crates/dsrs-core/tests/test_module_forward_all.rs
@@ -1,6 +1,6 @@
use std::time::Duration;
-use dspy_rs::{BamlType, CallMetadata, Module, PredictError, Predicted, forward_all};
+use dsrs_core::{BamlType, CallMetadata, Module, PredictError, Predicted, forward_all};
use tokio::time::sleep;
struct DelayEcho;
diff --git a/crates/dspy-rs/tests/test_predictions.rs b/crates/dsrs-core/tests/test_predictions.rs
similarity index 98%
rename from crates/dspy-rs/tests/test_predictions.rs
rename to crates/dsrs-core/tests/test_predictions.rs
index 815ebdba..4fd9aa03 100644
--- a/crates/dspy-rs/tests/test_predictions.rs
+++ b/crates/dsrs-core/tests/test_predictions.rs
@@ -1,7 +1,7 @@
use rstest::*;
use serde_json::json;
-use dspy_rs::{LmUsage, Prediction};
+use dsrs_core::{LmUsage, Prediction};
use std::collections::HashMap;
#[rstest]
diff --git a/crates/dspy-rs/tests/test_signature.rs b/crates/dsrs-core/tests/test_signature.rs
similarity index 98%
rename from crates/dspy-rs/tests/test_signature.rs
rename to crates/dsrs-core/tests/test_signature.rs
index 26becc59..cd6d23ac 100644
--- a/crates/dspy-rs/tests/test_signature.rs
+++ b/crates/dsrs-core/tests/test_signature.rs
@@ -1,4 +1,4 @@
-use dspy_rs::Signature;
+use dsrs_core::Signature;
#[derive(Signature, Clone, Debug)]
struct BasicSignature {
diff --git a/crates/dspy-rs/tests/test_signature_macro.rs b/crates/dsrs-core/tests/test_signature_macro.rs
similarity index 98%
rename from crates/dspy-rs/tests/test_signature_macro.rs
rename to crates/dsrs-core/tests/test_signature_macro.rs
index 01f527d4..0f446b86 100644
--- a/crates/dspy-rs/tests/test_signature_macro.rs
+++ b/crates/dsrs-core/tests/test_signature_macro.rs
@@ -1,4 +1,4 @@
-use dspy_rs::{InputRenderSpec, Signature};
+use dsrs_core::{InputRenderSpec, Signature};
#[derive(Signature, Clone, Debug)]
struct AliasAndFormatSignature {
diff --git a/crates/dspy-rs/tests/test_signature_schema.rs b/crates/dsrs-core/tests/test_signature_schema.rs
similarity index 97%
rename from crates/dspy-rs/tests/test_signature_schema.rs
rename to crates/dsrs-core/tests/test_signature_schema.rs
index bb0a9eb5..366dbd0b 100644
--- a/crates/dspy-rs/tests/test_signature_schema.rs
+++ b/crates/dsrs-core/tests/test_signature_schema.rs
@@ -1,4 +1,4 @@
-use dspy_rs::{BamlType, Signature, SignatureSchema};
+use dsrs_core::{BamlType, Signature, SignatureSchema};
#[derive(Clone, Debug)]
#[BamlType]
diff --git a/crates/dsrs-data/Cargo.toml b/crates/dsrs-data/Cargo.toml
new file mode 100644
index 00000000..d5ec99b9
--- /dev/null
+++ b/crates/dsrs-data/Cargo.toml
@@ -0,0 +1,42 @@
+[package]
+name = "dsrs-data"
+version = "0.0.0"
+edition = "2024"
+rust-version = "1.85"
+license = "Apache-2.0"
+repository = "https://github.com/krypticmouse/DSRs"
+description = "DSRs dataset loading support."
+
+[dependencies]
+anyhow = "1.0.99"
+bamltype = { path = "../bamltype" }
+csv = { version = "1.3.1", optional = true }
+dsrs-core = { path = "../dsrs-core" }
+dsrs-predict = { path = "../dsrs-predict" }
+parquet = { version = "56.1.0", optional = true }
+arrow = { version = "56.1.0", optional = true }
+hf-hub = { version = "0.4.3", features = ["tokio"], optional = true }
+rayon = "1.10.0"
+regex = "1.11.2"
+reqwest = { version = "0.13", features = ["blocking"], optional = true }
+serde = { version = "1.0.219", features = ["derive"] }
+serde_json = { version = "1.0.140", features = ["preserve_order"] }
+thiserror = "2.0.17"
+tracing = "0.1.44"
+
+[features]
+default = ["all"]
+all = ["csv", "json", "parquet", "hf-hub"]
+csv = ["dep:csv"]
+json = []
+parquet = ["dep:arrow", "dep:parquet"]
+hf-hub = ["dep:hf-hub", "dep:reqwest", "parquet"]
+
+[dev-dependencies]
+bon = "3.7.0"
+dsrs-evaluate = { path = "../dsrs-evaluate" }
+dsrs_macros = { version = "0.7.2", path = "../dsrs-macros" }
+facet = { git = "https://github.com/darinkishore/facet", rev = "cc8613c97cd1ec03e63659db34a947989b45c8a5", default-features = false, features = ["std"] }
+rstest = "0.25.0"
+tempfile = "3.23.0"
+tokio = { version = "1.46.1", features = ["macros", "rt"] }
diff --git a/crates/dspy-rs/src/data/dataloader.rs b/crates/dsrs-data/src/dataloader.rs
similarity index 99%
rename from crates/dspy-rs/src/data/dataloader.rs
rename to crates/dsrs-data/src/dataloader.rs
index 94b690b0..48228363 100644
--- a/crates/dspy-rs/src/data/dataloader.rs
+++ b/crates/dsrs-data/src/dataloader.rs
@@ -15,9 +15,8 @@ use std::io::Cursor;
use std::path::{Path, PathBuf};
use tracing::debug;
-use crate::data::utils::is_url;
-use crate::predictors::Example as TypedExample;
-use crate::{BamlType, BamlValue, Signature};
+use crate::utils::is_url;
+use dsrs_core::{BamlType, BamlValue, Example as TypedExample, Signature};
/// Controls how typed loaders handle source fields that are not part of the target signature.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
diff --git a/crates/dspy-rs/src/data/mod.rs b/crates/dsrs-data/src/lib.rs
similarity index 52%
rename from crates/dspy-rs/src/data/mod.rs
rename to crates/dsrs-data/src/lib.rs
index 09c73049..ffda8253 100644
--- a/crates/dspy-rs/src/data/mod.rs
+++ b/crates/dsrs-data/src/lib.rs
@@ -3,21 +3,37 @@
//! Typed ingestion is now first-class:
//!
//! - [`DataLoader`] provides `load_*` methods that return
-//! [`Example`](crate::predictors::Example) directly.
+//! [`Example`](dsrs_core::Example) directly.
//! - Typed examples flow directly into evaluation and optimizer APIs.
//!
//! The untyped row type (`RawExample`) remains for internal runtime/tracing/cache bridges.
+#[cfg(any(
+ feature = "csv",
+ feature = "json",
+ feature = "parquet",
+ feature = "hf-hub"
+))]
pub mod dataloader;
-pub mod example;
-pub mod prediction;
+pub mod example {
+ pub use dsrs_core::RawExample as Example;
+}
+pub mod prediction {
+ pub use dsrs_core::Prediction;
+}
pub mod serialize;
pub mod utils;
+#[cfg(any(
+ feature = "csv",
+ feature = "json",
+ feature = "parquet",
+ feature = "hf-hub"
+))]
pub use dataloader::*;
pub use example::*;
pub use prediction::*;
pub use serialize::*;
pub use utils::*;
-pub type RawExample = example::Example;
+pub type RawExample = dsrs_core::RawExample;
diff --git a/crates/dspy-rs/src/data/serialize.rs b/crates/dsrs-data/src/serialize.rs
similarity index 96%
rename from crates/dspy-rs/src/data/serialize.rs
rename to crates/dsrs-data/src/serialize.rs
index bde1724f..10d73b74 100644
--- a/crates/dspy-rs/src/data/serialize.rs
+++ b/crates/dsrs-data/src/serialize.rs
@@ -2,7 +2,7 @@ use rayon::prelude::*;
use std::fs::File;
use std::io::{BufRead, BufReader, BufWriter, Write};
-use crate::data::example::Example;
+use crate::example::Example;
#[allow(clippy::lines_filter_map_ok)]
pub fn load_jsonl(path: &str, input_keys: Vec, output_key: Vec) -> Vec {
diff --git a/crates/dspy-rs/src/data/utils.rs b/crates/dsrs-data/src/utils.rs
similarity index 100%
rename from crates/dspy-rs/src/data/utils.rs
rename to crates/dsrs-data/src/utils.rs
diff --git a/crates/dspy-rs/tests/test_dataloader.rs b/crates/dsrs-data/tests/test_dataloader.rs
similarity index 96%
rename from crates/dspy-rs/tests/test_dataloader.rs
rename to crates/dsrs-data/tests/test_dataloader.rs
index e98d8db8..15dc624b 100644
--- a/crates/dspy-rs/tests/test_dataloader.rs
+++ b/crates/dsrs-data/tests/test_dataloader.rs
@@ -3,11 +3,10 @@ use arrow::array::{ArrayRef, Int64Array, StringArray};
use arrow::datatypes::{DataType, Field, Schema};
use arrow::record_batch::RecordBatch;
use bon::Builder;
-use dspy_rs::{
- COPRO, CallMetadata, DataLoader, Example, MetricOutcome, Module, Optimizer, Predict,
- PredictError, Predicted, Signature, TypedLoadOptions, TypedMetric, UnknownFieldPolicy,
- average_score, evaluate_trainset,
-};
+use dsrs_core::{CallMetadata, Example, Module, PredictError, Predicted, Signature};
+use dsrs_data::{DataLoader, TypedLoadOptions, UnknownFieldPolicy};
+use dsrs_evaluate::{MetricOutcome, TypedMetric, average_score, evaluate_trainset};
+use dsrs_predict::Predict;
use parquet::arrow::ArrowWriter;
use std::collections::HashMap;
use std::fs;
@@ -505,16 +504,11 @@ async fn typed_loader_outputs_feed_evaluator_and_optimizer_paths() -> Result<()>
)?;
let metric = ExactMatch;
- let mut module = EchoModule::builder().build();
+ let module = EchoModule::builder().build();
let outcomes = evaluate_trainset(&module, &trainset, &metric).await?;
assert_eq!(outcomes.len(), 2);
assert_eq!(average_score(&outcomes), 1.0);
- let optimizer = COPRO::builder().breadth(2).depth(1).build();
- optimizer
- .compile::(&mut module, trainset, &metric)
- .await?;
-
Ok(())
}
diff --git a/crates/dspy-rs/tests/test_example.rs b/crates/dsrs-data/tests/test_example.rs
similarity index 97%
rename from crates/dspy-rs/tests/test_example.rs
rename to crates/dsrs-data/tests/test_example.rs
index 439ce535..a994aafa 100644
--- a/crates/dspy-rs/tests/test_example.rs
+++ b/crates/dsrs-data/tests/test_example.rs
@@ -1,6 +1,6 @@
-use dspy_rs::data::example::Example;
-use dspy_rs::data::serialize::{load_jsonl, save_examples_as_jsonl};
-use dspy_rs::hashmap;
+use dsrs_core::hashmap;
+use dsrs_data::example::Example;
+use dsrs_data::serialize::{load_jsonl, save_examples_as_jsonl};
use rstest::*;
#[rstest]
diff --git a/crates/dsrs-evaluate/Cargo.toml b/crates/dsrs-evaluate/Cargo.toml
new file mode 100644
index 00000000..29814923
--- /dev/null
+++ b/crates/dsrs-evaluate/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "dsrs-evaluate"
+version = "0.0.0"
+edition = "2024"
+rust-version = "1.85"
+license = "Apache-2.0"
+repository = "https://github.com/krypticmouse/DSRs"
+description = "DSRs typed evaluation and metric support."
+
+[dependencies]
+anyhow = "1.0.99"
+dsrs-core = { path = "../dsrs-core" }
+serde = { version = "1.0.219", features = ["derive"] }
+serde_json = { version = "1.0.140", features = ["preserve_order"] }
+
+[dev-dependencies]
+bamltype = { path = "../bamltype" }
+dsrs-data = { path = "../dsrs-data", features = ["all"] }
+dsrs-lm = { path = "../dsrs-lm" }
+dsrs_macros = { version = "0.7.2", path = "../dsrs-macros" }
+dsrs-predict = { path = "../dsrs-predict" }
+dsrs-trace = { path = "../dsrs-trace" }
+tokio = { version = "1.46.1", features = ["macros", "rt", "rt-multi-thread"] }
diff --git a/crates/dspy-rs/examples/03-evaluate-hotpotqa.rs b/crates/dsrs-evaluate/examples/03-evaluate-hotpotqa.rs
similarity index 85%
rename from crates/dspy-rs/examples/03-evaluate-hotpotqa.rs
rename to crates/dsrs-evaluate/examples/03-evaluate-hotpotqa.rs
index f9cf6a69..0f6308cc 100644
--- a/crates/dspy-rs/examples/03-evaluate-hotpotqa.rs
+++ b/crates/dsrs-evaluate/examples/03-evaluate-hotpotqa.rs
@@ -8,10 +8,12 @@ cargo run --example 03-evaluate-hotpotqa --features dataloaders
*/
use anyhow::Result;
-use dspy_rs::{
- ChatAdapter, DataLoader, Example, LM, MetricOutcome, Predict, Predicted, Signature,
- TypedLoadOptions, TypedMetric, average_score, configure, evaluate_trainset, init_tracing,
-};
+use dsrs_core::{Example, Predicted, Signature};
+use dsrs_data::{DataLoader, TypedLoadOptions};
+use dsrs_evaluate::{MetricOutcome, TypedMetric, average_score, evaluate_trainset};
+use dsrs_lm::{ChatAdapter, LM, configure};
+use dsrs_predict::Predict;
+use dsrs_trace::init_tracing;
#[derive(Signature, Clone, Debug)]
struct QA {
diff --git a/crates/dspy-rs/src/evaluate/evaluator.rs b/crates/dsrs-evaluate/src/evaluator.rs
similarity index 97%
rename from crates/dspy-rs/src/evaluate/evaluator.rs
rename to crates/dsrs-evaluate/src/evaluator.rs
index 8e7052ca..2f9dd32b 100644
--- a/crates/dspy-rs/src/evaluate/evaluator.rs
+++ b/crates/dsrs-evaluate/src/evaluator.rs
@@ -1,10 +1,8 @@
use anyhow::{Result, anyhow};
-use crate::core::Module;
-use crate::predictors::Example;
-use crate::{Predicted, Signature};
+use dsrs_core::{Example, Module, Predicted, Signature};
-use super::FeedbackMetric;
+use crate::FeedbackMetric;
/// Result of evaluating a single example: a score and optional textual feedback.
///
diff --git a/crates/dspy-rs/src/evaluate/feedback.rs b/crates/dsrs-evaluate/src/feedback.rs
similarity index 99%
rename from crates/dspy-rs/src/evaluate/feedback.rs
rename to crates/dsrs-evaluate/src/feedback.rs
index 25ae4593..619f5a8e 100644
--- a/crates/dspy-rs/src/evaluate/feedback.rs
+++ b/crates/dsrs-evaluate/src/feedback.rs
@@ -1,4 +1,4 @@
-use crate::{BamlValue, RawExample};
+use dsrs_core::{BamlValue, RawExample};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@@ -16,7 +16,7 @@ use std::collections::HashMap;
/// // and the Pareto frontier can operate on the full vector instead of a scalar collapse.
///
/// ```
-/// use dspy_rs::FeedbackMetric;
+/// use dsrs_evaluate::FeedbackMetric;
///
/// let fb = FeedbackMetric::new(0.7, "Correct answer but verbose explanation");
/// assert_eq!(fb.score, 0.7);
diff --git a/crates/dspy-rs/src/evaluate/feedback_helpers.rs b/crates/dsrs-evaluate/src/feedback_helpers.rs
similarity index 100%
rename from crates/dspy-rs/src/evaluate/feedback_helpers.rs
rename to crates/dsrs-evaluate/src/feedback_helpers.rs
diff --git a/crates/dsrs-evaluate/src/lib.rs b/crates/dsrs-evaluate/src/lib.rs
new file mode 100644
index 00000000..e6d3c2e2
--- /dev/null
+++ b/crates/dsrs-evaluate/src/lib.rs
@@ -0,0 +1,10 @@
+//! Evaluation and metrics for measuring module performance.
+
+pub mod evaluator;
+pub mod feedback;
+pub mod feedback_helpers;
+pub mod metrics;
+
+pub use evaluator::*;
+pub use feedback::*;
+pub use feedback_helpers::*;
diff --git a/crates/dspy-rs/src/evaluate/metrics.rs b/crates/dsrs-evaluate/src/metrics.rs
similarity index 100%
rename from crates/dspy-rs/src/evaluate/metrics.rs
rename to crates/dsrs-evaluate/src/metrics.rs
diff --git a/crates/dspy-rs/tests/test_evaluate_trainset_typed.rs b/crates/dsrs-evaluate/tests/test_evaluate_trainset_typed.rs
similarity index 94%
rename from crates/dspy-rs/tests/test_evaluate_trainset_typed.rs
rename to crates/dsrs-evaluate/tests/test_evaluate_trainset_typed.rs
index 95e3f26b..5d4fe8fe 100644
--- a/crates/dspy-rs/tests/test_evaluate_trainset_typed.rs
+++ b/crates/dsrs-evaluate/tests/test_evaluate_trainset_typed.rs
@@ -1,8 +1,6 @@
use anyhow::{Result, anyhow};
-use dspy_rs::{
- CallMetadata, Example, MetricOutcome, Module, PredictError, Predicted, Signature, TypedMetric,
- average_score, evaluate_trainset,
-};
+use dsrs_core::{CallMetadata, Example, Module, PredictError, Predicted, Signature};
+use dsrs_evaluate::{MetricOutcome, TypedMetric, average_score, evaluate_trainset};
use std::sync::{Arc, Mutex};
#[derive(Signature, Clone, Debug)]
diff --git a/crates/dsrs-gepa/Cargo.toml b/crates/dsrs-gepa/Cargo.toml
new file mode 100644
index 00000000..5909a892
--- /dev/null
+++ b/crates/dsrs-gepa/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "dsrs-gepa"
+version = "0.0.0"
+edition = "2024"
+rust-version = "1.85"
+license = "Apache-2.0"
+repository = "https://github.com/krypticmouse/DSRs"
+description = "DSRs GEPA optimizer support."
+
+[dependencies]
+anyhow = "1.0.99"
+bon = "3.7.0"
+dsrs-core = { path = "../dsrs-core" }
+dsrs-evaluate = { path = "../dsrs-evaluate" }
+dsrs-lm = { path = "../dsrs-lm" }
+dsrs-predict = { path = "../dsrs-predict" }
+facet = { git = "https://github.com/darinkishore/facet", rev = "cc8613c97cd1ec03e63659db34a947989b45c8a5", default-features = false, features = ["std"] }
+rand = "0.8.5"
+serde = { version = "1.0.219", features = ["derive"] }
+tracing = "0.1.44"
+
+[dev-dependencies]
+dsrs_macros = { version = "0.7.2", path = "../dsrs-macros" }
+dsrs-trace = { path = "../dsrs-trace" }
+tokio = { version = "1.46.1", features = ["macros", "rt", "rt-multi-thread"] }
diff --git a/crates/dspy-rs/examples/09-gepa-sentiment.rs b/crates/dsrs-gepa/examples/09-gepa-sentiment.rs
similarity index 93%
rename from crates/dspy-rs/examples/09-gepa-sentiment.rs
rename to crates/dsrs-gepa/examples/09-gepa-sentiment.rs
index 515fe70b..c697a511 100644
--- a/crates/dspy-rs/examples/09-gepa-sentiment.rs
+++ b/crates/dsrs-gepa/examples/09-gepa-sentiment.rs
@@ -9,11 +9,12 @@ OPENAI_API_KEY=your_key cargo run --example 09-gepa-sentiment
use anyhow::Result;
use bon::Builder;
-use dspy_rs::{
- ChatAdapter, Example, FeedbackMetric, GEPA, LM, MetricOutcome, Module, Optimizer, Predict,
- PredictError, Predicted, Signature, TypedMetric, average_score, configure, evaluate_trainset,
- init_tracing,
-};
+use dsrs_core::{Example, Module, PredictError, Predicted, Signature};
+use dsrs_evaluate::{FeedbackMetric, MetricOutcome, TypedMetric, average_score, evaluate_trainset};
+use dsrs_gepa::{GEPA, Optimizer};
+use dsrs_lm::{ChatAdapter, LM, configure};
+use dsrs_predict::Predict;
+use dsrs_trace::init_tracing;
#[derive(Signature, Clone, Debug)]
struct SentimentSignature {
diff --git a/crates/dspy-rs/examples/10-gepa-llm-judge.rs b/crates/dsrs-gepa/examples/10-gepa-llm-judge.rs
similarity index 95%
rename from crates/dspy-rs/examples/10-gepa-llm-judge.rs
rename to crates/dsrs-gepa/examples/10-gepa-llm-judge.rs
index 95255284..f0e977d8 100644
--- a/crates/dspy-rs/examples/10-gepa-llm-judge.rs
+++ b/crates/dsrs-gepa/examples/10-gepa-llm-judge.rs
@@ -9,11 +9,12 @@ OPENAI_API_KEY=your_key cargo run --example 10-gepa-llm-judge
use anyhow::Result;
use bon::Builder;
-use dspy_rs::{
- ChatAdapter, Example, FeedbackMetric, GEPA, LM, MetricOutcome, Module, Optimizer, Predict,
- PredictError, Predicted, Signature, TypedMetric, average_score, configure, evaluate_trainset,
- init_tracing,
-};
+use dsrs_core::{Example, Module, PredictError, Predicted, Signature};
+use dsrs_evaluate::{FeedbackMetric, MetricOutcome, TypedMetric, average_score, evaluate_trainset};
+use dsrs_gepa::{GEPA, Optimizer};
+use dsrs_lm::{ChatAdapter, LM, configure};
+use dsrs_predict::Predict;
+use dsrs_trace::init_tracing;
#[derive(Signature, Clone, Debug)]
struct MathWordProblem {
diff --git a/crates/dspy-rs/src/optimizer/gepa.rs b/crates/dsrs-gepa/src/gepa.rs
similarity index 96%
rename from crates/dspy-rs/src/optimizer/gepa.rs
rename to crates/dsrs-gepa/src/gepa.rs
index e4c799c6..1775b928 100644
--- a/crates/dspy-rs/src/optimizer/gepa.rs
+++ b/crates/dsrs-gepa/src/gepa.rs
@@ -2,14 +2,11 @@ use anyhow::{Context, Result, anyhow};
use bon::Builder;
use serde::{Deserialize, Serialize};
-use crate::evaluate::{MetricOutcome, TypedMetric, average_score};
-use crate::optimizer::{
- Optimizer, evaluate_module_with_metric, predictor_names, with_named_predictor,
-};
-use crate::predictors::Example;
-use crate::{BamlType, BamlValue, Facet, Module, Signature};
+use dsrs_core::{BamlType, BamlValue, Example, Facet, Module, Signature};
+use dsrs_evaluate::{MetricOutcome, TypedMetric, average_score};
-use super::pareto::ParetoFrontier;
+use crate::pareto::ParetoFrontier;
+use crate::{Optimizer, evaluate_module_with_metric, predictor_names, with_named_predictor};
/// A single instruction candidate tracked through GEPA's evolutionary search.
///
@@ -75,7 +72,7 @@ pub use super::pareto::ParetoStatistics;
/// Genetic-Pareto instruction optimizer with feedback-driven evolution.
///
/// GEPA uses an evolutionary search guided by per-example feedback from your metric.
-/// Unlike [`COPRO`](crate::COPRO) which only uses numerical scores, GEPA requires your
+/// GEPA requires your
/// [`TypedMetric`] to return [`MetricOutcome::with_feedback`] — textual feedback
/// explaining *why* each example scored the way it did. This feedback gets appended
/// to the instruction as a mutation prompt for the next generation, so the quality
@@ -155,7 +152,7 @@ pub struct GEPA {
/// Hard cap on total LM calls (rollouts + generation).
pub max_lm_calls: Option,
/// Optional separate LM for candidate generation.
- pub prompt_model: Option,
+ pub prompt_model: Option,
}
impl GEPA {
@@ -505,8 +502,10 @@ mod tests {
use anyhow::{Result, anyhow};
use super::*;
- use crate::evaluate::{MetricOutcome, TypedMetric};
- use crate::{CallMetadata, Predict, PredictError, Predicted, Signature};
+ use dsrs_core::{CallMetadata, PredictError, Predicted};
+ use dsrs_evaluate::{MetricOutcome, TypedMetric};
+ use dsrs_macros::Signature;
+ use dsrs_predict::Predict;
#[derive(Signature, Clone, Debug)]
struct GepaStateSig {
@@ -571,11 +570,15 @@ mod tests {
.instruction("seed-instruction")
.build(),
};
+ let predictor_name = predictor_names(&mut module)
+ .expect("predictor discovery should succeed")
+ .pop()
+ .expect("test module should expose one predictor");
let err = optimizer
.evaluate_candidate::(
&mut module,
- "predictor",
+ &predictor_name,
"candidate instruction",
&eval_set(),
&AlwaysFailMetric,
@@ -584,7 +587,7 @@ mod tests {
.expect_err("candidate evaluation should propagate metric failure");
assert!(err.to_string().contains("metric failure"));
- let instruction = with_named_predictor(&mut module, "predictor", |predictor| {
+ let instruction = with_named_predictor(&mut module, &predictor_name, |predictor| {
Ok(predictor.instruction())
})
.expect("predictor lookup should succeed");
diff --git a/crates/dspy-rs/src/optimizer/mod.rs b/crates/dsrs-gepa/src/lib.rs
similarity index 78%
rename from crates/dspy-rs/src/optimizer/mod.rs
rename to crates/dsrs-gepa/src/lib.rs
index 7d04a961..ecbfbdd2 100644
--- a/crates/dspy-rs/src/optimizer/mod.rs
+++ b/crates/dsrs-gepa/src/lib.rs
@@ -1,14 +1,13 @@
//! Automatic prompt optimization.
//!
//! An optimizer takes a module, a training set, and a metric, then searches for better
-//! instructions (and in some cases, demos) for each [`Predict`](crate::Predict) leaf.
+//! instructions (and in some cases, demos) for each `Predict` leaf.
//! The module is mutated in-place — after optimization, calling it produces better results
//! without any code changes.
//!
//! The [`Optimizer::compile`] method takes `&mut module` (exclusive access — no concurrent
//! `call()` during optimization) and returns a report. The specific report type depends
-//! on the optimizer: [`COPRO`] returns `()`, [`GEPA`] returns [`GEPAResult`] with full
-//! evolution history, [`MIPROv2`] returns `()`.
+//! on the optimizer. This crate carries GEPA and its Pareto frontier support.
//!
//! # How it works internally
//!
@@ -26,37 +25,29 @@
//!
//! | Optimizer | Strategy | Needs feedback? | Cost |
//! |-----------|----------|-----------------|------|
-//! | [`COPRO`] | Breadth-first instruction search | No | Low (breadth × depth × trainset) |
//! | [`GEPA`] | Genetic-Pareto evolution with feedback | **Yes** | Medium-high (iterations × eval) |
-//! | [`MIPROv2`] | Trace-guided candidate generation | No | Medium (candidates × trials × trainset) |
-pub mod copro;
pub mod gepa;
-pub mod mipro;
pub mod pareto;
-pub use copro::*;
pub use gepa::*;
-pub use mipro::*;
pub use pareto::*;
use anyhow::Result;
use anyhow::anyhow;
use std::ops::ControlFlow;
-use crate::core::{DynPredictor, visit_named_predictors_mut};
-use crate::evaluate::{MetricOutcome, TypedMetric, evaluate_trainset};
-use crate::predictors::Example;
-use crate::{Facet, Module, Signature};
+use dsrs_core::{DynPredictor, Example, Facet, Module, Signature, visit_named_predictors_mut};
+use dsrs_evaluate::{MetricOutcome, TypedMetric, evaluate_trainset};
-/// Tunes a module's [`Predict`](crate::Predict) leaves for better performance.
+/// Tunes a module's `Predict` leaves for better performance.
///
/// Takes exclusive `&mut` access to the module during optimization — you cannot call
/// the module concurrently. After `compile` returns, the module's instructions and/or
/// demos have been mutated in-place. Just call the module as before; no code changes needed.
///
/// ```ignore
-/// let optimizer = COPRO::builder().breadth(10).depth(3).build();
+/// let optimizer = GEPA::builder().num_generations(3).build();
/// optimizer.compile(&mut module, trainset, &metric).await?;
/// // module is now optimized — call it as usual
/// let result = module.call(input).await?;
@@ -87,7 +78,7 @@ pub trait Optimizer {
/// Evaluates a module on a trainset using a typed metric.
///
-/// Thin wrapper around [`evaluate_trainset`](crate::evaluate::evaluate_trainset) for
+/// Thin wrapper around [`evaluate_trainset`](dsrs_evaluate::evaluate_trainset) for
/// internal optimizer use. Returns one [`MetricOutcome`] per training example.
pub(crate) async fn evaluate_module_with_metric(
module: &M,
@@ -103,10 +94,10 @@ where
evaluate_trainset(module, trainset, metric).await
}
-/// Returns the dotted-path names of all [`Predict`](crate::Predict) leaves in a module.
+/// Returns the dotted-path names of all `Predict` leaves in a module.
///
/// Convenience wrapper around
-/// [`visit_named_predictors_mut`](crate::core::dyn_predictor::visit_named_predictors_mut)
+/// [`visit_named_predictors_mut`](dsrs_core::visit_named_predictors_mut)
/// that collects discovered paths.
pub(crate) fn predictor_names(module: &mut M) -> Result>
where
diff --git a/crates/dspy-rs/src/optimizer/pareto.rs b/crates/dsrs-gepa/src/pareto.rs
similarity index 99%
rename from crates/dspy-rs/src/optimizer/pareto.rs
rename to crates/dsrs-gepa/src/pareto.rs
index ecdec4f7..56544a98 100644
--- a/crates/dspy-rs/src/optimizer/pareto.rs
+++ b/crates/dsrs-gepa/src/pareto.rs
@@ -2,7 +2,7 @@ use rand::Rng;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
-use crate::optimizer::gepa::GEPACandidate;
+use crate::gepa::GEPACandidate;
/// Per-example dominance frontier for [`GEPA`](crate::GEPA)'s evolutionary search.
///
diff --git a/crates/dspy-rs/tests/test_gepa.rs b/crates/dsrs-gepa/tests/test_gepa.rs
similarity index 94%
rename from crates/dspy-rs/tests/test_gepa.rs
rename to crates/dsrs-gepa/tests/test_gepa.rs
index 0f081f5b..aa063ee4 100644
--- a/crates/dspy-rs/tests/test_gepa.rs
+++ b/crates/dsrs-gepa/tests/test_gepa.rs
@@ -1,4 +1,4 @@
-use dspy_rs::optimizer::gepa::GEPACandidate;
+use dsrs_gepa::GEPACandidate;
#[test]
fn test_candidate_creation() {
diff --git a/crates/dspy-rs/tests/test_gepa_typed_metric_feedback.rs b/crates/dsrs-gepa/tests/test_gepa_typed_metric_feedback.rs
similarity index 98%
rename from crates/dspy-rs/tests/test_gepa_typed_metric_feedback.rs
rename to crates/dsrs-gepa/tests/test_gepa_typed_metric_feedback.rs
index f6ab2a63..8e304171 100644
--- a/crates/dspy-rs/tests/test_gepa_typed_metric_feedback.rs
+++ b/crates/dsrs-gepa/tests/test_gepa_typed_metric_feedback.rs
@@ -1,8 +1,8 @@
use anyhow::Result;
-use dspy_rs::{
- CallMetadata, Example, FeedbackMetric, GEPA, MetricOutcome, Module, Optimizer, Predict,
- PredictError, Predicted, Signature, TypedMetric,
-};
+use dsrs_core::{CallMetadata, Example, Module, PredictError, Predicted, Signature};
+use dsrs_evaluate::{FeedbackMetric, MetricOutcome, TypedMetric};
+use dsrs_gepa::{GEPA, Optimizer};
+use dsrs_predict::Predict;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
diff --git a/crates/dspy-rs/tests/test_pareto.rs b/crates/dsrs-gepa/tests/test_pareto.rs
similarity index 96%
rename from crates/dspy-rs/tests/test_pareto.rs
rename to crates/dsrs-gepa/tests/test_pareto.rs
index a7c5f7bb..62367049 100644
--- a/crates/dspy-rs/tests/test_pareto.rs
+++ b/crates/dsrs-gepa/tests/test_pareto.rs
@@ -1,5 +1,4 @@
-use dspy_rs::optimizer::gepa::GEPACandidate;
-use dspy_rs::optimizer::pareto::ParetoFrontier;
+use dsrs_gepa::{GEPACandidate, ParetoFrontier};
fn make_test_candidate(instruction: &str) -> GEPACandidate {
GEPACandidate {
diff --git a/crates/dsrs-leaven/Cargo.toml b/crates/dsrs-leaven/Cargo.toml
new file mode 100644
index 00000000..b0dabb1a
--- /dev/null
+++ b/crates/dsrs-leaven/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "dsrs-leaven"
+version = "0.0.0"
+edition = "2024"
+rust-version = "1.85"
+license = "Apache-2.0"
+repository = "https://github.com/krypticmouse/DSRs"
+description = "Leaven integration for DSRs programs."
+
+[dependencies]
+dsrs-core = { path = "../dsrs-core" }
+dsrs-evaluate = { path = "../dsrs-evaluate" }
+dsrs-predict = { path = "../dsrs-predict" }
+leaven-core = { path = "../../../leaven/crates/leaven-core" }
+leaven-surface = { path = "../../../leaven/crates/leaven-surface" }
+leaven-engine = { path = "../../../leaven/crates/leaven-engine" }
+leaven-evidence = { path = "../../../leaven/crates/leaven-evidence" }
+serde = { version = "1.0.219", features = ["derive"] }
+serde_json = "1.0.140"
+thiserror = "2.0.17"
+
+[dev-dependencies]
+anyhow = "1.0.99"
+dsrs_macros = { version = "0.7.2", path = "../dsrs-macros" }
diff --git a/crates/dsrs-leaven/src/artifact.rs b/crates/dsrs-leaven/src/artifact.rs
new file mode 100644
index 00000000..16f40608
--- /dev/null
+++ b/crates/dsrs-leaven/src/artifact.rs
@@ -0,0 +1,53 @@
+use std::marker::PhantomData;
+
+use dsrs_core::{Module, Signature};
+
+use crate::{DsrsLeavenError, DsrsProgramChange};
+
+#[derive(Debug)]
+pub struct DsrsProgramArtifact
+where
+ S: Signature,
+ M: Module,
+{
+ _phantom: PhantomData<(S, M)>,
+}
+
+impl Clone for DsrsProgramArtifact
+where
+ S: Signature,
+ M: Module,
+{
+ fn clone(&self) -> Self {
+ Self::scaffold()
+ }
+}
+
+impl DsrsProgramArtifact
+where
+ S: Signature,
+ M: Module,
+{
+ pub const fn scaffold() -> Self {
+ Self {
+ _phantom: PhantomData,
+ }
+ }
+}
+
+impl leaven_core::Artifact for DsrsProgramArtifact
+where
+ S: Signature,
+ M: Module + Send + Sync + 'static,
+{
+ type Change = DsrsProgramChange;
+ type ApplyError = DsrsLeavenError;
+
+ fn identity(&self) -> leaven_core::ArtifactIdentity {
+ unimplemented!("dsrs-leaven: artifact identity")
+ }
+
+ fn apply_change(&self, _change: &Self::Change) -> Result {
+ unimplemented!("dsrs-leaven: artifact apply_change")
+ }
+}
diff --git a/crates/dsrs-leaven/src/change.rs b/crates/dsrs-leaven/src/change.rs
new file mode 100644
index 00000000..f95d038a
--- /dev/null
+++ b/crates/dsrs-leaven/src/change.rs
@@ -0,0 +1,5 @@
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+pub struct DsrsProgramChange {
+ pub address: String,
+ pub replacement: serde_json::Value,
+}
diff --git a/crates/dsrs-leaven/src/evaluator.rs b/crates/dsrs-leaven/src/evaluator.rs
new file mode 100644
index 00000000..07fc2282
--- /dev/null
+++ b/crates/dsrs-leaven/src/evaluator.rs
@@ -0,0 +1,46 @@
+use std::marker::PhantomData;
+
+use dsrs_core::{Module, Signature};
+
+use crate::{DsrsEvidence, DsrsProgramArtifact};
+
+#[derive(Clone, Debug)]
+pub struct DsrsEvaluator
+where
+ S: Signature,
+ M: Module,
+{
+ _phantom: PhantomData<(S, M)>,
+}
+
+impl DsrsEvaluator
+where
+ S: Signature,
+ M: Module,
+{
+ pub const fn scaffold() -> Self {
+ Self {
+ _phantom: PhantomData,
+ }
+ }
+}
+
+#[derive(Clone, Debug)]
+pub struct DsrsLeavenProblem
+where
+ S: Signature,
+ M: Module,
+{
+ _phantom: PhantomData<(S, M)>,
+}
+
+impl leaven_core::OptimizationProblem for DsrsLeavenProblem
+where
+ S: Signature,
+ M: Module + Send + Sync + 'static,
+{
+ type Artifact = DsrsProgramArtifact;
+ type Case = serde_json::Value;
+ type Evidence = DsrsEvidence;
+ type ProposalAnnotations = serde_json::Value;
+}
diff --git a/crates/dsrs-leaven/src/evidence.rs b/crates/dsrs-leaven/src/evidence.rs
new file mode 100644
index 00000000..3a9f18c8
--- /dev/null
+++ b/crates/dsrs-leaven/src/evidence.rs
@@ -0,0 +1,6 @@
+#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
+pub struct DsrsEvidence {
+ pub payload: serde_json::Value,
+}
+
+impl leaven_core::Evidence for DsrsEvidence {}
diff --git a/crates/dsrs-leaven/src/lib.rs b/crates/dsrs-leaven/src/lib.rs
new file mode 100644
index 00000000..dba68cff
--- /dev/null
+++ b/crates/dsrs-leaven/src/lib.rs
@@ -0,0 +1,97 @@
+//! DSRs to leaven integration scaffolding.
+//!
+//! Bodies are deliberately `unimplemented!()` until the leaven-side optimizer
+//! path is real. This crate exists to keep the capability trait signatures
+//! compiling against the current leaven crates.
+
+pub mod artifact;
+pub mod change;
+pub mod evaluator;
+pub mod evidence;
+pub mod surface;
+
+pub use artifact::DsrsProgramArtifact;
+pub use change::DsrsProgramChange;
+pub use evaluator::{DsrsEvaluator, DsrsLeavenProblem};
+pub use evidence::DsrsEvidence;
+pub use surface::DsrsProgramSurface;
+
+#[derive(Debug, thiserror::Error)]
+pub enum DsrsLeavenError {
+ #[error("dsrs-leaven scaffold is not implemented yet: {0}")]
+ Unimplemented(&'static str),
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use anyhow::Result;
+ use dsrs_core::{CallMetadata, Module, PredictError, Predicted, Signature};
+ use leaven_core::{Artifact, OptimizationProblem};
+
+ #[derive(Signature, Clone, Debug)]
+ struct TestSig {
+ #[input]
+ prompt: String,
+
+ #[output]
+ answer: String,
+ }
+
+ struct TestModule;
+
+ impl Module for TestModule {
+ type Input = TestSigInput;
+ type Output = TestSigOutput;
+
+ async fn forward(
+ &self,
+ input: Self::Input,
+ ) -> Result, PredictError> {
+ Ok(Predicted::new(
+ TestSigOutput {
+ answer: input.prompt,
+ },
+ CallMetadata::default(),
+ ))
+ }
+ }
+
+ #[test]
+ fn scaffold_constructors_are_cloneable_markers() {
+ let artifact = DsrsProgramArtifact::::scaffold();
+ let _clone = artifact.clone();
+ let _surface = DsrsProgramSurface::::scaffold();
+ let _evaluator = DsrsEvaluator::::scaffold();
+ }
+
+ #[test]
+ fn problem_associated_types_match_dsrs_scaffold() {
+ fn assert_problem() {}
+ assert_problem::>();
+ }
+
+ #[test]
+ #[should_panic(expected = "dsrs-leaven: artifact identity")]
+ fn artifact_identity_is_explicit_scaffold_panic() {
+ let artifact = DsrsProgramArtifact::::scaffold();
+ let _ = artifact.identity();
+ }
+
+ #[test]
+ fn change_and_evidence_round_trip_json_payloads() {
+ let change = DsrsProgramChange {
+ address: "predictor.instruction".to_string(),
+ replacement: serde_json::json!("new instruction"),
+ };
+ let encoded = serde_json::to_string(&change).unwrap();
+ let decoded: DsrsProgramChange = serde_json::from_str(&encoded).unwrap();
+ assert_eq!(decoded.address, "predictor.instruction");
+ assert_eq!(decoded.replacement, "new instruction");
+
+ let evidence = DsrsEvidence {
+ payload: serde_json::json!({"score": 1.0}),
+ };
+ assert_eq!(evidence.payload["score"], 1.0);
+ }
+}
diff --git a/crates/dsrs-leaven/src/surface.rs b/crates/dsrs-leaven/src/surface.rs
new file mode 100644
index 00000000..1bde122b
--- /dev/null
+++ b/crates/dsrs-leaven/src/surface.rs
@@ -0,0 +1,63 @@
+use std::marker::PhantomData;
+
+use dsrs_core::{Module, Signature};
+
+use crate::{DsrsProgramArtifact, DsrsProgramChange};
+
+#[derive(Clone, Debug)]
+pub struct DsrsProgramSurface
+where
+ S: Signature,
+ M: Module,
+{
+ _phantom: PhantomData<(S, M)>,
+}
+
+impl DsrsProgramSurface
+where
+ S: Signature,
+ M: Module,
+{
+ pub const fn scaffold() -> Self {
+ Self {
+ _phantom: PhantomData,
+ }
+ }
+}
+
+impl leaven_surface::EditSurface> for DsrsProgramSurface
+where
+ S: Signature,
+ M: Module + Send + Sync + 'static,
+{
+ type PartId = String;
+ type Address = String;
+ type View<'a>
+ = &'a str
+ where
+ DsrsProgramArtifact: 'a;
+ type Edit = serde_json::Value;
+
+ fn fingerprint(&self) -> leaven_surface::SurfaceFingerprint {
+ unimplemented!("dsrs-leaven: surface fingerprint")
+ }
+
+ fn parts<'a>(
+ &self,
+ _artifact: &'a DsrsProgramArtifact,
+ ) -> Result<
+ Vec>>,
+ leaven_surface::SurfaceError,
+ > {
+ unimplemented!("dsrs-leaven: surface parts")
+ }
+
+ fn change_part(
+ &self,
+ _artifact: &DsrsProgramArtifact,
+ _id: Self::PartId,
+ _edit: Self::Edit,
+ ) -> Result {
+ unimplemented!("dsrs-leaven: surface change_part")
+ }
+}
diff --git a/crates/dsrs-lm/Cargo.toml b/crates/dsrs-lm/Cargo.toml
new file mode 100644
index 00000000..05d94212
--- /dev/null
+++ b/crates/dsrs-lm/Cargo.toml
@@ -0,0 +1,35 @@
+[package]
+name = "dsrs-lm"
+version = "0.0.0"
+edition = "2024"
+rust-version = "1.85"
+license = "Apache-2.0"
+repository = "https://github.com/krypticmouse/DSRs"
+description = "DSRs LM integration and chat adapter."
+
+[dependencies]
+anyhow = "1.0.99"
+bon = "3.7.0"
+bamltype = { path = "../bamltype" }
+dsrs-cache = { path = "../dsrs-cache" }
+dsrs-core = { path = "../dsrs-core" }
+enum_dispatch = "0.3.13"
+facet = { git = "https://github.com/darinkishore/facet", rev = "cc8613c97cd1ec03e63659db34a947989b45c8a5", default-features = false, features = ["std"] }
+indexmap = "2.10.0"
+minijinja = { git = "https://github.com/boundaryml/minijinja.git", branch = "main", default-features = false, features = ["builtins", "serde"] }
+regex = "1.11.2"
+reqwest = { version = "0.13", features = ["blocking"] }
+rig-core = { git = "https://github.com/0xPlaygrounds/rig", rev = "aee3b8bf6576ce41c9ac1dd82520752a65fa0127" }
+serde = { version = "1.0.219", features = ["derive"] }
+serde_json = { version = "1.0.140", features = ["preserve_order"] }
+tokio = { version = "1.46.1", features = ["sync"] }
+tracing = "0.1.44"
+
+[dev-dependencies]
+dsrs_macros = { version = "0.7.2", path = "../dsrs-macros" }
+dsrs-predict = { path = "../dsrs-predict" }
+dsrs-trace = { path = "../dsrs-trace" }
+rig-core = { git = "https://github.com/0xPlaygrounds/rig", rev = "aee3b8bf6576ce41c9ac1dd82520752a65fa0127" }
+rstest = "0.25.0"
+temp-env = { version = "0.3.6", features = ["async_closure"] }
+tokio = { version = "1.46.1", features = ["macros", "rt", "sync"] }
diff --git a/crates/dspy-rs/examples/11-custom-client.rs b/crates/dsrs-lm/examples/11-custom-client.rs
similarity index 91%
rename from crates/dspy-rs/examples/11-custom-client.rs
rename to crates/dsrs-lm/examples/11-custom-client.rs
index 8bdcb6b0..111b763f 100644
--- a/crates/dspy-rs/examples/11-custom-client.rs
+++ b/crates/dsrs-lm/examples/11-custom-client.rs
@@ -8,7 +8,9 @@ cargo run --example 11-custom-client
*/
use anyhow::Result;
-use dspy_rs::{ChatAdapter, LM, LMClient, Predict, Signature, configure, init_tracing};
+use dsrs_lm::{ChatAdapter, LM, LMClient, Signature, configure};
+use dsrs_predict::Predict;
+use dsrs_trace::init_tracing;
use rig::providers::azure;
use std::env;
diff --git a/crates/dspy-rs/src/adapter/mod.rs b/crates/dsrs-lm/src/adapter.rs
similarity index 96%
rename from crates/dspy-rs/src/adapter/mod.rs
rename to crates/dsrs-lm/src/adapter.rs
index 5e27576c..38f2b548 100644
--- a/crates/dspy-rs/src/adapter/mod.rs
+++ b/crates/dsrs-lm/src/adapter.rs
@@ -11,10 +11,6 @@
//! [`format_input`](ChatAdapter::format_input),
//! [`parse_output`](ChatAdapter::parse_output).
-pub mod chat;
-
-pub use chat::*;
-
/// Marker trait for configurable adapters.
///
/// Typed call paths currently use `ChatAdapter` directly, while global settings keep
diff --git a/crates/dspy-rs/src/adapter/chat.rs b/crates/dsrs-lm/src/chat.rs
similarity index 95%
rename from crates/dspy-rs/src/adapter/chat.rs
rename to crates/dsrs-lm/src/chat.rs
index 8ff78cb8..3163d4f1 100644
--- a/crates/dspy-rs/src/adapter/chat.rs
+++ b/crates/dsrs-lm/src/chat.rs
@@ -12,17 +12,17 @@ use std::collections::HashMap;
use std::sync::{LazyLock, Mutex};
use tracing::{debug, trace};
-use super::Adapter;
-use crate::CallMetadata;
-use crate::{
- BamlType, BamlValue, ConstraintLevel, ConstraintResult, FieldMeta, Flag, InputRenderSpec,
- JsonishError, Message, OutputFormatContent, ParseError, PredictError, Predicted, RenderOptions,
- Signature, TypeIR,
+use crate::Adapter;
+use crate::Message;
+use dsrs_core::{
+ BamlType, BamlValue, CallMetadata, ConstraintLevel, ConstraintResult, Example, FieldMeta,
+ FieldPath, FieldSchema, Flag, InputRenderSpec, JsonishError, LmUsage, OutputFormatContent,
+ ParseError, PredictError, Predicted, RenderOptions, Signature, SignatureSchema, TypeIR,
};
/// Builds prompts and parses responses using the `[[ ## field ## ]]` delimiter protocol.
///
-/// The adapter is stateless — all state comes from the [`SignatureSchema`](crate::SignatureSchema)
+/// The adapter is stateless — all state comes from the [`SignatureSchema`](SignatureSchema)
/// passed to each method. Two usage patterns:
///
/// - **High-level** (what [`Predict`](crate::Predict) uses): `format_system_message_typed`,
@@ -112,6 +112,18 @@ fn truncate_filter(
Ok(format!("{truncated}{end}"))
}
+fn truncate_preview(value: &str, max_chars: usize) -> &str {
+ if value.chars().count() <= max_chars {
+ return value;
+ }
+ let byte_end = value
+ .char_indices()
+ .nth(max_chars)
+ .map(|(idx, _)| idx)
+ .unwrap_or(value.len());
+ &value[..byte_end]
+}
+
fn build_input_render_environment() -> minijinja::Environment<'static> {
// Keep this setup aligned with BAML's jinja env defaults, then add contrib filters.
let mut env = minijinja::Environment::new();
@@ -302,7 +314,7 @@ fn format_schema_for_prompt(schema: &str) -> String {
impl ChatAdapter {
fn format_task_description_schema(
&self,
- schema: &crate::SignatureSchema,
+ schema: &SignatureSchema,
instruction_override: Option<&str>,
) -> String {
let instruction = instruction_override.unwrap_or(schema.instruction());
@@ -334,7 +346,7 @@ impl ChatAdapter {
format!("In adhering to this structure, your objective is: {indented}")
}
- fn format_response_instructions_schema(&self, schema: &crate::SignatureSchema) -> String {
+ fn format_response_instructions_schema(&self, schema: &SignatureSchema) -> String {
let mut output_fields = schema.output_fields().iter();
let Some(first_field) = output_fields.next() else {
return "Respond with the marker for `[[ ## completed ## ]]`.".to_string();
@@ -382,7 +394,7 @@ impl ChatAdapter {
self.build_system(S::schema(), instruction_override)
}
- /// Builds a system message from a [`SignatureSchema`](crate::SignatureSchema) directly.
+ /// Builds a system message from a [`SignatureSchema`](SignatureSchema) directly.
///
/// The schema-based equivalent of [`format_system_message_typed_with_instruction`](ChatAdapter::format_system_message_typed_with_instruction).
/// Use this when you have a schema but not a concrete `S: Signature` type (e.g.
@@ -393,7 +405,7 @@ impl ChatAdapter {
/// Returns an error if the output format rendering fails (malformed type IR).
pub fn build_system(
&self,
- schema: &crate::SignatureSchema,
+ schema: &SignatureSchema,
instruction_override: Option<&str>,
) -> Result {
let parts = [
@@ -408,7 +420,7 @@ impl ChatAdapter {
Ok(system)
}
- fn format_field_descriptions_schema(&self, schema: &crate::SignatureSchema) -> String {
+ fn format_field_descriptions_schema(&self, schema: &SignatureSchema) -> String {
let output_format = schema.output_format();
let mut lines = Vec::new();
@@ -438,7 +450,7 @@ impl ChatAdapter {
lines.join("\n")
}
- fn format_field_structure_schema(&self, schema: &crate::SignatureSchema) -> Result {
+ fn format_field_structure_schema(&self, schema: &SignatureSchema) -> Result {
let mut lines = vec.
///
- /// Navigates the `BamlValue` using each field's [`FieldPath`](crate::FieldPath) to
+ /// Navigates the `BamlValue` using each field's [`FieldPath`](FieldPath) to
/// handle flattened structs correctly. A field with path `["inner", "question"]` is
/// extracted from the nested structure but rendered as a flat `[[ ## question ## ]]`
/// section in the prompt. Appends response instructions so the LM sees
/// output-field ordering guidance in the latest user turn.
- pub fn format_input(&self, schema: &crate::SignatureSchema, input: &I) -> String
+ pub fn format_input(&self, schema: &SignatureSchema, input: &I) -> String
where
I: BamlType + for<'a> facet::Facet<'a>,
{
@@ -532,7 +544,7 @@ impl ChatAdapter {
/// Formats an output value using a schema — the building-block version of
/// [`format_assistant_message_typed`](ChatAdapter::format_assistant_message_typed).
- pub fn format_output(&self, schema: &crate::SignatureSchema, output: &O) -> String
+ pub fn format_output(&self, schema: &SignatureSchema, output: &O) -> String
where
O: BamlType + for<'a> facet::Facet<'a>,
{
@@ -558,10 +570,7 @@ impl ChatAdapter {
///
/// Convenience method that calls [`format_user_message_typed`](ChatAdapter::format_user_message_typed)
/// and [`format_assistant_message_typed`](ChatAdapter::format_assistant_message_typed).
- pub fn format_demo_typed(
- &self,
- demo: &crate::predictors::Example,
- ) -> (String, String)
+ pub fn format_demo_typed(&self, demo: &Example) -> (String, String)
where
S::Input: BamlType,
S::Output: BamlType,
@@ -619,7 +628,7 @@ impl ChatAdapter {
/// Same as [`parse_response_typed`](ChatAdapter::parse_response_typed).
pub fn parse_output_with_meta(
&self,
- schema: &crate::SignatureSchema,
+ schema: &SignatureSchema,
response: &Message,
) -> std::result::Result<(O, IndexMap), ParseError>
where
@@ -664,7 +673,7 @@ impl ChatAdapter {
);
trace!(
field = %rust_name,
- raw_preview = %crate::truncate(&raw_text, 160),
+ raw_preview = %truncate_preview(&raw_text, 160),
"typed coercion failed preview"
);
errors.push(ParseError::CoercionFailed {
@@ -792,7 +801,7 @@ impl ChatAdapter {
/// Convenience wrapper around [`parse_output_with_meta`](ChatAdapter::parse_output_with_meta).
pub fn parse_output(
&self,
- schema: &crate::SignatureSchema,
+ schema: &SignatureSchema,
response: &Message,
) -> std::result::Result
where
@@ -808,7 +817,7 @@ impl ChatAdapter {
/// is included as a section (usually empty). Duplicate section names keep the first
/// occurrence. Content before the first delimiter is discarded.
pub fn parse_sections(content: &str) -> IndexMap {
- crate::adapter::chat::parse_sections(content)
+ parse_sections(content)
}
/// Parses a raw [`Message`] into a [`Predicted`](crate::Predicted).
@@ -834,11 +843,11 @@ impl ChatAdapter {
.map_err(|source| PredictError::Parse {
source,
raw_response: raw_response.clone(),
- lm_usage: crate::LmUsage::default(),
+ lm_usage: LmUsage::default(),
})?;
let metadata = CallMetadata::new(
raw_response,
- crate::LmUsage::default(),
+ LmUsage::default(),
Vec::new(),
Vec::new(),
None,
@@ -884,10 +893,7 @@ fn parse_sections(content: &str) -> IndexMap {
parsed
}
-fn value_for_path_relaxed<'a>(
- value: &'a BamlValue,
- path: &crate::FieldPath,
-) -> Option<&'a BamlValue> {
+fn value_for_path_relaxed<'a>(value: &'a BamlValue, path: &FieldPath) -> Option<&'a BamlValue> {
let mut current = value;
let parts: Vec<_> = path.iter().collect();
let mut idx = 0usize;
@@ -924,7 +930,7 @@ fn value_for_path_relaxed<'a>(
fn insert_baml_at_path(
root: &mut bamltype::baml_types::BamlMap,
- path: &crate::FieldPath,
+ path: &FieldPath,
value: BamlValue,
) {
let parts: Vec<_> = path.iter().collect();
@@ -970,7 +976,7 @@ fn format_baml_value_for_prompt(value: &BamlValue) -> String {
}
fn render_input_field(
- field_spec: &crate::FieldSchema,
+ field_spec: &FieldSchema,
value: &BamlValue,
input: &Value,
output_format: &OutputFormatContent,
@@ -992,7 +998,7 @@ fn render_input_field(
}
}
-fn build_input_context_value(schema: &crate::SignatureSchema, root: &BamlValue) -> Value {
+fn build_input_context_value(schema: &SignatureSchema, root: &BamlValue) -> Value {
let mut input_json = baml_value_to_render_json(root);
let Some(root_map) = input_json.as_object_mut() else {
return input_json;
@@ -1023,7 +1029,7 @@ fn baml_value_to_render_json(value: &BamlValue) -> Value {
fn render_input_field_jinja(
template: &'static str,
- field_spec: &crate::FieldSchema,
+ field_spec: &FieldSchema,
value: &BamlValue,
input: &Value,
_output_format: &OutputFormatContent,
diff --git a/crates/dsrs-lm/src/lib.rs b/crates/dsrs-lm/src/lib.rs
new file mode 100644
index 00000000..dccac4d5
--- /dev/null
+++ b/crates/dsrs-lm/src/lib.rs
@@ -0,0 +1,13 @@
+//! LM client, chat adapter, and global LM settings for DSRs.
+
+pub mod adapter;
+pub mod chat;
+pub mod lm;
+pub mod settings;
+
+pub use adapter::*;
+pub use chat::*;
+pub use dsrs_cache::*;
+pub use dsrs_core::*;
+pub use lm::*;
+pub use settings::*;
diff --git a/crates/dspy-rs/src/core/lm/chat.rs b/crates/dsrs-lm/src/lm/chat.rs
similarity index 100%
rename from crates/dspy-rs/src/core/lm/chat.rs
rename to crates/dsrs-lm/src/lm/chat.rs
diff --git a/crates/dspy-rs/src/core/lm/client_registry.rs b/crates/dsrs-lm/src/lm/client_registry.rs
similarity index 100%
rename from crates/dspy-rs/src/core/lm/client_registry.rs
rename to crates/dsrs-lm/src/lm/client_registry.rs
diff --git a/crates/dspy-rs/src/core/lm/mod.rs b/crates/dsrs-lm/src/lm/mod.rs
similarity index 99%
rename from crates/dspy-rs/src/core/lm/mod.rs
rename to crates/dsrs-lm/src/lm/mod.rs
index 52273430..ba5e46b6 100644
--- a/crates/dspy-rs/src/core/lm/mod.rs
+++ b/crates/dsrs-lm/src/lm/mod.rs
@@ -1,10 +1,9 @@
pub mod chat;
pub mod client_registry;
-pub mod usage;
pub use chat::*;
pub use client_registry::*;
-pub use usage::*;
+pub use dsrs_core::LmUsage;
use anyhow::Result;
use rig::{completion::AssistantContent, message::ToolCall, message::ToolChoice, tool::ToolDyn};
@@ -14,8 +13,8 @@ use std::{collections::HashMap, sync::Arc};
use tokio::sync::Mutex;
use tracing::{Instrument, debug, trace, warn};
-use crate::utils::cache::CacheEntry;
-use crate::{Cache, Prediction, RawExample, ResponseCache};
+use dsrs_cache::{Cache, CacheEntry, ResponseCache};
+use dsrs_core::{Prediction, RawExample};
#[derive(Clone, Debug)]
pub struct LMResponse {
diff --git a/crates/dspy-rs/src/core/settings.rs b/crates/dsrs-lm/src/settings.rs
similarity index 93%
rename from crates/dspy-rs/src/core/settings.rs
rename to crates/dsrs-lm/src/settings.rs
index 8a3416e3..abcd5091 100644
--- a/crates/dspy-rs/src/core/settings.rs
+++ b/crates/dsrs-lm/src/settings.rs
@@ -1,7 +1,6 @@
use std::sync::{Arc, LazyLock, RwLock};
-use super::LM;
-use crate::adapter::Adapter;
+use crate::{Adapter, LM};
pub struct Settings {
pub lm: Arc,
diff --git a/crates/dspy-rs/tests/test_adapters.rs b/crates/dsrs-lm/tests/test_adapters.rs
similarity index 98%
rename from crates/dspy-rs/tests/test_adapters.rs
rename to crates/dsrs-lm/tests/test_adapters.rs
index 65ee7279..bf43197f 100644
--- a/crates/dspy-rs/tests/test_adapters.rs
+++ b/crates/dsrs-lm/tests/test_adapters.rs
@@ -1,4 +1,4 @@
-use dspy_rs::{ChatAdapter, Message, Signature};
+use dsrs_lm::{ChatAdapter, Message, Signature};
#[derive(Signature, Clone, Debug, PartialEq)]
struct BasicSignature {
diff --git a/crates/dspy-rs/tests/test_bamltype_docs_contract.rs b/crates/dsrs-lm/tests/test_bamltype_docs_contract.rs
similarity index 98%
rename from crates/dspy-rs/tests/test_bamltype_docs_contract.rs
rename to crates/dsrs-lm/tests/test_bamltype_docs_contract.rs
index f1216b73..ed8364e5 100644
--- a/crates/dspy-rs/tests/test_bamltype_docs_contract.rs
+++ b/crates/dsrs-lm/tests/test_bamltype_docs_contract.rs
@@ -1,5 +1,5 @@
use bamltype::HoistClasses;
-use dspy_rs::{BamlType, ChatAdapter, RenderOptions, Signature};
+use dsrs_lm::{BamlType, ChatAdapter, RenderOptions, Signature};
#[derive(Clone, Debug)]
#[BamlType]
diff --git a/crates/dspy-rs/tests/test_chat.rs b/crates/dsrs-lm/tests/test_chat.rs
similarity index 99%
rename from crates/dspy-rs/tests/test_chat.rs
rename to crates/dsrs-lm/tests/test_chat.rs
index fd8e0fe9..9644d304 100644
--- a/crates/dspy-rs/tests/test_chat.rs
+++ b/crates/dsrs-lm/tests/test_chat.rs
@@ -1,4 +1,4 @@
-use dspy_rs::core::lm::chat::{Chat, ContentBlock, Message, Role};
+use dsrs_lm::{Chat, ContentBlock, Message, Role};
use rig::OneOrMany;
use rig::message::{
AssistantContent, Message as RigMessage, Reasoning, ToolCall, ToolFunction, ToolResult,
diff --git a/crates/dspy-rs/tests/test_chat_adapter_schema.rs b/crates/dsrs-lm/tests/test_chat_adapter_schema.rs
similarity index 94%
rename from crates/dspy-rs/tests/test_chat_adapter_schema.rs
rename to crates/dsrs-lm/tests/test_chat_adapter_schema.rs
index 388218a7..c8526198 100644
--- a/crates/dspy-rs/tests/test_chat_adapter_schema.rs
+++ b/crates/dsrs-lm/tests/test_chat_adapter_schema.rs
@@ -1,4 +1,4 @@
-use dspy_rs::{CallMetadata, ChatAdapter, Message, Predicted, Signature};
+use dsrs_lm::{CallMetadata, ChatAdapter, Message, Predicted, Signature};
#[derive(Signature, Clone, Debug)]
/// Adapter schema parse fixture.
@@ -36,7 +36,7 @@ fn parse_response_typed_uses_schema_field_names() {
let metadata = CallMetadata::new(
response.content(),
- dspy_rs::LmUsage::default(),
+ dsrs_lm::LmUsage::default(),
Vec::new(),
Vec::new(),
None,
diff --git a/crates/dspy-rs/tests/test_chat_prompt_composition.rs b/crates/dsrs-lm/tests/test_chat_prompt_composition.rs
similarity index 99%
rename from crates/dspy-rs/tests/test_chat_prompt_composition.rs
rename to crates/dsrs-lm/tests/test_chat_prompt_composition.rs
index e216c15a..76fbb791 100644
--- a/crates/dspy-rs/tests/test_chat_prompt_composition.rs
+++ b/crates/dsrs-lm/tests/test_chat_prompt_composition.rs
@@ -1,4 +1,4 @@
-use dspy_rs::{ChatAdapter, Example, Signature};
+use dsrs_lm::{ChatAdapter, Example, Signature};
#[derive(Signature, Clone, Debug)]
/// Answer the prompt using the provided context.
diff --git a/crates/dspy-rs/tests/test_chat_prompt_golden.rs b/crates/dsrs-lm/tests/test_chat_prompt_golden.rs
similarity index 98%
rename from crates/dspy-rs/tests/test_chat_prompt_golden.rs
rename to crates/dsrs-lm/tests/test_chat_prompt_golden.rs
index 0cca5ece..a5f10d46 100644
--- a/crates/dspy-rs/tests/test_chat_prompt_golden.rs
+++ b/crates/dsrs-lm/tests/test_chat_prompt_golden.rs
@@ -1,4 +1,4 @@
-use dspy_rs::{ChatAdapter, Example, Signature};
+use dsrs_lm::{ChatAdapter, Example, Signature};
#[derive(Signature, Clone, Debug)]
struct GoldenSig {
diff --git a/crates/dspy-rs/tests/test_input_format.rs b/crates/dsrs-lm/tests/test_input_format.rs
similarity index 99%
rename from crates/dspy-rs/tests/test_input_format.rs
rename to crates/dsrs-lm/tests/test_input_format.rs
index 8170ae9c..c71ec819 100644
--- a/crates/dspy-rs/tests/test_input_format.rs
+++ b/crates/dsrs-lm/tests/test_input_format.rs
@@ -1,4 +1,4 @@
-use dspy_rs::{BamlType, BamlValue, ChatAdapter, Signature};
+use dsrs_lm::{BamlType, BamlValue, ChatAdapter, Signature};
#[derive(Clone, Debug)]
#[BamlType]
diff --git a/crates/dspy-rs/tests/test_lm.rs b/crates/dsrs-lm/tests/test_lm.rs
similarity index 96%
rename from crates/dspy-rs/tests/test_lm.rs
rename to crates/dsrs-lm/tests/test_lm.rs
index 9fed309c..f739454d 100644
--- a/crates/dspy-rs/tests/test_lm.rs
+++ b/crates/dsrs-lm/tests/test_lm.rs
@@ -1,5 +1,5 @@
-use dspy_rs::data::RawExample;
-use dspy_rs::{Cache, Chat, DummyLM, LM, LmUsage, Message, hashmap};
+use dsrs_lm::RawExample;
+use dsrs_lm::{Cache, Chat, DummyLM, LM, LmUsage, Message, hashmap};
use rstest::*;
#[cfg_attr(miri, ignore)] // Miri doesn't support tokio's I/O driver
@@ -135,8 +135,8 @@ async fn test_lm_cache_initialization_on_first_call() {
#[tokio::test]
#[cfg_attr(miri, ignore)]
async fn test_lm_cache_direct_operations() {
- use dspy_rs::Prediction;
- use dspy_rs::data::RawExample;
+ use dsrs_lm::Prediction;
+ use dsrs_lm::RawExample;
use std::collections::HashMap;
let lm = temp_env::async_with_vars(
@@ -175,7 +175,7 @@ async fn test_lm_cache_direct_operations() {
// Create a channel to send the result
let (tx, rx) = tokio::sync::mpsc::channel(1);
- use dspy_rs::utils::cache::CacheEntry;
+ use dsrs_lm::CacheEntry;
tx.send(CacheEntry {
prompt: "test prompt".to_string(),
prediction: value.clone(),
@@ -229,8 +229,8 @@ async fn test_lm_cache_with_different_models() {
#[tokio::test]
#[cfg_attr(miri, ignore)]
async fn test_cache_with_complex_inputs() {
- use dspy_rs::Prediction;
- use dspy_rs::data::RawExample;
+ use dsrs_lm::Prediction;
+ use dsrs_lm::RawExample;
use std::collections::HashMap;
let lm = temp_env::async_with_vars(
@@ -292,7 +292,7 @@ async fn test_cache_with_complex_inputs() {
// Insert and retrieve
let (tx, rx) = tokio::sync::mpsc::channel(1);
- use dspy_rs::utils::cache::CacheEntry;
+ use dsrs_lm::CacheEntry;
tx.send(CacheEntry {
prompt: "complex test prompt".to_string(),
prediction: value.clone(),
diff --git a/crates/dspy-rs/tests/test_message_roundtrip.rs b/crates/dsrs-lm/tests/test_message_roundtrip.rs
similarity index 99%
rename from crates/dspy-rs/tests/test_message_roundtrip.rs
rename to crates/dsrs-lm/tests/test_message_roundtrip.rs
index 96461483..5d40f428 100644
--- a/crates/dspy-rs/tests/test_message_roundtrip.rs
+++ b/crates/dsrs-lm/tests/test_message_roundtrip.rs
@@ -4,7 +4,7 @@
//! all content through: DSRs Message → rig Message → DSRs Message, and
//! through JSON serialization/deserialization.
-use dspy_rs::core::lm::chat::{Chat, ContentBlock, Message, Role};
+use dsrs_lm::{Chat, ContentBlock, Message, Role};
use rig::OneOrMany;
use rig::message::{
Message as RigMessage, Reasoning, ToolCall, ToolFunction, ToolResult, ToolResultContent,
diff --git a/crates/dspy-rs/tests/test_settings.rs b/crates/dsrs-lm/tests/test_settings.rs
similarity index 93%
rename from crates/dspy-rs/tests/test_settings.rs
rename to crates/dsrs-lm/tests/test_settings.rs
index 3bc328fd..50aab421 100644
--- a/crates/dspy-rs/tests/test_settings.rs
+++ b/crates/dsrs-lm/tests/test_settings.rs
@@ -1,4 +1,4 @@
-use dspy_rs::{ChatAdapter, LM, configure, get_lm};
+use dsrs_lm::{ChatAdapter, LM, configure, get_lm};
#[tokio::test]
#[cfg_attr(miri, ignore)]
diff --git a/crates/dspy-rs/tests/test_tool_call.rs b/crates/dsrs-lm/tests/test_tool_call.rs
similarity index 96%
rename from crates/dspy-rs/tests/test_tool_call.rs
rename to crates/dsrs-lm/tests/test_tool_call.rs
index 80a8b741..20cf3c7b 100644
--- a/crates/dspy-rs/tests/test_tool_call.rs
+++ b/crates/dsrs-lm/tests/test_tool_call.rs
@@ -1,4 +1,4 @@
-use dspy_rs::{Chat, LM, Message};
+use dsrs_lm::{Chat, LM, Message};
use rig::completion::ToolDefinition;
use rig::tool::ToolDyn;
use std::error::Error;
@@ -108,7 +108,7 @@ async fn test_tool_call_with_no_tools() {
}
let response = response.unwrap();
- assert_eq!(response.output.role, dspy_rs::Role::Assistant);
+ assert_eq!(response.output.role, dsrs_lm::Role::Assistant);
let content = response.output.content();
// The response should contain some mention of 4
println!("Assistant response: {}", content);
@@ -137,7 +137,7 @@ async fn test_tool_call_with_calculator() {
// Call with the calculator tool
let response = lm.call(chat, tools).await.unwrap();
- assert_eq!(response.output.role, dspy_rs::Role::Assistant);
+ assert_eq!(response.output.role, dsrs_lm::Role::Assistant);
let content = response.output.content();
println!("Assistant response after tool use: {}", content);
// The response should mention the result (100) or that the tool was called
diff --git a/crates/dspy-rs/tests/test_typed_alias.rs b/crates/dsrs-lm/tests/test_typed_alias.rs
similarity index 97%
rename from crates/dspy-rs/tests/test_typed_alias.rs
rename to crates/dsrs-lm/tests/test_typed_alias.rs
index 55118527..b3060a22 100644
--- a/crates/dspy-rs/tests/test_typed_alias.rs
+++ b/crates/dsrs-lm/tests/test_typed_alias.rs
@@ -1,4 +1,4 @@
-use dspy_rs::{ChatAdapter, Message, Signature};
+use dsrs_lm::{ChatAdapter, Message, Signature};
#[derive(Signature, Clone, Debug)]
/// Provide an answer using aliases.
diff --git a/crates/dspy-rs/tests/test_typed_prompt_format.rs b/crates/dsrs-lm/tests/test_typed_prompt_format.rs
similarity index 99%
rename from crates/dspy-rs/tests/test_typed_prompt_format.rs
rename to crates/dsrs-lm/tests/test_typed_prompt_format.rs
index c8e9f7dd..8f9aa5d7 100644
--- a/crates/dspy-rs/tests/test_typed_prompt_format.rs
+++ b/crates/dsrs-lm/tests/test_typed_prompt_format.rs
@@ -3,7 +3,7 @@
reason = "Signature derive emits multi-field constructors for schema coverage tests."
)]
-use dspy_rs::{BamlType, ChatAdapter, Signature};
+use dsrs_lm::{BamlType, ChatAdapter, Signature};
#[derive(Clone, Debug)]
#[BamlType]
diff --git a/crates/dsrs-macros/Cargo.toml b/crates/dsrs-macros/Cargo.toml
index 4b666638..c010bcde 100644
--- a/crates/dsrs-macros/Cargo.toml
+++ b/crates/dsrs-macros/Cargo.toml
@@ -22,5 +22,10 @@ serde_json = { version = "1.0.143", features = ["preserve_order"] }
minijinja = { git = "https://github.com/boundaryml/minijinja.git", branch = "main", default-features = false, features = ["serde"] }
[dev-dependencies]
-dspy-rs = { path = "../dspy-rs" }
+bamltype = { path = "../bamltype" }
+dsrs-core = { path = "../dsrs-core" }
+facet = { git = "https://github.com/darinkishore/facet", rev = "cc8613c97cd1ec03e63659db34a947989b45c8a5", default-features = false, features = ["std"] }
+rstest = "0.25.0"
+schemars = "1.0.4"
+tempfile = "3.23.0"
trybuild = "1.0.110"
diff --git a/crates/dsrs-macros/src/lib.rs b/crates/dsrs-macros/src/lib.rs
index c00f3fba..a5e9e958 100644
--- a/crates/dsrs-macros/src/lib.rs
+++ b/crates/dsrs-macros/src/lib.rs
@@ -11,7 +11,7 @@ use syn::{
mod runtime_path;
-use runtime_path::resolve_dspy_rs_path;
+use runtime_path::resolve_dsrs_core_path;
#[proc_macro_derive(
Signature,
@@ -19,7 +19,7 @@ use runtime_path::resolve_dspy_rs_path;
)]
pub fn derive_signature(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
- let runtime = match resolve_dspy_rs_path() {
+ let runtime = match resolve_dsrs_core_path() {
Ok(path) => path,
Err(err) => return err.to_compile_error().into(),
};
@@ -33,7 +33,7 @@ pub fn derive_signature(input: TokenStream) -> TokenStream {
#[proc_macro_derive(Augmentation, attributes(output, augment, alias))]
pub fn derive_augmentation(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
- let runtime = match resolve_dspy_rs_path() {
+ let runtime = match resolve_dsrs_core_path() {
Ok(path) => path,
Err(err) => return err.to_compile_error().into(),
};
diff --git a/crates/dsrs-macros/src/runtime_path.rs b/crates/dsrs-macros/src/runtime_path.rs
index e97307d3..36df33dc 100644
--- a/crates/dsrs-macros/src/runtime_path.rs
+++ b/crates/dsrs-macros/src/runtime_path.rs
@@ -1,19 +1,19 @@
use proc_macro_crate::{FoundCrate, crate_name};
use proc_macro2::Span;
-pub(crate) fn resolve_dspy_rs_path() -> syn::Result {
- match crate_name("dspy-rs") {
- // `crate` fails in examples/binaries inside the dspy-rs package because
+pub(crate) fn resolve_dsrs_core_path() -> syn::Result {
+ match crate_name("dsrs-core") {
+ // `crate` fails in examples/binaries inside the dsrs-core package because
// there it points at the example crate, not the library. Use the crate
- // alias (`extern crate self as dspy_rs`) for a stable path.
- Ok(FoundCrate::Itself) => Ok(syn::parse_quote!(::dspy_rs)),
+ // alias (`extern crate self as dsrs_core`) for a stable path.
+ Ok(FoundCrate::Itself) => Ok(syn::parse_quote!(::dsrs_core)),
Ok(FoundCrate::Name(name)) => {
let ident = syn::Ident::new(&name.replace('-', "_"), Span::call_site());
Ok(syn::parse_quote!(::#ident))
}
Err(_) => Err(syn::Error::new(
Span::call_site(),
- "could not resolve `dspy-rs`; add it as a dependency (renamed dependencies are supported)",
+ "could not resolve `dsrs-core`; add it as a dependency (renamed dependencies are supported)",
)),
}
}
diff --git a/crates/dsrs-macros/tests/signature_derive.rs b/crates/dsrs-macros/tests/signature_derive.rs
index abe825f6..d6db9640 100644
--- a/crates/dsrs-macros/tests/signature_derive.rs
+++ b/crates/dsrs-macros/tests/signature_derive.rs
@@ -1,4 +1,4 @@
-use dspy_rs::{BamlType, Facet, InputRenderSpec, Signature as SignatureTrait, SignatureSchema};
+use dsrs_core::{BamlType, Facet, InputRenderSpec, Signature as SignatureTrait, SignatureSchema};
/// Test instruction
#[derive(dsrs_macros::Signature, Clone, Debug)]
diff --git a/crates/dspy-rs/tests/test_bamltype_attr_contract.rs b/crates/dsrs-macros/tests/test_bamltype_attr_contract.rs
similarity index 84%
rename from crates/dspy-rs/tests/test_bamltype_attr_contract.rs
rename to crates/dsrs-macros/tests/test_bamltype_attr_contract.rs
index 1d661b3b..85e1640a 100644
--- a/crates/dspy-rs/tests/test_bamltype_attr_contract.rs
+++ b/crates/dsrs-macros/tests/test_bamltype_attr_contract.rs
@@ -1,4 +1,5 @@
-use dspy_rs::{BamlType, RenderOptions};
+use dsrs_core::{BamlType, RenderOptions};
+use facet as _;
#[derive(Debug, Clone, PartialEq)]
#[BamlType]
@@ -10,7 +11,7 @@ struct DsrsUser {
}
#[test]
-fn bamltype_attribute_macro_works_from_dspy_rs() {
+fn bamltype_attribute_macro_works_from_dsrs_core() {
let schema = ::baml_output_format()
.render(RenderOptions::default())
.expect("render schema")
diff --git a/crates/dspy-rs/tests/test_field_macro.rs b/crates/dsrs-macros/tests/test_field_macro.rs
similarity index 99%
rename from crates/dspy-rs/tests/test_field_macro.rs
rename to crates/dsrs-macros/tests/test_field_macro.rs
index 3b7ae167..f909b252 100644
--- a/crates/dspy-rs/tests/test_field_macro.rs
+++ b/crates/dsrs-macros/tests/test_field_macro.rs
@@ -1,4 +1,4 @@
-use dspy_rs::field;
+use dsrs_core::field;
use rstest::*;
use serde_json::json;
diff --git a/crates/dspy-rs/tests/test_public_api_compile_fail.rs b/crates/dsrs-macros/tests/test_public_api_compile_fail.rs
similarity index 77%
rename from crates/dspy-rs/tests/test_public_api_compile_fail.rs
rename to crates/dsrs-macros/tests/test_public_api_compile_fail.rs
index 83c387bc..7c48e09b 100644
--- a/crates/dspy-rs/tests/test_public_api_compile_fail.rs
+++ b/crates/dsrs-macros/tests/test_public_api_compile_fail.rs
@@ -8,9 +8,15 @@ fn run_compile_fail_case(name: &str, source: &str) -> String {
fs::create_dir_all(case_dir.join("src")).expect("case src dir should be creatable");
let manifest_path = Path::new(env!("CARGO_MANIFEST_DIR"));
+ let crates_dir = manifest_path
+ .parent()
+ .expect("dsrs-macros should live under crates/");
let cargo_toml = format!(
- "[package]\nname = \"{name}\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\ndspy-rs = {{ path = \"{}\" }}\nanyhow = \"1\"\n",
- manifest_path.display()
+ "[package]\nname = \"{name}\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\nanyhow = \"1\"\ndsrs-core = {{ path = \"{}\" }}\ndsrs-evaluate = {{ path = \"{}\" }}\ndsrs-gepa = {{ path = \"{}\" }}\ndsrs-predict = {{ path = \"{}\" }}\n",
+ crates_dir.join("dsrs-core").display(),
+ crates_dir.join("dsrs-evaluate").display(),
+ crates_dir.join("dsrs-gepa").display(),
+ crates_dir.join("dsrs-predict").display(),
);
fs::write(case_dir.join("Cargo.toml"), cargo_toml).expect("cargo manifest should be writable");
@@ -39,33 +45,12 @@ fn assert_not_masked_by_e0401(stderr: &str) {
);
}
-#[test]
-fn dyn_predictor_is_not_publicly_importable() {
- let stderr = run_compile_fail_case(
- "private_dyn_predictor_case",
- r#"
-use dspy_rs::DynPredictor;
-
-fn main() {
- let _ = std::any::type_name::