poml 0.0.6 → 0.0.8

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 (38) 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 +185 -0
  11. data/docs/tutorial/output-formats.md +689 -0
  12. data/docs/tutorial/quickstart.md +30 -0
  13. data/docs/tutorial/template-engine.md +540 -0
  14. data/lib/poml/components/base.rb +146 -4
  15. data/lib/poml/components/content.rb +10 -3
  16. data/lib/poml/components/data.rb +539 -19
  17. data/lib/poml/components/examples.rb +235 -1
  18. data/lib/poml/components/formatting.rb +184 -18
  19. data/lib/poml/components/layout.rb +7 -2
  20. data/lib/poml/components/lists.rb +69 -35
  21. data/lib/poml/components/meta.rb +134 -5
  22. data/lib/poml/components/output_schema.rb +19 -1
  23. data/lib/poml/components/template.rb +72 -61
  24. data/lib/poml/components/text.rb +30 -1
  25. data/lib/poml/components/tool.rb +81 -0
  26. data/lib/poml/components/tool_definition.rb +339 -10
  27. data/lib/poml/components/tools.rb +14 -0
  28. data/lib/poml/components/utilities.rb +34 -18
  29. data/lib/poml/components.rb +19 -0
  30. data/lib/poml/context.rb +19 -4
  31. data/lib/poml/parser.rb +88 -63
  32. data/lib/poml/renderer.rb +191 -9
  33. data/lib/poml/template_engine.rb +138 -13
  34. data/lib/poml/version.rb +1 -1
  35. data/lib/poml.rb +16 -1
  36. data/readme.md +157 -30
  37. metadata +87 -4
  38. 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
@@ -115,6 +115,29 @@ module Poml
115
115
 
116
116
  # Store the schema directly for simplicity
117
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
118
141
  end
119
142
  end
120
143
 
@@ -143,6 +166,11 @@ module Poml
143
166
  nil
144
167
  end
145
168
 
169
+ # Apply enhanced tool registration features
170
+ if schema
171
+ schema = apply_tool_enhancements(schema)
172
+ end
173
+
146
174
  if schema
147
175
  @context.tools ||= []
148
176
 
@@ -150,10 +178,14 @@ module Poml
150
178
  if name
151
179
  tool_def = {
152
180
  'name' => name,
153
- 'description' => description
181
+ 'description' => description,
182
+ 'schema' => schema.is_a?(String) ? schema : JSON.generate(schema)
154
183
  }
155
- # Merge in the parsed schema (should include parameters, etc.)
156
- tool_def.merge!(schema) if schema.is_a?(Hash)
184
+
185
+ # Merge in the parsed schema for backward compatibility
186
+ if schema.is_a?(Hash)
187
+ tool_def.merge!(schema)
188
+ end
157
189
  elsif schema.is_a?(Hash) && schema['name']
158
190
  # If the schema contains the full tool definition, use it directly
159
191
  tool_def = schema
@@ -190,6 +222,11 @@ module Poml
190
222
  nil
191
223
  end
192
224
 
225
+ # Apply enhanced tool registration features
226
+ if schema
227
+ schema = apply_tool_enhancements(schema)
228
+ end
229
+
193
230
  if schema
194
231
  @context.tools ||= []
195
232
  # Store tool with string keys for JSON compatibility
@@ -312,5 +349,97 @@ module Poml
312
349
  end
313
350
  end
314
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
315
444
  end
316
445
  end
@@ -33,9 +33,27 @@ module Poml
33
33
  if @context.response_schema
34
34
  raise Poml::Error, "Multiple output-schema elements are not allowed. Only one response schema per document is supported."
35
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
36
46
 
37
- # Store the schema directly for simplicity
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)
38
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
39
57
  end
40
58
 
41
59
  # Meta-like components don't produce output
@@ -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