Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 25 additions & 23 deletions .github/workflows/qol.yaml
Original file line number Diff line number Diff line change
@@ -1,52 +1,54 @@
name: Lint and Test

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

jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.90.0
- run: cargo build
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: Test
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.90.0
- uses: Swatinem/rust-cache@v2
- run: cargo test --all-features
miri-test:
name: MIRI Test

python-adapter:
name: Python Adapter (dsrs-dspy)
runs-on: ubuntu-latest
env:
MIRIFLAGS: -Zmiri-disable-isolation
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: nightly-2026-01-25
components: miri
- run: cargo miri setup
- run: cargo miri test --all-features
toolchain: 1.90.0
- uses: Swatinem/rust-cache@v2
- uses: astral-sh/setup-uv@v5
- name: Build wheel and run adapter tests against DSPy
run: |
uv venv .venv
VIRTUAL_ENV=$PWD/.venv uv pip install ./crates/dspy-py dspy pytest
.venv/bin/pytest crates/dspy-py/tests/python/ -q

clippy:
name: Clippy Lint
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
timeout-minutes: 5
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.90.0
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- run: cargo fmt --check
- run: cargo clippy --all-targets --all-features
89 changes: 86 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,26 @@ println!("Sentiment: {}", analysis.sentiment);
println!("Summary: {}", summary.summary);
```

## 🐍 Using DSRs from Python (DSPy)

DSRs's BAML-style schema rendering and JSONish parsing are available to
[DSPy](https://github.com/stanfordnlp/dspy) as a drop-in adapter, via the
`dsrs-dspy` PyO3 package in [`crates/dspy-py`](crates/dspy-py):

```bash
uv add "dsrs-dspy @ git+https://github.com/krypticmouse/DSRs@main#subdirectory=crates/dspy-py"
```

```python
import dspy
from dsrs_dspy import DSRSBAMLAdapter

dspy.configure(lm=dspy.LM("openai/gpt-5.2"), adapter=DSRSBAMLAdapter())
```

Building from git requires a Rust toolchain. See
[`crates/dspy-py/README.md`](crates/dspy-py/README.md) for details.

## 🧪 Testing

Run the test suite:
Expand Down
1 change: 1 addition & 0 deletions crates/bamltype/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ pub use runtime::{

pub mod adapters;
mod schema_registry;
pub use schema_registry::SchemaRegistry;

pub mod facet_ext;

Expand Down
20 changes: 20 additions & 0 deletions crates/dspy-py/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "dspy-py"
version = "0.1.2"
edition = "2024"
description = "PyO3 bridge to expose BAML rendering and JSONish parsing to DSPy"
license = "Apache-2.0"

[lib]
name = "_dsrs_dspy"
crate-type = ["cdylib", "rlib"]

[dependencies]
bamltype = { path = "../bamltype" }
pyo3 = { version = "0.28.0", features = ["macros", "abi3-py39"] }
regex = "1.11.2"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = { version = "1.0.145", features = ["preserve_order"] }

[dev-dependencies]
proptest = "1.7.0"
93 changes: 93 additions & 0 deletions crates/dspy-py/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# dsrs-dspy (dspy-py)

A drop-in [DSPy](https://github.com/stanfordnlp/dspy) adapter backed by DSRs:
BAML-style schema rendering and JSONish response parsing, exposed to Python
through a PyO3 extension.

Compared to DSPy's stock `ChatAdapter`, this adapter:

- renders output-field schemas in BAML's compact, LLM-friendly format
(including nested pydantic models, enums, unions, `$defs`/`$ref`), and
- parses completions with JSONish, which tolerates the sins real models
commit: trailing commas, Python literals (`True`/`False`/`None`),
single quotes, and whitespace-mangled `[[ ## field ## ]]` markers.

There is no silent fallback to a second model call: parse failures surface as
`AdapterParseError`.

## Install

Not published to PyPI. Install from git (requires a Rust toolchain; the wheel
builds from source via maturin):

```bash
uv add "dsrs-dspy @ git+https://github.com/krypticmouse/DSRs@main#subdirectory=crates/dspy-py"
# or
pip install "dsrs-dspy @ git+https://github.com/krypticmouse/DSRs@main#subdirectory=crates/dspy-py"
```

## Use

```python
import dspy
from dsrs_dspy import DSRSBAMLAdapter

dspy.configure(lm=dspy.LM("openai/gpt-5.2"), adapter=DSRSBAMLAdapter())
```

Everything else is ordinary DSPy — signatures, modules, optimizers.

## Python API surface

The compiled module is `dsrs_dspy._dsrs_dspy`:

- `render_field_structure(spec_json: str) -> str`
— compiles DSPy/pydantic field schemas into BAML `TypeIR` and returns the
BAML-style field structure text.
- `parse_response(spec_json: str, completion: str, is_done: bool = True) -> str`
— parses `[[ ## field ## ]]` sections with JSONish and returns a JSON string
of parsed output fields.

`DSRSBAMLAdapter` (pure Python, in `dsrs_dspy/adapter.py`) subclasses DSPy's
`ChatAdapter` and overrides `format_field_structure()` and `parse()` with the
functions above, keeping DSPy's own field coercion via
`dspy.adapters.utils.parse_value`.

### Spec shape (`spec_json`)

```json
{
"input_fields": [
{"name": "question", "description": "", "format": null, "schema": {"type": "string"}}
],
"output_fields": [
{"name": "answer", "description": "", "format": null, "schema": {"type": "string"}}
],
"instruction": "Given ..."
}
```

`schema` is pydantic/JSON-Schema-like and may include `$defs` + local `$ref`.

Supported schema features: primitives, objects (`properties`/`required`),
maps (`additionalProperties`), arrays (`items`/`prefixItems`), unions
(`anyOf`/`oneOf`/`allOf`/`type: [..]`), enums, `const`, and local
`#/$defs/...` / `#/definitions/...` references.

## Tests

Rust unit + property tests (schema fuzzing, JSONish noise, description
propagation, and a matrix driven by real `pydantic.TypeAdapter(...).json_schema()`
output — that last one needs a Python with pydantic importable and is skipped
otherwise):

```bash
cargo test -p dspy-py
```

Python-side adapter tests against a real DSPy install:

```bash
uv venv .venv && VIRTUAL_ENV=$PWD/.venv uv pip install . dspy pytest
.venv/bin/pytest tests/python/
```
23 changes: 23 additions & 0 deletions crates/dspy-py/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[build-system]
requires = ["maturin>=1.7,<2.0"]
build-backend = "maturin"

[project]
name = "dsrs-dspy"
version = "0.1.2"
description = "DSRS-backed DSPy adapter using BAML rendering and JSONish parsing"
requires-python = ">=3.9"
dependencies = ["dspy>=3.0", "pydantic>=2"]

[project.urls]
Repository = "https://github.com/krypticmouse/DSRs"

[tool.maturin]
# Nest the compiled extension inside the python package so ONE wheel ships both
# `dsrs_dspy` (pure python) and its `_dsrs_dspy` native module — no PYTHONPATH,
# no separate install step.
module-name = "dsrs_dspy._dsrs_dspy"
python-source = "python"
# extension-module: don't hard-link libpython, so the abi3 wheel loads under any
# CPython >= 3.9 (without this the .so segfaults across python versions).
features = ["pyo3/extension-module"]
3 changes: 3 additions & 0 deletions crates/dspy-py/python/dsrs_dspy/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .adapter import DSRSBAMLAdapter

__all__ = ["DSRSBAMLAdapter"]
Loading
Loading