Skip to content

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:

OPENAI_API_KEY=sk-... sbt "examples/runMain dspy4s.examples.learn.programming.main"

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.