Skip to content

Compile-Time Type-Safe Prompts #24

Description

@luckenco

Summary

Implement compile-time type checking for LLM prompts in rsai, similar to how sqlx provides compile-time SQL query validation. This feature will transform prompts from untyped strings into type-safe templates with compile-time validation of parameters and structure.

Problem Statement

  • Prompts are constructed as raw String values with no type safety
  • Template variables are embedded without validation
  • Type mismatches between prompt expectations and provided data are only caught at runtime
  • No IDE support for prompt authoring (autocomplete, validation)
  • Refactoring prompts is error-prone

Proposed Solution

Introduce a prompt! macro that provides:

  1. Compile-time validation of prompt templates
  2. Type-safe parameter binding
  3. Automatic formatting and escaping
  4. Integration with rsai's existing structured output system

Example Usage

use rsai::prelude::*;

#[derive(PromptInput)]
struct AnalysisInput {
    text: String,
    max_length: usize,
    include_sentiment: bool,
}

// Compile-time validated prompt
let messages = prompt!(
    AnalysisInput,
    r#"
    Analyze the following text:
    {{text}}
    
    Requirements:
    - Maximum response length: {{max_length}} words
    {{#if include_sentiment}}
    - Include sentiment analysis
    {{/if}}
    "#,
    AnalysisInput {
        text: "This library is amazing!".to_string(),
        max_length: 100,
        include_sentiment: true,
    }
);

// Or with inline syntax for simple cases
let messages = prompt!(
    "Summarize this article: {{article}} in {{word_count}} words",
    article = content,
    word_count = 50
);

// Integration with builder pattern
let result = llm::with(Provider::OpenAI)
    .api_key(ApiKey::Default)?
    .model("gpt-4o-mini")
    .prompt!(
        "Analyze sentiment: {{text}}",
        text = "I love Rust!"
    )
    .complete::<SentimentAnalysis>()
    .await?;

Technical Design

1. Macro Implementation

The prompt! macro will:

  • Parse the template at compile time
  • Extract variable placeholders (e.g., {{variable_name}})
  • Validate that all required variables are provided
  • Generate type-safe binding code
  • Support conditional sections ({{#if condition}})
  • Support loops ({{#each items}})

2. PromptInput Derive Macro

#[derive(PromptInput)]
struct MyPromptData {
    #[prompt(description = "The main text to analyze")]
    text: String,
    
    #[prompt(optional)]
    context: Option<String>,
    
    #[prompt(format = "list")]
    items: Vec<String>,
}

3. Template Syntax

Support Handlebars-like syntax:

  • Variables: {{variable_name}}
  • Conditionals: {{#if condition}}...{{/if}}
  • Loops: {{#each collection}}{{this}}{{/each}}
  • Escaping: \{{literal}}
  • Comments: {{! This is a comment }}

4. Compile-Time Validation

Similar to sqlx's approach:

  • Offline mode: Generate a .rsai/prompts.json file with prompt metadata
  • Online mode: Validate prompt structure at compile time
  • Type checking: Ensure all template variables match struct fields
  • Unused variable detection: Warn about defined but unused fields

Instead of #[derive(PromptInput)] you can consider to combine this with #16

The idea is that prompts should be grouped with their generation params, models, providers etc.

There is definitely a trade-off to explore.

Relatively narrow possibilities to use a prompt

vs.

Unconstrained use of the "prompt text".

Being opinionated here can be valuable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesthelp wantedExtra attention is neededon-holdCurrently blocked or on waiting on another feature

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions