poml 0.0.3 → 0.0.6
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/examples/301_new_schema_syntax.poml +62 -0
- data/examples/302_schema_compatibility.poml +48 -0
- data/examples/303_new_component_syntax.poml +45 -0
- data/lib/poml/components/base.rb +6 -1
- data/lib/poml/components/content.rb +1 -1
- data/lib/poml/components/data.rb +4 -4
- data/lib/poml/components/lists.rb +22 -24
- data/lib/poml/components/meta.rb +90 -22
- data/lib/poml/components/output_schema.rb +85 -0
- data/lib/poml/components/styling.rb +1 -1
- data/lib/poml/components/tool_definition.rb +98 -0
- data/lib/poml/components/utilities.rb +2 -2
- data/lib/poml/components/workflow.rb +0 -16
- data/lib/poml/components.rb +10 -0
- data/lib/poml/parser.rb +4 -2
- data/lib/poml/renderer.rb +1 -1
- data/lib/poml/version.rb +1 -1
- data/lib/poml.rb +36 -62
- data/readme.md +5 -1
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '08ad238aa1b84997e3120a4379473c190293c3e6cb4a9fc08fe373457e3d10b7'
|
4
|
+
data.tar.gz: '028e54bb00290ffd9317902101435a2024a15972d6987e4acf9773b00ccba679'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: db5eb9e927864afd49fc424be4d97f0e99f206e967605a878da19e8ae150a1ea13b25dab5a4a93773653765933c1e191e4da2c19496d43b6708a37a4aec7e758
|
7
|
+
data.tar.gz: be6a7c9fc5570bc8bc88483f2d7afcf757cf8d8153128b3f3fb6aa90bf54faaaff5b8a2236d68be7ee03692b40b6b5c97d36494f4c1ccb622fea5d96e408514e
|
@@ -0,0 +1,62 @@
|
|
1
|
+
<!-- Example demonstrating the new schema syntax in POML -->
|
2
|
+
<poml>
|
3
|
+
<!-- Example 1: Output schema with new parser attribute -->
|
4
|
+
<meta type="responseschema" parser="json">
|
5
|
+
{
|
6
|
+
"type": "object",
|
7
|
+
"properties": {
|
8
|
+
"summary": {
|
9
|
+
"type": "string",
|
10
|
+
"description": "A brief summary of the content"
|
11
|
+
},
|
12
|
+
"sentiment": {
|
13
|
+
"type": "string",
|
14
|
+
"enum": ["positive", "negative", "neutral"]
|
15
|
+
},
|
16
|
+
"confidence": {
|
17
|
+
"type": "number",
|
18
|
+
"minimum": 0,
|
19
|
+
"maximum": 1
|
20
|
+
}
|
21
|
+
},
|
22
|
+
"required": ["summary", "sentiment"]
|
23
|
+
}
|
24
|
+
</meta>
|
25
|
+
|
26
|
+
<!-- Example 2: Tool definition with new parser attribute -->
|
27
|
+
<meta type="tool" name="analyze_text" description="Analyze text sentiment and content" parser="json">
|
28
|
+
{
|
29
|
+
"type": "object",
|
30
|
+
"properties": {
|
31
|
+
"text": {
|
32
|
+
"type": "string",
|
33
|
+
"description": "The text to analyze"
|
34
|
+
},
|
35
|
+
"include_keywords": {
|
36
|
+
"type": "boolean",
|
37
|
+
"description": "Whether to include keyword extraction",
|
38
|
+
"default": false
|
39
|
+
},
|
40
|
+
"language": {
|
41
|
+
"type": "string",
|
42
|
+
"enum": ["en", "es", "fr", "de"],
|
43
|
+
"default": "en"
|
44
|
+
}
|
45
|
+
},
|
46
|
+
"required": ["text"]
|
47
|
+
}
|
48
|
+
</meta>
|
49
|
+
|
50
|
+
<role>Expert text analyzer</role>
|
51
|
+
<task>
|
52
|
+
Analyze the provided text and return insights about its content,
|
53
|
+
sentiment, and other relevant characteristics. Use the analyze_text
|
54
|
+
tool when additional analysis is needed.
|
55
|
+
</task>
|
56
|
+
|
57
|
+
<p>
|
58
|
+
Please analyze this text: "The new POML schema syntax is much clearer
|
59
|
+
and more consistent with modern API standards. The migration from 'lang'
|
60
|
+
to 'parser' attributes makes the intent more obvious."
|
61
|
+
</p>
|
62
|
+
</poml>
|
@@ -0,0 +1,48 @@
|
|
1
|
+
<!-- Example showing backward compatibility support -->
|
2
|
+
<poml>
|
3
|
+
<!-- This still works: Old lang attribute -->
|
4
|
+
<meta type="responseschema" lang="json">
|
5
|
+
{
|
6
|
+
"type": "object",
|
7
|
+
"properties": {
|
8
|
+
"old_format": {"type": "string"}
|
9
|
+
}
|
10
|
+
}
|
11
|
+
</meta>
|
12
|
+
|
13
|
+
<!-- This is the new way: parser attribute -->
|
14
|
+
<meta type="responseschema" parser="json">
|
15
|
+
{
|
16
|
+
"type": "object",
|
17
|
+
"properties": {
|
18
|
+
"new_format": {"type": "string"}
|
19
|
+
}
|
20
|
+
}
|
21
|
+
</meta>
|
22
|
+
|
23
|
+
<!-- Expression evaluation with new syntax -->
|
24
|
+
<meta type="tool" name="calculator" parser="eval">
|
25
|
+
{
|
26
|
+
"type": "object",
|
27
|
+
"properties": {
|
28
|
+
"operation": {"type": "string", "enum": ["add", "subtract", "multiply", "divide"]},
|
29
|
+
"a": {"type": "number"},
|
30
|
+
"b": {"type": "number"}
|
31
|
+
}
|
32
|
+
}
|
33
|
+
</meta>
|
34
|
+
|
35
|
+
<role>Compatibility demonstration assistant</role>
|
36
|
+
<task>
|
37
|
+
Show that both old and new schema syntax work correctly.
|
38
|
+
The Ruby POML implementation supports both for backward compatibility.
|
39
|
+
</task>
|
40
|
+
|
41
|
+
<p>This example demonstrates that our Ruby implementation supports both:</p>
|
42
|
+
<list>
|
43
|
+
<item>Legacy `lang="json"` and `lang="expr"` attributes</item>
|
44
|
+
<item>New `parser="json"` and `parser="eval"` attributes</item>
|
45
|
+
</list>
|
46
|
+
|
47
|
+
<p>When both are present, `parser` takes precedence over `lang`.</p>
|
48
|
+
</poml>
|
@@ -0,0 +1,45 @@
|
|
1
|
+
<poml>
|
2
|
+
<!-- New syntax using standalone components -->
|
3
|
+
<output-schema parser="json">
|
4
|
+
{
|
5
|
+
"type": "object",
|
6
|
+
"properties": {
|
7
|
+
"analysis": {
|
8
|
+
"type": "object",
|
9
|
+
"properties": {
|
10
|
+
"sentiment": { "type": "string", "enum": ["positive", "negative", "neutral"] },
|
11
|
+
"confidence": { "type": "number", "minimum": 0, "maximum": 1 },
|
12
|
+
"key_themes": { "type": "array", "items": { "type": "string" } }
|
13
|
+
},
|
14
|
+
"required": ["sentiment", "confidence", "key_themes"]
|
15
|
+
},
|
16
|
+
"summary": { "type": "string" }
|
17
|
+
},
|
18
|
+
"required": ["analysis", "summary"]
|
19
|
+
}
|
20
|
+
</output-schema>
|
21
|
+
|
22
|
+
<tool-definition name="search_web" description="Search the web for information" parser="json">
|
23
|
+
{
|
24
|
+
"type": "object",
|
25
|
+
"properties": {
|
26
|
+
"query": { "type": "string", "description": "Search query" },
|
27
|
+
"max_results": { "type": "integer", "minimum": 1, "maximum": 10, "default": 5 }
|
28
|
+
},
|
29
|
+
"required": ["query"]
|
30
|
+
}
|
31
|
+
</tool-definition>
|
32
|
+
|
33
|
+
<role>Expert Text Analyst</role>
|
34
|
+
|
35
|
+
<task>
|
36
|
+
Analyze the provided text and return structured insights including sentiment analysis,
|
37
|
+
key themes, and a summary. Use the search_web tool if you need additional context.
|
38
|
+
</task>
|
39
|
+
|
40
|
+
<p>Text to analyze: "The new product launch exceeded all expectations with overwhelmingly positive customer feedback and record-breaking sales numbers."</p>
|
41
|
+
|
42
|
+
<output-format>
|
43
|
+
Return your analysis in the structured format defined by the output schema.
|
44
|
+
</output-format>
|
45
|
+
</poml>
|
data/lib/poml/components/base.rb
CHANGED
@@ -162,7 +162,12 @@ module Poml
|
|
162
162
|
COMPONENT_MAPPING = {}
|
163
163
|
|
164
164
|
def self.render_element(element, context)
|
165
|
-
|
165
|
+
# Try to find component using multiple key formats for compatibility
|
166
|
+
tag_name = element.tag_name
|
167
|
+
component_class = COMPONENT_MAPPING[tag_name] ||
|
168
|
+
COMPONENT_MAPPING[tag_name.to_s] ||
|
169
|
+
COMPONENT_MAPPING[tag_name.to_sym] ||
|
170
|
+
TextComponent
|
166
171
|
component = component_class.new(element, context)
|
167
172
|
component.render
|
168
173
|
end
|
data/lib/poml/components/data.rb
CHANGED
@@ -9,7 +9,7 @@ module Poml
|
|
9
9
|
|
10
10
|
src = get_attribute('src')
|
11
11
|
records_attr = get_attribute('records')
|
12
|
-
|
12
|
+
_columns_attr = get_attribute('columns') # Not used but may be needed for future features
|
13
13
|
parser = get_attribute('parser', 'auto')
|
14
14
|
syntax = get_attribute('syntax')
|
15
15
|
selected_columns = parse_array_attribute('selectedColumns')
|
@@ -85,7 +85,7 @@ module Poml
|
|
85
85
|
else
|
86
86
|
{ records: [], columns: [] }
|
87
87
|
end
|
88
|
-
rescue
|
88
|
+
rescue
|
89
89
|
{ records: [], columns: [] }
|
90
90
|
end
|
91
91
|
|
@@ -138,7 +138,7 @@ module Poml
|
|
138
138
|
records = if records_attr.is_a?(String)
|
139
139
|
begin
|
140
140
|
JSON.parse(records_attr)
|
141
|
-
rescue JSON::ParserError
|
141
|
+
rescue JSON::ParserError
|
142
142
|
# Return empty records on parse error
|
143
143
|
return { records: [], columns: [] }
|
144
144
|
end
|
@@ -479,7 +479,7 @@ module Poml
|
|
479
479
|
base64 = get_attribute('base64')
|
480
480
|
extract_text = get_attribute('extractText', false)
|
481
481
|
selector = get_attribute('selector', 'body')
|
482
|
-
|
482
|
+
_syntax = get_attribute('syntax', 'text') # May be used for future formatting options
|
483
483
|
|
484
484
|
content = if url
|
485
485
|
fetch_webpage_content(url, selector, extract_text)
|
@@ -33,30 +33,14 @@ module Poml
|
|
33
33
|
"- "
|
34
34
|
end
|
35
35
|
|
36
|
-
#
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
if nested_elements.any?
|
41
|
-
# Item has both text and nested elements (like nested lists)
|
42
|
-
nested_content = nested_elements.map { |nested_child|
|
43
|
-
Components.render_element(nested_child, @context)
|
44
|
-
}.join('').strip
|
45
|
-
|
46
|
-
# Format with text content on first line, nested content indented
|
47
|
-
indented_nested = nested_content.split("\n").map { |line|
|
48
|
-
line.strip.empty? ? "" : " #{line}"
|
49
|
-
}.join("\n").strip
|
50
|
-
|
51
|
-
if text_content.empty?
|
52
|
-
items << "#{bullet}#{indented_nested}"
|
53
|
-
else
|
54
|
-
items << "#{bullet}#{text_content} \n\n#{indented_nested}"
|
55
|
-
end
|
36
|
+
# Render all content (text + formatting) together
|
37
|
+
content = if child.children.any?
|
38
|
+
Components.render_element(child, @context).strip
|
56
39
|
else
|
57
|
-
|
58
|
-
items << "#{bullet}#{text_content}"
|
40
|
+
child.content.strip
|
59
41
|
end
|
42
|
+
|
43
|
+
items << "#{bullet}#{content}"
|
60
44
|
end
|
61
45
|
end
|
62
46
|
|
@@ -70,12 +54,26 @@ module Poml
|
|
70
54
|
class ItemComponent < Component
|
71
55
|
def render
|
72
56
|
apply_stylesheet
|
73
|
-
content = @element.content.empty? ? render_children : @element.content.strip
|
74
57
|
|
75
58
|
if xml_mode?
|
59
|
+
content = @element.content.empty? ? render_children : @element.content.strip
|
76
60
|
"<item>#{content}</item>\n"
|
77
61
|
else
|
78
|
-
content
|
62
|
+
# For raw mode, handle mixed content properly
|
63
|
+
if @element.children.any?
|
64
|
+
# Render text and child elements together
|
65
|
+
result = ""
|
66
|
+
@element.children.each do |child|
|
67
|
+
if child.text?
|
68
|
+
result += child.content
|
69
|
+
else
|
70
|
+
result += Components.render_element(child, @context)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
result.strip
|
74
|
+
else
|
75
|
+
@element.content.strip
|
76
|
+
end
|
79
77
|
end
|
80
78
|
end
|
81
79
|
end
|
data/lib/poml/components/meta.rb
CHANGED
@@ -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)
|
@@ -77,18 +83,22 @@ module Poml
|
|
77
83
|
end
|
78
84
|
|
79
85
|
def handle_response_schema
|
80
|
-
|
81
|
-
|
82
|
-
|
86
|
+
# Support both old 'lang' and new 'parser' attributes for compatibility
|
87
|
+
parser_attr = get_attribute('parser') || get_attribute('lang', 'auto')
|
88
|
+
_name = get_attribute('name') # May be used for schema naming in future
|
89
|
+
_description = get_attribute('description') # May be used for schema documentation in future
|
83
90
|
|
84
91
|
content = @element.content.strip
|
85
92
|
|
86
|
-
# Auto-detect format if
|
87
|
-
if
|
88
|
-
|
93
|
+
# Auto-detect format if parser_attr is auto
|
94
|
+
if parser_attr == 'auto'
|
95
|
+
parser_attr = content.start_with?('{') ? 'json' : 'eval'
|
89
96
|
end
|
90
97
|
|
91
|
-
|
98
|
+
# Handle new 'eval' parser type as alias for 'expr'
|
99
|
+
parser_attr = 'expr' if parser_attr == 'eval'
|
100
|
+
|
101
|
+
schema = case parser_attr.downcase
|
92
102
|
when 'json'
|
93
103
|
parse_json_schema(content)
|
94
104
|
when 'expr'
|
@@ -98,6 +108,11 @@ module Poml
|
|
98
108
|
end
|
99
109
|
|
100
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
|
+
|
101
116
|
# Store the schema directly for simplicity
|
102
117
|
@context.response_schema = schema
|
103
118
|
end
|
@@ -106,18 +121,67 @@ module Poml
|
|
106
121
|
def handle_tool_registration
|
107
122
|
name = get_attribute('name')
|
108
123
|
description = get_attribute('description')
|
109
|
-
|
124
|
+
# Support both old 'lang' and new 'parser' attributes for compatibility
|
125
|
+
parser_attr = get_attribute('parser') || get_attribute('lang', 'auto')
|
110
126
|
|
111
|
-
|
127
|
+
content = @element.content.strip
|
128
|
+
|
129
|
+
# Auto-detect format if parser_attr is auto
|
130
|
+
if parser_attr == 'auto'
|
131
|
+
parser_attr = content.start_with?('{') ? 'json' : 'eval'
|
132
|
+
end
|
133
|
+
|
134
|
+
# Handle new 'eval' parser type as alias for 'expr'
|
135
|
+
parser_attr = 'expr' if parser_attr == 'eval'
|
136
|
+
|
137
|
+
schema = case parser_attr.downcase
|
138
|
+
when 'json'
|
139
|
+
parse_json_schema(content)
|
140
|
+
when 'expr'
|
141
|
+
evaluate_expression_schema(content)
|
142
|
+
else
|
143
|
+
nil
|
144
|
+
end
|
145
|
+
|
146
|
+
if schema
|
147
|
+
@context.tools ||= []
|
148
|
+
|
149
|
+
# If name and description are provided as attributes, use them
|
150
|
+
if name
|
151
|
+
tool_def = {
|
152
|
+
'name' => name,
|
153
|
+
'description' => description
|
154
|
+
}
|
155
|
+
# Merge in the parsed schema (should include parameters, etc.)
|
156
|
+
tool_def.merge!(schema) if schema.is_a?(Hash)
|
157
|
+
elsif schema.is_a?(Hash) && schema['name']
|
158
|
+
# If the schema contains the full tool definition, use it directly
|
159
|
+
tool_def = schema
|
160
|
+
else
|
161
|
+
# No valid tool definition found
|
162
|
+
return
|
163
|
+
end
|
164
|
+
|
165
|
+
@context.tools << tool_def
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def handle_tool_registration_with_name(tool_name)
|
170
|
+
description = get_attribute('description')
|
171
|
+
# Support both old 'lang' and new 'parser' attributes for compatibility
|
172
|
+
parser_attr = get_attribute('parser') || get_attribute('lang', 'auto')
|
112
173
|
|
113
174
|
content = @element.content.strip
|
114
175
|
|
115
|
-
# Auto-detect format if
|
116
|
-
if
|
117
|
-
|
176
|
+
# Auto-detect format if parser_attr is auto
|
177
|
+
if parser_attr == 'auto'
|
178
|
+
parser_attr = content.start_with?('{') ? 'json' : 'eval'
|
118
179
|
end
|
119
180
|
|
120
|
-
|
181
|
+
# Handle new 'eval' parser type as alias for 'expr'
|
182
|
+
parser_attr = 'expr' if parser_attr == 'eval'
|
183
|
+
|
184
|
+
schema = case parser_attr.downcase
|
121
185
|
when 'json'
|
122
186
|
parse_json_schema(content)
|
123
187
|
when 'expr'
|
@@ -130,7 +194,7 @@ module Poml
|
|
130
194
|
@context.tools ||= []
|
131
195
|
# Store tool with string keys for JSON compatibility
|
132
196
|
tool_def = {
|
133
|
-
'name' =>
|
197
|
+
'name' => tool_name,
|
134
198
|
'description' => description
|
135
199
|
}
|
136
200
|
# Merge in the parsed schema (should include parameters, etc.)
|
@@ -168,20 +232,24 @@ module Poml
|
|
168
232
|
|
169
233
|
begin
|
170
234
|
JSON.parse(processed_content)
|
171
|
-
rescue JSON::ParserError
|
235
|
+
rescue JSON::ParserError
|
172
236
|
nil
|
173
237
|
end
|
174
238
|
end
|
175
239
|
|
176
240
|
def evaluate_expression_schema(content)
|
177
|
-
#
|
178
|
-
|
241
|
+
# Handle template expressions in the content
|
242
|
+
processed_content = @context.template_engine.substitute(content)
|
243
|
+
|
179
244
|
begin
|
180
|
-
# For
|
181
|
-
# In
|
182
|
-
|
183
|
-
|
184
|
-
|
245
|
+
# For expression schemas, try to parse as JSON first
|
246
|
+
# In the original POML, 'expr' mode evaluates JavaScript expressions
|
247
|
+
# but for simplicity in Ruby, we'll try JSON parsing
|
248
|
+
JSON.parse(processed_content)
|
249
|
+
rescue JSON::ParserError
|
250
|
+
# If JSON parsing fails, return a simple schema object
|
251
|
+
# This is a fallback for complex expressions that can't be easily evaluated
|
252
|
+
{ 'type' => 'string', 'description' => 'Expression schema result' }
|
185
253
|
end
|
186
254
|
end
|
187
255
|
|
@@ -0,0 +1,85 @@
|
|
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 directly for simplicity
|
38
|
+
@context.response_schema = schema
|
39
|
+
end
|
40
|
+
|
41
|
+
# Meta-like components don't produce output
|
42
|
+
''
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def parse_json_schema(content)
|
48
|
+
# Apply template substitution first
|
49
|
+
substituted_content = @context.template_engine.substitute(content)
|
50
|
+
|
51
|
+
begin
|
52
|
+
JSON.parse(substituted_content)
|
53
|
+
rescue JSON::ParserError => e
|
54
|
+
raise Poml::Error, "Invalid JSON schema: #{e.message}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def evaluate_expression_schema(content)
|
59
|
+
# Apply template substitution first
|
60
|
+
substituted_content = @context.template_engine.substitute(content)
|
61
|
+
|
62
|
+
begin
|
63
|
+
# In a real implementation, this would evaluate JavaScript expressions
|
64
|
+
# For now, we'll try to parse as JSON if it looks like JSON,
|
65
|
+
# otherwise treat it as a placeholder
|
66
|
+
if substituted_content.strip.start_with?('{', '[')
|
67
|
+
JSON.parse(substituted_content)
|
68
|
+
else
|
69
|
+
# This would need a JavaScript engine in a real implementation
|
70
|
+
# For now, return a placeholder that indicates expression evaluation
|
71
|
+
{
|
72
|
+
"_expression" => substituted_content,
|
73
|
+
"_note" => "Expression evaluation not implemented in Ruby gem"
|
74
|
+
}
|
75
|
+
end
|
76
|
+
rescue JSON::ParserError
|
77
|
+
# Return expression as-is if it can't be parsed as JSON
|
78
|
+
{
|
79
|
+
"_expression" => substituted_content,
|
80
|
+
"_note" => "Expression evaluation not implemented in Ruby gem"
|
81
|
+
}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,98 @@
|
|
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
|
+
# Support both old 'lang' and new 'parser' attributes for compatibility
|
10
|
+
parser_attr = get_attribute('parser') || get_attribute('lang', 'auto')
|
11
|
+
|
12
|
+
return '' unless name # Name is required for tools
|
13
|
+
|
14
|
+
content = @element.content.strip
|
15
|
+
|
16
|
+
# Auto-detect format if parser_attr is auto
|
17
|
+
if parser_attr == 'auto'
|
18
|
+
parser_attr = content.start_with?('{') ? 'json' : 'eval'
|
19
|
+
end
|
20
|
+
|
21
|
+
# Handle new 'eval' parser type as alias for 'expr'
|
22
|
+
parser_attr = 'expr' if parser_attr == 'eval'
|
23
|
+
|
24
|
+
schema = case parser_attr.downcase
|
25
|
+
when 'json'
|
26
|
+
parse_json_schema(content)
|
27
|
+
when 'expr'
|
28
|
+
evaluate_expression_schema(content)
|
29
|
+
else
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
|
33
|
+
if schema
|
34
|
+
@context.tools ||= []
|
35
|
+
# Store tool with string keys for JSON compatibility
|
36
|
+
tool_def = {
|
37
|
+
'name' => name,
|
38
|
+
'description' => description,
|
39
|
+
'schema' => schema.is_a?(String) ? schema : JSON.generate(schema)
|
40
|
+
}
|
41
|
+
|
42
|
+
# For compatibility with expected structure, also store as parameters
|
43
|
+
if schema.is_a?(Hash)
|
44
|
+
tool_def['parameters'] = schema
|
45
|
+
end
|
46
|
+
|
47
|
+
@context.tools << tool_def
|
48
|
+
end
|
49
|
+
|
50
|
+
# Meta-like components don't produce output
|
51
|
+
''
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def parse_json_schema(content)
|
57
|
+
# Apply template substitution first
|
58
|
+
substituted_content = @context.template_engine.substitute(content)
|
59
|
+
|
60
|
+
begin
|
61
|
+
parsed = JSON.parse(substituted_content)
|
62
|
+
# Return the parsed schema directly - don't wrap it
|
63
|
+
parsed
|
64
|
+
rescue JSON::ParserError => e
|
65
|
+
raise Poml::Error, "Invalid JSON schema for tool '#{get_attribute('name')}': #{e.message}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def evaluate_expression_schema(content)
|
70
|
+
# Apply template substitution first
|
71
|
+
substituted_content = @context.template_engine.substitute(content)
|
72
|
+
|
73
|
+
begin
|
74
|
+
# In a real implementation, this would evaluate JavaScript expressions
|
75
|
+
# For now, we'll try to parse as JSON if it looks like JSON,
|
76
|
+
# otherwise treat it as a placeholder
|
77
|
+
if substituted_content.strip.start_with?('{', '[')
|
78
|
+
parsed = JSON.parse(substituted_content)
|
79
|
+
# Return the parsed schema directly - don't wrap it
|
80
|
+
parsed
|
81
|
+
else
|
82
|
+
# This would need a JavaScript engine in a real implementation
|
83
|
+
# For now, return a placeholder that indicates expression evaluation
|
84
|
+
{
|
85
|
+
"_expression" => substituted_content,
|
86
|
+
"_note" => "Expression evaluation not implemented in Ruby gem"
|
87
|
+
}
|
88
|
+
end
|
89
|
+
rescue JSON::ParserError
|
90
|
+
# Return expression as-is if it can't be parsed as JSON
|
91
|
+
{
|
92
|
+
"_expression" => substituted_content,
|
93
|
+
"_note" => "Expression evaluation not implemented in Ruby gem"
|
94
|
+
}
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -161,7 +161,7 @@ module Poml
|
|
161
161
|
def render_conversation_xml(messages)
|
162
162
|
result = ['<conversation>']
|
163
163
|
messages.each do |msg|
|
164
|
-
speaker = msg['speaker'] || 'human'
|
164
|
+
speaker = msg['speaker'] || msg['role'] || 'human'
|
165
165
|
content = msg['content'] || ''
|
166
166
|
result << " <msg speaker=\"#{speaker}\">#{escape_xml(content)}</msg>"
|
167
167
|
end
|
@@ -172,7 +172,7 @@ module Poml
|
|
172
172
|
def render_conversation_markdown(messages)
|
173
173
|
result = []
|
174
174
|
messages.each do |msg|
|
175
|
-
speaker = msg['speaker'] || 'human'
|
175
|
+
speaker = msg['speaker'] || msg['role'] || 'human'
|
176
176
|
content = msg['content'] || ''
|
177
177
|
|
178
178
|
case speaker.downcase
|
@@ -28,22 +28,6 @@ module Poml
|
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
31
|
-
# Human Message component
|
32
|
-
class HumanMessageComponent < Component
|
33
|
-
def render
|
34
|
-
apply_stylesheet
|
35
|
-
|
36
|
-
content = @element.content.empty? ? render_children : @element.content
|
37
|
-
speaker = get_attribute('speaker', 'human')
|
38
|
-
|
39
|
-
if xml_mode?
|
40
|
-
render_as_xml('human-message', content)
|
41
|
-
else
|
42
|
-
"#{content}\n\n"
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
31
|
# Question-Answer component
|
48
32
|
class QAComponent < Component
|
49
33
|
def render
|
data/lib/poml/components.rb
CHANGED
@@ -14,6 +14,8 @@ require_relative 'components/media'
|
|
14
14
|
require_relative 'components/utilities'
|
15
15
|
require_relative 'components/meta'
|
16
16
|
require_relative 'components/template'
|
17
|
+
require_relative 'components/output_schema'
|
18
|
+
require_relative 'components/tool_definition'
|
17
19
|
|
18
20
|
module Poml
|
19
21
|
# Update the component mapping after all components are loaded
|
@@ -126,6 +128,14 @@ module Poml
|
|
126
128
|
# Meta components
|
127
129
|
meta: MetaComponent,
|
128
130
|
Meta: MetaComponent,
|
131
|
+
'output-schema': OutputSchemaComponent,
|
132
|
+
'outputschema': OutputSchemaComponent,
|
133
|
+
OutputSchema: OutputSchemaComponent,
|
134
|
+
'tool-definition': ToolDefinitionComponent,
|
135
|
+
'tooldefinition': ToolDefinitionComponent,
|
136
|
+
ToolDefinition: ToolDefinitionComponent,
|
137
|
+
tool: ToolDefinitionComponent, # 'tool' is an alias for 'tool-definition'
|
138
|
+
Tool: ToolDefinitionComponent,
|
129
139
|
|
130
140
|
# Template components
|
131
141
|
include: IncludeComponent,
|
data/lib/poml/parser.rb
CHANGED
@@ -163,7 +163,7 @@ module Poml
|
|
163
163
|
stylesheet = JSON.parse(stylesheet_text)
|
164
164
|
@context.stylesheet.merge!(stylesheet) if stylesheet.is_a?(Hash)
|
165
165
|
end
|
166
|
-
rescue
|
166
|
+
rescue
|
167
167
|
# Silently fail JSON parsing errors
|
168
168
|
end
|
169
169
|
end
|
@@ -235,7 +235,9 @@ module Poml
|
|
235
235
|
|
236
236
|
def preprocess_void_elements(content)
|
237
237
|
# List of HTML void elements that should be self-closing in XML
|
238
|
-
|
238
|
+
# Note: 'meta' is removed from this list because POML meta components can have content
|
239
|
+
# Note: 'input' is removed because POML input components (for examples) can have content
|
240
|
+
void_elements = %w[br hr img area base col embed link param source track wbr]
|
239
241
|
|
240
242
|
# Convert <element> to <element/> for void elements, but only if not already self-closing
|
241
243
|
void_elements.each do |element|
|
data/lib/poml/renderer.rb
CHANGED
@@ -134,7 +134,7 @@ module Poml
|
|
134
134
|
# Check for system-oriented components
|
135
135
|
has_role = tag_names.include?(:role)
|
136
136
|
has_task = tag_names.include?(:task)
|
137
|
-
|
137
|
+
_has_hint = tag_names.include?(:hint) # Prefix with underscore to indicate intentionally unused
|
138
138
|
|
139
139
|
# Check for human-oriented components or content that suggests user interaction
|
140
140
|
has_document = tag_names.include?(:document) || tag_names.include?('Document')
|
data/lib/poml/version.rb
CHANGED
data/lib/poml.rb
CHANGED
@@ -17,69 +17,15 @@ module Poml
|
|
17
17
|
# - stylesheet: Optional stylesheet as string or hash
|
18
18
|
# - chat: Boolean indicating chat format (default: true)
|
19
19
|
# - output_file: Path to save output (optional)
|
20
|
+
# Process POML markup and return the rendered result
|
21
|
+
#
|
22
|
+
# Parameters:
|
23
|
+
# - markup: POML markup string or file path
|
20
24
|
# - format: 'raw', 'dict', 'openai_chat', 'langchain', 'pydantic' (default: 'dict')
|
21
|
-
# -
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
variables: context || {},
|
26
|
-
stylesheet: stylesheet,
|
27
|
-
chat: chat
|
28
|
-
)
|
29
|
-
|
30
|
-
# Read markup content and set source path
|
31
|
-
content = if File.exist?(markup.to_s)
|
32
|
-
poml_context.source_path = File.expand_path(markup.to_s)
|
33
|
-
File.read(markup)
|
34
|
-
else
|
35
|
-
markup.to_s
|
36
|
-
end
|
37
|
-
|
38
|
-
# Parse POML content
|
39
|
-
parser = Parser.new(poml_context)
|
40
|
-
parsed_elements = parser.parse(content)
|
41
|
-
|
42
|
-
# Render to the desired format
|
43
|
-
renderer = Renderer.new(poml_context)
|
44
|
-
result = renderer.render(parsed_elements, format)
|
45
|
-
|
46
|
-
# Save to file if specified
|
47
|
-
if output_file
|
48
|
-
File.write(output_file, result)
|
49
|
-
end
|
50
|
-
|
51
|
-
result
|
52
|
-
end
|
53
|
-
|
54
|
-
def self.parse(content, context: nil)
|
55
|
-
context ||= Context.new
|
56
|
-
parser = Parser.new(context)
|
57
|
-
parser.parse(content)
|
58
|
-
end
|
59
|
-
|
60
|
-
def self.render(content, format: 'text', context: nil, **options)
|
61
|
-
context ||= Context.new(**options)
|
62
|
-
elements = parse(content, context: context)
|
63
|
-
renderer = Renderer.new(context)
|
64
|
-
renderer.render(elements, format)
|
65
|
-
end
|
66
|
-
|
67
|
-
# Convenience method for quick text rendering
|
68
|
-
def self.to_text(content, **options)
|
69
|
-
render(content, format: 'raw', **options)
|
70
|
-
end
|
71
|
-
|
72
|
-
# Convenience method for chat format
|
73
|
-
def self.to_chat(content, **options)
|
74
|
-
render(content, format: 'openai_chat', **options)
|
75
|
-
end
|
76
|
-
|
77
|
-
# Convenience method for dict format
|
78
|
-
def self.to_dict(content, **options)
|
79
|
-
render(content, format: 'dict', **options)
|
80
|
-
end
|
81
|
-
|
82
|
-
# Legacy method for backward compatibility
|
25
|
+
# - context/variables: Hash of template variables
|
26
|
+
# - stylesheet: CSS/styling rules
|
27
|
+
# - chat: Enable chat mode (default: true)
|
28
|
+
# - output_file: File path to write output to
|
83
29
|
def self.process(markup:, format: 'dict', **options)
|
84
30
|
# Handle file paths
|
85
31
|
content = if File.exist?(markup)
|
@@ -116,4 +62,32 @@ module Poml
|
|
116
62
|
result
|
117
63
|
end
|
118
64
|
end
|
65
|
+
|
66
|
+
def self.parse(content, context: nil)
|
67
|
+
context ||= Context.new
|
68
|
+
parser = Parser.new(context)
|
69
|
+
parser.parse(content)
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.render(content, format: 'text', context: nil, **options)
|
73
|
+
context ||= Context.new(**options)
|
74
|
+
elements = parse(content, context: context)
|
75
|
+
renderer = Renderer.new(context)
|
76
|
+
renderer.render(elements, format)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Convenience method for quick text rendering
|
80
|
+
def self.to_text(content, **options)
|
81
|
+
render(content, format: 'raw', **options)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Convenience method for chat format
|
85
|
+
def self.to_chat(content, **options)
|
86
|
+
render(content, format: 'openai_chat', **options)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Convenience method for dict format
|
90
|
+
def self.to_dict(content, **options)
|
91
|
+
render(content, format: 'dict', **options)
|
92
|
+
end
|
119
93
|
end
|
data/readme.md
CHANGED
@@ -6,6 +6,8 @@ A Ruby implementation of the POML (Prompt Oriented Markup Language) interpreter.
|
|
6
6
|
|
7
7
|
This is a **Ruby port** of the original [POML library](https://github.com/microsoft/poml) developed by Microsoft, which was originally implemented in JavaScript/TypeScript and Python. This Ruby gem is designed to be **fully compatible** with the original POML specification and will **closely follow** the development of the original library to maintain feature parity.
|
8
8
|
|
9
|
+
> **🔄 Recent Update**: The original POML library has introduced **breaking changes** in schema definitions. Schema attributes have been renamed from `lang` to `parser` (e.g., `lang="json"` → `parser="json"`, `lang="expr"` → `parser="eval"`). Our Ruby implementation is being updated to maintain compatibility with these changes.
|
10
|
+
|
9
11
|
## Demo Video
|
10
12
|
|
11
13
|
[](https://youtu.be/b9WDcFsKixo)
|
@@ -205,6 +207,8 @@ Customize component appearance:
|
|
205
207
|
- ✅ Stylesheet support
|
206
208
|
- ✅ Command-line interface
|
207
209
|
- ✅ Chat vs non-chat modes
|
210
|
+
- 🔄 **Schema definitions** (updating to new `parser` attribute syntax)
|
211
|
+
- 🔄 **Tool registration** (updating for enhanced tool use support)
|
208
212
|
|
209
213
|
## Document Support
|
210
214
|
|
@@ -239,7 +243,7 @@ Build and install locally:
|
|
239
243
|
|
240
244
|
```bash
|
241
245
|
gem build poml.gemspec
|
242
|
-
gem install poml-0.0.
|
246
|
+
gem install poml-0.0.3.gem
|
243
247
|
```
|
244
248
|
|
245
249
|
## License
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: poml
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ghennadii Mirosnicenco
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-08-
|
10
|
+
date: 2025-08-22 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: rexml
|
@@ -62,6 +62,9 @@ files:
|
|
62
62
|
- examples/201_orders_qa.poml
|
63
63
|
- examples/202_arc_agi.poml
|
64
64
|
- examples/301_generate_poml.poml
|
65
|
+
- examples/301_new_schema_syntax.poml
|
66
|
+
- examples/302_schema_compatibility.poml
|
67
|
+
- examples/303_new_component_syntax.poml
|
65
68
|
- examples/README.md
|
66
69
|
- examples/assets/101_jerry_mouse.jpg
|
67
70
|
- examples/assets/101_tom_and_jerry.docx
|
@@ -108,9 +111,11 @@ files:
|
|
108
111
|
- lib/poml/components/lists.rb
|
109
112
|
- lib/poml/components/media.rb
|
110
113
|
- lib/poml/components/meta.rb
|
114
|
+
- lib/poml/components/output_schema.rb
|
111
115
|
- lib/poml/components/styling.rb
|
112
116
|
- lib/poml/components/template.rb
|
113
117
|
- lib/poml/components/text.rb
|
118
|
+
- lib/poml/components/tool_definition.rb
|
114
119
|
- lib/poml/components/utilities.rb
|
115
120
|
- lib/poml/components/workflow.rb
|
116
121
|
- lib/poml/components_new.rb
|