artificial 0.0.1

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,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rexml/document'
4
+
5
+ module Artificial
6
+ module Parsers
7
+ class XMLParser
8
+ attr_reader :input, :parsed_data, :errors
9
+
10
+ def initialize(input)
11
+ @input = input
12
+ @parsed_data = {}
13
+ @errors = []
14
+ end
15
+
16
+ def parse
17
+ return self unless valid?
18
+
19
+ begin
20
+ doc = REXML::Document.new(@input)
21
+ @parsed_data = extract_data_from_xml(doc)
22
+ @parsed_data[:format] = 'xml'
23
+ @parsed_data[:type] = 'structured_xml'
24
+ rescue REXML::ParseException => e
25
+ @errors << "XML parsing error: #{e.message}"
26
+ end
27
+
28
+ self
29
+ end
30
+
31
+ def valid?
32
+ return false unless @input.is_a?(String)
33
+ return false if @input.strip.empty?
34
+
35
+ begin
36
+ REXML::Document.new(@input)
37
+ true
38
+ rescue REXML::ParseException => e
39
+ @errors << "Invalid XML: #{e.message}"
40
+ false
41
+ end
42
+ end
43
+
44
+ def to_hash
45
+ @parsed_data
46
+ end
47
+
48
+ private
49
+
50
+ def extract_data_from_xml(doc)
51
+ data = {}
52
+
53
+ # Extract common prompt elements
54
+ if (prompt_element = doc.elements['prompt'])
55
+ data[:system] = prompt_element.elements['system']&.text
56
+ data[:instructions] = prompt_element.elements['instructions']&.text
57
+ data[:text] = data[:instructions] || prompt_element.text&.strip
58
+
59
+ # Extract context
60
+ if (context_element = prompt_element.elements['context'])
61
+ data[:context] = extract_hash_from_element(context_element)
62
+ end
63
+
64
+ # Extract data section
65
+ if (data_element = prompt_element.elements['data'])
66
+ data[:data] = extract_hash_from_element(data_element)
67
+ end
68
+
69
+ # Extract examples
70
+ if (examples_element = prompt_element.elements['examples'])
71
+ data[:examples] = extract_examples(examples_element)
72
+ end
73
+
74
+ # Extract grounding
75
+ if (grounding_element = prompt_element.elements['grounding'])
76
+ data[:grounding] = extract_grounding(grounding_element)
77
+ end
78
+
79
+ # Extract tools
80
+ if (tools_element = prompt_element.elements['tools'])
81
+ data[:tools] = extract_tools(tools_element)
82
+ end
83
+ end
84
+
85
+ data
86
+ end
87
+
88
+ def extract_hash_from_element(element)
89
+ hash = {}
90
+ element.elements.each do |child|
91
+ hash[child.name.to_sym] = child.text
92
+ end
93
+ hash
94
+ end
95
+
96
+ def extract_examples(examples_element)
97
+ examples = []
98
+ examples_element.elements.each('example') do |example|
99
+ examples << if example.elements['input'] && example.elements['output']
100
+ {
101
+ input: example.elements['input'].text,
102
+ output: example.elements['output'].text
103
+ }
104
+ else
105
+ example.text
106
+ end
107
+ end
108
+ examples
109
+ end
110
+
111
+ def extract_grounding(grounding_element)
112
+ grounding = {}
113
+ grounding[:require_quotes] = grounding_element.elements['require_quotes']&.text == 'true'
114
+ grounding[:require_sources] = grounding_element.elements['require_sources']&.text == 'true'
115
+ grounding[:allow_uncertainty] = grounding_element.elements['allow_uncertainty']&.text == 'true'
116
+ grounding
117
+ end
118
+
119
+ def extract_tools(tools_element)
120
+ tools = []
121
+ tools_element.elements.each('tool') do |tool|
122
+ tool_data = { name: tool.attributes['name'] }
123
+ if (params_element = tool.elements['parameters'])
124
+ tool_data[:parameters] = extract_hash_from_element(params_element)
125
+ end
126
+ tools << tool_data
127
+ end
128
+ tools
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'psych'
4
+
5
+ module Artificial
6
+ module Parsers
7
+ class YAMLParser
8
+ attr_reader :input, :parsed_data, :errors
9
+
10
+ def initialize(input)
11
+ @input = input
12
+ @parsed_data = {}
13
+ @errors = []
14
+ end
15
+
16
+ def parse
17
+ return self unless valid?
18
+
19
+ begin
20
+ yaml_data = Psych.safe_load(@input, permitted_classes: [Date, Time, DateTime, Symbol])
21
+ @parsed_data = normalize_yaml_data(yaml_data)
22
+ @parsed_data[:format] = 'yaml'
23
+ @parsed_data[:type] = 'structured_yaml'
24
+ rescue Psych::SyntaxError => e
25
+ @errors << "YAML parsing error: #{e.message}"
26
+ rescue StandardError => e
27
+ @errors << "YAML processing error: #{e.message}"
28
+ end
29
+
30
+ self
31
+ end
32
+
33
+ def valid?
34
+ return false unless @input.is_a?(String)
35
+ return false if @input.strip.empty?
36
+
37
+ begin
38
+ Psych.safe_load(@input, permitted_classes: [Date, Time, DateTime, Symbol])
39
+ true
40
+ rescue Psych::SyntaxError => e
41
+ @errors << "Invalid YAML: #{e.message}"
42
+ false
43
+ rescue StandardError => e
44
+ @errors << "YAML validation error: #{e.message}"
45
+ false
46
+ end
47
+ end
48
+
49
+ def to_hash
50
+ @parsed_data
51
+ end
52
+
53
+ private
54
+
55
+ def normalize_yaml_data(yaml_data)
56
+ return {} unless yaml_data.is_a?(Hash)
57
+
58
+ # Convert string keys to symbols for consistency
59
+ normalized = {}
60
+ yaml_data.each do |key, value|
61
+ symbol_key = key.to_s.to_sym
62
+ normalized[symbol_key] = normalize_value(value)
63
+ end
64
+
65
+ # Ensure common fields are present
66
+ normalized[:text] ||= normalized[:instructions]
67
+ normalized[:examples] ||= []
68
+ normalized[:context] ||= {}
69
+ normalized[:grounding] ||= {}
70
+ normalized[:tools] ||= []
71
+
72
+ normalized
73
+ end
74
+
75
+ def normalize_value(value)
76
+ case value
77
+ when Hash
78
+ value.transform_keys { |k| k.to_s.to_sym }
79
+ when Array
80
+ value.map { |v| normalize_value(v) }
81
+ else
82
+ value
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,440 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rexml/document'
4
+ require 'json'
5
+ require 'psych'
6
+
7
+ module Artificial
8
+ class Prompt
9
+ attr_accessor :text, :system, :messages, :context, :examples, :thinking,
10
+ :assistant_prefill, :format, :grounding, :data, :instructions,
11
+ :constraints, :output_format, :tone, :tools, :retrieval,
12
+ :citation_style, :validation, :optimization, :documents
13
+
14
+ def initialize(input = nil, **options)
15
+ @format = options[:format] || 'xml'
16
+ @context = {}
17
+ @examples = []
18
+ @constraints = []
19
+ @grounding = {}
20
+ @tools = []
21
+ @documents = []
22
+
23
+ parse_input(input, options)
24
+ apply_options(options)
25
+ end
26
+
27
+ # Generate the final prompt structure
28
+ def to_s
29
+ case @format
30
+ when 'xml'
31
+ generate_xml_prompt
32
+ when 'string'
33
+ generate_string_prompt
34
+ else
35
+ generate_xml_prompt
36
+ end
37
+ end
38
+
39
+ # Method chaining support
40
+ def with_system(system_prompt)
41
+ validate_system_prompt(system_prompt) if system_prompt
42
+ @system = system_prompt
43
+ self
44
+ end
45
+
46
+ def with_context(**context_options)
47
+ @context.merge!(context_options)
48
+ self
49
+ end
50
+
51
+ def with_examples(*example_list)
52
+ @examples.concat(example_list)
53
+ self
54
+ end
55
+
56
+ def with_thinking(enabled: true, style: 'step_by_step', show_reasoning: true)
57
+ @thinking = { enabled: enabled, style: style, show_reasoning: show_reasoning }
58
+ self
59
+ end
60
+
61
+ def with_grounding(require_quotes: false, require_sources: false, allow_uncertainty: false)
62
+ @grounding = {
63
+ require_quotes: require_quotes,
64
+ require_sources: require_sources,
65
+ allow_uncertainty: allow_uncertainty
66
+ }
67
+ self
68
+ end
69
+
70
+ def with_constraints(*constraint_list)
71
+ @constraints.concat(constraint_list)
72
+ self
73
+ end
74
+
75
+ def with_tools(*tool_list)
76
+ @tools.concat(tool_list)
77
+ self
78
+ end
79
+
80
+ def with_data(data_hash)
81
+ @data = data_hash
82
+ self
83
+ end
84
+
85
+ def with_documents(*document_list)
86
+ @documents.concat(document_list)
87
+ self
88
+ end
89
+
90
+ def with_prefill(prefill_text)
91
+ @assistant_prefill = prefill_text
92
+ self
93
+ end
94
+
95
+ private
96
+
97
+ def parse_input(input, options)
98
+ case input
99
+ when String
100
+ @text = input
101
+ when Hash
102
+ parse_hash_input(input)
103
+ when Array
104
+ parse_message_array(input)
105
+ when nil
106
+ # Handle options-only initialization
107
+ @text = options[:text] || options[:instructions]
108
+ else
109
+ raise ArgumentError, "Unsupported input type: #{input.class}"
110
+ end
111
+ end
112
+
113
+ def parse_hash_input(hash)
114
+ @text = hash[:text] || hash[:instructions]
115
+ @system = hash[:system]
116
+ @messages = hash[:messages]
117
+ @context = hash[:context] || {}
118
+ @examples = hash[:examples] || []
119
+ @data = hash[:data]
120
+ @instructions = hash[:instructions]
121
+
122
+ # Handle YAML/JSON parsing if needed
123
+ return unless hash.key?(:yaml) || hash.key?(:json)
124
+
125
+ parse_structured_data(hash)
126
+ end
127
+
128
+ def parse_message_array(messages)
129
+ validate_messages(messages)
130
+ @messages = messages
131
+ end
132
+
133
+ def parse_structured_data(hash)
134
+ if hash[:yaml]
135
+ parsed = Psych.safe_load(hash[:yaml])
136
+ merge_parsed_data(parsed)
137
+ elsif hash[:json]
138
+ parsed = JSON.parse(hash[:json])
139
+ merge_parsed_data(parsed)
140
+ end
141
+ end
142
+
143
+ def merge_parsed_data(parsed)
144
+ @text ||= parsed['text'] || parsed['instructions']
145
+ @system ||= parsed['system']
146
+ @context = (@context || {}).merge(parsed['context'] || {})
147
+ @examples = (@examples || []).concat(parsed['examples'] || [])
148
+ end
149
+
150
+ def apply_options(options)
151
+ @system ||= options[:system]
152
+ validate_system_prompt(@system) if @system
153
+
154
+ @context = (@context || {}).merge(options[:context] || {})
155
+ @examples = (@examples || []).concat(options[:examples] || [])
156
+ @thinking = options[:thinking] if options[:thinking]
157
+ @assistant_prefill = options[:assistant_prefill] if options[:assistant_prefill]
158
+ @grounding = (@grounding || {}).merge(options[:grounding] || {})
159
+ @constraints = (@constraints || []).concat(options[:constraints] || [])
160
+ @output_format = options[:output_format] if options[:output_format]
161
+ @tone = options[:tone] if options[:tone]
162
+ @tools = (@tools || []).concat(options[:tools] || [])
163
+ @retrieval = options[:retrieval] if options[:retrieval]
164
+ @citation_style = options[:citation_style] if options[:citation_style]
165
+ @validation = options[:validation] if options[:validation]
166
+ @optimization = options[:optimization] if options[:optimization]
167
+ @documents = (@documents || []).concat(options[:documents] || [])
168
+ @data = options[:data] if options[:data]
169
+ end
170
+
171
+ def validate_messages(messages)
172
+ return unless messages.is_a?(Array)
173
+
174
+ validator = Artificial::Validators::MessageValidator.new(messages)
175
+ return if validator.valid?
176
+
177
+ raise ArgumentError, validator.error_messages.join('; ')
178
+ end
179
+
180
+ def validate_system_prompt(system_prompt)
181
+ return unless system_prompt && !system_prompt.strip.empty?
182
+
183
+ validator = Artificial::Validators::RoleValidator.new(system_prompt)
184
+ validator.validate
185
+
186
+ # Log suggestions but don't raise errors for effectiveness
187
+ if validator.optimization_suggestions.any?
188
+ # Could add logging here in the future
189
+ # puts "Role optimization suggestions: #{validator.optimization_suggestions.join('; ')}"
190
+ end
191
+
192
+ return if validator.valid?
193
+
194
+ raise ArgumentError, validator.error_messages.join('; ')
195
+ end
196
+
197
+ def generate_xml_prompt
198
+ doc = REXML::Document.new
199
+ root = doc.add_element('prompt')
200
+
201
+ # Handle message array format (conversation)
202
+ if @messages && !@messages.empty?
203
+ add_messages_section(root)
204
+ return output_xml(doc)
205
+ end
206
+
207
+ # Add long context documents first for optimization
208
+ add_documents_section(root) if @documents && !@documents.empty?
209
+
210
+ # Add system instructions
211
+ if @system
212
+ system_element = root.add_element('system')
213
+ system_element.text = @system
214
+ end
215
+
216
+ # Add context information
217
+ add_context_section(root) if @context && !@context.empty?
218
+
219
+ # Add data section (separated from instructions)
220
+ add_data_section(root) if @data
221
+
222
+ # Add instructions
223
+ if @instructions || @text
224
+ instructions_element = root.add_element('instructions')
225
+ instructions_element.text = @instructions || @text
226
+
227
+ # Add constraints
228
+ if @constraints && !@constraints.empty?
229
+ constraints_element = instructions_element.add_element('constraints')
230
+ @constraints.each do |constraint|
231
+ constraint_element = constraints_element.add_element('constraint')
232
+ constraint_element.text = constraint
233
+ end
234
+ end
235
+ end
236
+
237
+ # Add examples
238
+ add_examples_section(root) if @examples && !@examples.empty?
239
+
240
+ # Add thinking instructions
241
+ add_thinking_section(root) if @thinking && @thinking[:enabled]
242
+
243
+ # Add grounding requirements
244
+ add_grounding_section(root) if @grounding && !@grounding.empty?
245
+
246
+ # Add tools
247
+ add_tools_section(root) if @tools && !@tools.empty?
248
+
249
+ # Add retrieval configuration
250
+ add_retrieval_section(root) if @retrieval
251
+
252
+ # Add output formatting
253
+ add_output_section(root) if @output_format || @tone
254
+
255
+ # Add assistant prefill
256
+ if @assistant_prefill
257
+ prefill_element = root.add_element('assistant_prefill')
258
+ prefill_element.text = @assistant_prefill
259
+ end
260
+
261
+ # Convert to string and clean up
262
+ output_xml(doc)
263
+ end
264
+
265
+ def generate_string_prompt
266
+ parts = []
267
+
268
+ # Add system prompt
269
+ parts << "System: #{@system}" if @system
270
+
271
+ # Add context
272
+ if @context && !@context.empty?
273
+ context_parts = @context.map { |k, v| "#{k}: #{v}" }
274
+ parts << "Context: #{context_parts.join(', ')}"
275
+ end
276
+
277
+ # Add main text/instructions
278
+ parts << (@instructions || @text) if @instructions || @text
279
+
280
+ # Add constraints
281
+ parts << "Constraints: #{@constraints.join('; ')}" if @constraints && !@constraints.empty?
282
+
283
+ # Add examples
284
+ parts << "Examples: #{@examples.map(&:to_s).join('; ')}" if @examples && !@examples.empty?
285
+
286
+ # Add assistant prefill
287
+ parts << "Assistant: #{@assistant_prefill}" if @assistant_prefill
288
+
289
+ parts.join("\n\n")
290
+ end
291
+
292
+ def output_xml(doc)
293
+ output = String.new
294
+ doc.write(output, 0)
295
+ output
296
+ end
297
+
298
+ def add_messages_section(root)
299
+ messages_element = root.add_element('messages')
300
+ @messages.each do |message|
301
+ message_element = messages_element.add_element('message')
302
+ message_element.add_attribute('role', message[:role] || message['role'])
303
+ message_element.text = message[:content] || message['content']
304
+ end
305
+ end
306
+
307
+ def add_documents_section(root)
308
+ documents_element = root.add_element('documents')
309
+ @documents.each do |doc|
310
+ doc_element = documents_element.add_element('document')
311
+ if doc.is_a?(Hash)
312
+ doc_element.add_attribute('source', doc[:source]) if doc[:source]
313
+ doc_element.text = doc[:content]
314
+ if doc[:metadata]
315
+ metadata_element = doc_element.add_element('metadata')
316
+ doc[:metadata].each do |key, value|
317
+ meta_element = metadata_element.add_element(key.to_s)
318
+ meta_element.text = value.to_s
319
+ end
320
+ end
321
+ else
322
+ doc_element.text = doc.to_s
323
+ end
324
+ end
325
+ end
326
+
327
+ def add_context_section(root)
328
+ context_element = root.add_element('context')
329
+ @context.each do |key, value|
330
+ element = context_element.add_element(key.to_s)
331
+ element.text = value.to_s
332
+ end
333
+ end
334
+
335
+ def add_data_section(root)
336
+ data_element = root.add_element('data')
337
+ case @data
338
+ when Hash
339
+ @data.each do |key, value|
340
+ element = data_element.add_element(key.to_s)
341
+ element.text = value.to_s
342
+ end
343
+ else
344
+ data_element.text = @data.to_s
345
+ end
346
+ end
347
+
348
+ def add_examples_section(root)
349
+ examples_element = root.add_element('examples')
350
+ @examples.each do |example|
351
+ example_element = examples_element.add_element('example')
352
+ if example.is_a?(Hash)
353
+ if example[:input]
354
+ input_element = example_element.add_element('input')
355
+ input_element.text = example[:input].to_s
356
+ end
357
+ if example[:output]
358
+ output_element = example_element.add_element('output')
359
+ output_element.text = example[:output].to_s
360
+ end
361
+ else
362
+ example_element.text = example.to_s
363
+ end
364
+ end
365
+ end
366
+
367
+ def add_thinking_section(root)
368
+ thinking_element = root.add_element('thinking')
369
+ thinking_element.add_attribute('enabled', @thinking[:enabled].to_s)
370
+ thinking_element.add_attribute('style', @thinking[:style]) if @thinking[:style]
371
+
372
+ return unless @thinking[:show_reasoning]
373
+
374
+ instruction = 'Think through this step by step. Show your reasoning process.'
375
+ thinking_element.text = instruction
376
+ end
377
+
378
+ def add_grounding_section(root)
379
+ grounding_element = root.add_element('grounding')
380
+
381
+ if @grounding[:require_quotes]
382
+ require_quotes_element = grounding_element.add_element('require_quotes')
383
+ require_quotes_element.text = 'true'
384
+ end
385
+
386
+ if @grounding[:require_sources]
387
+ require_sources_element = grounding_element.add_element('require_sources')
388
+ require_sources_element.text = 'true'
389
+ end
390
+
391
+ return unless @grounding[:allow_uncertainty]
392
+
393
+ allow_uncertainty_element = grounding_element.add_element('allow_uncertainty')
394
+ allow_uncertainty_element.text = 'true'
395
+ end
396
+
397
+ def add_tools_section(root)
398
+ tools_element = root.add_element('tools')
399
+ @tools.each do |tool|
400
+ tool_element = tools_element.add_element('tool')
401
+ if tool.is_a?(Hash)
402
+ tool_element.add_attribute('name', tool[:name]) if tool[:name]
403
+ if tool[:parameters]
404
+ params_element = tool_element.add_element('parameters')
405
+ tool[:parameters].each do |key, value|
406
+ param_element = params_element.add_element(key.to_s)
407
+ param_element.text = value.to_s
408
+ end
409
+ end
410
+ else
411
+ tool_element.text = tool.to_s
412
+ end
413
+ end
414
+ end
415
+
416
+ def add_retrieval_section(root)
417
+ retrieval_element = root.add_element('retrieval')
418
+
419
+ if @retrieval[:sources]
420
+ sources_element = retrieval_element.add_element('sources')
421
+ @retrieval[:sources].each do |source|
422
+ source_element = sources_element.add_element('source')
423
+ source_element.text = source
424
+ end
425
+ end
426
+
427
+ retrieval_element.add_attribute('max_results', @retrieval[:max_results].to_s) if @retrieval[:max_results]
428
+ return unless @retrieval[:similarity_threshold]
429
+
430
+ retrieval_element.add_attribute('similarity_threshold',
431
+ @retrieval[:similarity_threshold].to_s)
432
+ end
433
+
434
+ def add_output_section(root)
435
+ output_element = root.add_element('output')
436
+ output_element.add_attribute('format', @output_format) if @output_format
437
+ output_element.add_attribute('tone', @tone) if @tone
438
+ end
439
+ end
440
+ end