Explain the following doc to me like i am a layperson: ""

Documentation: DSPy Docs

DSPy is a framework for algorithmically optimizing LM prompts and weights, especially when LMs are used one or more times within a pipeline. To use LMs to build a complex system without DSPy, you generally have to: (1) break the problem down into steps, (2) prompt your LM well until each step works well in isolation, (3) tweak the steps to work well together, (4) generate synthetic examples to tune each step, and (5) use these examples to finetune smaller LMs to cut costs. Currently, this is hard and messy: every time you change your pipeline, your LM, or your data, all prompts (or finetuning steps) may need to change.

To make this more systematic and much more powerful, DSPy does two things. First, it separates the flow of your program (modules) from the parameters (LM prompts and weights) of each step. Second, DSPy introduces new optimizers, which are LM-driven algorithms that can tune the prompts and/or the weights of your LM calls, given a metric you want to maximize.

DSPy can routinely teach powerful models like GPT-3.5 or GPT-4 and local models like T5-base or Llama2-13b to be much more reliable at tasks, i.e. having higher quality and/or avoiding specific failure patterns. DSPy optimizers will "compile" the same program into different instructions, few-shot prompts, and/or weight updates (finetunes) for each LM. This is a new paradigm in which LMs and their prompts fade into the background as optimizable pieces of a larger system that can learn from data. tldr; less prompting, higher scores, and a more systematic approach to solving hard tasks with LMs.



Installation
Tutorials & Documentation
Framework Syntax
Compiling: Two Powerful Concepts
Pydantic Types
FAQ: Is DSPy right for me?
Analogy to Neural Networks
When we build neural networks, we don't write manual for-loops over lists of hand-tuned floats. Instead, you might use a framework like PyTorch to compose declarative layers (e.g., Convolution or Dropout) and then use optimizers (e.g., SGD or Adam) to learn the parameters of the network.

Ditto! DSPy gives you the right general-purpose modules (e.g., ChainOfThought, ReAct, etc.), which replace string-based prompting tricks. To replace prompt hacking and one-off synthetic data generators, DSPy also gives you general optimizers (BootstrapFewShotWithRandomSearch or BayesianSignatureOptimizer), which are algorithms that update parameters in your program. Whenever you modify your code, your data, your assertions, or your metric, you can compile your program again and DSPy will create new effective prompts that fit your changes.

Mini-FAQs
What do DSPy optimizers tune? Each optimizer is different, but they all seek to maximize a metric on your program by updating prompts or LM weights. Current DSPy optimizers can inspect your data, simulate traces through your program to generate good/bad examples of each step, propose or refine instructions for each step based on past results, finetune the weights of your LM on self-generated examples, or combine several of these to improve quality or cut cost. We'd love to merge new optimizers that explore a richer space: most manual steps you currently go through for prompt engineering, "synthetic data" generation, or self-improvement can probably generalized into a DSPy optimizer that acts on arbitrary LM programs.

How should I use DSPy for my task? Using DSPy is an iterative process. You first define your task and the metrics you want to maximize, and prepare a few example inputs — typically without labels (or only with labels for the final outputs, if your metric requires them). Then, you build your pipeline by selecting built-in layers (modules) to use, giving each layer a signature (input/output spec), and then calling your modules freely in your Python code. Lastly, you use a DSPy optimizer to compile your code into high-quality instructions, automatic few-shot examples, or updated LM weights for your LM.

What if I have a better idea for prompting or synthetic data generation? Perfect. We encourage you to think if it's best expressed as a module or an optimizer, and we'd love to merge it in DSPy so everyone can use it. DSPy is not a complete project; it's an ongoing effort to create structure (modules and optimizers) in place of hacky prompt and pipeline engineering tricks.

What does DSPy stand for? It's a long story but the backronym now is Declarative Self-improving Language Programs, pythonically.

1) Installation
All you need is:

pip install dspy-ai
Or open our intro notebook in Google Colab: 

By default, DSPy installs the latest openai from pip. However, if you install old version before OpenAI changed their API openai~=0.28.1, the library will use that just fine. Both are supported.

For the optional (alphabetically sorted) Chromadb, Qdrant, Marqo, Pinecone, or Weaviate retrieval integration(s), include the extra(s) below:

pip install dspy-ai[chromadb]  # or [qdrant] or [marqo] or [mongodb] or [pinecone] or [weaviate]
2) Documentation
The DSPy documentation is divided into tutorials (step-by-step illustration of solving a task in DSPy), guides (how to use specific parts of the API), and examples (self-contained programs that illustrate usage).

A) Tutorials
Level	Tutorial	Run in Colab	Description
Beginner	Getting Started		Introduces the basic building blocks in DSPy. Tackles the task of complex question answering with HotPotQA.
Beginner	Minimal Working Example	N/A	Builds and optimizes a very simple chain-of-thought program in DSPy for math question answering. Very short.
Beginner	Compiling for Tricky Tasks	N/A	Teaches LMs to reason about logical statements and negation. Uses GPT-4 to bootstrap few-shot CoT demonstations for GPT-3.5. Establishes a state-of-the-art result on ScoNe. Contributed by Chris Potts.
Beginner	Local Models & Custom Datasets		Illustrates two different things together: how to use local models (Llama-2-13B in particular) and how to use your own data examples for training and development.
Intermediate	The DSPy Paper	N/A	Sections 3, 5, 6, and 7 of the DSPy paper can be consumed as a tutorial. They include explained code snippets, results, and discussions of the abstractions and API.
Intermediate	DSPy Assertions		Introduces example of applying DSPy Assertions while generating long-form responses to questions with citations. Presents comparative evaluation in both zero-shot and compiled settings.
Intermediate	Finetuning for Complex Programs		Teaches a local T5 model (770M) to do exceptionally well on HotPotQA. Uses only 200 labeled answers. Uses no hand-written prompts, no calls to OpenAI, and no labels for retrieval or reasoning.
Advanced	Information Extraction		Tackles extracting information from long articles (biomedical research papers). Combines in-context learning and retrieval to set SOTA on BioDEX. Contributed by Karel D’Oosterlinck.
Other resources people find useful:

DSPy talk at ScaleByTheBay Nov 2023.
DSPy webinar with MLOps Learners, a bit longer with Q&A.
Hands-on Overviews of DSPy by the community: DSPy Explained! by Connor Shorten, DSPy explained by code_your_own_ai
Interviews: Weaviate Podcast in-person, and you can find 6-7 other remote podcasts on YouTube from a few different perspectives/audiences.
Tracing in DSPy with Arize Phoenix: Tutorial for tracing your prompts and the steps of your DSPy programs
B) Guides
If you're new to DSPy, it's probably best to go in sequential order. You will probably refer to these guides frequently after that, e.g. to copy/paste snippets that you can edit for your own DSPy programs.

Language Models

Signatures

Modules

Data

Metrics

Optimizers (formerly Teleprompters)

DSPy Assertions

C) Examples
The DSPy team believes complexity has to be justified. We take this seriously: we never release a complex tutorial (above) or example (below) unless we can demonstrate empirically that this complexity has generally led to improved quality or cost. This kind of rule is rarely enforced by other frameworks or docs, but you can count on it in DSPy examples.

There's a bunch of examples in the examples/ directory and in the top-level directory. We welcome contributions!

You can find other examples tweeted by @lateinteraction on Twitter/X.

Some other examples (not exhaustive, feel free to add more via PR):

Applying DSPy Assertions
Long-form Answer Generation with Citations, by Arnav Singhvi
Generating Answer Choices for Quiz Questions, by Arnav Singhvi
Generating Tweets for QA, by Arnav Singhvi
Compiling LCEL runnables from LangChain in DSPy
AI feedback, or writing LM-based metrics in DSPy
DSPy Optimizers Benchmark on a bunch of different tasks, by Michael Ryan
Indian Languages NLI with gains due to compiling by Saiful Haq
Sophisticated Extreme Multi-Class Classification, IReRa, by Karel D’Oosterlinck
DSPy on BIG-Bench Hard Example, by Chris Levy
Using Ollama with DSPy for Mistral (quantized) by @jrknox1977
Using DSPy, "The Unreasonable Effectiveness of Eccentric Automatic Prompts" (paper) by VMware's Rick Battle & Teja Gollapudi, and interview at TheRegister
Typed DSPy (contributed by @normal-computing)
Using DSPy to train Gpt 3.5 on HumanEval by Thomas Ahle
Building a chess playing agent using DSPy by Franck SN
There are also recent cool examples at Weaviate's DSPy cookbook by Connor Shorten. See tutorial on YouTube.

3) Syntax: You're in charge of the workflow—it's free-form Python code!
DSPy hides tedious prompt engineering, but it cleanly exposes the important decisions you need to make: [1] what's your system design going to look like? [2] what are the important constraints on the behavior of your program?

You express your system as free-form Pythonic modules. DSPy will tune the quality of your program in whatever way you use foundation models: you can code with loops, if statements, or exceptions, and use DSPy modules within any Python control flow you think works for your task.

Suppose you want to build a simple retrieval-augmented generation (RAG) system for question answering. You can define your own RAG program like this:

class RAG(dspy.Module):
    def __init__(self, num_passages=3):
        super().__init__()
        self.retrieve = dspy.Retrieve(k=num_passages)
        self.generate_answer = dspy.ChainOfThought("context, question -> answer")
    
    def forward(self, question):
        context = self.retrieve(question).passages
        answer = self.generate_answer(context=context, question=question)
        return answer
A program has two key methods, which you can edit to fit your needs.

Your __init__ method declares the modules you will use. Here, RAG will use the built-in Retrieve for retrieval and ChainOfThought for generating answers. DSPy offers general-purpose modules that take the shape of your own sub-tasks — and not pre-built functions for specific applications.

Modules that use the LM, like ChainOfThought, require a signature. That is a declarative spec that tells the module what it's expected to do. In this example, we use the short-hand signature notation context, question -> answer to tell ChainOfThought it will be given some context and a question and must produce an answer. We will discuss more advanced signatures below.

Your forward method expresses any computation you want to do with your modules. In this case, we use the module self.retrieve to search for some context and then use the module self.generate_answer, which uses the context and question to generate the answer!

You can now either use this RAG program in zero-shot mode. Or compile it to obtain higher quality. Zero-shot usage is simple. Just define an instance of your program and then call it:

rag = RAG()  # zero-shot, uncompiled version of RAG
rag("what is the capital of France?").answer  # -> "Paris"
The next section will discuss how to compile our simple RAG program. When we compile it, the DSPy compiler will annotate demonstrations of its steps: (1) retrieval, (2) using context, and (3) using chain-of-thought to answer questions. From these demonstrations, the DSPy compiler will make sure it produces an effective few-shot prompt that works well with your LM, retrieval model, and data. If you're working with small models, it'll finetune your model (instead of prompting) to do this task.

If you later decide you need another step in your pipeline, just add another module and compile again. Maybe add a module that takes the chat history into account during search?

4) Two Powerful Concepts: Signatures & Teleprompters
Note: We will soon rename teleprompters to optimizers. This will not affect their functionality, but will simplify the terms used.

To make it possible to compile any program you write, DSPy introduces two simple concepts: Signatures and Teleprompters.

4.a) Declaring the input/output behavior of LMs with dspy.Signature
When we assign tasks to LMs in DSPy, we specify the behavior we need as a Signature. A signature is a declarative specification of input/output behavior of a DSPy module.

Instead of investing effort into how to get your LM to do a sub-task, signatures enable you to inform DSPy what the sub-task is. Later, the DSPy compiler will figure out how to build a complex prompt for your large LM (or finetune your small LM) specifically for your signature, on your data, and within your pipeline.

A signature consists of three simple elements:

A minimal description of the sub-task the LM is supposed to solve.
A description of one or more input fields (e.g., input question) that will we will give to the LM.
A description of one or more output fields (e.g., the question's answer) that we will expect from the LM.
We support two notations for expressing signatures. The short-hand signature notation is for quick development. You just provide your module (e.g., dspy.ChainOfThought) with a string with input_field_name_1, ... -> output_field_name_1, ... with the fields separated by commas.

In the RAG class earlier, we saw:

self.generate_answer = dspy.ChainOfThought("context, question -> answer")
In many cases, this barebones signature is sufficient. However, sometimes you need more control. In these cases, we can use the full notation to express a more fully-fledged signature below.

class GenerateSearchQuery(dspy.Signature):
    """Write a simple search query that will help answer a complex question."""

    context = dspy.InputField(desc="may contain relevant facts")
    question = dspy.InputField()
    query = dspy.OutputField()

### inside your program's __init__ function
self.generate_answer = dspy.ChainOfThought(GenerateSearchQuery)
You can optionally provide a prefix and/or desc key for each input or output field to refine or constraint the behavior of modules using your signature. The description of the sub-task itself is specified as the docstring (i.e., """Write a simple...""").

4.b) Asking DSPy to automatically optimize your program with dspy.teleprompt.*
After defining the RAG program, we can compile it. Compiling a program will update the parameters stored in each module. For large LMs, this is primarily in the form of creating and validating good demonstrations for inclusion in your prompt(s).

Compiling depends on three things: a (potentially tiny) training set, a metric for validation, and your choice of teleprompter from DSPy. Teleprompters are powerful optimizers (included in DSPy) that can learn to bootstrap and select effective prompts for the modules of any program. (The "tele-" in the name means "at a distance", i.e., automatic prompting at a distance.)

DSPy typically requires very minimal labeling. For example, our RAG pipeline may work well with just a handful of examples that contain a question and its (human-annotated) answer. Your pipeline may involve multiple complex steps: our basic RAG example includes a retrieved context, a chain of thought, and the answer. However, you only need labels for the initial question and the final answer. DSPy will bootstrap any intermediate labels needed to support your pipeline. If you change your pipeline in any way, the data bootstrapped will change accordingly!

my_rag_trainset = [
  dspy.Example(
    question="Which award did Gary Zukav's first book receive?",
    answer="National Book Award"
  ),
  ...
]
Second, define your validation logic, which will express some constraints on the behavior of your program or individual modules. For RAG, we might express a simple check like this:

def validate_context_and_answer(example, pred, trace=None):
    # check the gold label and the predicted answer are the same
    answer_match = example.answer.lower() == pred.answer.lower()

    # check the predicted answer comes from one of the retrieved contexts
    context_match = any((pred.answer.lower() in c) for c in pred.context)

    return answer_match and context_match
Different teleprompters offer various tradeoffs in terms of how much they optimize cost versus quality, etc. For RAG, we might use the simple teleprompter called BootstrapFewShot. To do so, we instantiate the teleprompter itself with a validation function my_rag_validation_logic and then compile against some training set my_rag_trainset.

from dspy.teleprompt import BootstrapFewShot

teleprompter = BootstrapFewShot(metric=my_rag_validation_logic)
compiled_rag = teleprompter.compile(RAG(), trainset=my_rag_trainset)
If we now use compiled_rag, it will invoke our LM with rich prompts with few-shot demonstrations of chain-of-thought retrieval-augmented question answering on our data.

5) Pydantic Types
Sometimes you need more than just string inputs/outputs. Assume, for example, you need to find

from pydantic import BaseModel, Field
from dspy.functional import TypedPredictor

class TravelInformation(BaseModel):
    origin: str = Field(pattern=r"^[A-Z]{3}$")
    destination: str = Field(pattern=r"^[A-Z]{3}$")
    date: datetime.date
    confidence: float = Field(gt=0, lt=1)

class TravelSignature(Signature):
    """ Extract all travel information in the given email """
    email: str = InputField()
    flight_information: list[TravelInformation] = OutputField()

predictor = TypedPredictor(TravelSignature)
predictor(email='...')
Which will output a list of TravelInformation objects.

There are other ways to create typed signatures too. Such as

predictor = TypedChainOfThought("question:str -> answer:int")
which applies chain of thought, and is guaranteed to return an int.

There's even an approach inspired by tanuki.py, which can be convenient when defining modules:

from dspy.functional import FunctionalModule, predictor, cot

class MyModule(FunctionalModule):
    @predictor
    def hard_question(possible_topics: list[str]) -> str:
        """Write a hard question based on one of the topics. It should be answerable by a number."""

    @cot
    def answer(question: str) -> float:
        pass

    def forward(possible_topics: list[str]):
        q = hard_question(possible_topics=possible_topics)
        a = answer(question=q)
        return (q, a)
For more examples, see the list above, as well as the unit tests for the module.


""