poml 0.0.1 → 0.0.2
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/lib/poml/components/base.rb +45 -7
- data/lib/poml/components/data.rb +259 -0
- data/lib/poml/components/examples.rb +159 -13
- data/lib/poml/components/formatting.rb +148 -0
- data/lib/poml/components/media.rb +34 -0
- data/lib/poml/components/meta.rb +248 -0
- data/lib/poml/components/template.rb +334 -0
- data/lib/poml/components/utilities.rb +508 -0
- data/lib/poml/components.rb +91 -2
- data/lib/poml/context.rb +41 -2
- data/lib/poml/parser.rb +128 -15
- data/lib/poml/renderer.rb +26 -7
- data/lib/poml/template_engine.rb +101 -4
- data/lib/poml/version.rb +1 -1
- data/lib/poml.rb +67 -1
- data/{README.md → readme.md} +9 -1
- metadata +8 -4
- data/examples/_generate_expects.py +0 -35
@@ -0,0 +1,148 @@
|
|
1
|
+
module Poml
|
2
|
+
# Bold component for emphasizing text
|
3
|
+
class BoldComponent < Component
|
4
|
+
def render
|
5
|
+
apply_stylesheet
|
6
|
+
content = @element.children.empty? ? @element.content : render_children
|
7
|
+
|
8
|
+
if xml_mode?
|
9
|
+
render_as_xml('b', content)
|
10
|
+
else
|
11
|
+
"**#{content}**"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Italic component for emphasizing text
|
17
|
+
class ItalicComponent < Component
|
18
|
+
def render
|
19
|
+
apply_stylesheet
|
20
|
+
content = @element.children.empty? ? @element.content : render_children
|
21
|
+
|
22
|
+
if xml_mode?
|
23
|
+
render_as_xml('i', content)
|
24
|
+
else
|
25
|
+
"*#{content}*"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Underline component for underlining text
|
31
|
+
class UnderlineComponent < Component
|
32
|
+
def render
|
33
|
+
apply_stylesheet
|
34
|
+
content = @element.children.empty? ? @element.content : render_children
|
35
|
+
|
36
|
+
if xml_mode?
|
37
|
+
render_as_xml('u', content)
|
38
|
+
else
|
39
|
+
# Markdown-style underline
|
40
|
+
"__#{content}__"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Strikethrough component for crossed-out text
|
46
|
+
class StrikethroughComponent < Component
|
47
|
+
def render
|
48
|
+
apply_stylesheet
|
49
|
+
content = @element.children.empty? ? @element.content : render_children
|
50
|
+
|
51
|
+
if xml_mode?
|
52
|
+
render_as_xml('s', content)
|
53
|
+
else
|
54
|
+
"~~#{content}~~"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Inline span component for generic inline content
|
60
|
+
class InlineComponent < Component
|
61
|
+
def render
|
62
|
+
apply_stylesheet
|
63
|
+
content = @element.content.empty? ? render_children : @element.content
|
64
|
+
|
65
|
+
if xml_mode?
|
66
|
+
render_as_xml('span', content)
|
67
|
+
else
|
68
|
+
content
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Header component for headings
|
74
|
+
class HeaderComponent < Component
|
75
|
+
def render
|
76
|
+
apply_stylesheet
|
77
|
+
|
78
|
+
content = @element.content.empty? ? render_children : @element.content
|
79
|
+
level = get_attribute('level', @context.header_level).to_i
|
80
|
+
level = [level, 6].min # Cap at h6
|
81
|
+
level = [level, 1].max # Minimum h1
|
82
|
+
|
83
|
+
if xml_mode?
|
84
|
+
render_as_xml('h', content, { level: level })
|
85
|
+
else
|
86
|
+
header_prefix = '#' * level
|
87
|
+
"#{header_prefix} #{content}\n\n"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Newline component for explicit line breaks
|
93
|
+
class NewlineComponent < Component
|
94
|
+
def render
|
95
|
+
apply_stylesheet
|
96
|
+
|
97
|
+
count = get_attribute('newLineCount', 1).to_i
|
98
|
+
|
99
|
+
if xml_mode?
|
100
|
+
render_as_xml('nl', '', { count: count })
|
101
|
+
else
|
102
|
+
"\n" * count
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Code component for code snippets
|
108
|
+
class CodeComponent < Component
|
109
|
+
def render
|
110
|
+
apply_stylesheet
|
111
|
+
|
112
|
+
content = @element.content.empty? ? render_children : @element.content
|
113
|
+
inline = get_attribute('inline', true)
|
114
|
+
lang = get_attribute('lang', '')
|
115
|
+
|
116
|
+
if xml_mode?
|
117
|
+
attributes = { inline: inline }
|
118
|
+
attributes[:lang] = lang unless lang.empty?
|
119
|
+
render_as_xml('code', content, attributes)
|
120
|
+
else
|
121
|
+
if inline
|
122
|
+
"`#{content}`"
|
123
|
+
else
|
124
|
+
if lang.empty?
|
125
|
+
"```\n#{content}\n```\n\n"
|
126
|
+
else
|
127
|
+
"```#{lang}\n#{content}\n```\n\n"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# SubContent component for nested sections
|
135
|
+
class SubContentComponent < Component
|
136
|
+
def render
|
137
|
+
apply_stylesheet
|
138
|
+
|
139
|
+
content = @context.with_increased_header_level { render_children }
|
140
|
+
|
141
|
+
if xml_mode?
|
142
|
+
render_as_xml('section', content)
|
143
|
+
else
|
144
|
+
"#{content}"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Poml
|
2
|
+
# Audio component for embedding audio files
|
3
|
+
class AudioComponent < Component
|
4
|
+
def render
|
5
|
+
apply_stylesheet
|
6
|
+
|
7
|
+
src = get_attribute('src')
|
8
|
+
base64 = get_attribute('base64')
|
9
|
+
alt = get_attribute('alt', '')
|
10
|
+
audio_type = get_attribute('type', '')
|
11
|
+
syntax = get_attribute('syntax', 'multimedia')
|
12
|
+
position = get_attribute('position', 'here')
|
13
|
+
|
14
|
+
if xml_mode?
|
15
|
+
attributes = {}
|
16
|
+
attributes[:src] = src if src
|
17
|
+
attributes[:base64] = base64 if base64
|
18
|
+
attributes[:alt] = alt unless alt.empty?
|
19
|
+
attributes[:type] = audio_type unless audio_type.empty?
|
20
|
+
attributes[:position] = position
|
21
|
+
render_as_xml('audio', '', attributes)
|
22
|
+
else
|
23
|
+
if syntax == 'multimedia'
|
24
|
+
audio_ref = src || '[embedded audio]'
|
25
|
+
result = "[Audio: #{audio_ref}]"
|
26
|
+
result += " (#{alt})" unless alt.empty?
|
27
|
+
result
|
28
|
+
else
|
29
|
+
alt.empty? ? '[Audio]' : alt
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,248 @@
|
|
1
|
+
module Poml
|
2
|
+
# Meta component for document metadata and configuration
|
3
|
+
class MetaComponent < Component
|
4
|
+
def render
|
5
|
+
apply_stylesheet
|
6
|
+
|
7
|
+
type = get_attribute('type')
|
8
|
+
|
9
|
+
if type
|
10
|
+
handle_typed_meta(type)
|
11
|
+
else
|
12
|
+
handle_general_meta
|
13
|
+
end
|
14
|
+
|
15
|
+
# If meta has children and variables are set, render children with variable substitution
|
16
|
+
if @element.children.any? && get_attribute('variables')
|
17
|
+
child_content = @element.children.map do |child|
|
18
|
+
# Render the child element
|
19
|
+
rendered_child = Components.render_element(child, @context)
|
20
|
+
# Apply template substitution to the rendered content
|
21
|
+
@context.template_engine.substitute(rendered_child)
|
22
|
+
end.join('')
|
23
|
+
return child_content
|
24
|
+
end
|
25
|
+
|
26
|
+
# Meta components don't produce output by default
|
27
|
+
''
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def handle_typed_meta(type)
|
33
|
+
case type.downcase
|
34
|
+
when 'responseschema', 'response_schema'
|
35
|
+
handle_response_schema
|
36
|
+
when 'tool'
|
37
|
+
handle_tool_registration
|
38
|
+
when 'runtime'
|
39
|
+
handle_runtime_parameters
|
40
|
+
else
|
41
|
+
# Unknown meta type, ignore
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def handle_general_meta
|
46
|
+
# Handle template variables
|
47
|
+
variables_attr = get_attribute('variables')
|
48
|
+
if variables_attr
|
49
|
+
handle_variables(variables_attr)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Handle general metadata attributes
|
53
|
+
%w[title description author keywords].each do |attr|
|
54
|
+
value = get_attribute(attr)
|
55
|
+
if value
|
56
|
+
@context.custom_metadata[attr] = value
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Handle version control
|
61
|
+
min_version = get_attribute('minVersion')
|
62
|
+
max_version = get_attribute('maxVersion')
|
63
|
+
|
64
|
+
if min_version
|
65
|
+
check_version_compatibility(min_version, 'minimum')
|
66
|
+
end
|
67
|
+
|
68
|
+
if max_version
|
69
|
+
check_version_compatibility(max_version, 'maximum')
|
70
|
+
end
|
71
|
+
|
72
|
+
# Handle component control
|
73
|
+
components = get_attribute('components')
|
74
|
+
if components
|
75
|
+
apply_component_control(components)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def handle_response_schema
|
80
|
+
lang = get_attribute('lang', 'auto')
|
81
|
+
name = get_attribute('name')
|
82
|
+
description = get_attribute('description')
|
83
|
+
|
84
|
+
content = @element.content.strip
|
85
|
+
|
86
|
+
# Auto-detect format if lang is auto
|
87
|
+
if lang == 'auto'
|
88
|
+
lang = content.start_with?('{') ? 'json' : 'expr'
|
89
|
+
end
|
90
|
+
|
91
|
+
schema = case lang.downcase
|
92
|
+
when 'json'
|
93
|
+
parse_json_schema(content)
|
94
|
+
when 'expr'
|
95
|
+
evaluate_expression_schema(content)
|
96
|
+
else
|
97
|
+
nil
|
98
|
+
end
|
99
|
+
|
100
|
+
if schema
|
101
|
+
# Store the schema directly for simplicity
|
102
|
+
@context.response_schema = schema
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def handle_tool_registration
|
107
|
+
name = get_attribute('name')
|
108
|
+
description = get_attribute('description')
|
109
|
+
lang = get_attribute('lang', 'auto')
|
110
|
+
|
111
|
+
return unless name
|
112
|
+
|
113
|
+
content = @element.content.strip
|
114
|
+
|
115
|
+
# Auto-detect format if lang is auto
|
116
|
+
if lang == 'auto'
|
117
|
+
lang = content.start_with?('{') ? 'json' : 'expr'
|
118
|
+
end
|
119
|
+
|
120
|
+
schema = case lang.downcase
|
121
|
+
when 'json'
|
122
|
+
parse_json_schema(content)
|
123
|
+
when 'expr'
|
124
|
+
evaluate_expression_schema(content)
|
125
|
+
else
|
126
|
+
nil
|
127
|
+
end
|
128
|
+
|
129
|
+
if schema
|
130
|
+
@context.tools ||= []
|
131
|
+
# Store tool with string keys for JSON compatibility
|
132
|
+
tool_def = {
|
133
|
+
'name' => name,
|
134
|
+
'description' => description
|
135
|
+
}
|
136
|
+
# Merge in the parsed schema (should include parameters, etc.)
|
137
|
+
tool_def.merge!(schema) if schema.is_a?(Hash)
|
138
|
+
|
139
|
+
@context.tools << tool_def
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def handle_runtime_parameters
|
144
|
+
# Collect all attributes except 'type' as runtime parameters
|
145
|
+
runtime_params = {}
|
146
|
+
|
147
|
+
@element.attributes.each do |key, value|
|
148
|
+
next if key == 'type'
|
149
|
+
|
150
|
+
# Convert common parameter types
|
151
|
+
runtime_params[key] = case key.downcase
|
152
|
+
when 'temperature', 'topp', 'frequencypenalty', 'presencepenalty'
|
153
|
+
value.to_f
|
154
|
+
when 'maxoutputtokens', 'seed'
|
155
|
+
value.to_i
|
156
|
+
else
|
157
|
+
value
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
@context.runtime_parameters ||= {}
|
162
|
+
@context.runtime_parameters.merge!(runtime_params)
|
163
|
+
end
|
164
|
+
|
165
|
+
def parse_json_schema(content)
|
166
|
+
# Handle template expressions in JSON
|
167
|
+
processed_content = @context.template_engine.substitute(content)
|
168
|
+
|
169
|
+
begin
|
170
|
+
JSON.parse(processed_content)
|
171
|
+
rescue JSON::ParserError => e
|
172
|
+
nil
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def evaluate_expression_schema(content)
|
177
|
+
# This is a simplified version - in a full implementation,
|
178
|
+
# you'd want to evaluate JavaScript expressions with Zod support
|
179
|
+
begin
|
180
|
+
# For now, just return the content as-is for expression schemas
|
181
|
+
# In a complete implementation, you'd evaluate this as JavaScript
|
182
|
+
{ type: 'expression', content: content }
|
183
|
+
rescue => e
|
184
|
+
nil
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def check_version_compatibility(version, type)
|
189
|
+
current_version = Poml::VERSION
|
190
|
+
|
191
|
+
if type == 'minimum' && compare_versions(current_version, version) < 0
|
192
|
+
raise "POML version #{version} or higher required, but current version is #{current_version}"
|
193
|
+
elsif type == 'maximum' && compare_versions(current_version, version) > 0
|
194
|
+
# Just warn for maximum version
|
195
|
+
puts "Warning: POML version #{current_version} may not be compatible with documents requiring version #{version} or lower"
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def compare_versions(v1, v2)
|
200
|
+
# Simple semantic version comparison
|
201
|
+
v1_parts = v1.split('.').map(&:to_i)
|
202
|
+
v2_parts = v2.split('.').map(&:to_i)
|
203
|
+
|
204
|
+
[v1_parts.length, v2_parts.length].max.times do |i|
|
205
|
+
a = v1_parts[i] || 0
|
206
|
+
b = v2_parts[i] || 0
|
207
|
+
|
208
|
+
return a <=> b if a != b
|
209
|
+
end
|
210
|
+
|
211
|
+
0
|
212
|
+
end
|
213
|
+
|
214
|
+
def handle_variables(variables_attr)
|
215
|
+
# Parse variables JSON and add to context
|
216
|
+
begin
|
217
|
+
variables = JSON.parse(variables_attr)
|
218
|
+
if variables.is_a?(Hash)
|
219
|
+
# Merge variables into context
|
220
|
+
variables.each do |key, value|
|
221
|
+
@context.variables[key] = value
|
222
|
+
end
|
223
|
+
end
|
224
|
+
rescue JSON::ParserError => e
|
225
|
+
# Invalid JSON, ignore silently or log error
|
226
|
+
puts "Warning: Invalid JSON in meta variables: #{e.message}" if ENV['POML_DEBUG']
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def apply_component_control(components_attr)
|
231
|
+
# Parse component control string like "-table,-image" or "+table"
|
232
|
+
components_attr.split(',').each do |component_spec|
|
233
|
+
component_spec = component_spec.strip
|
234
|
+
|
235
|
+
if component_spec.start_with?('+')
|
236
|
+
# Re-enable component
|
237
|
+
component_name = component_spec[1..-1]
|
238
|
+
@context.disabled_components&.delete(component_name)
|
239
|
+
elsif component_spec.start_with?('-')
|
240
|
+
# Disable component
|
241
|
+
component_name = component_spec[1..-1]
|
242
|
+
@context.disabled_components ||= Set.new
|
243
|
+
@context.disabled_components.add(component_name)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|