Email extraction¶
This example builds an email-analysis pipeline that classifies an email, pulls out structured entities, summarizes it, and decides what follow-up actions are needed. It demonstrates signatures over enums and case classes, and composing four ChainOfThought steps into one program that returns a single result.
Enums and models¶
enum EmailType derives Schema:
case order_confirmation, support_request, meeting_invitation, newsletter,
promotional, invoice, shipping_notification, other
An enum that derives Schema crosses the signature boundary as a wire string. EmailType and UrgencyLevel are produced as outputs by one step and consumed as inputs by later steps, so the value remains an enum throughout. Case classes such as ExtractedEntity derive Schema the same way and travel as JSON.
Signatures¶
trait ClassifyEmail extends Spec:
def email_subject: InputField[String]
def email_body: InputField[String]
def sender: InputField[String]
def email_type: OutputField[EmailType]
def urgency: OutputField[UrgencyLevel]
def reasoning: OutputField[String]
Each signature is a trait extending Spec with InputField and OutputField members. Field types carry the structure: outputs can be enums (EmailType), lists of case classes (List[ExtractedEntity]), or optional values (Option[Double]). The pipeline uses four signatures: ClassifyEmail, ExtractEntities, SummarizeEmail, and GenerateActionItems.
Composing the steps¶
final class EmailProcessor:
private val classifier = ChainOfThought(Signature.of[ClassifyEmail])
private val entityExtractor = ChainOfThought(Signature.of[ExtractEntities])
private val actionGenerator = ChainOfThought(Signature.of[GenerateActionItems])
private val summarizer = ChainOfThought(Signature.of[SummarizeEmail])
def forward(
emailSubject: String,
emailBody : String,
sender : String = ""
)(using RuntimeContext): Either[DspyError, EmailAnalysis] =
for
// Step 1: Classify the email
classification <- classifier((
email_subject = emailSubject,
email_body = emailBody,
sender = sender
))
// Step 2: Extract entities
fullContent = s"Subject: $emailSubject\n\nFrom: $sender\n\n$emailBody"
entities <- entityExtractor((
email_content = fullContent,
email_type = classification.output.email_type
))
// Step 3: Generate summary
summary <- summarizer((
email_subject = emailSubject,
email_body = emailBody,
key_entities = entities.output.key_entities
))
// Step 4: Determine actions
actions <- actionGenerator((
email_type = classification.output.email_type,
urgency = classification.output.urgency,
email_summary = summary.output.summary,
extracted_entities = entities.output.key_entities
))
// Step 5: Structure the results
yield EmailAnalysis(
email_type = classification.output.email_type,
urgency = classification.output.urgency,
summary = summary.output.summary,
key_entities = entities.output.key_entities,
financial_amount = entities.output.financial_amount,
important_dates = entities.output.important_dates,
action_required = actions.output.action_required,
action_items = actions.output.action_items,
deadline = actions.output.deadline,
priority_score = actions.output.priority_score,
reasoning = classification.output.reasoning,
contact_info = entities.output.contact_info
)
EmailProcessor holds one ChainOfThought per signature, built with Signature.of[...]. Each call returns Either[DspyError, ...], so forward threads the steps through a for-comprehension: a Left from any step short-circuits, and the success path reads fields from each step's output to assemble the final EmailAnalysis. The classification result feeds entity extraction, both feed the summary, and the three together feed action generation.
Running it¶
OPENAI_API_KEY=sk-... sbt "examples/runMain dspy4s.examples.tutorials.email_extraction.emailExtractionMain"
Notes¶
Integration with an external experiment-tracking server is out of scope.
Per-field description hints are not carried on the Spec surface, so any
field-level descriptions are dropped.
Full source: EmailExtraction.scala