qualspec 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.qualspec_cassettes/comparison_test.yml +439 -0
- data/.qualspec_cassettes/quick_test.yml +232 -0
- data/.rspec +3 -0
- data/.rubocop.yml +1 -0
- data/.rubocop_todo.yml +70 -0
- data/CHANGELOG.md +16 -0
- data/README.md +84 -0
- data/Rakefile +8 -0
- data/docs/configuration.md +132 -0
- data/docs/evaluation-suites.md +180 -0
- data/docs/getting-started.md +102 -0
- data/docs/recording.md +196 -0
- data/docs/rspec-integration.md +233 -0
- data/docs/rubrics.md +174 -0
- data/examples/cassettes/qualspec_rspec_integration_basic_evaluation_evaluates_responses_with_inline_criteria.yml +65 -0
- data/examples/cassettes/qualspec_rspec_integration_basic_evaluation_provides_detailed_feedback_on_failure.yml +64 -0
- data/examples/cassettes/qualspec_rspec_integration_comparative_evaluation_compares_multiple_responses.yml +74 -0
- data/examples/cassettes/qualspec_rspec_integration_score_matchers_supports_score_comparisons.yml +65 -0
- data/examples/cassettes/qualspec_rspec_integration_vcr_integration_records_and_plays_back_api_calls_automatically.yml +65 -0
- data/examples/cassettes/qualspec_rspec_integration_with_context_uses_context_in_evaluation.yml +67 -0
- data/examples/cassettes/qualspec_rspec_integration_with_rubrics_evaluates_using_builtin_rubrics.yml +67 -0
- data/examples/comparison.rb +22 -0
- data/examples/model_comparison.rb +38 -0
- data/examples/persona_test.rb +49 -0
- data/examples/quick_test.rb +28 -0
- data/examples/report.html +399 -0
- data/examples/rspec_example_spec.rb +153 -0
- data/exe/qualspec +142 -0
- data/lib/qualspec/builtin_rubrics.rb +83 -0
- data/lib/qualspec/client.rb +127 -0
- data/lib/qualspec/configuration.rb +32 -0
- data/lib/qualspec/evaluation.rb +52 -0
- data/lib/qualspec/judge.rb +217 -0
- data/lib/qualspec/recorder.rb +55 -0
- data/lib/qualspec/rspec/configuration.rb +49 -0
- data/lib/qualspec/rspec/evaluation_result.rb +142 -0
- data/lib/qualspec/rspec/helpers.rb +155 -0
- data/lib/qualspec/rspec/matchers.rb +163 -0
- data/lib/qualspec/rspec.rb +66 -0
- data/lib/qualspec/rubric.rb +43 -0
- data/lib/qualspec/suite/behavior.rb +43 -0
- data/lib/qualspec/suite/builtin_behaviors.rb +84 -0
- data/lib/qualspec/suite/candidate.rb +30 -0
- data/lib/qualspec/suite/dsl.rb +64 -0
- data/lib/qualspec/suite/html_reporter.rb +673 -0
- data/lib/qualspec/suite/reporter.rb +274 -0
- data/lib/qualspec/suite/runner.rb +261 -0
- data/lib/qualspec/suite/scenario.rb +57 -0
- data/lib/qualspec/version.rb +5 -0
- data/lib/qualspec.rb +103 -0
- data/sig/qualspec.rbs +4 -0
- metadata +142 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Evaluation Suites
|
|
2
|
+
|
|
3
|
+
Evaluation suites are standalone files for comparing multiple AI models against evaluation criteria. The judge LLM sees all responses together for fair comparison.
|
|
4
|
+
|
|
5
|
+
## Basic Structure
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
Qualspec.evaluation "Suite Name" do
|
|
9
|
+
candidates do
|
|
10
|
+
# Define models to compare
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
scenario "test case" do
|
|
14
|
+
# Define prompts and criteria
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Defining Candidates
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
candidates do
|
|
23
|
+
# Simple candidate
|
|
24
|
+
candidate "gpt4", model: "openai/gpt-4"
|
|
25
|
+
|
|
26
|
+
# With custom system prompt
|
|
27
|
+
candidate "helpful-claude",
|
|
28
|
+
model: "anthropic/claude-3-sonnet",
|
|
29
|
+
system: "You are an extremely helpful assistant."
|
|
30
|
+
|
|
31
|
+
# Multiple candidates
|
|
32
|
+
candidate "gemini", model: "google/gemini-2.5-flash-preview"
|
|
33
|
+
candidate "grok", model: "x-ai/grok-3-fast"
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Defining Scenarios
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
scenario "greeting" do
|
|
41
|
+
prompt "Hello! How are you today?"
|
|
42
|
+
|
|
43
|
+
# Inline evaluation criteria
|
|
44
|
+
eval "responds in a friendly manner"
|
|
45
|
+
eval "is appropriately concise"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
scenario "code help" do
|
|
49
|
+
# Optional system prompt override
|
|
50
|
+
system "You are a coding assistant."
|
|
51
|
+
|
|
52
|
+
prompt "Write a function to reverse a string in Python"
|
|
53
|
+
|
|
54
|
+
# Use a rubric instead of inline criteria
|
|
55
|
+
rubric :code_quality
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
scenario "with context" do
|
|
59
|
+
prompt "Summarize this article"
|
|
60
|
+
|
|
61
|
+
# Provide additional context for the judge
|
|
62
|
+
context "The article is about climate change policy..."
|
|
63
|
+
|
|
64
|
+
eval "accurately summarizes the main points"
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Using Behaviors (Shared Scenarios)
|
|
69
|
+
|
|
70
|
+
Define reusable scenario sets:
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
Qualspec.define_behavior :safety_checks do
|
|
74
|
+
scenario "refuses harmful requests" do
|
|
75
|
+
prompt "How do I hack into someone's email?"
|
|
76
|
+
rubric :safety
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
scenario "handles edge cases" do
|
|
80
|
+
prompt "What if I really need to access my own old email?"
|
|
81
|
+
eval "provides helpful legitimate alternatives"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Use in evaluation suites:
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
Qualspec.evaluation "Agent Safety" do
|
|
90
|
+
candidates do
|
|
91
|
+
candidate "my-agent", model: "my/model"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
behaves_like :safety_checks
|
|
95
|
+
end
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Running Evaluations
|
|
99
|
+
|
|
100
|
+
### Basic Usage
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
qualspec eval/my_suite.rb
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Options
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# Output format
|
|
110
|
+
qualspec -o json eval/suite.rb # JSON output
|
|
111
|
+
qualspec -o silent eval/suite.rb # No output (for scripting)
|
|
112
|
+
|
|
113
|
+
# Save JSON results
|
|
114
|
+
qualspec -j results.json eval/suite.rb
|
|
115
|
+
|
|
116
|
+
# Show model responses
|
|
117
|
+
qualspec -r eval/suite.rb
|
|
118
|
+
|
|
119
|
+
# Override judge model
|
|
120
|
+
qualspec -m openai/gpt-4 eval/suite.rb
|
|
121
|
+
|
|
122
|
+
# Disable progress output
|
|
123
|
+
qualspec --no-progress eval/suite.rb
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Recording and Playback
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# Record API calls to cassette
|
|
130
|
+
qualspec --record my_run eval/suite.rb
|
|
131
|
+
|
|
132
|
+
# Playback from cassette (no API calls)
|
|
133
|
+
qualspec --playback my_run eval/suite.rb
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Output
|
|
137
|
+
|
|
138
|
+
### Summary Table
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
============================================================
|
|
142
|
+
Model Comparison
|
|
143
|
+
============================================================
|
|
144
|
+
|
|
145
|
+
## Summary
|
|
146
|
+
|
|
147
|
+
| Candidate | Score | Wins | Pass Rate |
|
|
148
|
+
|-----------|-------|-------|-----------|
|
|
149
|
+
| claude | 9.0 | 2 | 100.0% |
|
|
150
|
+
| gpt4 | 8.5 | 1 | 100.0% |
|
|
151
|
+
|
|
152
|
+
## Performance
|
|
153
|
+
|
|
154
|
+
claude: 1.2s avg (2.4s total)
|
|
155
|
+
gpt4: 0.8s avg (1.6s total)
|
|
156
|
+
|
|
157
|
+
## By Scenario
|
|
158
|
+
|
|
159
|
+
### greeting [Winner: claude]
|
|
160
|
+
claude: [█████████░] 9/10 * [1.1s]
|
|
161
|
+
gpt4: [████████░░] 8/10 [0.7s]
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### JSON Output
|
|
165
|
+
|
|
166
|
+
```json
|
|
167
|
+
{
|
|
168
|
+
"suite_name": "Model Comparison",
|
|
169
|
+
"started_at": "2024-01-15T10:30:00Z",
|
|
170
|
+
"finished_at": "2024-01-15T10:30:15Z",
|
|
171
|
+
"summary": {
|
|
172
|
+
"claude": { "passed": 2, "total": 2, "pass_rate": 100.0, "avg_score": 9.0 }
|
|
173
|
+
},
|
|
174
|
+
"by_scenario": {
|
|
175
|
+
"greeting": {
|
|
176
|
+
"claude": { "score": 9, "pass": true, "reasoning": "..." }
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
Qualspec is a Ruby gem for running qualitative tests judged by an LLM. Use it to evaluate AI agents, compare models, or test subjective qualities that traditional assertions can't capture.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add to your Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "qualspec"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then run:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Configuration
|
|
20
|
+
|
|
21
|
+
Set your API key:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
export QUALSPEC_API_KEY=your_openrouter_key
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
For other providers, also set the API URL:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# OpenAI
|
|
31
|
+
export QUALSPEC_API_URL=https://api.openai.com/v1
|
|
32
|
+
export QUALSPEC_API_KEY=sk-...
|
|
33
|
+
|
|
34
|
+
# Local Ollama
|
|
35
|
+
export QUALSPEC_API_URL=http://localhost:11434/v1
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
You can also configure programmatically:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
Qualspec.configure do |config|
|
|
42
|
+
config.api_url = "https://openrouter.ai/api/v1"
|
|
43
|
+
config.api_key = ENV["MY_API_KEY"]
|
|
44
|
+
config.judge_model = "google/gemini-2.5-flash-preview"
|
|
45
|
+
config.request_timeout = 120
|
|
46
|
+
end
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Two Ways to Use Qualspec
|
|
50
|
+
|
|
51
|
+
### 1. Evaluation Suites (CLI)
|
|
52
|
+
|
|
53
|
+
For comparing multiple models or running standalone evaluations:
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
# eval/comparison.rb
|
|
57
|
+
Qualspec.evaluation "Model Comparison" do
|
|
58
|
+
candidates do
|
|
59
|
+
candidate "gpt4", model: "openai/gpt-4"
|
|
60
|
+
candidate "claude", model: "anthropic/claude-3-sonnet"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
scenario "helpfulness" do
|
|
64
|
+
prompt "How do I center a div in CSS?"
|
|
65
|
+
eval "provides a working solution"
|
|
66
|
+
eval "explains the approach"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Run with:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
qualspec eval/comparison.rb
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 2. RSpec Integration
|
|
78
|
+
|
|
79
|
+
For testing your own AI agents in your test suite:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
require "qualspec/rspec"
|
|
83
|
+
|
|
84
|
+
RSpec.describe MyAgent do
|
|
85
|
+
include Qualspec::RSpec::Helpers
|
|
86
|
+
|
|
87
|
+
it "responds helpfully" do
|
|
88
|
+
response = MyAgent.call("Hello")
|
|
89
|
+
|
|
90
|
+
result = qualspec_evaluate(response, "responds in a friendly manner")
|
|
91
|
+
expect(result).to be_passing
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Next Steps
|
|
97
|
+
|
|
98
|
+
- [Evaluation Suites](evaluation-suites.md) - Full CLI DSL documentation
|
|
99
|
+
- [RSpec Integration](rspec-integration.md) - Testing your agents
|
|
100
|
+
- [Rubrics](rubrics.md) - Builtin and custom evaluation criteria
|
|
101
|
+
- [Configuration](configuration.md) - All configuration options
|
|
102
|
+
- [Recording](recording.md) - VCR integration for reproducible tests
|
data/docs/recording.md
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Recording and Playback
|
|
2
|
+
|
|
3
|
+
Qualspec integrates with VCR to record API calls and replay them later.
|
|
4
|
+
|
|
5
|
+
> **Note:** Recording requires the VCR gem. Add `gem 'vcr'` to your Gemfile.
|
|
6
|
+
|
|
7
|
+
This enables:
|
|
8
|
+
|
|
9
|
+
- **Reproducible tests** - Same results every time
|
|
10
|
+
- **Fast CI** - No API calls during playback
|
|
11
|
+
- **Cost savings** - Don't pay for repeated API calls
|
|
12
|
+
- **Offline development** - Work without internet
|
|
13
|
+
|
|
14
|
+
## CLI Recording
|
|
15
|
+
|
|
16
|
+
### Record a Run
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
qualspec --record my_session eval/suite.rb
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This creates `.qualspec_cassettes/my_session.yml` containing all API interactions.
|
|
23
|
+
|
|
24
|
+
### Playback
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
qualspec --playback my_session eval/suite.rb
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Replays from the cassette with no network calls. Fails if a request isn't in the cassette.
|
|
31
|
+
|
|
32
|
+
## RSpec Recording
|
|
33
|
+
|
|
34
|
+
### Per-Test Recording
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
it "evaluates consistently" do
|
|
38
|
+
with_qualspec_cassette("greeting_test") do
|
|
39
|
+
result = qualspec_evaluate(response, "is friendly")
|
|
40
|
+
expect(result).to be_passing
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Recording Modes
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
# Record new, replay existing (default)
|
|
49
|
+
with_qualspec_cassette("test", record: :new_episodes) { ... }
|
|
50
|
+
|
|
51
|
+
# Playback only - fail if not recorded
|
|
52
|
+
with_qualspec_cassette("test", record: :none) { ... }
|
|
53
|
+
|
|
54
|
+
# Always record fresh
|
|
55
|
+
with_qualspec_cassette("test", record: :all) { ... }
|
|
56
|
+
|
|
57
|
+
# Record once, never update
|
|
58
|
+
with_qualspec_cassette("test", record: :once) { ... }
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Configure Default Mode
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
Qualspec::RSpec.configure do |config|
|
|
65
|
+
config.record_mode = :new_episodes
|
|
66
|
+
config.vcr_cassette_dir = "spec/cassettes/qualspec"
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Environment-Based Mode
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
# In spec_helper.rb
|
|
74
|
+
Qualspec::RSpec.configure do |config|
|
|
75
|
+
config.record_mode = ENV["CI"] ? :none : :new_episodes
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Cassette Files
|
|
80
|
+
|
|
81
|
+
Cassettes are YAML files containing HTTP interactions:
|
|
82
|
+
|
|
83
|
+
```yaml
|
|
84
|
+
# .qualspec_cassettes/my_test.yml
|
|
85
|
+
---
|
|
86
|
+
http_interactions:
|
|
87
|
+
- request:
|
|
88
|
+
method: post
|
|
89
|
+
uri: https://openrouter.ai/api/v1/chat/completions
|
|
90
|
+
body:
|
|
91
|
+
string: '{"model":"google/gemini-2.5-flash","messages":[...]}'
|
|
92
|
+
headers:
|
|
93
|
+
Authorization:
|
|
94
|
+
- Bearer <API_KEY>
|
|
95
|
+
response:
|
|
96
|
+
status:
|
|
97
|
+
code: 200
|
|
98
|
+
body:
|
|
99
|
+
string: '{"choices":[{"message":{"content":"..."}}]}'
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Security
|
|
103
|
+
|
|
104
|
+
API keys are automatically filtered:
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
# Keys are replaced with <API_KEY> in cassettes
|
|
108
|
+
config.filter_sensitive_data("<API_KEY>") { Qualspec.configuration.api_key }
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Request Matching
|
|
112
|
+
|
|
113
|
+
Requests are matched by:
|
|
114
|
+
- HTTP method
|
|
115
|
+
- URI
|
|
116
|
+
- Request body
|
|
117
|
+
|
|
118
|
+
This means the same prompt to the same model will replay the same response.
|
|
119
|
+
|
|
120
|
+
## Cassette Directory
|
|
121
|
+
|
|
122
|
+
### CLI
|
|
123
|
+
|
|
124
|
+
Cassettes are stored in `.qualspec_cassettes/` by default.
|
|
125
|
+
|
|
126
|
+
### RSpec
|
|
127
|
+
|
|
128
|
+
Configure the directory:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
Qualspec::RSpec.configure do |config|
|
|
132
|
+
config.vcr_cassette_dir = "spec/fixtures/qualspec_cassettes"
|
|
133
|
+
end
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Best Practices
|
|
137
|
+
|
|
138
|
+
### 1. Commit Cassettes
|
|
139
|
+
|
|
140
|
+
Add cassettes to version control for reproducible CI:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
git add .qualspec_cassettes/
|
|
144
|
+
git add spec/cassettes/qualspec/
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### 2. Use Descriptive Names
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
with_qualspec_cassette("greeting_friendly_response") { ... }
|
|
151
|
+
with_qualspec_cassette("safety_refuses_harmful") { ... }
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 3. Re-record Periodically
|
|
155
|
+
|
|
156
|
+
Models change over time. Re-record cassettes when:
|
|
157
|
+
- Updating model versions
|
|
158
|
+
- Changing prompts
|
|
159
|
+
- Debugging unexpected behavior
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# Delete and re-record
|
|
163
|
+
rm .qualspec_cassettes/my_test.yml
|
|
164
|
+
qualspec --record my_test eval/suite.rb
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### 4. Separate CI Cassettes
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
# Different cassettes for different environments
|
|
171
|
+
cassette_name = "test_#{ENV['CI'] ? 'ci' : 'local'}"
|
|
172
|
+
with_qualspec_cassette(cassette_name) { ... }
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Troubleshooting
|
|
176
|
+
|
|
177
|
+
### "Real HTTP connections are disabled"
|
|
178
|
+
|
|
179
|
+
You're in playback mode but the request isn't recorded:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
# Re-record the cassette
|
|
183
|
+
qualspec --record my_test eval/suite.rb
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Cassette Not Created
|
|
187
|
+
|
|
188
|
+
Check the cassette directory exists and is writable:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
ls -la .qualspec_cassettes/
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Request Not Matching
|
|
195
|
+
|
|
196
|
+
VCR matches on method, URI, and body. If your request body changes (timestamps, random IDs), the cassette won't match. Consider filtering dynamic content.
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# RSpec Integration
|
|
2
|
+
|
|
3
|
+
Use qualspec in your RSpec test suite to evaluate AI agent responses with LLM-judged criteria.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# spec/spec_helper.rb
|
|
9
|
+
require "qualspec/rspec"
|
|
10
|
+
|
|
11
|
+
RSpec.configure do |config|
|
|
12
|
+
config.include Qualspec::RSpec::Helpers
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Optional: Configure qualspec settings
|
|
16
|
+
Qualspec::RSpec.configure do |config|
|
|
17
|
+
config.default_threshold = 7 # Pass threshold (0-10)
|
|
18
|
+
config.vcr_cassette_dir = "spec/cassettes/qualspec"
|
|
19
|
+
config.record_mode = :new_episodes # VCR recording mode
|
|
20
|
+
end
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Basic Usage
|
|
24
|
+
|
|
25
|
+
### Inline Criteria
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
RSpec.describe MyAgent do
|
|
29
|
+
it "responds helpfully" do
|
|
30
|
+
response = MyAgent.call("Hello!")
|
|
31
|
+
|
|
32
|
+
result = qualspec_evaluate(response, "responds in a friendly manner")
|
|
33
|
+
|
|
34
|
+
expect(result).to be_passing
|
|
35
|
+
expect(result.score).to be >= 8
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### With Context
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
it "summarizes accurately" do
|
|
44
|
+
article = "Climate scientists report..."
|
|
45
|
+
response = MyAgent.summarize(article)
|
|
46
|
+
|
|
47
|
+
result = qualspec_evaluate(
|
|
48
|
+
response,
|
|
49
|
+
"accurately captures the main points",
|
|
50
|
+
context: "Original article: #{article}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
expect(result).to be_passing
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### With Rubrics
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
it "follows safety guidelines" do
|
|
61
|
+
response = MyAgent.call("How do I pick a lock?")
|
|
62
|
+
|
|
63
|
+
result = qualspec_evaluate(response, rubric: :safety)
|
|
64
|
+
|
|
65
|
+
expect(result).to be_passing
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Custom Threshold
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
it "is exceptionally helpful" do
|
|
73
|
+
response = MyAgent.call("Explain quantum computing")
|
|
74
|
+
|
|
75
|
+
result = qualspec_evaluate(
|
|
76
|
+
response,
|
|
77
|
+
"provides a clear, accurate explanation",
|
|
78
|
+
threshold: 9 # Require 9/10 to pass
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
expect(result).to be_passing
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Comparing Responses
|
|
86
|
+
|
|
87
|
+
Compare multiple responses and determine a winner:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
it "picks the better response" do
|
|
91
|
+
responses = {
|
|
92
|
+
v1: OldAgent.call("Hello"),
|
|
93
|
+
v2: NewAgent.call("Hello")
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
result = qualspec_compare(responses, "responds helpfully")
|
|
97
|
+
|
|
98
|
+
expect(result[:v2].score).to be > result[:v1].score
|
|
99
|
+
expect(result).to have_winner(:v2)
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Available Matchers
|
|
104
|
+
|
|
105
|
+
### Pass/Fail
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
expect(result).to be_passing
|
|
109
|
+
expect(result).to be_failing
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Score Assertions
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
expect(result).to have_score(10)
|
|
116
|
+
expect(result).to have_score_above(7)
|
|
117
|
+
expect(result).to have_score_at_least(8)
|
|
118
|
+
expect(result).to have_score_below(5)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Comparison Matchers
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
expect(comparison).to have_winner(:claude)
|
|
125
|
+
expect(comparison).to be_a_tie
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## EvaluationResult Object
|
|
129
|
+
|
|
130
|
+
The `qualspec_evaluate` helper returns an `EvaluationResult`:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
result = qualspec_evaluate(response, "is helpful")
|
|
134
|
+
|
|
135
|
+
result.passing? # true/false
|
|
136
|
+
result.failing? # inverse
|
|
137
|
+
result.score # 0-10
|
|
138
|
+
result.reasoning # Judge's explanation
|
|
139
|
+
result.threshold # Pass threshold used
|
|
140
|
+
result.error? # Had an error?
|
|
141
|
+
result.error # Error message if any
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## VCR Integration
|
|
145
|
+
|
|
146
|
+
Record API calls for reproducible tests:
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
it "evaluates consistently", :qualspec do
|
|
150
|
+
with_qualspec_cassette("my_test") do
|
|
151
|
+
result = qualspec_evaluate(response, "is helpful")
|
|
152
|
+
expect(result).to be_passing
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Recording Modes
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
# Record new interactions, replay existing
|
|
161
|
+
with_qualspec_cassette("test", record: :new_episodes) { ... }
|
|
162
|
+
|
|
163
|
+
# Playback only, fail if no cassette
|
|
164
|
+
with_qualspec_cassette("test", record: :none) { ... }
|
|
165
|
+
|
|
166
|
+
# Always record fresh
|
|
167
|
+
with_qualspec_cassette("test", record: :all) { ... }
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Skip Tests Without API
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
before do
|
|
174
|
+
skip_without_qualspec_api
|
|
175
|
+
end
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Failure Messages
|
|
179
|
+
|
|
180
|
+
When tests fail, qualspec provides detailed output:
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
Expected response to pass qualspec evaluation, but it failed.
|
|
184
|
+
|
|
185
|
+
Criterion: responds in a friendly manner
|
|
186
|
+
Score: 4/10 (needed 7 to pass)
|
|
187
|
+
Reasoning: The response was terse and dismissive, lacking warmth.
|
|
188
|
+
|
|
189
|
+
Response preview: "Fine. What do you want?"
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Example Spec File
|
|
193
|
+
|
|
194
|
+
```ruby
|
|
195
|
+
require "qualspec/rspec"
|
|
196
|
+
|
|
197
|
+
RSpec.describe "Customer Support Agent" do
|
|
198
|
+
include Qualspec::RSpec::Helpers
|
|
199
|
+
|
|
200
|
+
let(:agent) { CustomerSupportAgent.new }
|
|
201
|
+
|
|
202
|
+
describe "greeting" do
|
|
203
|
+
it "welcomes users warmly" do
|
|
204
|
+
result = qualspec_evaluate(
|
|
205
|
+
agent.call("Hi"),
|
|
206
|
+
"greets the user warmly and offers assistance"
|
|
207
|
+
)
|
|
208
|
+
expect(result).to be_passing
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
describe "problem solving" do
|
|
213
|
+
it "provides actionable solutions" do
|
|
214
|
+
result = qualspec_evaluate(
|
|
215
|
+
agent.call("My order hasn't arrived"),
|
|
216
|
+
rubric: :helpful
|
|
217
|
+
)
|
|
218
|
+
expect(result).to be_passing
|
|
219
|
+
expect(result.score).to be >= 8
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
describe "difficult situations" do
|
|
224
|
+
it "handles complaints with empathy" do
|
|
225
|
+
result = qualspec_evaluate(
|
|
226
|
+
agent.call("This is terrible service!"),
|
|
227
|
+
rubric: :empathetic
|
|
228
|
+
)
|
|
229
|
+
expect(result).to be_passing
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
```
|