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.
- 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 +184 -0
- data/docs/tutorial/output-formats.md +688 -0
- data/docs/tutorial/quickstart.md +30 -0
- data/docs/tutorial/template-engine.md +540 -0
- data/examples/303_new_component_syntax.poml +45 -0
- data/lib/poml/components/base.rb +150 -3
- 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 +191 -6
- data/lib/poml/components/output_schema.rb +103 -0
- 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 +427 -0
- data/lib/poml/components/tools.rb +14 -0
- data/lib/poml/components/utilities.rb +34 -18
- data/lib/poml/components.rb +29 -0
- data/lib/poml/context.rb +19 -4
- data/lib/poml/parser.rb +90 -64
- 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 +154 -27
- metadata +34 -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
|
@@ -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' : '
|
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
|
-
|
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' : '
|
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' =>
|
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
|
-
#
|
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
|