poml 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,508 @@
1
+ module Poml
2
+ # AI Message component for wrapping AI responses
3
+ class AiMessageComponent < Component
4
+ def render
5
+ apply_stylesheet
6
+
7
+ content = @element.content.empty? ? render_children : @element.content
8
+
9
+ # Add to structured chat messages if context supports it
10
+ if @context.respond_to?(:chat_messages)
11
+ @context.chat_messages << {
12
+ 'role' => 'assistant',
13
+ 'content' => content
14
+ }
15
+ # Return empty for raw format to avoid duplication
16
+ return ''
17
+ end
18
+
19
+ if xml_mode?
20
+ render_as_xml('ai-msg', content, { speaker: 'ai' })
21
+ else
22
+ content
23
+ end
24
+ end
25
+ end
26
+
27
+ # Human Message component for wrapping user messages
28
+ class HumanMessageComponent < Component
29
+ def render
30
+ apply_stylesheet
31
+
32
+ content = @element.content.empty? ? render_children : @element.content
33
+
34
+ # Add to structured chat messages if context supports it
35
+ if @context.respond_to?(:chat_messages)
36
+ @context.chat_messages << {
37
+ 'role' => 'user',
38
+ 'content' => content
39
+ }
40
+ # Return empty for raw format to avoid duplication
41
+ return ''
42
+ end
43
+
44
+ if xml_mode?
45
+ render_as_xml('user-msg', content, { speaker: 'human' })
46
+ else
47
+ content
48
+ end
49
+ end
50
+ end
51
+
52
+ # System Message component for wrapping system messages
53
+ class SystemMessageComponent < Component
54
+ def render
55
+ apply_stylesheet
56
+
57
+ content = @element.content.empty? ? render_children : @element.content
58
+
59
+ # Add to structured chat messages if context supports it
60
+ if @context.respond_to?(:chat_messages)
61
+ @context.chat_messages << {
62
+ 'role' => 'system',
63
+ 'content' => content
64
+ }
65
+ # Return empty for raw format to avoid duplication
66
+ return ''
67
+ end
68
+
69
+ if xml_mode?
70
+ render_as_xml('system-msg', content, { speaker: 'system' })
71
+ else
72
+ content
73
+ end
74
+ end
75
+ end
76
+
77
+ # Message Content component for displaying message content
78
+ class MessageContentComponent < Component
79
+ def render
80
+ apply_stylesheet
81
+
82
+ content_attr = get_attribute('content')
83
+
84
+ if content_attr.is_a?(Array)
85
+ # Handle array of content items
86
+ content_attr.map { |item|
87
+ item.is_a?(String) ? item : item.to_s
88
+ }.join('')
89
+ elsif content_attr.is_a?(String)
90
+ content_attr
91
+ else
92
+ content_attr.to_s
93
+ end
94
+ end
95
+ end
96
+
97
+ # Conversation component for displaying chat conversations
98
+ class ConversationComponent < Component
99
+ def render
100
+ apply_stylesheet
101
+
102
+ messages_attr = get_attribute('messages')
103
+ messages = if messages_attr.is_a?(String)
104
+ begin
105
+ require 'json'
106
+ JSON.parse(messages_attr)
107
+ rescue JSON::ParserError
108
+ []
109
+ end
110
+ else
111
+ messages_attr || []
112
+ end
113
+
114
+ selected_messages = get_attribute('selectedMessages')
115
+
116
+ # Apply message selection if specified
117
+ if selected_messages
118
+ messages = apply_message_selection(messages, selected_messages)
119
+ end
120
+
121
+ if xml_mode?
122
+ render_conversation_xml(messages)
123
+ else
124
+ render_conversation_markdown(messages)
125
+ end
126
+ end
127
+
128
+ private
129
+
130
+ def apply_message_selection(messages, selection)
131
+ return messages unless selection
132
+
133
+ if selection.include?(':')
134
+ # Handle slice notation like "2:4" or "-6:"
135
+ start_idx, end_idx = parse_slice(selection, messages.length)
136
+ messages[start_idx...end_idx] || []
137
+ elsif selection.is_a?(Integer)
138
+ # Single message index
139
+ [messages[selection]].compact
140
+ else
141
+ messages
142
+ end
143
+ end
144
+
145
+ def parse_slice(slice_str, total_length)
146
+ if slice_str.start_with?('-') && slice_str.end_with?(':')
147
+ # Handle "-6:" (last 6 messages)
148
+ count = slice_str[1..-2].to_i
149
+ [total_length - count, total_length]
150
+ elsif slice_str.include?(':')
151
+ parts = slice_str.split(':')
152
+ start_idx = parts[0].empty? ? 0 : parts[0].to_i
153
+ end_idx = parts[1].empty? ? total_length : parts[1].to_i
154
+ [start_idx, end_idx]
155
+ else
156
+ index = slice_str.to_i
157
+ [index, index + 1]
158
+ end
159
+ end
160
+
161
+ def render_conversation_xml(messages)
162
+ result = ['<conversation>']
163
+ messages.each do |msg|
164
+ speaker = msg['speaker'] || 'human'
165
+ content = msg['content'] || ''
166
+ result << " <msg speaker=\"#{speaker}\">#{escape_xml(content)}</msg>"
167
+ end
168
+ result << '</conversation>'
169
+ result.join("\n")
170
+ end
171
+
172
+ def render_conversation_markdown(messages)
173
+ result = []
174
+ messages.each do |msg|
175
+ speaker = msg['speaker'] || 'human'
176
+ content = msg['content'] || ''
177
+
178
+ case speaker.downcase
179
+ when 'human', 'user'
180
+ result << "**Human:** #{content}"
181
+ when 'ai', 'assistant'
182
+ result << "**Assistant:** #{content}"
183
+ when 'system'
184
+ result << "**System:** #{content}"
185
+ else
186
+ result << "**#{speaker.capitalize}:** #{content}"
187
+ end
188
+ result << ""
189
+ end
190
+ result.join("\n")
191
+ end
192
+
193
+ def escape_xml(text)
194
+ text.to_s.gsub('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;').gsub('"', '&quot;')
195
+ end
196
+ end
197
+
198
+ # Folder component for displaying directory structures
199
+ class FolderComponent < Component
200
+ require 'find'
201
+
202
+ def render
203
+ apply_stylesheet
204
+
205
+ src = get_attribute('src')
206
+ filter = get_attribute('filter')
207
+ max_depth = get_attribute('maxDepth', 3).to_i
208
+ show_content = get_attribute('showContent', false)
209
+ syntax = get_attribute('syntax', 'text')
210
+
211
+ return '[Folder: no src specified]' unless src
212
+ return '[Folder: directory not found]' unless Dir.exist?(src)
213
+
214
+ tree_data = build_tree_structure(src, filter, max_depth, show_content)
215
+
216
+ if xml_mode?
217
+ render_folder_xml(tree_data)
218
+ else
219
+ case syntax
220
+ when 'markdown'
221
+ render_folder_markdown(tree_data)
222
+ when 'json'
223
+ require 'json'
224
+ JSON.pretty_generate(tree_data)
225
+ else
226
+ render_folder_text(tree_data)
227
+ end
228
+ end
229
+ end
230
+
231
+ private
232
+
233
+ def build_tree_structure(path, filter, max_depth, show_content, current_depth = 0)
234
+ return nil if current_depth >= max_depth
235
+
236
+ items = []
237
+
238
+ begin
239
+ Dir.entries(path).sort.each do |entry|
240
+ next if entry.start_with?('.')
241
+
242
+ full_path = File.join(path, entry)
243
+
244
+ if File.directory?(full_path)
245
+ # Check if directory should be included
246
+ if !filter || entry.match?(Regexp.new(filter))
247
+ sub_items = build_tree_structure(full_path, filter, max_depth, show_content, current_depth + 1)
248
+ if sub_items && !sub_items.empty?
249
+ items << {
250
+ name: "#{entry}/",
251
+ type: 'directory',
252
+ children: sub_items
253
+ }
254
+ end
255
+ end
256
+ else
257
+ # Check if file should be included
258
+ if !filter || entry.match?(Regexp.new(filter))
259
+ item = {
260
+ name: entry,
261
+ type: 'file'
262
+ }
263
+
264
+ if show_content
265
+ begin
266
+ content = File.read(full_path, encoding: 'utf-8')
267
+ item[:content] = content
268
+ rescue
269
+ item[:content] = '[Binary file or read error]'
270
+ end
271
+ end
272
+
273
+ items << item
274
+ end
275
+ end
276
+ end
277
+ rescue => e
278
+ return [{ name: "[Error: #{e.message}]", type: 'error' }]
279
+ end
280
+
281
+ items
282
+ end
283
+
284
+ def render_folder_text(items, indent = 0)
285
+ result = []
286
+ prefix = ' ' * indent
287
+
288
+ items.each do |item|
289
+ result << "#{prefix}#{item[:name]}"
290
+
291
+ if item[:content]
292
+ content_lines = item[:content].split("\n")
293
+ content_lines.each do |line|
294
+ result << "#{prefix} #{line}"
295
+ end
296
+ result << ""
297
+ end
298
+
299
+ if item[:children]
300
+ result << render_folder_text(item[:children], indent + 1)
301
+ end
302
+ end
303
+
304
+ result.join("\n")
305
+ end
306
+
307
+ def render_folder_markdown(items, indent = 0)
308
+ result = []
309
+ prefix = ' ' * indent
310
+
311
+ items.each do |item|
312
+ if item[:type] == 'directory'
313
+ result << "#{prefix}- **#{item[:name]}**"
314
+ else
315
+ result << "#{prefix}- #{item[:name]}"
316
+ end
317
+
318
+ if item[:content]
319
+ result << "#{prefix} ```"
320
+ item[:content].split("\n").each do |line|
321
+ result << "#{prefix} #{line}"
322
+ end
323
+ result << "#{prefix} ```"
324
+ result << ""
325
+ end
326
+
327
+ if item[:children]
328
+ result << render_folder_markdown(item[:children], indent + 1)
329
+ end
330
+ end
331
+
332
+ result.join("\n")
333
+ end
334
+
335
+ def render_folder_xml(items)
336
+ result = ['<folder>']
337
+ items.each do |item|
338
+ if item[:type] == 'directory'
339
+ result << " <directory name=\"#{escape_xml(item[:name])}\">"
340
+ if item[:children]
341
+ render_folder_xml_items(item[:children], result, 2)
342
+ end
343
+ result << " </directory>"
344
+ else
345
+ if item[:content]
346
+ result << " <file name=\"#{escape_xml(item[:name])}\">"
347
+ result << " <content>#{escape_xml(item[:content])}</content>"
348
+ result << " </file>"
349
+ else
350
+ result << " <file name=\"#{escape_xml(item[:name])}\"/>"
351
+ end
352
+ end
353
+ end
354
+ result << '</folder>'
355
+ result.join("\n")
356
+ end
357
+
358
+ def render_folder_xml_items(items, result, indent_level)
359
+ indent = ' ' * indent_level
360
+ items.each do |item|
361
+ if item[:type] == 'directory'
362
+ result << "#{indent}<directory name=\"#{escape_xml(item[:name])}\">"
363
+ if item[:children]
364
+ render_folder_xml_items(item[:children], result, indent_level + 1)
365
+ end
366
+ result << "#{indent}</directory>"
367
+ else
368
+ if item[:content]
369
+ result << "#{indent}<file name=\"#{escape_xml(item[:name])}\">"
370
+ result << "#{indent} <content>#{escape_xml(item[:content])}</content>"
371
+ result << "#{indent}</file>"
372
+ else
373
+ result << "#{indent}<file name=\"#{escape_xml(item[:name])}\"/>"
374
+ end
375
+ end
376
+ end
377
+ end
378
+
379
+ def escape_xml(text)
380
+ text.to_s.gsub('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;').gsub('"', '&quot;')
381
+ end
382
+ end
383
+
384
+ # Tree component for rendering tree structures
385
+ class TreeComponent < Component
386
+ def render
387
+ apply_stylesheet
388
+
389
+ items_attr = get_attribute('items')
390
+ items = if items_attr.is_a?(String)
391
+ begin
392
+ require 'json'
393
+ JSON.parse(items_attr)
394
+ rescue JSON::ParserError
395
+ []
396
+ end
397
+ else
398
+ items_attr || []
399
+ end
400
+
401
+ show_content = get_attribute('showContent', false)
402
+ syntax = get_attribute('syntax', 'text')
403
+
404
+ if xml_mode?
405
+ render_tree_xml(items, show_content)
406
+ else
407
+ case syntax
408
+ when 'markdown'
409
+ render_tree_markdown(items, show_content)
410
+ when 'json'
411
+ require 'json'
412
+ JSON.pretty_generate(items)
413
+ else
414
+ render_tree_text(items, show_content)
415
+ end
416
+ end
417
+ end
418
+
419
+ private
420
+
421
+ def render_tree_text(items, show_content, indent = 0)
422
+ result = []
423
+ prefix = ' ' * indent
424
+
425
+ items.each do |item|
426
+ result << "#{prefix}#{item['name'] || item[:name]}"
427
+
428
+ if show_content && (item['content'] || item[:content])
429
+ content = item['content'] || item[:content]
430
+ content.to_s.split("\n").each do |line|
431
+ result << "#{prefix} #{line}"
432
+ end
433
+ end
434
+
435
+ children = item['children'] || item[:children]
436
+ if children && !children.empty?
437
+ result << render_tree_text(children, show_content, indent + 1)
438
+ end
439
+ end
440
+
441
+ result.join("\n")
442
+ end
443
+
444
+ def render_tree_markdown(items, show_content, indent = 0)
445
+ result = []
446
+ prefix = ' ' * indent
447
+
448
+ items.each do |item|
449
+ name = item['name'] || item[:name]
450
+ result << "#{prefix}- #{name}"
451
+
452
+ if show_content && (item['content'] || item[:content])
453
+ content = item['content'] || item[:content]
454
+ result << "#{prefix} ```"
455
+ content.to_s.split("\n").each do |line|
456
+ result << "#{prefix} #{line}"
457
+ end
458
+ result << "#{prefix} ```"
459
+ end
460
+
461
+ children = item['children'] || item[:children]
462
+ if children && !children.empty?
463
+ result << render_tree_markdown(children, show_content, indent + 1)
464
+ end
465
+ end
466
+
467
+ result.join("\n")
468
+ end
469
+
470
+ def render_tree_xml(items, show_content, indent_level = 1)
471
+ result = ['<tree>']
472
+ render_tree_xml_items(items, result, show_content, indent_level)
473
+ result << '</tree>'
474
+ result.join("\n")
475
+ end
476
+
477
+ def render_tree_xml_items(items, result, show_content, indent_level)
478
+ indent = ' ' * indent_level
479
+
480
+ items.each do |item|
481
+ name = item['name'] || item[:name]
482
+ children = item['children'] || item[:children]
483
+ content = item['content'] || item[:content] if show_content
484
+
485
+ if children && !children.empty?
486
+ result << "#{indent}<item name=\"#{escape_xml(name)}\">"
487
+ if content
488
+ result << "#{indent} <content>#{escape_xml(content)}</content>"
489
+ end
490
+ render_tree_xml_items(children, result, show_content, indent_level + 1)
491
+ result << "#{indent}</item>"
492
+ else
493
+ if content
494
+ result << "#{indent}<item name=\"#{escape_xml(name)}\">"
495
+ result << "#{indent} <content>#{escape_xml(content)}</content>"
496
+ result << "#{indent}</item>"
497
+ else
498
+ result << "#{indent}<item name=\"#{escape_xml(name)}\"/>"
499
+ end
500
+ end
501
+ end
502
+ end
503
+
504
+ def escape_xml(text)
505
+ text.to_s.gsub('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;').gsub('"', '&quot;')
506
+ end
507
+ end
508
+ end
@@ -9,39 +9,128 @@ require_relative 'components/lists'
9
9
  require_relative 'components/layout'
10
10
  require_relative 'components/workflow'
11
11
  require_relative 'components/styling'
12
+ require_relative 'components/formatting'
13
+ require_relative 'components/media'
14
+ require_relative 'components/utilities'
15
+ require_relative 'components/meta'
16
+ require_relative 'components/template'
12
17
 
13
18
  module Poml
14
19
  # Update the component mapping after all components are loaded
15
20
  Components::COMPONENT_MAPPING.merge!({
21
+ # Basic components
16
22
  text: TextComponent,
23
+ poml: TextComponent,
24
+ p: ParagraphComponent,
25
+
26
+ # Formatting components
27
+ b: BoldComponent,
28
+ bold: BoldComponent,
29
+ i: ItalicComponent,
30
+ italic: ItalicComponent,
31
+ u: UnderlineComponent,
32
+ underline: UnderlineComponent,
33
+ s: StrikethroughComponent,
34
+ strike: StrikethroughComponent,
35
+ strikethrough: StrikethroughComponent,
36
+ span: InlineComponent,
37
+ inline: InlineComponent,
38
+ h: HeaderComponent,
39
+ header: HeaderComponent,
40
+ br: NewlineComponent,
41
+ newline: NewlineComponent,
42
+ code: CodeComponent,
43
+ section: SubContentComponent,
44
+ subcontent: SubContentComponent,
45
+
46
+ # Media components
47
+ audio: AudioComponent,
48
+
49
+ # Instruction components
17
50
  role: RoleComponent,
18
51
  task: TaskComponent,
19
52
  hint: HintComponent,
53
+
54
+ # Content components
20
55
  document: DocumentComponent,
21
56
  Document: DocumentComponent, # Capitalized version
57
+
58
+ # Data components
22
59
  table: TableComponent,
23
60
  Table: TableComponent, # Capitalized version
24
61
  img: ImageComponent,
25
- p: ParagraphComponent,
62
+ obj: ObjectComponent,
63
+ object: ObjectComponent,
64
+ dataobj: ObjectComponent,
65
+ 'data-obj': ObjectComponent,
66
+ webpage: WebpageComponent,
67
+
68
+ # Example components
26
69
  example: ExampleComponent,
27
70
  input: InputComponent,
28
71
  output: OutputComponent,
29
72
  'output-format': OutputFormatComponent,
30
73
  'outputformat': OutputFormatComponent,
74
+ examples: ExampleSetComponent,
75
+ 'example-set': ExampleSetComponent,
76
+ introducer: IntroducerComponent,
77
+
78
+ # List components
31
79
  list: ListComponent,
32
80
  item: ItemComponent,
81
+
82
+ # Layout components
33
83
  cp: CPComponent,
84
+ 'captioned-paragraph': CPComponent,
85
+
86
+ # Workflow components
34
87
  'stepwise-instructions': StepwiseInstructionsComponent,
35
88
  'stepwiseinstructions': StepwiseInstructionsComponent,
36
89
  StepwiseInstructions: StepwiseInstructionsComponent,
37
90
  'human-message': HumanMessageComponent,
38
91
  'humanmessage': HumanMessageComponent,
39
92
  HumanMessage: HumanMessageComponent,
93
+ 'user-msg': HumanMessageComponent,
40
94
  qa: QAComponent,
41
95
  QA: QAComponent,
96
+ question: QAComponent,
97
+
98
+ # Utility components
99
+ 'ai-msg': AiMessageComponent,
100
+ 'aimessage': AiMessageComponent,
101
+ ai: AiMessageComponent,
102
+ Ai: AiMessageComponent,
103
+ human: HumanMessageComponent,
104
+ Human: HumanMessageComponent,
105
+ system: SystemMessageComponent,
106
+ System: SystemMessageComponent,
107
+ 'system-msg': SystemMessageComponent,
108
+ 'systemmessage': SystemMessageComponent,
109
+ 'msg-content': MessageContentComponent,
110
+ 'message-content': MessageContentComponent,
111
+ conversation: ConversationComponent,
112
+ Conversation: ConversationComponent,
113
+ folder: FolderComponent,
114
+ Folder: FolderComponent,
115
+ tree: TreeComponent,
116
+ Tree: TreeComponent,
117
+
118
+ # Styling components
42
119
  let: LetComponent,
43
120
  Let: LetComponent,
44
121
  stylesheet: StylesheetComponent,
45
- Stylesheet: StylesheetComponent
122
+ Stylesheet: StylesheetComponent,
123
+
124
+ # Meta components
125
+ meta: MetaComponent,
126
+ Meta: MetaComponent,
127
+
128
+ # Template components
129
+ include: IncludeComponent,
130
+ Include: IncludeComponent,
131
+ if: IfComponent,
132
+ If: IfComponent,
133
+ for: ForComponent,
134
+ For: ForComponent
46
135
  })
47
136
  end
data/lib/poml/context.rb CHANGED
@@ -1,9 +1,12 @@
1
1
  require 'json'
2
+ require 'set'
2
3
 
3
4
  module Poml
4
5
  # Context object that holds variables, stylesheets, and processing state
5
6
  class Context
6
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
7
10
 
8
11
  def initialize(variables: {}, stylesheet: nil, chat: true, syntax: nil)
9
12
  @variables = variables || {}
@@ -13,6 +16,13 @@ module Poml
13
16
  @source_path = nil
14
17
  @syntax = syntax
15
18
  @header_level = 1 # Track current header nesting level
19
+ @response_schema = nil
20
+ @tools = []
21
+ @runtime_parameters = {}
22
+ @disabled_components = Set.new
23
+ @template_engine = TemplateEngine.new(self)
24
+ @chat_messages = [] # Track structured chat messages
25
+ @custom_metadata = {} # Track general metadata like title, description etc.
16
26
  end
17
27
 
18
28
  def xml_mode?
@@ -28,9 +38,38 @@ module Poml
28
38
  def with_increased_header_level
29
39
  old_level = @header_level
30
40
  @header_level += 1
31
- yield
32
- ensure
41
+ result = yield
33
42
  @header_level = old_level
43
+ result
44
+ end
45
+
46
+ def with_chat_context(chat_enabled)
47
+ old_chat = @chat
48
+ @chat = chat_enabled
49
+ result = yield
50
+ @chat = old_chat
51
+ result
52
+ end
53
+
54
+ def create_child_context
55
+ child = Context.new(
56
+ variables: @variables.dup,
57
+ stylesheet: @stylesheet.dup,
58
+ chat: @chat,
59
+ syntax: @syntax
60
+ )
61
+ child.header_level = @header_level
62
+ child.response_schema = @response_schema
63
+ child.tools = @tools.dup
64
+ child.runtime_parameters = @runtime_parameters.dup
65
+ child.disabled_components = @disabled_components.dup
66
+ child.chat_messages = @chat_messages # Share the same array reference
67
+ child.custom_metadata = @custom_metadata # Share the same hash reference
68
+ child
69
+ end
70
+
71
+ def component_enabled?(component_name)
72
+ !@disabled_components.include?(component_name.to_s)
34
73
  end
35
74
 
36
75
  private