Excellent question. This gets to the heart of software architecture and framework design. To define the smallest, cohesive, and reusable foundational layer for DSPy, we must identify the absolute, non-negotiable core abstractions and interfaces that are essential for defining and executing a DSPy program, while pushing all concrete implementations to a higher layer.
This foundational layer, let’s call it dspy-core
, would be the stable, minimal foundation upon which all other DSPy functionality is built.
The Smallest Cohesive Foundational Layer: dspy-core
This layer’s purpose is to provide the abstract machinery for defining computational graphs of LM interactions, without dictating how those interactions are implemented.
Here is the breakdown of what constitutes this minimal core:
1. The Core Object Model: Defining a Program’s Structure
These components define the “what” of a DSPy programāits structure and data flow. They are irreducible.
primitives/module.py
->dspy.Module
: The absolute centerpiece. It’s the base class for any component in the computational graph. Its role in containing other modules and parameters (named_parameters
,named_sub_modules
) is fundamental to program composition and optimization.signatures/signature.py
->dspy.Signature
: The declarative schema for an LM interaction. This includes theSignatureMeta
metaclass and theensure_signature
factory function. It defines the intent of an LM call, decoupling it from the prompt’s string format.signatures/field.py
->dspy.InputField
&dspy.OutputField
: The building blocks of aSignature
. They carry metadata (desc
,prefix
) that is essential for both prompting and optimization.primitives/example.py
->dspy.Example
: The fundamental data structure for inputs and labels. It’s the “Tensor” of DSPy.primitives/prediction.py
->dspy.Prediction
: The standard return type for modules, encapsulating the structured outputs and the raw LM completions.
2. The Backend Interface Layer: Defining How to Connect to the World
This layer provides the abstract “plugs” for external services. dspy-core
defines the shape of the plugs, but provides no actual wires.
clients/base_lm.py
->dspy.BaseLM
: The abstract base class for Language Models. It defines the contract that any LM provider must fulfill (i.e., aforward
andaforward
method). It does not include thelitellm
-baseddspy.LM
.adapters/base.py
->dspy.Adapter
: The abstract base class for Adapters. It defines theformat
andparse
methods, which are the crucial link between a logicalSignature
and a physical LM prompt.retrieve/retrieve.py
->dspy.Retrieve
: The baseModule
for retrieval. It defines the primitive action of retrieving information, abstracting away the specific vector store.
3. Execution Context and Control: The Runtime Engine
These components manage the execution flow and state.
dsp/utils/settings.py
->dspy.settings
: The global, thread-aware configuration singleton. This is the dependency injection mechanism of DSPy. It’s how modules dynamically receive theirlm
,rm
, andadapter
at runtime. Decoupling this would require a massive, and likely less ergonomic, redesign of the entire framework’s API (e.g., passinglm
andrm
into everyforward
call). It is therefore core.utils/callback.py
->dspy.BaseCallback
andwith_callbacks
: The callback system is deeply integrated into the__call__
methods ofModule
andBaseLM
. It provides the essential observability and control hooks for the entire system, making it a foundational part of the runtime.
What is Explicitly Excluded from dspy-core
?
To understand the boundary, it’s equally important to see what is left out:
dspy.Predict
: This is the first and most critical exclusion.Predict
is a concrete implementation of aModule
that uses the core primitives (Signature
,Adapter
,BaseLM
). The core layer provides the tools to build a predictor, but not the predictor itself.litellm
-baseddspy.LM
(clients/lm.py
): This is a concrete implementation ofdspy.BaseLM
. A user ofdspy-core
would need to provide their ownBaseLM
implementation.- Concrete Adapters (
ChatAdapter
,JSONAdapter
): These are implementations of thedspy.Adapter
interface. - Concrete Retrievers (
PineconeRM
,ChromaRM
, etc.): These are implementations of thedspy.Retrieve
module. - All Optimizers (
teleprompt/
): Teleprompters operate on fully-defined, runnable programs. They are a higher-level concern that uses the programs defined with the core and standard libraries. - Complex Composite Modules (
ReAct
,ChainOfThought
): These are part of the standard library, built from the core primitives.
“Hello, World” with Only dspy-core
This example demonstrates the cohesion of the proposed dspy-core
. A developer could write a complete, runnable program using only this foundational layer, provided they supply the concrete implementations for the backend interfaces.
# Assuming dspy-core is installed, but not dspy-lib
import dspy_core as dspy
from dspy_core.primitives import Module
from dspy_core.signatures import Signature, InputField, OutputField
from dspy_core.clients.base_lm import BaseLM
from dspy_core.adapters.base import Adapter
# 1. Define the Signature (from dspy-core)
class BasicQA(Signature):
"""Answer questions with a single word."""
question = InputField()
answer = OutputField()
# 2. User provides their OWN concrete LM client (implementing the dspy-core interface)
class MyToyLM(BaseLM):
def forward(self, prompt, **kwargs):
# A dummy LM that just reverses the last word of the prompt.
answer = prompt.split()[-1].replace('?', '')[::-1]
return [{"text": f"Answer: {answer}"}]
# 3. User provides their OWN concrete Adapter (implementing the dspy-core interface)
class MySimpleAdapter(Adapter):
def format(self, signature, demos, inputs):
return f"Q: {inputs['question']}"
def parse(self, signature, completion):
return {"answer": completion.split("Answer:")[1].strip()}
# 4. User builds their program using dspy-core's Module
# They must also build their own simple Predictor, since it's not in core.
class MyPredict(Module):
def __init__(self, signature):
super().__init__()
self.signature = signature
def forward(self, **kwargs):
# This logic is what's inside the real dspy.Predict
adapter = dspy.settings.adapter
lm = dspy.settings.lm
prompt = adapter.format(self.signature, [], kwargs)
completions = lm(prompt)
parsed_output = adapter.parse(self.signature, completions[0])
return dspy.Prediction(parsed_output)
# 5. The user configures the dspy-core runtime and executes the program
dspy.settings.configure(
lm=MyToyLM(),
adapter=MySimpleAdapter()
)
my_predictor = MyPredict(BasicQA)
result = my_predictor(question="What is the capital of France?")
print(result.answer)
# Expected Output: ecnarF
This example proves that the proposed dspy-core
layer is both minimal (it doesn’t include dspy.Predict
) and cohesive (it contains everything needed to define and run a program, once the backend “drivers” are provided). This forms a stable, reusable foundation perfect for building a richer ecosystem upon.