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
@@ -0,0 +1,427 @@
|
|
1
|
+
module Poml
|
2
|
+
# ToolDefinition component for registering AI tools
|
3
|
+
class ToolDefinitionComponent < Component
|
4
|
+
def render
|
5
|
+
apply_stylesheet
|
6
|
+
|
7
|
+
name = get_attribute('name')
|
8
|
+
description = get_attribute('description')
|
9
|
+
|
10
|
+
return '' unless name # Name is required for tools
|
11
|
+
|
12
|
+
# Check if this is declarative XML format (has description/parameter children)
|
13
|
+
has_xml_children = @element.children.any? { |child| ['description', 'parameter'].include?(child.tag_name.to_s) }
|
14
|
+
|
15
|
+
if has_xml_children
|
16
|
+
# Handle declarative XML format
|
17
|
+
handle_declarative_format(name, description)
|
18
|
+
else
|
19
|
+
# Handle JSON format (original behavior)
|
20
|
+
handle_json_format(name, description)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Meta-like components don't produce output
|
24
|
+
''
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def handle_declarative_format(name, description)
|
30
|
+
tool_def = {
|
31
|
+
'name' => name,
|
32
|
+
'description' => extract_description(description)
|
33
|
+
}
|
34
|
+
|
35
|
+
# Extract tool-level metadata
|
36
|
+
extract_tool_metadata(tool_def)
|
37
|
+
|
38
|
+
# Extract parameters
|
39
|
+
tool_def['parameters'] = extract_parameters
|
40
|
+
|
41
|
+
# Extract tool-level examples
|
42
|
+
extract_tool_examples(tool_def)
|
43
|
+
|
44
|
+
# Register the tool
|
45
|
+
@context.tools ||= []
|
46
|
+
@context.tools << tool_def
|
47
|
+
end
|
48
|
+
|
49
|
+
def extract_tool_metadata(tool_def)
|
50
|
+
# Extract version, category, requires_auth, etc.
|
51
|
+
%w[version category requires_auth deprecated].each do |attr|
|
52
|
+
element = @element.children.find { |child| child.tag_name.to_s == attr }
|
53
|
+
if element
|
54
|
+
content = element.content&.strip
|
55
|
+
if attr == 'requires_auth' || attr == 'deprecated'
|
56
|
+
# Convert to boolean
|
57
|
+
tool_def[attr] = content == 'true'
|
58
|
+
else
|
59
|
+
tool_def[attr] = content
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def extract_tool_examples(tool_def)
|
66
|
+
example_elements = @element.children.select { |child| child.tag_name.to_s == 'example' }
|
67
|
+
return if example_elements.empty?
|
68
|
+
|
69
|
+
if example_elements.length == 1
|
70
|
+
# Single example
|
71
|
+
tool_def['example'] = parse_example_element(example_elements.first)
|
72
|
+
else
|
73
|
+
# Multiple examples
|
74
|
+
tool_def['examples'] = example_elements.map { |elem| parse_example_element(elem) }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def parse_example_element(example_element)
|
79
|
+
example = {}
|
80
|
+
|
81
|
+
# Check for title and description
|
82
|
+
title_elem = example_element.children.find { |child| child.tag_name.to_s == 'title' }
|
83
|
+
description_elem = example_element.children.find { |child| child.tag_name.to_s == 'description' }
|
84
|
+
parameters_elem = example_element.children.find { |child| child.tag_name.to_s == 'parameters' }
|
85
|
+
|
86
|
+
example['title'] = title_elem.content&.strip if title_elem
|
87
|
+
example['description'] = description_elem.content&.strip if description_elem
|
88
|
+
|
89
|
+
if parameters_elem
|
90
|
+
# Parse JSON parameters
|
91
|
+
content = parameters_elem.content.strip
|
92
|
+
begin
|
93
|
+
# Clean up the JSON content - remove extra whitespace and normalize
|
94
|
+
cleaned_content = content.gsub(/\n\s*/, ' ').gsub(/\s+/, ' ')
|
95
|
+
example['parameters'] = JSON.parse(cleaned_content)
|
96
|
+
rescue JSON::ParserError
|
97
|
+
# If JSON parsing fails, try to clean it up more aggressively
|
98
|
+
begin
|
99
|
+
# Replace problematic characters and try again
|
100
|
+
fixed_content = content.gsub(/\n/, '\\n').gsub(/\r/, '\\r').gsub(/\t/, '\\t')
|
101
|
+
example['parameters'] = JSON.parse(fixed_content)
|
102
|
+
rescue JSON::ParserError
|
103
|
+
# If still failing, store as string
|
104
|
+
example['parameters'] = content
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
example
|
110
|
+
end
|
111
|
+
|
112
|
+
def handle_json_format(name, description)
|
113
|
+
# Support both old 'lang' and new 'parser' attributes for compatibility
|
114
|
+
parser_attr = get_attribute('parser') || get_attribute('lang', 'auto')
|
115
|
+
|
116
|
+
content = @element.content.strip
|
117
|
+
|
118
|
+
# Auto-detect format if parser_attr is auto
|
119
|
+
if parser_attr == 'auto'
|
120
|
+
parser_attr = content.start_with?('{') ? 'json' : 'eval'
|
121
|
+
end
|
122
|
+
|
123
|
+
# Handle new 'eval' parser type as alias for 'expr'
|
124
|
+
parser_attr = 'expr' if parser_attr == 'eval'
|
125
|
+
|
126
|
+
schema = case parser_attr.downcase
|
127
|
+
when 'json'
|
128
|
+
parse_json_schema(content)
|
129
|
+
when 'expr'
|
130
|
+
evaluate_expression_schema(content)
|
131
|
+
else
|
132
|
+
nil
|
133
|
+
end
|
134
|
+
|
135
|
+
if schema
|
136
|
+
@context.tools ||= []
|
137
|
+
|
138
|
+
# Extract description from JSON if not provided as attribute
|
139
|
+
json_description = nil
|
140
|
+
if schema.is_a?(Hash) && schema['description']
|
141
|
+
json_description = schema['description']
|
142
|
+
end
|
143
|
+
|
144
|
+
# Use attribute description first, then JSON description, then nil
|
145
|
+
final_description = description || json_description
|
146
|
+
|
147
|
+
# Store tool with string keys for JSON compatibility
|
148
|
+
tool_def = {
|
149
|
+
'name' => name,
|
150
|
+
'description' => final_description,
|
151
|
+
'schema' => schema.is_a?(String) ? schema : JSON.generate(schema)
|
152
|
+
}
|
153
|
+
|
154
|
+
# For compatibility with expected structure, also store as parameters
|
155
|
+
if schema.is_a?(Hash)
|
156
|
+
# Remove description from parameters if it was moved to top level
|
157
|
+
params = schema.dup
|
158
|
+
params.delete('description')
|
159
|
+
|
160
|
+
# If the schema has a 'parameters' key, extract it
|
161
|
+
if params['parameters']
|
162
|
+
tool_def['parameters'] = params['parameters']
|
163
|
+
else
|
164
|
+
tool_def['parameters'] = params
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
@context.tools << tool_def
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def extract_description(fallback_description = nil)
|
173
|
+
description_element = @element.children.find { |child| child.tag_name.to_s == 'description' }
|
174
|
+
return description_element&.content&.strip if description_element
|
175
|
+
|
176
|
+
# Fallback to attribute description
|
177
|
+
fallback_description || ''
|
178
|
+
end
|
179
|
+
|
180
|
+
def extract_parameters
|
181
|
+
parameter_elements = @element.children.select { |child| child.tag_name.to_s == 'parameter' }
|
182
|
+
|
183
|
+
if parameter_elements.empty?
|
184
|
+
# If no parameter elements, return empty object
|
185
|
+
return {}
|
186
|
+
end
|
187
|
+
|
188
|
+
parameters = {}
|
189
|
+
|
190
|
+
parameter_elements.each do |param_element|
|
191
|
+
param_name = param_element.attributes['name']
|
192
|
+
next unless param_name
|
193
|
+
|
194
|
+
param_type = param_element.attributes['type'] || 'string'
|
195
|
+
param_required = param_element.attributes['required']
|
196
|
+
param_description = extract_parameter_description(param_element)
|
197
|
+
|
198
|
+
param_def = {
|
199
|
+
'type' => param_type,
|
200
|
+
'required' => param_required == 'true' || param_required == true
|
201
|
+
}
|
202
|
+
|
203
|
+
param_def['description'] = param_description if param_description && !param_description.empty?
|
204
|
+
|
205
|
+
# Handle enum values
|
206
|
+
enum_element = param_element.children.find { |child| child.tag_name.to_s == 'enum' }
|
207
|
+
if enum_element
|
208
|
+
values = enum_element.children.select { |child| child.tag_name.to_s == 'value' }
|
209
|
+
.map(&:content)
|
210
|
+
.compact
|
211
|
+
param_def['enum'] = values unless values.empty?
|
212
|
+
end
|
213
|
+
|
214
|
+
# Handle array items
|
215
|
+
items_element = param_element.children.find { |child| child.tag_name.to_s == 'items' }
|
216
|
+
if items_element && param_type == 'array'
|
217
|
+
items_type = items_element.attributes['type'] || 'string'
|
218
|
+
items_description = extract_parameter_description(items_element)
|
219
|
+
|
220
|
+
items_def = { 'type' => items_type }
|
221
|
+
items_def['description'] = items_description if items_description && !items_description.empty?
|
222
|
+
|
223
|
+
# Handle example in items
|
224
|
+
items_example_elem = items_element.children.find { |child| child.tag_name.to_s == 'example' }
|
225
|
+
if items_example_elem
|
226
|
+
items_def['example'] = items_example_elem.content&.strip
|
227
|
+
end
|
228
|
+
|
229
|
+
param_def['items'] = items_def
|
230
|
+
end
|
231
|
+
|
232
|
+
# Handle parameter examples
|
233
|
+
example_element = param_element.children.find { |child| child.tag_name.to_s == 'example' }
|
234
|
+
if example_element
|
235
|
+
param_def['example'] = example_element.content&.strip
|
236
|
+
end
|
237
|
+
|
238
|
+
# Handle object properties
|
239
|
+
properties_element = param_element.children.find { |child| child.tag_name.to_s == 'properties' }
|
240
|
+
if properties_element && param_type == 'object'
|
241
|
+
properties = {}
|
242
|
+
|
243
|
+
property_elements = properties_element.children.select { |child| child.tag_name.to_s == 'property' }
|
244
|
+
property_elements.each do |prop_element|
|
245
|
+
prop_name = prop_element.attributes['name']
|
246
|
+
next unless prop_name
|
247
|
+
|
248
|
+
prop_type = prop_element.attributes['type'] || 'string'
|
249
|
+
prop_description = extract_parameter_description(prop_element)
|
250
|
+
|
251
|
+
prop_def = { 'type' => prop_type }
|
252
|
+
prop_def['description'] = prop_description if prop_description && !prop_description.empty?
|
253
|
+
|
254
|
+
properties[prop_name] = prop_def
|
255
|
+
end
|
256
|
+
|
257
|
+
param_def['properties'] = properties unless properties.empty?
|
258
|
+
end
|
259
|
+
|
260
|
+
# Handle schema references (raw JSON schema)
|
261
|
+
schema_element = param_element.children.find { |child| child.tag_name.to_s == 'schema' }
|
262
|
+
if schema_element
|
263
|
+
schema_content = schema_element.content.strip
|
264
|
+
begin
|
265
|
+
# Try to parse as JSON to validate
|
266
|
+
parsed_schema = JSON.parse(schema_content)
|
267
|
+
param_def['schema'] = parsed_schema
|
268
|
+
rescue JSON::ParserError
|
269
|
+
# If invalid JSON, store as string
|
270
|
+
param_def['schema'] = schema_content
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
parameters[param_name] = param_def
|
275
|
+
end
|
276
|
+
|
277
|
+
parameters
|
278
|
+
end
|
279
|
+
|
280
|
+
def extract_parameter_description(element)
|
281
|
+
# Check for description child element first
|
282
|
+
description_child = element.children.find { |child| child.tag_name.to_s == 'description' }
|
283
|
+
return description_child.content&.strip if description_child
|
284
|
+
|
285
|
+
# Fallback to element content if no description child
|
286
|
+
element.content&.strip
|
287
|
+
end
|
288
|
+
|
289
|
+
private
|
290
|
+
|
291
|
+
def parse_json_schema(content)
|
292
|
+
# Apply template substitution first
|
293
|
+
substituted_content = @context.template_engine.substitute(content)
|
294
|
+
|
295
|
+
begin
|
296
|
+
parsed = JSON.parse(substituted_content)
|
297
|
+
# Apply enhanced tool registration features
|
298
|
+
enhanced_schema = apply_tool_enhancements(parsed)
|
299
|
+
enhanced_schema
|
300
|
+
rescue JSON::ParserError => e
|
301
|
+
raise Poml::Error, "Invalid JSON schema for tool '#{get_attribute('name')}': #{e.message}"
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
def evaluate_expression_schema(content)
|
306
|
+
# Apply template substitution first
|
307
|
+
substituted_content = @context.template_engine.substitute(content)
|
308
|
+
|
309
|
+
begin
|
310
|
+
# In a real implementation, this would evaluate JavaScript expressions
|
311
|
+
# For now, we'll try to parse as JSON if it looks like JSON,
|
312
|
+
# otherwise treat it as a placeholder
|
313
|
+
if substituted_content.strip.start_with?('{', '[')
|
314
|
+
parsed = JSON.parse(substituted_content)
|
315
|
+
# Apply enhanced tool registration features
|
316
|
+
enhanced_schema = apply_tool_enhancements(parsed)
|
317
|
+
enhanced_schema
|
318
|
+
else
|
319
|
+
# This would need a JavaScript engine in a real implementation
|
320
|
+
# For now, return a placeholder that indicates expression evaluation
|
321
|
+
{
|
322
|
+
"_expression" => substituted_content,
|
323
|
+
"_note" => "Expression evaluation not implemented in Ruby gem"
|
324
|
+
}
|
325
|
+
end
|
326
|
+
rescue JSON::ParserError
|
327
|
+
# Return expression as-is if it can't be parsed as JSON
|
328
|
+
{
|
329
|
+
"_expression" => substituted_content,
|
330
|
+
"_note" => "Expression evaluation not implemented in Ruby gem"
|
331
|
+
}
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
# Apply enhanced tool registration features
|
336
|
+
def apply_tool_enhancements(schema)
|
337
|
+
return schema unless schema.is_a?(Hash)
|
338
|
+
|
339
|
+
# Apply parameter key conversion and runtime type conversion
|
340
|
+
enhanced_schema = convert_parameter_keys(schema)
|
341
|
+
enhanced_schema = apply_runtime_parameter_conversion(enhanced_schema)
|
342
|
+
enhanced_schema
|
343
|
+
end
|
344
|
+
|
345
|
+
# Convert kebab-case keys to camelCase recursively
|
346
|
+
def convert_parameter_keys(obj)
|
347
|
+
case obj
|
348
|
+
when Hash
|
349
|
+
converted = {}
|
350
|
+
obj.each do |key, value|
|
351
|
+
# Convert kebab-case to camelCase
|
352
|
+
new_key = kebab_to_camel_case(key.to_s)
|
353
|
+
|
354
|
+
# Special handling for 'required' array
|
355
|
+
if key == 'required' && value.is_a?(Array)
|
356
|
+
converted[new_key] = value.map { |req_key| kebab_to_camel_case(req_key.to_s) }
|
357
|
+
else
|
358
|
+
converted[new_key] = convert_parameter_keys(value)
|
359
|
+
end
|
360
|
+
end
|
361
|
+
converted
|
362
|
+
when Array
|
363
|
+
obj.map { |item| convert_parameter_keys(item) }
|
364
|
+
else
|
365
|
+
obj
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
# Convert kebab-case string to camelCase
|
370
|
+
def kebab_to_camel_case(str)
|
371
|
+
# Split on hyphens and convert to camelCase
|
372
|
+
parts = str.split('-')
|
373
|
+
return str if parts.length == 1 # No conversion needed
|
374
|
+
|
375
|
+
parts[0] + parts[1..-1].map(&:capitalize).join
|
376
|
+
end
|
377
|
+
|
378
|
+
# Apply runtime parameter type conversion
|
379
|
+
def apply_runtime_parameter_conversion(schema)
|
380
|
+
return schema unless schema.is_a?(Hash)
|
381
|
+
|
382
|
+
converted = schema.dup
|
383
|
+
|
384
|
+
# Process properties if they exist
|
385
|
+
if converted['properties'].is_a?(Hash)
|
386
|
+
converted['properties'] = converted['properties'].map do |key, prop|
|
387
|
+
[key, convert_property_value(prop)]
|
388
|
+
end.to_h
|
389
|
+
end
|
390
|
+
|
391
|
+
converted
|
392
|
+
end
|
393
|
+
|
394
|
+
# Convert individual property values based on 'convert' attribute
|
395
|
+
def convert_property_value(property)
|
396
|
+
return property unless property.is_a?(Hash) && property['convert']
|
397
|
+
|
398
|
+
converted_prop = property.dup
|
399
|
+
convert_value = property['convert']
|
400
|
+
prop_type = property['type']
|
401
|
+
|
402
|
+
begin
|
403
|
+
case prop_type
|
404
|
+
when 'boolean'
|
405
|
+
# Convert various truthy/falsy values to boolean
|
406
|
+
case convert_value.to_s.downcase
|
407
|
+
when 'true', '1', 'yes', 'on'
|
408
|
+
# Keep the convert attribute for now but could be removed
|
409
|
+
when 'false', '0', 'no', 'off'
|
410
|
+
# Keep the convert attribute for now but could be removed
|
411
|
+
end
|
412
|
+
when 'number'
|
413
|
+
# Validate that the value can be converted to a number
|
414
|
+
Float(convert_value) # This will raise an exception if invalid
|
415
|
+
when 'object', 'array'
|
416
|
+
# Validate that the value is valid JSON
|
417
|
+
JSON.parse(convert_value) # This will raise an exception if invalid
|
418
|
+
end
|
419
|
+
rescue StandardError
|
420
|
+
# If conversion fails, remove the convert attribute silently
|
421
|
+
converted_prop.delete('convert')
|
422
|
+
end
|
423
|
+
|
424
|
+
converted_prop
|
425
|
+
end
|
426
|
+
end
|
427
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Poml
|
2
|
+
# Tools wrapper component that contains tool definitions
|
3
|
+
class ToolsComponent < Component
|
4
|
+
def render
|
5
|
+
# Process child tool elements
|
6
|
+
@element.children.each do |child|
|
7
|
+
Components.render_element(child, @context)
|
8
|
+
end
|
9
|
+
|
10
|
+
# Tools components don't produce output by default
|
11
|
+
''
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -4,7 +4,8 @@ module Poml
|
|
4
4
|
def render
|
5
5
|
apply_stylesheet
|
6
6
|
|
7
|
-
|
7
|
+
# Always use render_children if there are child elements, otherwise use content
|
8
|
+
content = @element.children.empty? ? @element.content : render_children
|
8
9
|
|
9
10
|
# Add to structured chat messages if context supports it
|
10
11
|
if @context.respond_to?(:chat_messages)
|
@@ -12,14 +13,18 @@ module Poml
|
|
12
13
|
'role' => 'assistant',
|
13
14
|
'content' => content
|
14
15
|
}
|
15
|
-
# Return empty for raw format to avoid duplication
|
16
|
-
return ''
|
17
16
|
end
|
18
17
|
|
19
18
|
if xml_mode?
|
20
19
|
render_as_xml('ai-msg', content, { speaker: 'ai' })
|
21
20
|
else
|
22
|
-
|
21
|
+
case @context.output_format
|
22
|
+
when 'raw'
|
23
|
+
# In raw format, return content only if chat mode is disabled
|
24
|
+
@context.chat ? "" : content
|
25
|
+
else
|
26
|
+
content
|
27
|
+
end
|
23
28
|
end
|
24
29
|
end
|
25
30
|
end
|
@@ -29,7 +34,8 @@ module Poml
|
|
29
34
|
def render
|
30
35
|
apply_stylesheet
|
31
36
|
|
32
|
-
|
37
|
+
# Always use render_children if there are child elements, otherwise use content
|
38
|
+
content = @element.children.empty? ? @element.content : render_children
|
33
39
|
|
34
40
|
# Add to structured chat messages if context supports it
|
35
41
|
if @context.respond_to?(:chat_messages)
|
@@ -37,14 +43,18 @@ module Poml
|
|
37
43
|
'role' => 'user',
|
38
44
|
'content' => content
|
39
45
|
}
|
40
|
-
# Return empty for raw format to avoid duplication
|
41
|
-
return ''
|
42
46
|
end
|
43
47
|
|
44
48
|
if xml_mode?
|
45
49
|
render_as_xml('user-msg', content, { speaker: 'human' })
|
46
50
|
else
|
47
|
-
|
51
|
+
case @context.output_format
|
52
|
+
when 'raw'
|
53
|
+
# In raw format, return content only if chat mode is disabled
|
54
|
+
@context.chat ? "" : content
|
55
|
+
else
|
56
|
+
content
|
57
|
+
end
|
48
58
|
end
|
49
59
|
end
|
50
60
|
end
|
@@ -54,7 +64,8 @@ module Poml
|
|
54
64
|
def render
|
55
65
|
apply_stylesheet
|
56
66
|
|
57
|
-
|
67
|
+
# Always use render_children if there are child elements, otherwise use content
|
68
|
+
content = @element.children.empty? ? @element.content : render_children
|
58
69
|
|
59
70
|
# Add to structured chat messages if context supports it
|
60
71
|
if @context.respond_to?(:chat_messages)
|
@@ -62,14 +73,18 @@ module Poml
|
|
62
73
|
'role' => 'system',
|
63
74
|
'content' => content
|
64
75
|
}
|
65
|
-
# Return empty for raw format to avoid duplication
|
66
|
-
return ''
|
67
76
|
end
|
68
77
|
|
69
78
|
if xml_mode?
|
70
79
|
render_as_xml('system-msg', content, { speaker: 'system' })
|
71
80
|
else
|
72
|
-
|
81
|
+
case @context.output_format
|
82
|
+
when 'raw'
|
83
|
+
# In raw format, return content only if chat mode is disabled
|
84
|
+
@context.chat ? "" : content
|
85
|
+
else
|
86
|
+
content
|
87
|
+
end
|
73
88
|
end
|
74
89
|
end
|
75
90
|
end
|
@@ -94,7 +109,7 @@ module Poml
|
|
94
109
|
end
|
95
110
|
end
|
96
111
|
|
97
|
-
# Conversation component for
|
112
|
+
# Conversation component for formatting chat messages (matches original POML API)
|
98
113
|
class ConversationComponent < Component
|
99
114
|
def render
|
100
115
|
apply_stylesheet
|
@@ -124,7 +139,7 @@ module Poml
|
|
124
139
|
render_conversation_markdown(messages)
|
125
140
|
end
|
126
141
|
end
|
127
|
-
|
142
|
+
|
128
143
|
private
|
129
144
|
|
130
145
|
def apply_message_selection(messages, selection)
|
@@ -187,7 +202,8 @@ module Poml
|
|
187
202
|
end
|
188
203
|
result << ""
|
189
204
|
end
|
190
|
-
result.join("
|
205
|
+
result.join("
|
206
|
+
")
|
191
207
|
end
|
192
208
|
|
193
209
|
def escape_xml(text)
|
@@ -271,10 +287,10 @@ module Poml
|
|
271
287
|
|
272
288
|
if show_content
|
273
289
|
begin
|
274
|
-
content =
|
290
|
+
content = read_file_with_encoding(full_path)
|
275
291
|
item[:content] = content
|
276
|
-
rescue
|
277
|
-
item[:content] =
|
292
|
+
rescue => e
|
293
|
+
item[:content] = "[Error reading file: #{e.message}]"
|
278
294
|
end
|
279
295
|
end
|
280
296
|
|
data/lib/poml/components.rb
CHANGED
@@ -13,7 +13,10 @@ require_relative 'components/formatting'
|
|
13
13
|
require_relative 'components/media'
|
14
14
|
require_relative 'components/utilities'
|
15
15
|
require_relative 'components/meta'
|
16
|
+
require_relative 'components/tools'
|
16
17
|
require_relative 'components/template'
|
18
|
+
require_relative 'components/output_schema'
|
19
|
+
require_relative 'components/tool_definition'
|
17
20
|
|
18
21
|
module Poml
|
19
22
|
# Update the component mapping after all components are loaded
|
@@ -36,10 +39,21 @@ module Poml
|
|
36
39
|
span: InlineComponent,
|
37
40
|
inline: InlineComponent,
|
38
41
|
h: HeaderComponent,
|
42
|
+
h1: HeaderComponent,
|
43
|
+
h2: HeaderComponent,
|
44
|
+
h3: HeaderComponent,
|
45
|
+
h4: HeaderComponent,
|
46
|
+
h5: HeaderComponent,
|
47
|
+
h6: HeaderComponent,
|
39
48
|
header: HeaderComponent,
|
40
49
|
br: NewlineComponent,
|
41
50
|
newline: NewlineComponent,
|
42
51
|
code: CodeComponent,
|
52
|
+
'code-block': CodeBlockComponent,
|
53
|
+
'codeblock': CodeBlockComponent,
|
54
|
+
callout: CalloutComponent,
|
55
|
+
blockquote: BlockquoteComponent,
|
56
|
+
quote: BlockquoteComponent,
|
43
57
|
section: SubContentComponent,
|
44
58
|
subcontent: SubContentComponent,
|
45
59
|
|
@@ -77,6 +91,7 @@ module Poml
|
|
77
91
|
|
78
92
|
# List components
|
79
93
|
list: ListComponent,
|
94
|
+
'numbered-list': ListComponent, # alias for numbered lists
|
80
95
|
item: ItemComponent,
|
81
96
|
|
82
97
|
# Layout components
|
@@ -127,6 +142,20 @@ module Poml
|
|
127
142
|
meta: MetaComponent,
|
128
143
|
Meta: MetaComponent,
|
129
144
|
|
145
|
+
# Tools components
|
146
|
+
tools: ToolsComponent,
|
147
|
+
Tools: ToolsComponent,
|
148
|
+
'output-schema': OutputSchemaComponent,
|
149
|
+
'outputschema': OutputSchemaComponent,
|
150
|
+
OutputSchema: OutputSchemaComponent,
|
151
|
+
schema: OutputSchemaComponent, # 'schema' is an alias for 'output-schema'
|
152
|
+
Schema: OutputSchemaComponent,
|
153
|
+
'tool-definition': ToolDefinitionComponent,
|
154
|
+
'tooldefinition': ToolDefinitionComponent,
|
155
|
+
ToolDefinition: ToolDefinitionComponent,
|
156
|
+
tool: ToolDefinitionComponent, # 'tool' is an alias for 'tool-definition'
|
157
|
+
Tool: ToolDefinitionComponent,
|
158
|
+
|
130
159
|
# Template components
|
131
160
|
include: IncludeComponent,
|
132
161
|
Include: IncludeComponent,
|
data/lib/poml/context.rb
CHANGED
@@ -5,15 +5,15 @@ module Poml
|
|
5
5
|
# Context object that holds variables, stylesheets, and processing state
|
6
6
|
class Context
|
7
7
|
attr_accessor :variables, :stylesheet, :chat, :texts, :source_path, :syntax, :header_level
|
8
|
-
attr_accessor :response_schema, :tools, :runtime_parameters, :disabled_components
|
9
|
-
attr_accessor :template_engine, :chat_messages, :custom_metadata
|
8
|
+
attr_accessor :response_schema, :response_schema_with_metadata, :tools, :runtime_parameters, :disabled_components
|
9
|
+
attr_accessor :template_engine, :chat_messages, :custom_metadata, :output_format, :output_content
|
10
10
|
|
11
|
-
def initialize(variables: {}, stylesheet: nil, chat: true, syntax: nil)
|
11
|
+
def initialize(variables: {}, stylesheet: nil, chat: true, syntax: nil, source_path: nil, output_format: nil)
|
12
12
|
@variables = variables || {}
|
13
13
|
@stylesheet = parse_stylesheet(stylesheet)
|
14
14
|
@chat = chat
|
15
15
|
@texts = {}
|
16
|
-
@source_path =
|
16
|
+
@source_path = source_path
|
17
17
|
@syntax = syntax
|
18
18
|
@header_level = 1 # Track current header nesting level
|
19
19
|
@response_schema = nil
|
@@ -23,6 +23,8 @@ module Poml
|
|
23
23
|
@template_engine = TemplateEngine.new(self)
|
24
24
|
@chat_messages = [] # Track structured chat messages
|
25
25
|
@custom_metadata = {} # Track general metadata like title, description etc.
|
26
|
+
@output_format = output_format # Track target output format for component behavior
|
27
|
+
@output_content = nil # Track content from <output> components
|
26
28
|
end
|
27
29
|
|
28
30
|
def xml_mode?
|
@@ -65,6 +67,19 @@ module Poml
|
|
65
67
|
child.disabled_components = @disabled_components.dup
|
66
68
|
child.chat_messages = @chat_messages # Share the same array reference
|
67
69
|
child.custom_metadata = @custom_metadata # Share the same hash reference
|
70
|
+
child.output_format = @output_format # Copy output format
|
71
|
+
child.output_content = @output_content # Copy output content
|
72
|
+
child.template_engine = @template_engine # Copy template engine
|
73
|
+
|
74
|
+
# Copy instance variables that components may set (like list context)
|
75
|
+
instance_variables.each do |var|
|
76
|
+
unless [:@variables, :@stylesheet, :@chat, :@syntax, :@header_level,
|
77
|
+
:@response_schema, :@tools, :@runtime_parameters, :@disabled_components,
|
78
|
+
:@chat_messages, :@custom_metadata, :@output_format, :@output_content, :@template_engine].include?(var)
|
79
|
+
child.instance_variable_set(var, instance_variable_get(var))
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
68
83
|
child
|
69
84
|
end
|
70
85
|
|