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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/docs/tutorial/advanced/performance.md +695 -0
  3. data/docs/tutorial/advanced/tool-registration.md +776 -0
  4. data/docs/tutorial/basic-usage.md +351 -0
  5. data/docs/tutorial/components/chat-components.md +552 -0
  6. data/docs/tutorial/components/formatting.md +623 -0
  7. data/docs/tutorial/components/index.md +366 -0
  8. data/docs/tutorial/components/media-components.md +259 -0
  9. data/docs/tutorial/components/schema-components.md +668 -0
  10. data/docs/tutorial/index.md +184 -0
  11. data/docs/tutorial/output-formats.md +688 -0
  12. data/docs/tutorial/quickstart.md +30 -0
  13. data/docs/tutorial/template-engine.md +540 -0
  14. data/examples/303_new_component_syntax.poml +45 -0
  15. data/lib/poml/components/base.rb +150 -3
  16. data/lib/poml/components/content.rb +10 -3
  17. data/lib/poml/components/data.rb +539 -19
  18. data/lib/poml/components/examples.rb +235 -1
  19. data/lib/poml/components/formatting.rb +184 -18
  20. data/lib/poml/components/layout.rb +7 -2
  21. data/lib/poml/components/lists.rb +69 -35
  22. data/lib/poml/components/meta.rb +191 -6
  23. data/lib/poml/components/output_schema.rb +103 -0
  24. data/lib/poml/components/template.rb +72 -61
  25. data/lib/poml/components/text.rb +30 -1
  26. data/lib/poml/components/tool.rb +81 -0
  27. data/lib/poml/components/tool_definition.rb +427 -0
  28. data/lib/poml/components/tools.rb +14 -0
  29. data/lib/poml/components/utilities.rb +34 -18
  30. data/lib/poml/components.rb +29 -0
  31. data/lib/poml/context.rb +19 -4
  32. data/lib/poml/parser.rb +90 -64
  33. data/lib/poml/renderer.rb +191 -9
  34. data/lib/poml/template_engine.rb +138 -13
  35. data/lib/poml/version.rb +1 -1
  36. data/lib/poml.rb +16 -1
  37. data/readme.md +154 -27
  38. metadata +34 -4
  39. 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
- content = @element.content.empty? ? render_children : @element.content
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
- content
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
- content = @element.content.empty? ? render_children : @element.content
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
- content
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
- content = @element.content.empty? ? render_children : @element.content
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
- content
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 displaying chat conversations
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("\n")
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 = File.read(full_path, encoding: 'utf-8')
290
+ content = read_file_with_encoding(full_path)
275
291
  item[:content] = content
276
- rescue
277
- item[:content] = '[Binary file or read error]'
292
+ rescue => e
293
+ item[:content] = "[Error reading file: #{e.message}]"
278
294
  end
279
295
  end
280
296
 
@@ -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 = nil
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