sorbet-baml 0.4.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4de7b80da4ee748d5c22196c22f78ca92e855f9b4983437d4ea6ad02a5201fba
4
- data.tar.gz: 22a81719e5d2021b52945d913e3134482ff999e917d655f67ec894a35fa2a40b
3
+ metadata.gz: '0853732b922565aa837ede825e6c074a29975d1fb7eb16140b699ec5db495e92'
4
+ data.tar.gz: 55f2775ead79f10978a7c7858889f124cd5c3e01f430b6cc7ff32f6484dad955
5
5
  SHA512:
6
- metadata.gz: d64e1cef217666030c2f34cacd48333d6ce46d5fac40e2a2855c0f73fe43dadf39a620ad01b37df904ca5aa792a7d46cd3a1bdaf45ce797b34f3010ce4a2be12
7
- data.tar.gz: 6131b255163a544f2ef7591f58e173ab0436c44426003513888b51ea4d3a35a5ff90d186b491d867035b9d0c86929b176c6ff8bfeb6de7106b87662546ea0c3e
6
+ metadata.gz: 4d519cf11e3cbdfebff145542b48a51fc2e90ebbbe841f047f3ba344de508d26efa27b248fcc3aa4ac9777870c3b94b208c53bd7d238fcbc291048a350b4275d
7
+ data.tar.gz: fc3b1ce2f6d829c49e0a0f2c7e16f6d46e211258e094c9f83deefbd0aeafd07f8797cb3e3366568b652b759722445eb45de84ecf95fa6aa40847279fc5748f7d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.5.1] - 2026-03-07
4
+
5
+ ### Fixed
6
+
7
+ - Avoid emitting invalid double optional markers like `int??` when converting DSPy tool parameters backed by `T.nilable(...)`
8
+ - Add regression coverage for nilable scalar, enum, and enum-array DSPy tool parameters so malformed `??` output fails tests
9
+
10
+ ### Improved
11
+
12
+ - Tighten DSPy tool conversion assertions to validate full rendered optional parameter lines instead of permissive substrings
13
+
3
14
  ## [0.4.0] - 2025-10-14
4
15
 
5
16
  ### Changed
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'sorbet_baml'
@@ -16,12 +16,21 @@ module SorbetBaml
16
16
  return descriptions unless klass.respond_to?(:props)
17
17
 
18
18
  begin
19
- T.unsafe(klass).props.each do |field_name, prop_info|
20
- next unless prop_info.is_a?(Hash)
19
+ # DSPy >= 0.34 stores descriptions via DSPy::Ext::StructDescriptions#field_descriptions
20
+ if klass.respond_to?(:field_descriptions)
21
+ T.unsafe(klass).field_descriptions.each do |field_name, desc|
22
+ descriptions[field_name.to_s] = desc if desc.is_a?(String)
23
+ end
24
+ end
25
+
26
+ # Fall back to the legacy :extra hash for structs that store descriptions there
27
+ if descriptions.empty?
28
+ T.unsafe(klass).props.each do |field_name, prop_info|
29
+ next unless prop_info.is_a?(Hash)
21
30
 
22
- # Check if the prop has a description in the :extra field
23
- extra = prop_info[:extra]
24
- descriptions[field_name.to_s] = extra[:description] if extra.is_a?(Hash) && extra[:description].is_a?(String)
31
+ extra = prop_info[:extra]
32
+ descriptions[field_name.to_s] = extra[:description] if extra.is_a?(Hash) && extra[:description].is_a?(String)
33
+ end
25
34
  end
26
35
  rescue StandardError
27
36
  # Handle any errors gracefully and return empty hash
@@ -26,25 +26,50 @@ module SorbetBaml
26
26
  tool_name = klass.tool_name_value || klass.name&.split('::')&.last
27
27
  tool_description = klass.tool_description_value
28
28
 
29
- # Get parameters from DSPy's call_schema which extracts from Sorbet signatures
30
- call_schema = klass.call_schema
29
+ # Get parameters from DSPy's call_schema_object which extracts from Sorbet signatures.
30
+ call_schema = klass.call_schema_object
31
31
  parameters = call_schema[:properties] || {}
32
32
  required_params = call_schema[:required] || []
33
33
 
34
+ # Extract Sorbet types directly from the call method signature for richer type info.
35
+ # This gives us access to T::Enum classes, T::Struct references, etc. that are lost
36
+ # when converting to JSON schema.
37
+ sorbet_types = extract_sorbet_types(klass)
38
+
39
+ # Collect enum classes referenced by parameters for generating enum definitions.
40
+ enum_classes = collect_enum_classes(sorbet_types)
41
+
34
42
  lines = []
35
43
 
36
44
  # Add tool description as class-level comment if available
37
45
  lines << "// #{tool_description}" if @include_descriptions && tool_description
38
46
 
47
+ # Generate enum definitions between description and class
48
+ if enum_classes.any?
49
+ lines << '' unless lines.empty?
50
+ enum_converter = Converter.new(@options)
51
+ enum_classes.each_with_index do |enum_klass, i|
52
+ lines << '' unless i.zero?
53
+ lines << enum_converter.convert_enum(enum_klass)
54
+ end
55
+ lines << ''
56
+ end
57
+
39
58
  # Generate BAML class definition
40
59
  lines << "class #{tool_name} {"
41
60
 
42
61
  parameters.each do |param_name, param_info|
43
- # Convert JSON schema type to BAML type
44
- baml_type = json_schema_type_to_baml(param_info)
45
-
46
- # Add optional marker if parameter is not required
47
- baml_type += '?' unless required_params.include?(param_name.to_s)
62
+ # Use Sorbet type mapping when available (preserves enum/struct type names).
63
+ # Fall back to JSON schema conversion for tools without Sorbet signatures.
64
+ baml_type = if sorbet_types.key?(param_name)
65
+ TypeMapper.map_type(sorbet_types[param_name])
66
+ else
67
+ json_schema_type_to_baml(param_info)
68
+ end
69
+
70
+ # TypeMapper already encodes T.nilable(...) as a trailing `?`.
71
+ # Avoid emitting invalid `??` when DSPy also marks the kwarg optional.
72
+ baml_type += '?' if !required_params.include?(param_name.to_s) && !baml_type.end_with?('?')
48
73
 
49
74
  line = "#{' ' * @indent_size}#{param_name} #{baml_type}"
50
75
 
@@ -63,10 +88,61 @@ module SorbetBaml
63
88
 
64
89
  private
65
90
 
91
+ # Extract Sorbet types from the tool's call method signature.
92
+ sig { params(klass: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
93
+ def extract_sorbet_types(klass)
94
+ method_obj = klass.instance_method(:call)
95
+ sig_info = T::Utils.signature_for_method(method_obj)
96
+ return {} if sig_info.nil?
97
+
98
+ types = {}
99
+ sig_info.arg_types.each { |name, type| types[name] = type }
100
+ sig_info.kwarg_types.each { |name, type| types[name] = type }
101
+ types
102
+ end
103
+
104
+ # Collect all T::Enum classes referenced by the given Sorbet types.
105
+ sig { params(sorbet_types: T::Hash[Symbol, T.untyped]).returns(T::Array[T.class_of(T::Enum)]) }
106
+ def collect_enum_classes(sorbet_types)
107
+ enum_classes = T.let([], T::Array[T.class_of(T::Enum)])
108
+ sorbet_types.each_value do |type|
109
+ enum_classes.concat(extract_enum_types(type))
110
+ end
111
+ enum_classes.uniq
112
+ end
113
+
114
+ # Recursively extract T::Enum classes from a Sorbet type object.
115
+ sig { params(type: T.untyped).returns(T::Array[T.class_of(T::Enum)]) }
116
+ def extract_enum_types(type)
117
+ case type
118
+ when T::Types::Simple
119
+ raw = type.raw_type
120
+ raw.is_a?(Class) && raw < T::Enum ? [raw] : []
121
+ when T::Types::TypedArray
122
+ extract_enum_types(type.type)
123
+ when T::Types::TypedHash
124
+ extract_enum_types(type.keys) + extract_enum_types(type.values)
125
+ else
126
+ if type.is_a?(Class) && type < T::Enum
127
+ [type]
128
+ elsif type.respond_to?(:types)
129
+ type.types.flat_map { |t| extract_enum_types(t) }
130
+ else
131
+ []
132
+ end
133
+ end
134
+ end
135
+
136
+ # Fallback: convert JSON schema type info to BAML type string.
137
+ # Used when Sorbet signature is not available.
66
138
  sig { params(json_schema_info: T::Hash[Symbol, T.untyped]).returns(String) }
67
139
  def json_schema_type_to_baml(json_schema_info)
68
140
  type = json_schema_info[:type]
69
141
 
142
+ # dspy >= 0.34 represents nilable types as ["integer", "null"] rather than just "integer".
143
+ # Extract the primary (non-null) type so the case below matches correctly.
144
+ type = type.reject { |t| t == 'null' }.first if type.is_a?(Array)
145
+
70
146
  case type
71
147
  when :string, 'string'
72
148
  'string'
@@ -19,6 +19,9 @@ module SorbetBaml
19
19
  map_array_type(type_object)
20
20
  when T::Types::TypedHash
21
21
  map_hash_type(type_object)
22
+ when T::Types::Untyped
23
+ # T.untyped maps to BAML's json type for dynamic values
24
+ 'json'
22
25
  else
23
26
  # Check if it's a union type (T.nilable or T.any)
24
27
  if type_object.respond_to?(:types)
@@ -61,8 +64,10 @@ module SorbetBaml
61
64
  sig { params(type_object: T.untyped).returns(String) }
62
65
  def self.map_union_type(type_object)
63
66
  types = type_object.types
64
- nil_type = types.find { |t| t.raw_type == NilClass }
65
- non_nil_types = types.reject { |t| t.raw_type == NilClass }
67
+
68
+ # Only T::Types::Simple has raw_type - check type class before accessing
69
+ nil_type = types.find { |t| nil_type?(t) }
70
+ non_nil_types = types.reject { |t| nil_type?(t) }
66
71
 
67
72
  return 'null' if non_nil_types.empty?
68
73
 
@@ -91,6 +96,13 @@ module SorbetBaml
91
96
  end
92
97
  end
93
98
 
99
+ # Check if a type represents NilClass safely
100
+ # Only T::Types::Simple has raw_type; other type classes (TypedHash, TypedArray, Untyped) do not
101
+ sig { params(type_obj: T.untyped).returns(T::Boolean) }
102
+ def self.nil_type?(type_obj)
103
+ type_obj.is_a?(T::Types::Simple) && type_obj.raw_type == NilClass || false
104
+ end
105
+
94
106
  sig { params(type_object: T.untyped).returns(String) }
95
107
  def self.map_array_type(type_object)
96
108
  element_type = map_type(type_object.type)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SorbetBaml
4
- VERSION = '0.4.0'
4
+ VERSION = '0.5.1'
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sorbet-baml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vicente Reig Rincon de Arellano
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-10-14 00:00:00.000000000 Z
10
+ date: 2026-03-07 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: sorbet-runtime
@@ -38,6 +38,7 @@ files:
38
38
  - README.md
39
39
  - Rakefile
40
40
  - examples/description_parameters.rb
41
+ - lib/sorbet-baml.rb
41
42
  - lib/sorbet_baml.rb
42
43
  - lib/sorbet_baml/comment_extractor.rb
43
44
  - lib/sorbet_baml/converter.rb