ruby_llm-agent-evaluations 0.3.0 → 0.3.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.
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "pathname"
5
+ require "json_schemer"
6
+ require_relative "../data"
7
+
8
+ module RubyLLM
9
+ module Evaluations
10
+ module A2UI
11
+ class Catalogs
12
+ STANDARD_ID = "https://a2ui.org/specification/v0_8/standard_catalog_definition.json".freeze
13
+ ASSETS = File.expand_path("assets/v0_8", __dir__).freeze
14
+ OFFLINE_RESOLVER = proc { |uri| raise ArgumentError, "external or unresolved schema reference: #{uri}" }.freeze
15
+
16
+ attr_reader :definitions
17
+
18
+ def initialize(custom_catalogs = {})
19
+ raise ArgumentError, "catalogs must be a Hash" unless custom_catalogs.is_a?(Hash)
20
+ standard = load_definition(File.join(ASSETS, "standard_catalog_definition.json"))
21
+ standard["catalogId"] = STANDARD_ID
22
+ @definitions = { STANDARD_ID => standard }
23
+ custom_catalogs.each do |id, source|
24
+ raise ArgumentError, "catalog ID must be a string or symbol" unless id.is_a?(String) || id.is_a?(Symbol)
25
+ id = id.to_s
26
+ raise ArgumentError, "cannot override standard catalog" if id == STANDARD_ID || id == "standard"
27
+ raise ArgumentError, "duplicate catalog ID #{id.inspect}" if @definitions.key?(id)
28
+ definition = load_definition(source)
29
+ if definition.key?("catalogId") && definition["catalogId"] != id
30
+ raise ArgumentError, "catalog #{id.inspect}: catalogId must match its configured ID"
31
+ end
32
+ definition["catalogId"] = id
33
+ @definitions[id] = definition
34
+ end
35
+ @components = {}
36
+ @styles = {}
37
+ @definitions.each { |id, definition| configure(id, definition) }
38
+ @message_schema = compile(JSON.parse(File.read(File.join(ASSETS, "server_to_client.json"))), "message schema")
39
+ freeze_tree(@definitions)
40
+ end
41
+
42
+ def known?(id)
43
+ @definitions.key?(id)
44
+ end
45
+
46
+ def message_errors(message)
47
+ errors(@message_schema, message, "message")
48
+ end
49
+
50
+ def component_errors(catalog_id, type, props)
51
+ id = catalog_id
52
+ return ["unknown catalog #{catalog_id.inspect}"] unless known?(id)
53
+ schema = @components.fetch(id)[type.to_s]
54
+ return ["catalog #{id.inspect}: unknown component type #{type.inspect}"] unless schema
55
+ errors(schema, props, "component #{type.inspect}")
56
+ end
57
+
58
+ def style_errors(catalog_id, styles)
59
+ id = catalog_id
60
+ return ["unknown catalog #{catalog_id.inspect}"] unless known?(id)
61
+ return ["styles must be an object"] unless styles.is_a?(Hash)
62
+ styles.flat_map do |name, value|
63
+ schema = @styles.fetch(id)[name.to_s]
64
+ schema ? errors(schema, value, "style #{name.inspect}") : ["catalog #{id.inspect}: unknown style #{name.inspect}"]
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def load_definition(source)
71
+ value = case source
72
+ when Hash then Data.snapshot(source)
73
+ when String, Pathname then JSON.parse(File.read(source.to_s))
74
+ else raise ArgumentError, "catalog definition must be a Hash or local JSON file path"
75
+ end
76
+ raise ArgumentError, "catalog definition must be an object" unless value.is_a?(Hash)
77
+ value
78
+ rescue JSON::ParserError, SystemCallError => error
79
+ raise ArgumentError, "invalid catalog definition: #{error.message}"
80
+ end
81
+
82
+ def configure(id, definition)
83
+ compile(definition.slice("$schema", "$defs", "definitions"), "catalog #{id.inspect}")
84
+ %w[components styles].each do |section|
85
+ entries = definition[section]
86
+ raise ArgumentError, "catalog #{id.inspect}: #{section} must be an object" unless entries.is_a?(Hash)
87
+ compiled = entries.to_h do |name, schema|
88
+ # Catalog-level definitions are shared; components can override them locally.
89
+ schema = definition.slice("$schema", "$defs", "definitions").merge(schema) if schema.is_a?(Hash)
90
+ [name, compile(schema, "catalog #{id.inspect} #{section}/#{name}")]
91
+ end
92
+ (section == "components" ? @components : @styles)[id] = compiled
93
+ end
94
+ end
95
+
96
+ def compile(schema, label)
97
+ unless schema.is_a?(Hash) || schema == true || schema == false
98
+ raise ArgumentError, "#{label}: schema must be an object or boolean"
99
+ end
100
+ compiled = JSONSchemer.schema(schema, ref_resolver: OFFLINE_RESOLVER)
101
+ failures = compiled.validate_schema.to_a
102
+ unless failures.empty?
103
+ raise ArgumentError, "#{label}: malformed schema: #{failures.map { |error| error['error'] }.join('; ')}"
104
+ end
105
+ # Resolve real schema references eagerly; annotation/default data is not schema.
106
+ compiled.bundle
107
+ compiled
108
+ rescue ArgumentError
109
+ raise
110
+ rescue StandardError => error
111
+ raise ArgumentError, "#{label}: invalid schema: #{error.message}"
112
+ end
113
+
114
+ def errors(schema, value, label)
115
+ schema.validate(Data.json_value(value)).map do |error|
116
+ pointer = error.fetch("data_pointer")
117
+ "#{label} at #{pointer.empty? ? '/' : pointer}: #{error['error'] || error['type']}"
118
+ end
119
+ rescue ArgumentError => error
120
+ ["#{label}: #{error.message}"]
121
+ end
122
+
123
+ def freeze_tree(value)
124
+ case value
125
+ when Hash
126
+ value.each { |key, item| key.freeze; freeze_tree(item) }
127
+ when Array
128
+ value.each { |item| freeze_tree(item) }
129
+ end
130
+ value.freeze
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,275 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module RubyLLM
6
+ module Evaluations
7
+ module A2UI
8
+ # Replays protocol messages without rendering or resolving remote catalogs.
9
+ class Document
10
+ ACTIONS = %w[surfaceUpdate beginRendering dataModelUpdate deleteSurface].freeze
11
+ VALUES = %w[valueString valueNumber valueBoolean valueMap].freeze
12
+ private_constant :ACTIONS, :VALUES
13
+
14
+ attr_reader :errors, :surfaces
15
+
16
+ def initialize(catalogs)
17
+ @catalogs = catalogs
18
+ @errors = []
19
+ @surfaces = {}
20
+ end
21
+
22
+ def read(response_components)
23
+ @errors = []
24
+ @surfaces = {}
25
+ @records = []
26
+ @locations = {}
27
+ @component_locations = {}
28
+ @message_index = 0
29
+ unless response_components.is_a?(Array) && !response_components.empty?
30
+ errors << "A2UI requires a nonempty response component envelope array"
31
+ return self
32
+ end
33
+ response_components.each_with_index do |envelope, index|
34
+ unless envelope.is_a?(Hash) && envelope.key?("payload")
35
+ errors << "Envelope #{index + 1}: expected an object containing payload"
36
+ next
37
+ end
38
+ messages(envelope["payload"], "Envelope #{index + 1}").each do |message|
39
+ @message_index += 1
40
+ replay(message)
41
+ end
42
+ end
43
+ validate_records
44
+ validate_surfaces
45
+ unless surfaces.values.any? { |surface| surface["rendered"] }
46
+ errors << "A2UI requires at least one live rendered surface"
47
+ end
48
+ self
49
+ end
50
+
51
+ private
52
+
53
+ def messages(payload, location)
54
+ if payload.is_a?(String)
55
+ if payload.strip.empty?
56
+ errors << "#{location}: payload is empty"
57
+ return []
58
+ end
59
+ begin
60
+ payload = JSON.parse(payload)
61
+ rescue JSON::ParserError => error
62
+ lines = payload.lines.reject { |line| line.strip.empty? }
63
+ if lines.size < 2
64
+ errors << "#{location}: malformed JSON: #{error.message}"
65
+ return []
66
+ end
67
+ return lines.each_with_index.flat_map do |line, index|
68
+ begin
69
+ messages(JSON.parse(line), "#{location}, JSONL line #{index + 1}")
70
+ rescue JSON::ParserError => line_error
71
+ errors << "#{location}, JSONL line #{index + 1}: malformed JSON: #{line_error.message}"
72
+ []
73
+ end
74
+ end
75
+ end
76
+ end
77
+ case payload
78
+ when Hash then [payload]
79
+ when Array
80
+ errors << "#{location}: message array is empty" if payload.empty?
81
+ payload
82
+ else
83
+ errors << "#{location}: payload must be a message object, message array, or JSON/JSONL string"
84
+ []
85
+ end
86
+ end
87
+
88
+ def replay(message)
89
+ location = "Message #{@message_index}"
90
+ unless message.is_a?(Hash)
91
+ errors << "#{location}: message must be an object"
92
+ return
93
+ end
94
+ actions = ACTIONS.select { |action| message.key?(action) }
95
+ if actions.size != 1
96
+ errors << "#{location}: message must contain exactly one action (#{ACTIONS.join(', ')})"
97
+ end
98
+ schema_errors = @catalogs.message_errors(message)
99
+ action = actions.first
100
+ body = message[action]
101
+ if body.is_a?(Hash)
102
+ location += ", surface #{body['surfaceId'].inspect}, /#{action}"
103
+ end
104
+ schema_errors.each { |error| errors << "#{location}: #{error}" }
105
+ return unless actions.size == 1 && schema_errors.empty?
106
+
107
+ id = body["surfaceId"]
108
+ if action == "deleteSurface"
109
+ surfaces.delete(id)
110
+ return
111
+ end
112
+ surface = surfaces[id] ||= {
113
+ "root" => nil, "catalog" => Catalogs::STANDARD_ID,
114
+ "components" => {}, "styles" => {}, "rendered" => false
115
+ }
116
+ @locations[surface.object_id] = location
117
+ case action
118
+ when "surfaceUpdate"
119
+ seen = {}
120
+ body["components"].each_with_index do |component, index|
121
+ component_id = component["id"]
122
+ path = "#{location}/components/#{index}, component #{component_id.inspect}"
123
+ errors << "#{path}: duplicate component ID within this update" if seen[component_id]
124
+ seen[component_id] = true
125
+ wrapper = component["component"]
126
+ unless wrapper.size == 1
127
+ errors << "#{path}/component: expected exactly one component type"
128
+ next
129
+ end
130
+ type, props = wrapper.first
131
+ record = { "type" => type, "props" => copy(props) }
132
+ surface["components"][component_id] = record
133
+ (@component_locations[surface.object_id] ||= {})[component_id] = path
134
+ catalog = surface["rendered"] ? surface["catalog"] : surface
135
+ @records << [catalog, :component, record, "#{path}/component/#{type}"]
136
+ end
137
+ when "beginRendering"
138
+ catalog = copy(body.fetch("catalogId", Catalogs::STANDARD_ID))
139
+ if surface["rendered"] && surface["catalog"] != catalog
140
+ surface["components"].each do |component_id, component|
141
+ path = "#{location}, component #{component_id.inspect}/component/#{component['type']}"
142
+ @records << [catalog, :component, component, path]
143
+ end
144
+ end
145
+ @records.each { |record| record[0] = catalog if record[0].equal?(surface) }
146
+ surface["root"] = copy(body["root"])
147
+ surface["catalog"] = catalog
148
+ surface["styles"] = copy(body.fetch("styles", {}))
149
+ surface["rendered"] = true
150
+ @records << [catalog, :styles, surface["styles"], "#{location}/styles"]
151
+ unless @catalogs.known?(surface["catalog"])
152
+ errors << "#{location}/catalogId: unknown catalog #{surface['catalog'].inspect} (only registered offline catalogs are allowed)"
153
+ end
154
+ when "dataModelUpdate"
155
+ validate_entries(body["contents"], "#{location}/contents")
156
+ end
157
+ end
158
+
159
+ def validate_entries(entries, location)
160
+ entries.each_with_index do |entry, index|
161
+ path = "#{location}/#{index}"
162
+ typed = VALUES.select { |key| entry.key?(key) }
163
+ errors << "#{path}: data entry must contain exactly one typed value" unless typed.size == 1
164
+ validate_entries(entry["valueMap"], "#{path}/valueMap") if entry["valueMap"].is_a?(Array)
165
+ end
166
+ end
167
+
168
+ # Components may precede beginRendering, so catalog checks wait until replay
169
+ # has selected the surface's catalog. Keep overwritten/deleted evidence too.
170
+ def validate_records
171
+ @records.each do |selection, kind, value, location|
172
+ catalog = selection.is_a?(Hash) ? selection["catalog"] : selection
173
+ next unless @catalogs.known?(catalog)
174
+ failures = if kind == :component
175
+ @catalogs.component_errors(catalog, value["type"], value["props"])
176
+ else
177
+ @catalogs.style_errors(catalog, value)
178
+ end
179
+ failures.each { |error| errors << "#{location}: #{error}" }
180
+ if kind == :component && catalog == Catalogs::STANDARD_ID
181
+ references(value, location)
182
+ end
183
+ end
184
+ end
185
+
186
+ def validate_surfaces
187
+ surfaces.each do |id, surface|
188
+ location = "#{@locations[surface.object_id]}, surface #{id.inspect}"
189
+ if surface["rendered"] && !surface["components"].key?(surface["root"])
190
+ errors << "#{location}/root: rendered root #{surface['root'].inspect} does not exist"
191
+ end
192
+ next unless surface["catalog"] == Catalogs::STANDARD_ID
193
+ graph = {}
194
+ surface["components"].each do |component_id, component|
195
+ path = @component_locations.fetch(surface.object_id, {}).fetch(component_id, location)
196
+ graph[component_id] = references(component, path, report: false)
197
+ graph[component_id].each do |target, reference_path|
198
+ unless surface["components"].key?(target)
199
+ errors << "#{reference_path}: dangling component reference #{target.inspect}"
200
+ end
201
+ end
202
+ end
203
+ validate_cycles(graph, location)
204
+ end
205
+ end
206
+
207
+ def references(component, location, report: true)
208
+ props = component["props"]
209
+ return [] unless props.is_a?(Hash)
210
+ refs = []
211
+ case component["type"]
212
+ when "Row", "Column", "List"
213
+ children = props["children"]
214
+ if children.is_a?(Hash)
215
+ alternatives = %w[explicitList template].select { |key| children.key?(key) }
216
+ if report && alternatives.size != 1
217
+ errors << "#{location}/children: expected exactly one of explicitList or template"
218
+ end
219
+ if children["explicitList"].is_a?(Array)
220
+ children["explicitList"].each_with_index { |id, index| refs << [id, "#{location}/children/explicitList/#{index}"] }
221
+ end
222
+ template = children["template"]
223
+ refs << [template["componentId"], "#{location}/children/template/componentId"] if template.is_a?(Hash) && template.key?("componentId")
224
+ end
225
+ when "Card", "Button"
226
+ refs << [props["child"], "#{location}/child"] if props.key?("child")
227
+ when "Modal"
228
+ %w[entryPointChild contentChild].each { |key| refs << [props[key], "#{location}/#{key}"] if props.key?(key) }
229
+ when "Tabs"
230
+ if props["tabItems"].is_a?(Array)
231
+ props["tabItems"].each_with_index do |tab, index|
232
+ refs << [tab["child"], "#{location}/tabItems/#{index}/child"] if tab.is_a?(Hash) && tab.key?("child")
233
+ end
234
+ end
235
+ end
236
+ refs
237
+ end
238
+
239
+ def validate_cycles(graph, location)
240
+ state = {}
241
+ graph.each_key do |start|
242
+ next if state[start]
243
+ stack = [[start, false]]
244
+ until stack.empty?
245
+ id, leaving = stack.pop
246
+ if leaving
247
+ state[id] = :done
248
+ next
249
+ end
250
+ next if state[id] == :done
251
+ state[id] = :visiting
252
+ stack << [id, true]
253
+ graph.fetch(id, []).reverse_each do |target, path|
254
+ if state[target] == :visiting
255
+ errors << "#{path}: component reference cycle involving #{target.inspect}"
256
+ elsif graph.key?(target) && !state[target]
257
+ stack << [target, false]
258
+ end
259
+ end
260
+ end
261
+ end
262
+ end
263
+
264
+ def copy(value)
265
+ case value
266
+ when Hash then value.to_h { |key, item| [key, copy(item)] }
267
+ when Array then value.map { |item| copy(item) }
268
+ when String then value.dup
269
+ else value
270
+ end
271
+ end
272
+ end
273
+ end
274
+ end
275
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Evaluations
5
+ module A2UI
6
+ class Evaluator
7
+ attr_reader :definition
8
+
9
+ def initialize(version: "0.8", catalogs: {})
10
+ raise ArgumentError, "Supported A2UI version is 0.8" unless version == "0.8"
11
+ @catalogs = Catalogs.new(catalogs)
12
+ @definition = { "version" => version.dup.freeze, "catalogs" => @catalogs.definitions }.freeze
13
+ @check = Evaluators.response_components(protocol: "a2ui", version: version) do |components, context|
14
+ document = Document.new(@catalogs).read(components)
15
+ errors = document.errors
16
+ errors += Expectations.new(context.metadata, @catalogs).check(document) if errors.empty?
17
+ Assessment.new(score: errors.empty?, reason: errors.empty? ? "Valid A2UI 0.8 response" : errors.join("; "))
18
+ end
19
+ end
20
+
21
+ def validate_expectations!(metadata)
22
+ Expectations.new(metadata, @catalogs)
23
+ end
24
+
25
+ def call(output, context)
26
+ @check.call(output, context)
27
+ end
28
+ end
29
+ end
30
+
31
+ module Evaluators
32
+ def self.a2ui(version: "0.8", catalogs: {})
33
+ A2UI::Evaluator.new(version: version, catalogs: catalogs)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Evaluations
5
+ module A2UI
6
+ # Business assertions are question data, so changing them changes the suite fingerprint.
7
+ class Expectations
8
+ def initialize(metadata, catalogs)
9
+ value = metadata.key?(:a2ui) ? metadata[:a2ui] : metadata["a2ui"]
10
+ @surfaces = {}
11
+ return unless metadata.key?(:a2ui) || metadata.key?("a2ui")
12
+
13
+ value = Data.json_value(value)
14
+ fields!(value, %w[surfaces], "metadata.a2ui")
15
+ @surfaces = value.fetch("surfaces", {})
16
+ raise DefinitionError, "metadata.a2ui.surfaces must be a Hash" unless @surfaces.is_a?(Hash)
17
+ @surfaces.each do |id, surface|
18
+ label!(id, "surface ID")
19
+ fields!(surface, %w[catalog root components], "surface #{id}")
20
+ if surface.key?("catalog")
21
+ label!(surface["catalog"], "surface #{id} catalog")
22
+ unless catalogs.known?(catalog_id(surface["catalog"]))
23
+ raise DefinitionError, "Unknown expected A2UI catalog: #{surface['catalog']}"
24
+ end
25
+ end
26
+ label!(surface["root"], "surface #{id} root") if surface.key?("root")
27
+ components = surface.fetch("components", {})
28
+ raise DefinitionError, "surface #{id} components must be a Hash" unless components.is_a?(Hash)
29
+ components.each do |component_id, component|
30
+ label!(component_id, "component ID")
31
+ fields!(component, %w[type props], "surface #{id} component #{component_id}")
32
+ label!(component["type"], "component #{component_id} type") if component.key?("type")
33
+ if component.key?("props") && !component["props"].is_a?(Hash)
34
+ raise DefinitionError, "component #{component_id} props must be a Hash"
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ def check(document)
41
+ errors = []
42
+ @surfaces.each do |id, expected|
43
+ path = "surface #{id}"
44
+ actual = document.surfaces[id]
45
+ unless actual && actual["rendered"]
46
+ errors << "#{path}: expected a rendered surface"
47
+ next
48
+ end
49
+ compare(catalog_id(expected["catalog"]), actual["catalog"], "#{path} catalog", errors) if expected.key?("catalog")
50
+ compare(expected["root"], actual["root"], "#{path} root", errors) if expected.key?("root")
51
+ expected.fetch("components", {}).each do |component_id, component|
52
+ component_path = "#{path} component #{component_id}"
53
+ observed = actual.fetch("components")[component_id]
54
+ unless observed
55
+ errors << "#{component_path}: missing component"
56
+ next
57
+ end
58
+ compare(component["type"], observed["type"], "#{component_path} type", errors) if component.key?("type")
59
+ compare(component["props"], observed["props"], "#{component_path} props", errors) if component.key?("props")
60
+ end
61
+ end
62
+ errors
63
+ end
64
+
65
+ private
66
+
67
+ def compare(expected, actual, path, errors)
68
+ if expected.is_a?(Hash) && actual.is_a?(Hash)
69
+ expected.each do |key, value|
70
+ if actual.key?(key)
71
+ compare(value, actual[key], "#{path}.#{key}", errors)
72
+ else
73
+ errors << "#{path}.#{key}: missing property"
74
+ end
75
+ end
76
+ elsif expected != actual
77
+ errors << "#{path}: expected #{expected.inspect}, got #{actual.inspect}"
78
+ end
79
+ end
80
+
81
+ def catalog_id(value)
82
+ value == "standard" ? Catalogs::STANDARD_ID : value
83
+ end
84
+
85
+ def fields!(value, allowed, path)
86
+ raise DefinitionError, "#{path} must be a Hash" unless value.is_a?(Hash)
87
+ unknown = value.keys - allowed
88
+ raise DefinitionError, "#{path}: unknown assertions #{unknown.join(', ')}" unless unknown.empty?
89
+ end
90
+
91
+ def label!(value, label)
92
+ raise DefinitionError, "#{label} must be a nonempty String" unless value.is_a?(String) && !value.strip.empty?
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -81,6 +81,7 @@ module RubyLLM
81
81
  custom = evaluators.find { |definition| definition.name == metric_name }
82
82
  callable = custom ? custom.callable : standard_metric(metric_name)
83
83
  next if judge.nil? && callable.is_a?(JudgeMetric)
84
+ callable.validate_expectations!(item.metadata) if callable.is_a?(A2UI::Evaluator)
84
85
  Evaluator.new(name: metric_name, callable: callable, threshold: threshold).freeze
85
86
  end.freeze
86
87
  Question.new(**item.to_h, minimum_scores: immutable_copy(minimum_scores),
@@ -98,6 +99,10 @@ module RubyLLM
98
99
  data = data.merge(api_interactions: item.api_interactions.to_h) if item.api_interactions
99
100
  skipped = item.minimum_scores.keys - item.evaluators.map(&:name)
100
101
  data = data.merge(skipped_metrics: skipped) unless skipped.empty?
102
+ ui = item.evaluators.filter_map do |evaluator|
103
+ [evaluator.name, evaluator.callable.definition] if evaluator.callable.is_a?(A2UI::Evaluator)
104
+ end.to_h
105
+ data = data.merge(a2ui_evaluators: ui) unless ui.empty?
101
106
  definition_value(data)
102
107
  end }
103
108
  Digest::SHA256.hexdigest(JSON.generate(Data.canonical(definition)))
@@ -111,6 +116,8 @@ module RubyLLM
111
116
  Evaluators.public_send(name)
112
117
  when "action_group"
113
118
  Evaluators.action_group
119
+ when "a2ui"
120
+ @a2ui_evaluator ||= Evaluators.a2ui
114
121
  else
115
122
  raise DefinitionError, "unknown metric: #{name}; register it with evaluate"
116
123
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RubyLLM
4
4
  module Evaluations
5
- VERSION = "0.3.0"
5
+ VERSION = "0.3.1"
6
6
  end
7
7
  end
@@ -8,6 +8,10 @@ require_relative "evaluations/response_component_capture"
8
8
  require_relative "evaluations/response_component_tool"
9
9
  require_relative "evaluations/judge_metric"
10
10
  require_relative "evaluations/evaluators"
11
+ require_relative "evaluations/a2ui/catalogs"
12
+ require_relative "evaluations/a2ui/document"
13
+ require_relative "evaluations/a2ui/expectations"
14
+ require_relative "evaluations/a2ui/evaluator"
11
15
  require_relative "evaluations/api_replay"
12
16
  require_relative "evaluations/suite"
13
17
  require_relative "evaluations/response"
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_llm-agent-evaluations
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - RubyLLM Evaluations contributors
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-10-12 00:00:00.000000000 Z
10
+ date: 2026-10-13 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activerecord
@@ -29,6 +29,20 @@ dependencies:
29
29
  - - "<"
30
30
  - !ruby/object:Gem::Version
31
31
  version: '9'
32
+ - !ruby/object:Gem::Dependency
33
+ name: json_schemer
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - "~>"
37
+ - !ruby/object:Gem::Version
38
+ version: '2.0'
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - "~>"
44
+ - !ruby/object:Gem::Version
45
+ version: '2.0'
32
46
  - !ruby/object:Gem::Dependency
33
47
  name: webrick
34
48
  requirement: !ruby/object:Gem::Requirement
@@ -89,6 +103,14 @@ files:
89
103
  - examples/support_eval.rb
90
104
  - exe/ruby_llm-agent-evaluations
91
105
  - lib/ruby_llm/evaluations.rb
106
+ - lib/ruby_llm/evaluations/a2ui/assets/LICENSE
107
+ - lib/ruby_llm/evaluations/a2ui/assets/PROVENANCE.json
108
+ - lib/ruby_llm/evaluations/a2ui/assets/v0_8/server_to_client.json
109
+ - lib/ruby_llm/evaluations/a2ui/assets/v0_8/standard_catalog_definition.json
110
+ - lib/ruby_llm/evaluations/a2ui/catalogs.rb
111
+ - lib/ruby_llm/evaluations/a2ui/document.rb
112
+ - lib/ruby_llm/evaluations/a2ui/evaluator.rb
113
+ - lib/ruby_llm/evaluations/a2ui/expectations.rb
92
114
  - lib/ruby_llm/evaluations/action_group.rb
93
115
  - lib/ruby_llm/evaluations/actions.rb
94
116
  - lib/ruby_llm/evaluations/adapters.rb