Overview
dspy.propose supplies the part of DSPy that writes instruction candidates. MIPROv2 asks a proposer for proposals, then searches over those proposals.
For the search side, see Inside MIPROv2 and the official MIPROv2 docs . This page stays with the writer side: who wrote the instruction, what it looked at, and why grounded context beats a blind rewrite of the current prompt.
dspy.propose.__init__ exports GroundedProposer as the package entry point, and Proposer in dspy/propose/propose_base.py defines the abstract contract. That contract keeps the subsystem focused on one job: produce instruction candidates for downstream optimizers.
Candidate generation and search are different jobs
GroundedProposer writes candidates. MIPROv2 evaluates and selects among those candidates with Optuna. The proposer does not rank the final answer or pick the best prompt combination; it prepares the search space.
That split matters because a good candidate writer needs different inputs from a good search engine. The writer wants evidence about the task, the program, and the examples. The search layer wants a pool of varied candidates that it can score against the task metric.
What “grounded” means here
GroundedProposer builds a richer prompt than a bare instruction rewrite. In the MIPROv2 path, it enables program awareness, dataset summaries, few shot demos, and tip selection before it calls an LM.
The broader proposer also supports instruction history. When a caller enables it, create_predictor_level_history_string() and create_instruction_set_history_string() summarize earlier attempts and the scores they earned.
create_dataset_summary()produces a short description of the dataset.get_dspy_source_code(program)captures the program structure, andDescribeProgramandDescribeModuleturn that code into natural language descriptions.create_example_string()turns few shot demo candidates into example text for the generator.create_predictor_level_history_string()andcreate_instruction_set_history_string()summarize earlier attempts when history stays enabled.TIPSadds a short prompting cue, andset_tip_randomlycan pick a different entry for each rollout.
Each source improves proposals in a different way. The dataset summary tells the LM what the data tends to look like. Program awareness tells it which module it should write for and how that module fits the larger pipeline. Demo examples show the exact input and output shape that the instruction must support. History shows which ideas already scored well and which ones the system already tried. Tips nudge the style of the proposal without hard coding a single phrasing.
In the MIPROv2 call path, the proposer keeps program awareness, dataset summaries, few shot demos, and tip selection turned on, but it turns instruction history off with use_instruct_history=False and set_history_randomly=False. That means MIPROv2 asks for fresh proposals that still see the code, the data, and the examples, but do not lean on earlier instruction attempts.
Compared with a blind rewrite of the current instruction, grounded proposal generation can change the prompt to match what the program and dataset actually contain. It avoids simply paraphrasing the old wording and instead uses the surrounding evidence to decide what the instruction should emphasize.
The dataset summary pass
create_dataset_summary() in dspy/propose/dataset_summary_generator.py summarizes the training set in two stages. First, it observes a batch of examples with DatasetDescriptor. Then it walks through later batches with DatasetDescriptorWithPriorObservations, which adds new observations on top of the ones already found. ObservationSummarizer then compresses the accumulated observations into a brief two or three sentence summary.
GroundedProposer.__init__ stores that summary as self.data_summary. GenerateModuleInstruction.forward() then passes it into GenerateSingleModuleInstruction as dataset_description=data_summary. The runtime name changes, but the job stays the same: give the instruction generator a concise description of the task data instead of the raw training set.
DSPy uses DSPy to write its own prompts
This subsystem makes its own instructions with the same primitives it uses for task programs. DescribeProgram and DescribeModule are dspy.Signature classes. generate_instruction_class() builds GenerateSingleModuleInstruction as another dspy.Signature, then wraps it in dspy.Predict. GenerateModuleInstruction acts as a dspy.Module that composes those predictors.
That design matters. GenerateModuleInstruction first asks DescribeProgram to summarize the whole program, then asks DescribeModule to explain the chosen predictor, then feeds both descriptions into GenerateSingleModuleInstruction. The code does not rely on a handwritten string template. It uses signatures and predictors to author the meta prompt that will author the task prompt.
dspy/propose/utils.py supports that loop with get_dspy_source_code(), create_example_string(), create_predictor_level_history_string(), create_instruction_set_history_string(), and strip_prefix(). Those helpers turn live program state, demos, and past trials into text the proposer can reason over.
How the proposer varies its candidates
GroundedProposer.propose_instructions_for_program() loops over each predictor in the program and over the available demo set candidates. That gives each predictor its own proposal stream and lets the demo context shift across adjacent candidate sets instead of staying fixed.
set_tip_randomly adds another source of variation. When it stays on, the proposer samples one entry from TIPS for each rollout, so one proposal may lean toward simplicity while another may lean toward creativity, a persona, or a high stakes framing.
propose_instruction_for_predictor() also creates a fresh LM rollout with a unique rollout_id and temperature=self.init_temperature. The test in tests/propose/test_grounded_proposer.py confirms that the proposer forwards the configured temperature into the LM copy call. The result is a set of candidates that differ in context and rollout state, not a repeated copy of one answer.
create_predictor_level_history_string() can also narrow the context to the top scoring earlier instructions for the same predictor. MIPROv2 disables that path, but the proposer keeps it available for consumers that want to ground proposals in prior trial logs.
Who consumes it
MIPROv2 is the main in-repo consumer of dspy.propose.GroundedProposer. Its _propose_instructions() method builds the proposer with the program, training set, and prompt model, then asks it for instruction candidates before the Optuna search begins. The same module keeps the proposer grounded in data and demos while it lets Optuna handle the actual search over instruction and demo combinations.
Other optimizers follow different paths. dspy/teleprompt/copro_optimizer.py defines its own instruction generation signatures, BasicGenerateInstruction and GenerateInstructionGivenAttempts, instead of importing dspy.propose. dspy/teleprompt/gepa/gepa.py accepts a custom ProposalFn, and dspy/teleprompt/gepa/instruction_proposal.py supplies MultiModalInstructionProposer for visual inputs. Those paths share the same goal, but they do not share the same proposer subsystem.
Where to look in the code
dspy/propose/__init__.pyexportsGroundedProposer.dspy/propose/propose_base.pydefines the proposer contract.dspy/propose/utils.pyconverts source code, demos, and history into proposal context.dspy/propose/dataset_summary_generator.pybuilds the dataset summary.dspy/propose/grounded_proposer.pyassembles grounded context and writes candidates.dspy/teleprompt/mipro_optimizer_v2.pyconsumesGroundedProposerand performs search.dspy/teleprompt/copro_optimizer.pyanddspy/teleprompt/gepa/instruction_proposal.pyshow alternate instruction writers.