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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 57f8a3904a43267d79cbc6fb76465dd51c05da5923e58635304242eb90b04b96
4
- data.tar.gz: 591512861013d2ae3d302b342b4678696a4d3691c98a26a2d9cdcd042ec70422
3
+ metadata.gz: '08ad238aa1b84997e3120a4379473c190293c3e6cb4a9fc08fe373457e3d10b7'
4
+ data.tar.gz: '028e54bb00290ffd9317902101435a2024a15972d6987e4acf9773b00ccba679'
5
5
  SHA512:
6
- metadata.gz: ba9aeb9b1e06de010b5daa9489d847dc41f5f5d73a9a02e70d81432e997b1c3a3a32116cf9d85ee2f4c70755024201272e62951381cf6363be59dc807c6c55b0
7
- data.tar.gz: 2f763c41a44b095f64fbe75cc64f58af266b50c8d966e0d44e6c382945a21fdde9abdd83a49b17ab57608c90a8481fb3066eca6dbc3633533e16269af1693f61
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>
@@ -162,7 +162,12 @@ module Poml
162
162
  COMPONENT_MAPPING = {}
163
163
 
164
164
  def self.render_element(element, context)
165
- component_class = COMPONENT_MAPPING[element.tag_name] || TextComponent
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
@@ -135,7 +135,7 @@ module Poml
135
135
  end
136
136
  end
137
137
  return content unless content.empty?
138
- rescue => e
138
+ rescue
139
139
  # Zip gem not available or error occurred
140
140
  end
141
141
 
@@ -9,7 +9,7 @@ module Poml
9
9
 
10
10
  src = get_attribute('src')
11
11
  records_attr = get_attribute('records')
12
- columns_attr = get_attribute('columns')
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 => e
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 => e
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
- syntax = get_attribute('syntax', 'text')
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
- # Get text content and nested elements separately
37
- text_content = child.content.strip
38
- nested_elements = child.children.reject { |c| c.tag_name == :text }
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
- # Simple text-only item
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
@@ -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
- lang = get_attribute('lang', 'auto')
81
- name = get_attribute('name')
82
- description = get_attribute('description')
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 lang is auto
87
- if lang == 'auto'
88
- lang = content.start_with?('{') ? 'json' : 'expr'
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
- schema = case lang.downcase
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
- lang = get_attribute('lang', 'auto')
124
+ # Support both old 'lang' and new 'parser' attributes for compatibility
125
+ parser_attr = get_attribute('parser') || get_attribute('lang', 'auto')
110
126
 
111
- return unless name
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 lang is auto
116
- if lang == 'auto'
117
- lang = content.start_with?('{') ? 'json' : 'expr'
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
- schema = case lang.downcase
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' => 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 => e
235
+ rescue JSON::ParserError
172
236
  nil
173
237
  end
174
238
  end
175
239
 
176
240
  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
241
+ # Handle template expressions in the content
242
+ processed_content = @context.template_engine.substitute(content)
243
+
179
244
  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
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
@@ -25,7 +25,7 @@ module Poml
25
25
  stylesheet = JSON.parse(stylesheet_content)
26
26
  @context.stylesheet.merge!(stylesheet) if stylesheet.is_a?(Hash)
27
27
  end
28
- rescue => e
28
+ rescue
29
29
  # Silently fail JSON parsing errors
30
30
  end
31
31
 
@@ -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
@@ -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 => e
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
- void_elements = %w[br hr img input area base col embed link meta param source track wbr]
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
- has_hint = tag_names.include?(:hint)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Poml
4
- VERSION = "0.0.3"
4
+ VERSION = "0.0.6"
5
5
  end
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
- # - extra_args: Array of extra CLI args (ignored in pure Ruby implementation)
22
- def self.process(markup:, context: nil, stylesheet: nil, chat: true, output_file: nil, format: 'dict', extra_args: [])
23
- # Create POML context
24
- poml_context = Context.new(
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
  [![The 5-minute guide to POML](https://i3.ytimg.com/vi/b9WDcFsKixo/maxresdefault.jpg)](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.1.gem
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.3
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-19 00:00:00.000000000 Z
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