poml 0.0.5 → 0.0.7

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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/docs/tutorial/advanced/performance.md +695 -0
  3. data/docs/tutorial/advanced/tool-registration.md +776 -0
  4. data/docs/tutorial/basic-usage.md +351 -0
  5. data/docs/tutorial/components/chat-components.md +552 -0
  6. data/docs/tutorial/components/formatting.md +623 -0
  7. data/docs/tutorial/components/index.md +366 -0
  8. data/docs/tutorial/components/media-components.md +259 -0
  9. data/docs/tutorial/components/schema-components.md +668 -0
  10. data/docs/tutorial/index.md +184 -0
  11. data/docs/tutorial/output-formats.md +688 -0
  12. data/docs/tutorial/quickstart.md +30 -0
  13. data/docs/tutorial/template-engine.md +540 -0
  14. data/examples/303_new_component_syntax.poml +45 -0
  15. data/lib/poml/components/base.rb +150 -3
  16. data/lib/poml/components/content.rb +10 -3
  17. data/lib/poml/components/data.rb +539 -19
  18. data/lib/poml/components/examples.rb +235 -1
  19. data/lib/poml/components/formatting.rb +184 -18
  20. data/lib/poml/components/layout.rb +7 -2
  21. data/lib/poml/components/lists.rb +69 -35
  22. data/lib/poml/components/meta.rb +191 -6
  23. data/lib/poml/components/output_schema.rb +103 -0
  24. data/lib/poml/components/template.rb +72 -61
  25. data/lib/poml/components/text.rb +30 -1
  26. data/lib/poml/components/tool.rb +81 -0
  27. data/lib/poml/components/tool_definition.rb +427 -0
  28. data/lib/poml/components/tools.rb +14 -0
  29. data/lib/poml/components/utilities.rb +34 -18
  30. data/lib/poml/components.rb +29 -0
  31. data/lib/poml/context.rb +19 -4
  32. data/lib/poml/parser.rb +90 -64
  33. data/lib/poml/renderer.rb +191 -9
  34. data/lib/poml/template_engine.rb +138 -13
  35. data/lib/poml/version.rb +1 -1
  36. data/lib/poml.rb +16 -1
  37. data/readme.md +154 -27
  38. metadata +34 -4
  39. data/TUTORIAL.md +0 -987
@@ -31,9 +31,9 @@ module Poml
31
31
 
32
32
  def handle_typed_meta(type)
33
33
  case type.downcase
34
- when 'responseschema', 'response_schema'
34
+ when 'responseschema', 'response_schema', 'output-schema', 'output_schema'
35
35
  handle_response_schema
36
- when 'tool'
36
+ when 'tool', 'tool-definition', 'tooldefinition'
37
37
  handle_tool_registration
38
38
  when 'runtime'
39
39
  handle_runtime_parameters
@@ -49,6 +49,12 @@ module Poml
49
49
  handle_variables(variables_attr)
50
50
  end
51
51
 
52
+ # Handle tool registration via tool attribute
53
+ tool_attr = get_attribute('tool')
54
+ if tool_attr
55
+ handle_tool_registration_with_name(tool_attr)
56
+ end
57
+
52
58
  # Handle general metadata attributes
53
59
  %w[title description author keywords].each do |attr|
54
60
  value = get_attribute(attr)
@@ -86,7 +92,7 @@ module Poml
86
92
 
87
93
  # Auto-detect format if parser_attr is auto
88
94
  if parser_attr == 'auto'
89
- parser_attr = content.start_with?('{') ? 'json' : 'expr'
95
+ parser_attr = content.start_with?('{') ? 'json' : 'eval'
90
96
  end
91
97
 
92
98
  # Handle new 'eval' parser type as alias for 'expr'
@@ -102,8 +108,36 @@ module Poml
102
108
  end
103
109
 
104
110
  if schema
111
+ # Check if there's already a response schema defined
112
+ if @context.response_schema
113
+ raise Poml::Error, "Multiple response schemas are not allowed. Only one response schema per document is supported."
114
+ end
115
+
105
116
  # Store the schema directly for simplicity
106
117
  @context.response_schema = schema
118
+
119
+ # Also store for backward compatibility with tests that expect schemas array
120
+ # If the parsed JSON contains a name field, use the entire schema as-is
121
+ # Otherwise wrap it in a metadata structure
122
+ if schema.is_a?(Hash) && schema.key?('name')
123
+ @context.response_schema_with_metadata = schema
124
+ else
125
+ schema_with_metadata = {
126
+ 'schema' => schema
127
+ }
128
+
129
+ # Add name if provided as attribute
130
+ if _name
131
+ schema_with_metadata['name'] = _name
132
+ end
133
+
134
+ # Add description if provided as attribute
135
+ if _description
136
+ schema_with_metadata['description'] = _description
137
+ end
138
+
139
+ @context.response_schema_with_metadata = schema_with_metadata
140
+ end
107
141
  end
108
142
  end
109
143
 
@@ -113,13 +147,67 @@ module Poml
113
147
  # Support both old 'lang' and new 'parser' attributes for compatibility
114
148
  parser_attr = get_attribute('parser') || get_attribute('lang', 'auto')
115
149
 
116
- return unless name
150
+ content = @element.content.strip
151
+
152
+ # Auto-detect format if parser_attr is auto
153
+ if parser_attr == 'auto'
154
+ parser_attr = content.start_with?('{') ? 'json' : 'eval'
155
+ end
156
+
157
+ # Handle new 'eval' parser type as alias for 'expr'
158
+ parser_attr = 'expr' if parser_attr == 'eval'
159
+
160
+ schema = case parser_attr.downcase
161
+ when 'json'
162
+ parse_json_schema(content)
163
+ when 'expr'
164
+ evaluate_expression_schema(content)
165
+ else
166
+ nil
167
+ end
168
+
169
+ # Apply enhanced tool registration features
170
+ if schema
171
+ schema = apply_tool_enhancements(schema)
172
+ end
173
+
174
+ if schema
175
+ @context.tools ||= []
176
+
177
+ # If name and description are provided as attributes, use them
178
+ if name
179
+ tool_def = {
180
+ 'name' => name,
181
+ 'description' => description,
182
+ 'schema' => schema.is_a?(String) ? schema : JSON.generate(schema)
183
+ }
184
+
185
+ # Merge in the parsed schema for backward compatibility
186
+ if schema.is_a?(Hash)
187
+ tool_def.merge!(schema)
188
+ end
189
+ elsif schema.is_a?(Hash) && schema['name']
190
+ # If the schema contains the full tool definition, use it directly
191
+ tool_def = schema
192
+ else
193
+ # No valid tool definition found
194
+ return
195
+ end
196
+
197
+ @context.tools << tool_def
198
+ end
199
+ end
200
+
201
+ def handle_tool_registration_with_name(tool_name)
202
+ description = get_attribute('description')
203
+ # Support both old 'lang' and new 'parser' attributes for compatibility
204
+ parser_attr = get_attribute('parser') || get_attribute('lang', 'auto')
117
205
 
118
206
  content = @element.content.strip
119
207
 
120
208
  # Auto-detect format if parser_attr is auto
121
209
  if parser_attr == 'auto'
122
- parser_attr = content.start_with?('{') ? 'json' : 'expr'
210
+ parser_attr = content.start_with?('{') ? 'json' : 'eval'
123
211
  end
124
212
 
125
213
  # Handle new 'eval' parser type as alias for 'expr'
@@ -134,11 +222,16 @@ module Poml
134
222
  nil
135
223
  end
136
224
 
225
+ # Apply enhanced tool registration features
226
+ if schema
227
+ schema = apply_tool_enhancements(schema)
228
+ end
229
+
137
230
  if schema
138
231
  @context.tools ||= []
139
232
  # Store tool with string keys for JSON compatibility
140
233
  tool_def = {
141
- 'name' => name,
234
+ 'name' => tool_name,
142
235
  'description' => description
143
236
  }
144
237
  # Merge in the parsed schema (should include parameters, etc.)
@@ -256,5 +349,97 @@ module Poml
256
349
  end
257
350
  end
258
351
  end
352
+
353
+ # Apply enhanced tool registration features
354
+ def apply_tool_enhancements(schema)
355
+ return schema unless schema.is_a?(Hash)
356
+
357
+ # Apply parameter key conversion and runtime type conversion
358
+ enhanced_schema = convert_parameter_keys(schema)
359
+ enhanced_schema = apply_runtime_parameter_conversion(enhanced_schema)
360
+ enhanced_schema
361
+ end
362
+
363
+ # Convert kebab-case keys to camelCase recursively
364
+ def convert_parameter_keys(obj)
365
+ case obj
366
+ when Hash
367
+ converted = {}
368
+ obj.each do |key, value|
369
+ # Convert kebab-case to camelCase
370
+ new_key = kebab_to_camel_case(key.to_s)
371
+
372
+ # Special handling for 'required' array
373
+ if key == 'required' && value.is_a?(Array)
374
+ converted[new_key] = value.map { |req_key| kebab_to_camel_case(req_key.to_s) }
375
+ else
376
+ converted[new_key] = convert_parameter_keys(value)
377
+ end
378
+ end
379
+ converted
380
+ when Array
381
+ obj.map { |item| convert_parameter_keys(item) }
382
+ else
383
+ obj
384
+ end
385
+ end
386
+
387
+ # Convert kebab-case string to camelCase
388
+ def kebab_to_camel_case(str)
389
+ # Split on hyphens and convert to camelCase
390
+ parts = str.split('-')
391
+ return str if parts.length == 1 # No conversion needed
392
+
393
+ parts[0] + parts[1..-1].map(&:capitalize).join
394
+ end
395
+
396
+ # Apply runtime parameter type conversion
397
+ def apply_runtime_parameter_conversion(schema)
398
+ return schema unless schema.is_a?(Hash)
399
+
400
+ converted = schema.dup
401
+
402
+ # Process properties if they exist
403
+ if converted['properties'].is_a?(Hash)
404
+ converted['properties'] = converted['properties'].map do |key, prop|
405
+ [key, convert_property_value(prop)]
406
+ end.to_h
407
+ end
408
+
409
+ converted
410
+ end
411
+
412
+ # Convert individual property values based on 'convert' attribute
413
+ def convert_property_value(property)
414
+ return property unless property.is_a?(Hash) && property['convert']
415
+
416
+ converted_prop = property.dup
417
+ convert_value = property['convert']
418
+ prop_type = property['type']
419
+
420
+ begin
421
+ case prop_type
422
+ when 'boolean'
423
+ # Convert various truthy/falsy values to boolean
424
+ case convert_value.to_s.downcase
425
+ when 'true', '1', 'yes', 'on'
426
+ # Keep the convert attribute for now but could be removed
427
+ when 'false', '0', 'no', 'off'
428
+ # Keep the convert attribute for now but could be removed
429
+ end
430
+ when 'number'
431
+ # Validate that the value can be converted to a number
432
+ Float(convert_value) # This will raise an exception if invalid
433
+ when 'object', 'array'
434
+ # Validate that the value is valid JSON
435
+ JSON.parse(convert_value) # This will raise an exception if invalid
436
+ end
437
+ rescue StandardError
438
+ # If conversion fails, remove the convert attribute silently
439
+ converted_prop.delete('convert')
440
+ end
441
+
442
+ converted_prop
443
+ end
259
444
  end
260
445
  end
@@ -0,0 +1,103 @@
1
+ module Poml
2
+ # OutputSchema component for defining AI response schemas
3
+ class OutputSchemaComponent < Component
4
+ def render
5
+ apply_stylesheet
6
+
7
+ # Support both old 'lang' and new 'parser' attributes for compatibility
8
+ parser_attr = get_attribute('parser') || get_attribute('lang', 'auto')
9
+ _name = get_attribute('name') # May be used for schema naming in future
10
+ _description = get_attribute('description') # May be used for schema documentation in future
11
+
12
+ content = @element.content.strip
13
+
14
+ # Auto-detect format if parser_attr is auto
15
+ if parser_attr == 'auto'
16
+ parser_attr = content.start_with?('{') ? 'json' : 'eval'
17
+ end
18
+
19
+ # Handle new 'eval' parser type as alias for 'expr'
20
+ parser_attr = 'expr' if parser_attr == 'eval'
21
+
22
+ schema = case parser_attr.downcase
23
+ when 'json'
24
+ parse_json_schema(content)
25
+ when 'expr'
26
+ evaluate_expression_schema(content)
27
+ else
28
+ nil
29
+ end
30
+
31
+ if schema
32
+ # Check if there's already a response schema defined
33
+ if @context.response_schema
34
+ raise Poml::Error, "Multiple output-schema elements are not allowed. Only one response schema per document is supported."
35
+ end
36
+
37
+ # Store the schema with metadata structure expected by tests
38
+ schema_with_metadata = {
39
+ 'schema' => schema
40
+ }
41
+
42
+ # Add name if provided
43
+ if _name
44
+ schema_with_metadata['name'] = _name
45
+ end
46
+
47
+ # Add description if provided
48
+ if _description
49
+ schema_with_metadata['description'] = _description
50
+ end
51
+
52
+ # Store the schema directly for simplicity (this is what the renderer uses for response_schema)
53
+ @context.response_schema = schema
54
+
55
+ # Also store the structured version for the schemas array
56
+ @context.response_schema_with_metadata = schema_with_metadata
57
+ end
58
+
59
+ # Meta-like components don't produce output
60
+ ''
61
+ end
62
+
63
+ private
64
+
65
+ def parse_json_schema(content)
66
+ # Apply template substitution first
67
+ substituted_content = @context.template_engine.substitute(content)
68
+
69
+ begin
70
+ JSON.parse(substituted_content)
71
+ rescue JSON::ParserError => e
72
+ raise Poml::Error, "Invalid JSON schema: #{e.message}"
73
+ end
74
+ end
75
+
76
+ def evaluate_expression_schema(content)
77
+ # Apply template substitution first
78
+ substituted_content = @context.template_engine.substitute(content)
79
+
80
+ begin
81
+ # In a real implementation, this would evaluate JavaScript expressions
82
+ # For now, we'll try to parse as JSON if it looks like JSON,
83
+ # otherwise treat it as a placeholder
84
+ if substituted_content.strip.start_with?('{', '[')
85
+ JSON.parse(substituted_content)
86
+ else
87
+ # This would need a JavaScript engine in a real implementation
88
+ # For now, return a placeholder that indicates expression evaluation
89
+ {
90
+ "_expression" => substituted_content,
91
+ "_note" => "Expression evaluation not implemented in Ruby gem"
92
+ }
93
+ end
94
+ rescue JSON::ParserError
95
+ # Return expression as-is if it can't be parsed as JSON
96
+ {
97
+ "_expression" => substituted_content,
98
+ "_note" => "Expression evaluation not implemented in Ruby gem"
99
+ }
100
+ end
101
+ end
102
+ end
103
+ end
@@ -24,12 +24,18 @@ module Poml
24
24
  # Handle both raw variable names and template expressions
25
25
  condition = condition.strip
26
26
 
27
+ # Check for negation operator at the beginning
28
+ negate = false
29
+ if condition.start_with?('!')
30
+ negate = true
31
+ condition = condition[1..-1].strip
32
+ end
33
+
27
34
  # First, substitute any template variables in the condition
28
35
  substituted_condition = @context.template_engine.substitute(condition)
29
36
 
30
- # If it's a template expression, it may have been pre-substituted
31
- # If condition looks like a substituted value, try to parse it
32
- case substituted_condition
37
+ # Evaluate the condition
38
+ result = case substituted_condition
33
39
  when 'true'
34
40
  true
35
41
  when 'false'
@@ -47,13 +53,13 @@ module Poml
47
53
  # Perform comparison
48
54
  case operator
49
55
  when '>'
50
- left_value > right_value
56
+ compare_values(left_value, right_value, :>)
51
57
  when '<'
52
- left_value < right_value
58
+ compare_values(left_value, right_value, :<)
53
59
  when '>='
54
- left_value >= right_value
60
+ compare_values(left_value, right_value, :>=)
55
61
  when '<='
56
- left_value <= right_value
62
+ compare_values(left_value, right_value, :<=)
57
63
  when '=='
58
64
  left_value == right_value
59
65
  when '!='
@@ -71,12 +77,20 @@ module Poml
71
77
  result = @context.template_engine.evaluate_attribute_expression(substituted_condition)
72
78
  convert_to_boolean(result)
73
79
  end
80
+
81
+ # Apply negation if needed
82
+ negate ? !result : result
74
83
  end
75
84
 
76
85
  def convert_operand(operand)
77
86
  # First substitute any template variables
78
87
  substituted = @context.template_engine.substitute(operand)
79
88
 
89
+ # Handle quoted strings
90
+ if substituted =~ /^['"](.*)['"]$/
91
+ return $1
92
+ end
93
+
80
94
  # Try to convert to number if possible, otherwise keep as string
81
95
  if substituted =~ /^-?\d+$/
82
96
  substituted.to_i
@@ -87,6 +101,16 @@ module Poml
87
101
  end
88
102
  end
89
103
 
104
+ def compare_values(left, right, operator)
105
+ # Handle type mismatches gracefully
106
+ begin
107
+ left.send(operator, right)
108
+ rescue ArgumentError
109
+ # If types are incompatible, try to convert both to strings and compare
110
+ left.to_s.send(operator, right.to_s)
111
+ end
112
+ end
113
+
90
114
  def convert_to_boolean(result)
91
115
  case result
92
116
  when true, false
@@ -117,25 +141,42 @@ module Poml
117
141
  items = evaluate_items(items_expr)
118
142
  return '' unless items.is_a?(Array)
119
143
 
120
- # Store original content of elements to avoid mutation
121
- original_contents = store_original_contents(@element)
122
-
123
- # Render content for each item
144
+ # Render content for each item without mutating original elements
124
145
  results = []
125
146
  items.each_with_index do |item, index|
126
- # Restore original content before each iteration
127
- restore_original_contents(@element, original_contents)
128
-
129
147
  # Create child context with loop variable
130
148
  child_context = @context.create_child_context
131
149
  child_context.variables[variable] = item
132
150
  child_context.variables['loop'] = { 'index' => index + 1 } # 1-based index
133
151
 
134
- # Render children with loop variable substitution
152
+ # Update template engine context to use new variables
153
+ child_context.template_engine = Poml::TemplateEngine.new(child_context)
154
+
155
+ # Ensure list index is synchronized with parent context
156
+ if @context.instance_variable_get(:@list_style)
157
+ # Copy current list index from parent to child before processing
158
+ current_list_index = @context.instance_variable_get(:@list_index) || 0
159
+ child_context.instance_variable_set(:@list_index, current_list_index)
160
+ end
161
+
162
+ # Process each child element in the child context
135
163
  item_content = @element.children.map do |child|
136
- render_element_with_substitution(child, child_context)
164
+ # Create a deep copy of the child element to avoid mutations
165
+ child_copy = deep_copy_element(child)
166
+
167
+ # Process templates in the copied element
168
+ process_templates_in_element(child_copy, child_context)
169
+
170
+ # Render the processed element
171
+ Components.render_element(child_copy, child_context)
137
172
  end.join('')
138
173
 
174
+ # Update parent context with the list index from child context
175
+ if @context.instance_variable_get(:@list_style)
176
+ updated_list_index = child_context.instance_variable_get(:@list_index)
177
+ @context.instance_variable_set(:@list_index, updated_list_index)
178
+ end
179
+
139
180
  results << item_content
140
181
  end
141
182
 
@@ -144,63 +185,33 @@ module Poml
144
185
 
145
186
  private
146
187
 
147
- def store_original_contents(element)
148
- contents = {}
149
- contents[element.object_id] = element.content.dup if element.content
150
-
151
- element.children.each do |child|
152
- contents.merge!(store_original_contents(child))
153
- end
154
-
155
- contents
188
+ def deep_copy_element(element)
189
+ # Create a new element with the same properties
190
+ Element.new(
191
+ tag_name: element.tag_name,
192
+ attributes: element.attributes.dup,
193
+ content: element.content.dup,
194
+ children: element.children.map { |child| deep_copy_element(child) }
195
+ )
156
196
  end
157
197
 
158
- def restore_original_contents(element, original_contents)
159
- if original_contents[element.object_id]
160
- element.content = original_contents[element.object_id].dup
161
- end
162
-
163
- element.children.each do |child|
164
- restore_original_contents(child, original_contents)
165
- end
166
- end
167
-
168
- private
169
-
170
- def render_element_with_substitution(element, context)
171
- # First substitute in the element's own content if it has template variables
198
+ def process_templates_in_element(element, context)
199
+ # Process template variables in the element's content
172
200
  if element.content && element.content.include?('{{')
173
201
  element.content = context.template_engine.substitute(element.content)
174
202
  end
175
203
 
176
- # If this is a text element, return substituted content directly
177
- if element.tag_name == :text
178
- return element.content
179
- end
180
-
181
- # For non-text elements, recursively substitute in their children
182
- substitute_in_element(element, context)
183
- Components.render_element(element, context)
184
- end
185
-
186
- def substitute_in_element(element, context)
187
- # Substitute in the element's own content
188
- if element.content && element.content.include?('{{')
189
- element.content = context.template_engine.substitute(element.content)
190
- end
191
-
192
- # Recursively substitute variables in children
204
+ # Recursively process templates in children
193
205
  element.children.each do |child|
194
- if child.tag_name == :text && child.content.include?('{{')
206
+ # For text elements, process their content directly
207
+ if child.tag_name == :text && child.content && child.content.include?('{{')
195
208
  child.content = context.template_engine.substitute(child.content)
196
- else
197
- substitute_in_element(child, context)
198
209
  end
210
+
211
+ process_templates_in_element(child, context)
199
212
  end
200
213
  end
201
214
 
202
- private
203
-
204
215
  def evaluate_items(items_expr)
205
216
  # Handle both raw variable names and template expressions
206
217
  items_expr = items_expr.strip
@@ -2,7 +2,36 @@ module Poml
2
2
  # Text component for plain text content
3
3
  class TextComponent < Component
4
4
  def render
5
- @element.content
5
+ # If there are child elements, render them; otherwise return the content
6
+ if @element.children.empty?
7
+ # Apply template substitution to text content
8
+ @context.template_engine.substitute(@element.content)
9
+ else
10
+ render_children
11
+ end
12
+ end
13
+ end
14
+
15
+ # Unknown component handler - preserves tag name and content for debugging
16
+ class UnknownComponent < Component
17
+ def render
18
+ apply_stylesheet
19
+
20
+ tag_name = @element.tag_name.to_s
21
+ content = @element.children.empty? ? @element.content : render_children
22
+
23
+ # Apply template substitution to content
24
+ if content.is_a?(String)
25
+ content = @context.template_engine.substitute(content)
26
+ end
27
+
28
+ if xml_mode?
29
+ # In XML mode, preserve the original tag
30
+ render_as_xml(tag_name, content)
31
+ else
32
+ # In text mode, include both tag name and content for debugging
33
+ "#{tag_name}: #{content}"
34
+ end
6
35
  end
7
36
  end
8
37
  end
@@ -0,0 +1,81 @@
1
+ module Poml
2
+ # Tool component for declarative tool definition
3
+ class ToolComponent < Component
4
+ def render
5
+ name = get_attribute('name')
6
+ return '' unless name
7
+
8
+ # Build tool definition from declarative syntax
9
+ tool_def = {
10
+ 'name' => name,
11
+ 'description' => extract_description,
12
+ 'parameters' => extract_parameters
13
+ }
14
+
15
+ # Register the tool
16
+ @context.tools ||= []
17
+ @context.tools << tool_def
18
+
19
+ # Tools components don't produce output by default
20
+ ''
21
+ end
22
+
23
+ private
24
+
25
+ def extract_description
26
+ description_element = @element.children.find { |child| child.name == 'description' }
27
+ return description_element&.content&.strip if description_element
28
+
29
+ # Fallback to description attribute
30
+ get_attribute('description') || ''
31
+ end
32
+
33
+ def extract_parameters
34
+ parameter_elements = @element.children.select { |child| child.name == 'parameter' }
35
+
36
+ if parameter_elements.empty?
37
+ # If no parameter elements, return empty object structure
38
+ return {
39
+ 'type' => 'object',
40
+ 'properties' => {},
41
+ 'required' => []
42
+ }
43
+ end
44
+
45
+ properties = {}
46
+ required = []
47
+
48
+ parameter_elements.each do |param_element|
49
+ param_name = param_element.attributes['name']
50
+ next unless param_name
51
+
52
+ param_type = param_element.attributes['type'] || 'string'
53
+ param_required = param_element.attributes['required']
54
+ param_description = param_element.content&.strip
55
+
56
+ # If the parameter has a description child element, use that instead
57
+ description_child = param_element.children.find { |child| child.name == 'description' }
58
+ if description_child
59
+ param_description = description_child.content&.strip
60
+ end
61
+
62
+ properties[param_name] = {
63
+ 'type' => param_type
64
+ }
65
+
66
+ properties[param_name]['description'] = param_description if param_description && !param_description.empty?
67
+
68
+ # Handle required flag
69
+ if param_required == 'true' || param_required == true
70
+ required << param_name
71
+ end
72
+ end
73
+
74
+ {
75
+ 'type' => 'object',
76
+ 'properties' => properties,
77
+ 'required' => required
78
+ }
79
+ end
80
+ end
81
+ end