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.
- checksums.yaml +4 -4
- data/docs/tutorial/advanced/performance.md +695 -0
- data/docs/tutorial/advanced/tool-registration.md +776 -0
- data/docs/tutorial/basic-usage.md +351 -0
- data/docs/tutorial/components/chat-components.md +552 -0
- data/docs/tutorial/components/formatting.md +623 -0
- data/docs/tutorial/components/index.md +366 -0
- data/docs/tutorial/components/media-components.md +259 -0
- data/docs/tutorial/components/schema-components.md +668 -0
- data/docs/tutorial/index.md +185 -0
- data/docs/tutorial/output-formats.md +689 -0
- data/docs/tutorial/quickstart.md +30 -0
- data/docs/tutorial/template-engine.md +540 -0
- data/lib/poml/components/base.rb +146 -4
- data/lib/poml/components/content.rb +10 -3
- data/lib/poml/components/data.rb +539 -19
- data/lib/poml/components/examples.rb +235 -1
- data/lib/poml/components/formatting.rb +184 -18
- data/lib/poml/components/layout.rb +7 -2
- data/lib/poml/components/lists.rb +69 -35
- data/lib/poml/components/meta.rb +134 -5
- data/lib/poml/components/output_schema.rb +19 -1
- data/lib/poml/components/template.rb +72 -61
- data/lib/poml/components/text.rb +30 -1
- data/lib/poml/components/tool.rb +81 -0
- data/lib/poml/components/tool_definition.rb +339 -10
- data/lib/poml/components/tools.rb +14 -0
- data/lib/poml/components/utilities.rb +34 -18
- data/lib/poml/components.rb +19 -0
- data/lib/poml/context.rb +19 -4
- data/lib/poml/parser.rb +88 -63
- data/lib/poml/renderer.rb +191 -9
- data/lib/poml/template_engine.rb +138 -13
- data/lib/poml/version.rb +1 -1
- data/lib/poml.rb +16 -1
- data/readme.md +157 -30
- metadata +87 -4
- data/TUTORIAL.md +0 -987
data/lib/poml/components/meta.rb
CHANGED
@@ -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
|
-
|
156
|
-
|
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
|
-
#
|
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
|
-
#
|
31
|
-
|
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
|
56
|
+
compare_values(left_value, right_value, :>)
|
51
57
|
when '<'
|
52
|
-
left_value
|
58
|
+
compare_values(left_value, right_value, :<)
|
53
59
|
when '>='
|
54
|
-
left_value
|
60
|
+
compare_values(left_value, right_value, :>=)
|
55
61
|
when '<='
|
56
|
-
left_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
|
-
#
|
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
|
-
#
|
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
|
-
|
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
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
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
|
159
|
-
|
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
|
-
#
|
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
|
-
|
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
|
data/lib/poml/components/text.rb
CHANGED
@@ -2,7 +2,36 @@ module Poml
|
|
2
2
|
# Text component for plain text content
|
3
3
|
class TextComponent < Component
|
4
4
|
def render
|
5
|
-
|
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
|