Skip to content

Signatures

A signature declares what a program takes in and what it produces. Instead of hand-writing a prompt, you describe the shape of the task and dspy4s builds the prompt from it.

A signature is an ordinary Scala type, so the compiler checks every field.

Every snippet here is verified

The Scala blocks on this page are extracted from Signatures.scala, which builds under -Werror -Wunused:all. They stay in sync with the library.

Inline signatures

The shortest form is Signature.fromType, applied to a function type whose parameters and result are named tuples:

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)

classify.apply((sentence = ...)) is checked at compile time, and _.output.sentiment is a Boolean.

The same signature can also be written as a string with Signature.fromString("sentence -> sentiment: bool"). Both forms are parsed at compile time into the same Signature, so pick whichever reads better.

Adding instructions

fromType takes an optional instructions string to steer the model:

object ToxicityExample:
  val signature = Signature.fromType[(comment: String) => (toxic: Boolean)](
    instructions = "Mark as 'toxic' if the comment includes insults, harassment, or sarcastic derogatory remarks."
  )

  val toxicity = Predict(signature)

  def call(comment: String)(using RuntimeContext): Either[DspyError, Boolean] =
    toxicity((comment = comment)).map(_.output.toxic)

Class-based signatures

When you want named fields or a constrained output type, declare a Spec trait with InputField / OutputField members. A fixed set of output values is a Scala enum:

enum Emotion derives Schema:
  case sadness, joy, love, anger, fear, surprise

trait EmotionSpec extends Spec:
  def sentence: InputField[String]
  def sentiment: OutputField[Emotion]

object EmotionExample:
  val classify = Predict(Signature.of[EmotionSpec](instructions = "Classify emotion."))

  def call(sentence: String)(using RuntimeContext): Either[DspyError, Emotion] =
    classify((sentence = sentence)).map(_.output.sentiment)

derives Schema gives the enum a flat-string wire form (the case name) at the output boundary, so the model's answer decodes straight into an Emotion.

Case-class signatures

When you already model inputs and outputs as case classes, Signature.derived builds a signature from an input type and an output type directly. The output is a Scala value, so _.output.sentiment has type Emotion with no cast:

case class EmotionInput(sentence: String) derives Schema

enum Emotion derives Schema:
  case sadness, joy, love, anger, fear, surprise

case class EmotionOutput(sentiment: Emotion) derives Schema
val signature: Signature[EmotionInput, EmotionOutput] = Signature.derived[EmotionInput, EmotionOutput](
  name = "Emotion",
  instructions = "Classify emotion in the given sentence."
)

Building a signature programmatically

When a case class per signature is overkill (exploration, shapes assembled from config, tests), Signature.builder constructs one fluently. Each input/ output call summons the field's Schema:

val toxicity: SignatureLayout = Signature
  .builder("Toxicity")
  .input[String]("comment")
  .output[Boolean]("toxic")
  .output[Double]("confidence")
  .instructions(
    "Mark `toxic` as true when the comment includes insults, harassment, " +
      "or derogatory remarks. Report the confidence as a number in [0.0, 1.0]."
  )
  .build

Custom types

Inputs and outputs are not limited to primitives. Any case class (or nested container) that derives Schema can be a field. The schema drives both the wire shape and nested encode/decode:

case class QueryResult(text: String, score: Double) derives Schema

object MyContainer:
  case class Query(text: String) derives Schema
  case class Score(score: Double) derives Schema

object CustomTypesExample:
  val signature = Signature.fromType[(query: String) => (result: QueryResult)]

  val nestedSignature = Signature.fromType[
    (query: MyContainer.Query) => (score: MyContainer.Score)
  ]

Summary

Form Declared with Use when
Inline Signature.fromType[(in: I) => (out: O)] Quick, primitive in/out.
With instructions Signature.fromType[...](instructions = ...) You need to steer the model.
Class-based Signature.of[T <: Spec] Named fields, enums, constrained outputs.
Case classes Signature.derived[In, Out] Inputs and outputs are already case classes.
Builder Signature.builder(...) Shapes built at runtime, or quick exploration.
Custom types any case class derives Schema Structured inputs/outputs.

A signature only declares the task. To run it, you wrap it in a module.

Next: Modules.