Composing programs¶
Programs are ordinary Scala values, so you compose them the way you compose any
other code: call one program inside another. To package a multi-step pipeline as
a single reusable program, extend DynamicModule and chain predictors in its
forward method.
This module answers a question, then rewrites the answer in simpler terms, using two predictors in sequence:
final class SimplifyModule extends DynamicModule:
override val moduleName: String = "simplify_module"
private val predict1 = DynamicPredict(Signature.fromString("question -> answer").layout, name = Some("predict1"))
private val predict2 =
DynamicPredict(Signature.fromString("answer -> simplified_answer").layout, name = Some("predict2"))
override protected def forwardDynamic(call: ProgramCall[DynamicValue.Record])(using
RuntimeContext
): Either[DspyError, RawPrediction] =
for
step1 <- predict1(call)
answer = textField(step1.output, "answer")
step2 <- predict2(ProgramCall(input = DynamicValues.recordFromEntries(Vector("answer" := answer))))
yield step2.raw
The result is itself a program: you call it with inputs, it returns a
prediction, and it can be evaluated, optimized, saved, or nested inside a still
larger module, exactly like a single Predict.
What to notice¶
- Each step is a named
DynamicPredict. Naming matters when you later want to optimize or stream a specific step. forwardDynamicreturnsEither[DspyError, RawPrediction], so a failure in any step short-circuits theforcomprehension.DynamicModulethen lifts that value into the uniformPrediction[DynamicValue.Record]boundary.- Nothing about composition is special-cased. A composite module is the same kind of value as the modules it contains.
When to use it¶
| You want | Approach |
|---|---|
| A one-off pipeline | Call programs in sequence in plain code. |
| A reusable multi-step program | Extend DynamicModule, chain predictors in forward. |
| The model to choose the steps | Use ReAct instead of fixing them. |
Next: Language models.