Files
wehub-resource-sync 0d3cb498a3
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Has been cancelled
Test and Publish Multi-arch Docker Image / test (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Has been cancelled
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Has been cancelled
Validate Renovate Config / Validate Renovate Configuration (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:24:08 +08:00

665 lines
16 KiB
Markdown

---
sidebar_label: Custom Ruby
description: 'Create custom Ruby scripts for advanced model integrations, evaluations, and complex testing logic with full flexibility'
---
# Ruby Provider
The Ruby provider enables you to create custom evaluation logic using Ruby scripts. This allows you to integrate Promptfoo with any Ruby-based model, API, or custom logic.
**Common use cases:**
- Integrating proprietary or local models
- Adding custom preprocessing/postprocessing logic
- Implementing complex evaluation workflows
- Using Ruby-specific ML libraries
- Creating mock providers for testing
## Prerequisites
Before using the Ruby provider, ensure you have:
- Ruby 2.7 or higher installed
- Basic familiarity with Promptfoo configuration
- Understanding of Ruby hashes and JSON
## Quick Start
Let's create a simple Ruby provider that echoes back the input with a prefix.
### Step 1: Create your Ruby script
```ruby
# echo_provider.rb
def call_api(prompt, options, context)
# Simple provider that echoes the prompt with a prefix
config = options['config'] || {}
prefix = config['prefix'] || 'Tell me about: '
{
'output' => "#{prefix}#{prompt}"
}
end
```
### Step 2: Configure Promptfoo
```yaml
# promptfooconfig.yaml
providers:
- id: 'file://echo_provider.rb'
prompts:
- 'Tell me a joke'
- 'What is 2+2?'
```
### Step 3: Run the evaluation
```bash
npx promptfoo@latest eval
```
That's it! You've created your first custom Ruby provider.
## How It Works
When Promptfoo evaluates a test case with a Ruby provider:
1. **Promptfoo** prepares the prompt based on your configuration
2. **Promptfoo** invokes `call_api` in your Ruby script with three parameters:
- `prompt`: The final prompt string
- `options`: Provider configuration from your YAML
- `context`: Variables and metadata for the current test
3. **Your Code** processes the prompt and returns a response
4. **Promptfoo** validates the response and continues evaluation
```text
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Promptfoo │────▶│ Your Ruby │────▶│ Your Logic │
│ Evaluation │ │ Provider │ │ (API/Model) │
└─────────────┘ └──────────────┘ └─────────────┘
▲ │
│ ▼
│ ┌──────────────┐
└────────────│ Response │
└──────────────┘
```
## Basic Usage
### Function Interface
Your Ruby script must implement one or more of these functions:
```ruby
def call_api(prompt, options, context)
# Main function for text generation tasks
end
def call_embedding_api(prompt, options)
# For embedding generation tasks
end
def call_classification_api(prompt, options)
# For classification tasks
end
```
`context` is passed to `call_api` only. Embedding and classification handlers receive `prompt` and
`options`.
### Understanding Parameters
#### The `prompt` Parameter
The prompt can be either:
- A simple string: `"What is the capital of France?"`
- A JSON-encoded conversation: `'[{"role": "user", "content": "Hello"}]'`
```ruby
require 'json'
def call_api(prompt, options, context)
# Check if prompt is a conversation
begin
messages = JSON.parse(prompt)
# Handle as chat messages
messages.each do |msg|
puts "#{msg['role']}: #{msg['content']}"
end
rescue JSON::ParserError
# Handle as simple string
puts "Prompt: #{prompt}"
end
end
```
#### The `options` Parameter
Contains your provider configuration and metadata:
```ruby
{
'id' => 'file://my_provider.rb',
'config' => {
# Your custom configuration from promptfooconfig.yaml
'model_name' => 'gpt-3.5-turbo',
'temperature' => 0.7,
'max_tokens' => 100,
# Automatically added by promptfoo:
'basePath' => '/absolute/path/to/config' # Directory containing your config (promptfooconfig.yaml)
}
}
```
#### The `context` Parameter
For `call_api`, this provides information about the current test case:
```ruby
{
'vars' => {
'user_input' => 'Hello world',
'system_prompt' => 'You are a helpful assistant'
},
'prompt' => {
'raw' => '...',
'label' => '...',
},
'test' => {
'vars' => { ... },
'metadata' => {
'pluginId' => '...', # Redteam plugin (e.g. "promptfoo:redteam:harmful:hate")
'strategyId' => '...', # Redteam strategy (e.g. "jailbreak", "jailbreak-templates")
},
},
}
```
For redteam evals, use `context['test']['metadata']['pluginId']` and `context['test']['metadata']['strategyId']` to identify which plugin and strategy generated the test case.
:::note
Non-serializable fields (`logger`, `getCache`, `filters`, `originalProvider`) are removed before passing context to Ruby. Additional fields like `evaluationId`, `testCaseId`, `testIdx`, `promptIdx`, and `repeatIndex` are also available.
:::
### Return Format
Your function must return a hash with these fields:
```ruby
def call_api(prompt, options, context)
# Required field
result = {
'output' => 'Your response here'
}
# Optional fields
result['tokenUsage'] = {
'total' => 150,
'prompt' => 50,
'completion' => 100
}
result['cost'] = 0.0025 # in dollars
result['cached'] = false
result['logProbs'] = [-0.5, -0.3, -0.1]
# Error handling
if something_went_wrong
result['error'] = 'Description of what went wrong'
end
result
end
```
### Types
The types passed into the Ruby script function and the `ProviderResponse` return type are defined as follows:
```ruby
# ProviderOptions
{
'id' => String (optional),
'config' => Hash (optional)
}
# CallApiContextParams
{
'vars' => Hash[String, String],
'prompt' => Hash (optional), # Prompt template (raw, label, config)
'test' => Hash (optional), # Full test case including metadata
}
# TokenUsage
{
'total' => Integer,
'prompt' => Integer,
'completion' => Integer
}
# ProviderResponse
{
'output' => String or Hash (optional),
'error' => String (optional),
'tokenUsage' => TokenUsage (optional),
'cost' => Float (optional),
'cached' => Boolean (optional),
'logProbs' => Array[Float] (optional),
'metadata' => Hash (optional)
}
# ProviderEmbeddingResponse
{
'embedding' => Array[Float],
'tokenUsage' => TokenUsage (optional),
'cached' => Boolean (optional)
}
# ProviderClassificationResponse
{
'classification' => Hash,
'tokenUsage' => TokenUsage (optional),
'cached' => Boolean (optional)
}
```
:::tip
Always include the `output` field in your response, even if it's an empty string when an error occurs.
:::
## Complete Examples
### Example 1: OpenAI-Compatible Provider
```ruby
# openai_provider.rb
require 'json'
require 'net/http'
require 'uri'
def call_api(prompt, options, context)
# Provider that calls OpenAI API
config = options['config'] || {}
# Parse messages if needed
begin
messages = JSON.parse(prompt)
rescue JSON::ParserError
messages = [{ 'role' => 'user', 'content' => prompt }]
end
# Prepare API request
uri = URI.parse(config['base_url'] || 'https://api.openai.com/v1/chat/completions')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri.path)
request['Content-Type'] = 'application/json'
request['Authorization'] = "Bearer #{ENV['OPENAI_API_KEY']}"
request.body = JSON.generate({
model: config['model'] || 'gpt-3.5-turbo',
messages: messages,
temperature: config['temperature'] || 0.7,
max_tokens: config['max_tokens'] || 150
})
# Make API call
begin
response = http.request(request)
data = JSON.parse(response.body)
{
'output' => data['choices'][0]['message']['content'],
'tokenUsage' => {
'total' => data['usage']['total_tokens'],
'prompt' => data['usage']['prompt_tokens'],
'completion' => data['usage']['completion_tokens']
}
}
rescue StandardError => e
{
'output' => '',
'error' => e.message
}
end
end
```
### Example 2: Mock Provider for Testing
```ruby
# mock_provider.rb
def call_api(prompt, options, context)
# Mock provider for testing evaluation pipelines
config = options['config'] || {}
# Simulate processing time
delay = config['delay'] || 0.1
sleep(delay)
# Simulate different response types
if prompt.downcase.include?('error')
return {
'output' => '',
'error' => 'Simulated error for testing'
}
end
# Generate mock response
responses = config['responses'] || [
'This is a mock response.',
'Mock provider is working correctly.',
'Test response generated successfully.'
]
response = responses.sample
mock_tokens = prompt.split.size + response.split.size
{
'output' => response,
'tokenUsage' => {
'total' => mock_tokens,
'prompt' => prompt.split.size,
'completion' => response.split.size
},
'cost' => mock_tokens * 0.00001
}
end
```
### Example 3: Local Processing with Preprocessing
```ruby
# text_processor.rb
def preprocess_prompt(prompt, context)
# Add context-specific preprocessing
template = context['vars']['template'] || '{prompt}'
template.gsub('{prompt}', prompt)
end
def call_api(prompt, options, context)
# Provider with custom preprocessing
config = options['config'] || {}
# Preprocess
processed_prompt = preprocess_prompt(prompt, context)
# Simulate processing
result = "Processed: #{processed_prompt.upcase}"
{
'output' => result,
'cached' => false
}
end
```
## Configuration
### Basic Configuration
```yaml
providers:
- id: 'file://my_provider.rb'
label: 'My Custom Provider' # Optional display name
config:
# Any configuration your provider needs
api_key: '{{ env.CUSTOM_API_KEY }}'
endpoint: https://api.example.com
model_params:
temperature: 0.7
max_tokens: 100
```
### Using External Configuration Files
You can load configuration from external files:
```yaml
providers:
- id: 'file://my_provider.rb'
config:
# Load entire config from JSON
settings: file://config/model_settings.json
# Load from YAML with specific function
prompts: file://config/prompts.yaml
# Nested file references
models:
primary: file://config/primary_model.json
fallback: file://config/fallback_model.yaml
```
Supported formats:
- **JSON** (`.json`) - Parsed as objects/arrays
- **YAML** (`.yaml`, `.yml`) - Parsed as objects/arrays
- **Text** (`.txt`, `.md`) - Loaded as strings
### Environment Configuration
#### Custom Ruby Executable
You can specify a custom Ruby executable in several ways:
**Option 1: Per-provider configuration**
```yaml
providers:
- id: 'file://my_provider.rb'
config:
rubyExecutable: /path/to/ruby
```
**Option 2: Global environment variable**
```bash
# Use specific Ruby version globally
export PROMPTFOO_RUBY=/usr/local/bin/ruby
npx promptfoo@latest eval
```
#### Ruby Detection Process
Promptfoo automatically detects your Ruby installation in this priority order:
1. **Provider config**: `rubyExecutable` in your config
2. **Environment variable**: `PROMPTFOO_RUBY` (if set)
3. **Windows detection**: Uses `where ruby` (Windows only)
4. **Smart detection**: Uses `ruby -e "puts RbConfig.ruby"` to find the actual Ruby path
5. **Fallback commands**:
- Windows: `ruby`
- macOS/Linux: `ruby`
## Advanced Features
### Custom Function Names
Override the default function name:
```yaml
providers:
- id: 'file://my_provider.rb:generate_response'
config:
model: 'custom-model'
```
```ruby
# my_provider.rb
def generate_response(prompt, options, context)
# Your custom function
{ 'output' => 'Custom response' }
end
```
### Handling Different Input Types
```ruby
require 'json'
def call_api(prompt, options, context)
# Handle various prompt formats
# Text prompt
if prompt.is_a?(String)
begin
# Try parsing as JSON
data = JSON.parse(prompt)
if data.is_a?(Array)
# Chat format
return handle_chat(data, options)
elsif data.is_a?(Hash)
# Structured prompt
return handle_structured(data, options)
end
rescue JSON::ParserError
# Plain text
return handle_text(prompt, options)
end
end
end
```
### Implementing Guardrails
```ruby
def call_api(prompt, options, context)
# Provider with safety guardrails
config = options['config'] || {}
# Check for prohibited content
prohibited_terms = config['prohibited_terms'] || []
prohibited_terms.each do |term|
if prompt.downcase.include?(term.downcase)
return {
'output' => 'I cannot process this request.',
'guardrails' => {
'flagged' => true,
'reason' => 'Prohibited content detected'
}
}
end
end
# Process normally
result = generate_response(prompt)
# Post-process checks
if check_output_safety(result)
{ 'output' => result }
else
{
'output' => '[Content filtered]',
'guardrails' => { 'flagged' => true }
}
end
end
```
## Troubleshooting
### Common Issues and Solutions
| Issue | Solution |
| ------------------------------ | -------------------------------------------------------------------- |
| "Ruby not found" errors | Set `PROMPTFOO_RUBY` env var or use `rubyExecutable` in config |
| "cannot load such file" errors | Ensure required gems are installed with `gem install` or use bundler |
| Script not executing | Check file path is relative to `promptfooconfig.yaml` |
| No output visible | Use `LOG_LEVEL=debug` to see print statements |
| JSON parsing errors | Ensure prompt format matches your parsing logic |
| Timeout errors | Optimize initialization code, load resources once |
### Debugging Tips
1. **Enable debug logging:**
```bash
LOG_LEVEL=debug npx promptfoo@latest eval
```
2. **Add logging to your provider:**
```ruby
def call_api(prompt, options, context)
$stderr.puts "Received prompt: #{prompt}"
$stderr.puts "Config: #{options['config'].inspect}"
# Your logic here
end
```
3. **Test your provider standalone:**
```ruby
# test_provider.rb
require_relative 'my_provider'
result = call_api(
'Test prompt',
{ 'config' => { 'model' => 'test' } },
{ 'vars' => {} }
)
puts result.inspect
```
## Migration Guide
### From HTTP Provider
If you're currently using an HTTP provider, you can wrap your API calls:
```ruby
# http_wrapper.rb
require 'net/http'
require 'json'
require 'uri'
def call_api(prompt, options, context)
config = options['config'] || {}
uri = URI.parse(config['url'])
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = (uri.scheme == 'https')
request = Net::HTTP::Post.new(uri.path)
request['Content-Type'] = 'application/json'
config['headers']&.each { |k, v| request[k] = v }
request.body = JSON.generate({ 'prompt' => prompt })
response = http.request(request)
JSON.parse(response.body)
end
```
### From Python Provider
The Ruby provider follows the same interface as Python providers:
```python
# Python
def call_api(prompt, options, context):
return {"output": f"Echo: {prompt}"}
```
```ruby
# Ruby equivalent
def call_api(prompt, options, context)
{ 'output' => "Echo: #{prompt}" }
end
```
## See Also
- [Custom assertions](/docs/configuration/expected-outputs/) - Define expected outputs and validation rules
- [Python Provider](/docs/providers/python) - Similar scripting provider for Python
- [Custom Script Provider](/docs/providers/custom-script) - JavaScript/TypeScript provider alternative