squishling 0.1.0 → 0.2.0

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.
@@ -7,6 +7,14 @@ module Squishling
7
7
  CACHE = {}.compare_by_identity
8
8
  CACHE_LOCK = Mutex.new
9
9
 
10
+ # Conditional keywords (Schematist's `given` and `dependent`) that providers' strict modes don't support.
11
+ # They're validated locally and kept out of the schema sent to the LLM.
12
+ LOCAL_ONLY_KEYWORDS = %w[if then else dependentRequired dependentSchemas].freeze
13
+ # Keywords whose value is a map of names to schemas: the names are data, never keywords to strip.
14
+ SCHEMA_MAPS = %w[properties patternProperties $defs definitions].freeze
15
+ # Keywords whose value is literal data, copied untouched.
16
+ DATA_KEYWORDS = %w[enum const default examples].freeze
17
+
10
18
  class << self
11
19
  def for(raw)
12
20
  CACHE_LOCK.synchronize { CACHE[raw] ||= new(raw) }
@@ -18,9 +26,9 @@ module Squishling
18
26
  end
19
27
  end
20
28
 
21
- # The payload handed to RubyLLM's `with_schema`: always strict.
29
+ # The payload handed to RubyLLM's `with_schema`: always strict, without LOCAL_ONLY_KEYWORDS.
22
30
  attr_reader :llm_schema
23
- # The bare JSON Schema used for validation and result typing.
31
+ # The full JSON Schema used for validation and result typing.
24
32
  attr_reader :json_schema
25
33
 
26
34
  def initialize(raw)
@@ -36,7 +44,7 @@ module Squishling
36
44
  @llm_schema = {
37
45
  "name" => hash["name"] || body["title"],
38
46
  "description" => hash["description"] || body["description"],
39
- "schema" => @json_schema,
47
+ "schema" => provider_schema(@json_schema),
40
48
  "strict" => true
41
49
  }.compact
42
50
  @validator = JSONSchemer.schema(@json_schema)
@@ -72,6 +80,40 @@ module Squishling
72
80
  end
73
81
  end
74
82
 
83
+ # A copy of the schema without the conditional keywords. An allOf made only of conditionals (how
84
+ # Schematist emits several `given` blocks) goes too.
85
+ def provider_schema(node)
86
+ case node
87
+ when Array then node.map { |item| provider_schema(item) }
88
+ when Hash
89
+ copy = node.each_with_object({}) do |(key, value), stripped|
90
+ next if LOCAL_ONLY_KEYWORDS.include?(key)
91
+
92
+ stripped[key] =
93
+ if DATA_KEYWORDS.include?(key)
94
+ value
95
+ elsif SCHEMA_MAPS.include?(key) && value.is_a?(Hash)
96
+ value.transform_values { |schema| provider_schema(schema) }
97
+ else
98
+ provider_schema(value)
99
+ end
100
+ end
101
+ without_conditional_all_of(copy, node)
102
+ else node
103
+ end
104
+ end
105
+
106
+ def without_conditional_all_of(copy, original)
107
+ return copy unless original["allOf"].is_a?(Array)
108
+
109
+ kept = copy["allOf"].zip(original["allOf"]).reject { |_, branch| conditional_only?(branch) }.map(&:first)
110
+ kept.empty? ? copy.except("allOf") : copy.merge("allOf" => kept)
111
+ end
112
+
113
+ def conditional_only?(branch)
114
+ branch.is_a?(Hash) && branch.any? && (branch.keys - LOCAL_ONLY_KEYWORDS - %w[description $comment]).empty?
115
+ end
116
+
75
117
  # Some schema objects emit a { name:, description:, schema: {...} } wrapper instead of a bare schema.
76
118
  def wrapped?(hash)
77
119
  hash["schema"].is_a?(Hash) && !hash.key?("type") && !hash.key?("properties")
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Squishling
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/squishling.rb CHANGED
@@ -9,6 +9,7 @@ require_relative "squishling/version"
9
9
  require_relative "squishling/errors"
10
10
  require_relative "squishling/configuration"
11
11
  require_relative "squishling/params"
12
+ require_relative "squishling/model_path"
12
13
  require_relative "squishling/result"
13
14
  require_relative "squishling/schema"
14
15
  require_relative "squishling/source"
@@ -58,8 +59,10 @@ module Squishling
58
59
  # handle this input. Returns the typed result (squished? true, or false when the declared fallback supplied
59
60
  # it); return it from the method. The overrides apply to this call only: append_instructions adds to (or,
60
61
  # with false, replaces) the declared sections, context is sent alongside the declared squish_context,
61
- # model/provider/instructions replace the declared ones, and params merge key by key over them.
62
- def squish!(append_instructions: nil, context: nil, instructions: nil, model: nil, provider: nil, params: nil)
63
- Router.escalate(self, { append_instructions:, context:, instructions:, model:, provider:, params: })
62
+ # model/escalation (one or the other)/provider/instructions replace the declared ones, and params merge key
63
+ # by key over them.
64
+ def squish!(append_instructions: nil, context: nil, instructions: nil, model: nil, escalation: nil, provider: nil,
65
+ params: nil)
66
+ Router.hand_off(self, { append_instructions:, context:, instructions:, model:, escalation:, provider:, params: })
64
67
  end
65
68
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: squishling
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Carroll
@@ -81,6 +81,7 @@ files:
81
81
  - lib/squishling/definition.rb
82
82
  - lib/squishling/errors.rb
83
83
  - lib/squishling/invoker.rb
84
+ - lib/squishling/model_path.rb
84
85
  - lib/squishling/params.rb
85
86
  - lib/squishling/result.rb
86
87
  - lib/squishling/router.rb