← Back to 003 decomposition

004 gemini

Documentation for 004_gemini from the Ds ex repository.

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 the SignatureMeta metaclass and the ensure_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 a Signature. 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., a forward and aforward method). It does not include the litellm-based dspy.LM.
  • adapters/base.py -> dspy.Adapter: The abstract base class for Adapters. It defines the format and parse methods, which are the crucial link between a logical Signature and a physical LM prompt.
  • retrieve/retrieve.py -> dspy.Retrieve: The base Module 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 their lm, rm, and adapter at runtime. Decoupling this would require a massive, and likely less ergonomic, redesign of the entire framework’s API (e.g., passing lm and rm into every forward call). It is therefore core.
  • utils/callback.py -> dspy.BaseCallback and with_callbacks: The callback system is deeply integrated into the __call__ methods of Module and BaseLM. 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 a Module that uses the core primitives (Signature, Adapter, BaseLM). The core layer provides the tools to build a predictor, but not the predictor itself.
  • litellm-based dspy.LM (clients/lm.py): This is a concrete implementation of dspy.BaseLM. A user of dspy-core would need to provide their own BaseLM implementation.
  • Concrete Adapters (ChatAdapter, JSONAdapter): These are implementations of the dspy.Adapter interface.
  • Concrete Retrievers (PineconeRM, ChromaRM, etc.): These are implementations of the dspy.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.