Quickstart¶
This walks through declaring a signature, building a program, and running it
end to end. Every Scala block below is pulled directly from
Signatures.scala
in the examples module, so it compiles under the project's strict flags.
1. Declare a signature and a program¶
A signature declares inputs and outputs. A program (here
Predict) runs it against a language model. This one classifies sentiment:
object SentimentExample:
val classify = Predict(Signature.fromType[(sentence: String) => (sentiment: Boolean)])
def call(sentence: String)(using RuntimeContext): Either[DspyError, Boolean] =
classify((sentence = sentence)).map(_.output.sentiment)
Because the input and output are named tuples, sentence and sentiment are
real fields. A typo is a compile error, and _.output.sentiment is a
Boolean, not a string lookup.
2. Wire up a runtime and run it¶
A program needs a RuntimeContext carrying a live LM and an adapter. This is a
complete, runnable program (it reads OPENAI_API_KEY from the environment):
@main def main(): Unit =
val model = sys.env.getOrElse("DSPY_MODEL", "gpt-5.5")
val lm = OpenAiLanguageModel.fromEnv(model) match
case Right(m) => m
case Left(err) => sys.error(s"Could not initialize LM (is OPENAI_API_KEY set?): $err")
val settings = RuntimeContext(lm = Some(lm), adapter = Some(ChatAdapter()))
RuntimeEnvironment.withSettings(settings) {
given RuntimeContext = RuntimeEnvironment.current
println("Toxicity: " + ToxicityExample.call("you are beautiful."))
println("Sentiment: " + SentimentExample.call("it's a charming and often affecting journey."))
println("Emotion: " + EmotionExample.call("i started feeling a little vulnerable"))
println("Summary: " + SummarizeExample.call("The cat sat on the mat. The sun was warm."))
}
Run it with:
3. Add reasoning with ChainOfThought¶
Swapping Predict for ChainOfThought prepends reasoning: String to
the output, with no signature changes required:
object SummarizeExample:
val program = ChainOfThought(Signature.fromType[(document: String) => (summary: String)])
/** Snippet 3: just the summary. */
def call(document: String)(using RuntimeContext): Either[DspyError, String] =
program((document = document)).map(_.output.summary)
/** Snippet 4: both reasoning and summary. */
def callWithReasoning(document: String)(using RuntimeContext): Either[DspyError, (String, String)] =
program((document = document)).map { tp =>
(tp.output.reasoning, tp.output.summary)
}
Where to next¶
- Signatures: the full set of ways to declare inputs and outputs (inline, traits, enums, custom types).
- How it fits together: the mental model behind signatures, modules, programs, and optimizers.