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.