language-operator 0.1.30 → 0.1.35

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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -8
  3. data/CHANGELOG.md +49 -0
  4. data/CI_STATUS.md +56 -0
  5. data/Gemfile.lock +2 -2
  6. data/Makefile +28 -7
  7. data/Rakefile +29 -0
  8. data/docs/dsl/SCHEMA_VERSION.md +250 -0
  9. data/docs/dsl/agent-reference.md +13 -0
  10. data/lib/language_operator/agent/base.rb +10 -6
  11. data/lib/language_operator/agent/executor.rb +19 -97
  12. data/lib/language_operator/agent/safety/ast_validator.rb +62 -43
  13. data/lib/language_operator/agent/safety/safe_executor.rb +39 -2
  14. data/lib/language_operator/agent/scheduler.rb +60 -0
  15. data/lib/language_operator/agent/task_executor.rb +548 -0
  16. data/lib/language_operator/agent.rb +90 -27
  17. data/lib/language_operator/cli/base_command.rb +117 -0
  18. data/lib/language_operator/cli/commands/agent.rb +351 -466
  19. data/lib/language_operator/cli/commands/cluster.rb +276 -256
  20. data/lib/language_operator/cli/commands/install.rb +110 -119
  21. data/lib/language_operator/cli/commands/model.rb +284 -184
  22. data/lib/language_operator/cli/commands/persona.rb +220 -289
  23. data/lib/language_operator/cli/commands/quickstart.rb +4 -5
  24. data/lib/language_operator/cli/commands/status.rb +36 -53
  25. data/lib/language_operator/cli/commands/system.rb +760 -0
  26. data/lib/language_operator/cli/commands/tool.rb +356 -422
  27. data/lib/language_operator/cli/commands/use.rb +19 -22
  28. data/lib/language_operator/cli/formatters/code_formatter.rb +3 -7
  29. data/lib/language_operator/cli/formatters/log_formatter.rb +3 -5
  30. data/lib/language_operator/cli/formatters/progress_formatter.rb +3 -7
  31. data/lib/language_operator/cli/formatters/status_formatter.rb +37 -0
  32. data/lib/language_operator/cli/formatters/table_formatter.rb +10 -26
  33. data/lib/language_operator/cli/helpers/pastel_helper.rb +24 -0
  34. data/lib/language_operator/cli/helpers/resource_dependency_checker.rb +0 -18
  35. data/lib/language_operator/cli/main.rb +4 -0
  36. data/lib/language_operator/cli/wizards/quickstart_wizard.rb +0 -1
  37. data/lib/language_operator/client/config.rb +20 -21
  38. data/lib/language_operator/config.rb +115 -3
  39. data/lib/language_operator/constants.rb +54 -0
  40. data/lib/language_operator/dsl/agent_context.rb +7 -7
  41. data/lib/language_operator/dsl/agent_definition.rb +111 -26
  42. data/lib/language_operator/dsl/config.rb +30 -66
  43. data/lib/language_operator/dsl/main_definition.rb +114 -0
  44. data/lib/language_operator/dsl/schema.rb +1143 -0
  45. data/lib/language_operator/dsl/task_definition.rb +315 -0
  46. data/lib/language_operator/dsl.rb +1 -1
  47. data/lib/language_operator/instrumentation/task_tracer.rb +285 -0
  48. data/lib/language_operator/logger.rb +4 -4
  49. data/lib/language_operator/synthesis_test_harness.rb +324 -0
  50. data/lib/language_operator/templates/README.md +23 -0
  51. data/lib/language_operator/templates/examples/agent_synthesis.tmpl +133 -0
  52. data/lib/language_operator/templates/examples/persona_distillation.tmpl +19 -0
  53. data/lib/language_operator/templates/schema/.gitkeep +0 -0
  54. data/lib/language_operator/templates/schema/CHANGELOG.md +119 -0
  55. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +306 -0
  56. data/lib/language_operator/templates/schema/agent_dsl_schema.json +494 -0
  57. data/lib/language_operator/type_coercion.rb +250 -0
  58. data/lib/language_operator/ux/base.rb +81 -0
  59. data/lib/language_operator/ux/concerns/README.md +155 -0
  60. data/lib/language_operator/ux/concerns/headings.rb +90 -0
  61. data/lib/language_operator/ux/concerns/input_validation.rb +146 -0
  62. data/lib/language_operator/ux/concerns/provider_helpers.rb +167 -0
  63. data/lib/language_operator/ux/create_agent.rb +252 -0
  64. data/lib/language_operator/ux/create_model.rb +267 -0
  65. data/lib/language_operator/ux/quickstart.rb +594 -0
  66. data/lib/language_operator/version.rb +1 -1
  67. data/lib/language_operator.rb +2 -0
  68. data/requirements/ARCHITECTURE.md +1 -0
  69. data/requirements/SCRATCH.md +153 -0
  70. data/requirements/dsl.md +0 -0
  71. data/requirements/features +1 -0
  72. data/requirements/personas +1 -0
  73. data/requirements/proposals +1 -0
  74. data/requirements/tasks/iterate.md +14 -15
  75. data/requirements/tasks/optimize.md +13 -4
  76. data/synth/001/Makefile +90 -0
  77. data/synth/001/agent.rb +26 -0
  78. data/synth/001/agent.yaml +7 -0
  79. data/synth/001/output.log +44 -0
  80. data/synth/Makefile +39 -0
  81. data/synth/README.md +342 -0
  82. metadata +49 -18
  83. data/examples/README.md +0 -569
  84. data/examples/agent_example.rb +0 -86
  85. data/examples/chat_endpoint_agent.rb +0 -118
  86. data/examples/github_webhook_agent.rb +0 -171
  87. data/examples/mcp_agent.rb +0 -158
  88. data/examples/oauth_callback_agent.rb +0 -296
  89. data/examples/stripe_webhook_agent.rb +0 -219
  90. data/examples/webhook_agent.rb +0 -80
  91. data/lib/language_operator/dsl/workflow_definition.rb +0 -259
  92. data/test_agent_dsl.rb +0 -108
@@ -0,0 +1,760 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'yaml'
5
+ require_relative '../base_command'
6
+ require_relative '../formatters/progress_formatter'
7
+ require_relative '../../dsl/schema'
8
+
9
+ module LanguageOperator
10
+ module CLI
11
+ module Commands
12
+ # System commands for schema introspection and metadata
13
+ class System < BaseCommand
14
+ desc 'schema', 'Export the DSL schema in various formats'
15
+ long_desc <<-DESC
16
+ Export the Language Operator Agent DSL schema in various formats.
17
+
18
+ The schema documents all available DSL methods, parameters, validation
19
+ patterns, and structure. Useful for template validation, documentation
20
+ generation, and IDE autocomplete.
21
+
22
+ Examples:
23
+ # Export JSON schema (default)
24
+ aictl system schema
25
+
26
+ # Export as YAML
27
+ aictl system schema --format yaml
28
+
29
+ # Export OpenAPI 3.0 specification
30
+ aictl system schema --format openapi
31
+
32
+ # Show schema version only
33
+ aictl system schema --version
34
+
35
+ # Save to file
36
+ aictl system schema > schema.json
37
+ aictl system schema --format openapi > openapi.json
38
+ DESC
39
+ option :format, type: :string, default: 'json', desc: 'Output format (json, yaml, openapi)'
40
+ option :version, type: :boolean, default: false, desc: 'Show schema version only'
41
+ def schema
42
+ handle_command_error('generate schema') do
43
+ # Handle version flag
44
+ if options[:version]
45
+ puts Dsl::Schema.version
46
+ return
47
+ end
48
+
49
+ # Generate schema based on format
50
+ format = options[:format].downcase
51
+ case format
52
+ when 'json'
53
+ output_json_schema
54
+ when 'yaml'
55
+ output_yaml_schema
56
+ when 'openapi'
57
+ output_openapi_schema
58
+ else
59
+ Formatters::ProgressFormatter.error("Invalid format: #{format}")
60
+ puts
61
+ puts 'Supported formats: json, yaml, openapi'
62
+ exit 1
63
+ end
64
+ end
65
+ end
66
+
67
+ no_commands do
68
+ # Output JSON Schema v7
69
+ def output_json_schema
70
+ schema = Dsl::Schema.to_json_schema
71
+ puts JSON.pretty_generate(schema)
72
+ end
73
+
74
+ # Output YAML Schema
75
+ def output_yaml_schema
76
+ schema = Dsl::Schema.to_json_schema
77
+ puts YAML.dump(schema.transform_keys(&:to_s))
78
+ end
79
+
80
+ # Output OpenAPI 3.0 specification
81
+ def output_openapi_schema
82
+ spec = Dsl::Schema.to_openapi
83
+ puts JSON.pretty_generate(spec)
84
+ end
85
+ end
86
+
87
+ desc 'validate_template', 'Validate synthesis template against DSL schema'
88
+ long_desc <<-DESC
89
+ Validate a synthesis template file against the DSL schema.
90
+
91
+ Extracts Ruby code examples from the template and validates each example
92
+ against the Language Operator Agent DSL schema. Checks for dangerous
93
+ methods, syntax errors, and compliance with safe coding practices.
94
+
95
+ Examples:
96
+ # Validate a custom template file
97
+ aictl system validate_template --template /path/to/template.tmpl
98
+
99
+ # Validate the bundled agent template (default)
100
+ aictl system validate_template
101
+
102
+ # Validate the bundled persona template
103
+ aictl system validate_template --type persona
104
+
105
+ # Verbose output with all violations
106
+ aictl system validate_template --template mytemplate.tmpl --verbose
107
+ DESC
108
+ option :template, type: :string, desc: 'Path to template file (defaults to bundled template)'
109
+ option :type, type: :string, default: 'agent', desc: 'Template type if using bundled template (agent, persona)'
110
+ option :verbose, type: :boolean, default: false, desc: 'Show detailed violation information'
111
+ def validate_template
112
+ handle_command_error('validate template') do
113
+ # Determine template source
114
+ if options[:template]
115
+ # Load custom template from file
116
+ unless File.exist?(options[:template])
117
+ Formatters::ProgressFormatter.error("Template file not found: #{options[:template]}")
118
+ exit 1
119
+ end
120
+ template_content = File.read(options[:template])
121
+ template_name = File.basename(options[:template])
122
+ else
123
+ # Load bundled template
124
+ template_type = options[:type].downcase
125
+ unless %w[agent persona].include?(template_type)
126
+ Formatters::ProgressFormatter.error("Invalid template type: #{template_type}")
127
+ puts
128
+ puts 'Supported types: agent, persona'
129
+ exit 1
130
+ end
131
+ template_content = load_bundled_template(template_type)
132
+ template_name = "bundled #{template_type} template"
133
+ end
134
+
135
+ # Display header
136
+ puts "Validating template: #{template_name}"
137
+ puts '=' * 60
138
+ puts
139
+
140
+ # Extract code examples
141
+ code_examples = extract_code_examples(template_content)
142
+
143
+ if code_examples.empty?
144
+ Formatters::ProgressFormatter.warn('No Ruby code examples found in template')
145
+ puts
146
+ puts 'Templates should contain Ruby code blocks like:'
147
+ puts '```ruby'
148
+ puts 'agent "my-agent" do'
149
+ puts ' # ...'
150
+ puts 'end'
151
+ puts '```'
152
+ exit 1
153
+ end
154
+
155
+ puts "Found #{code_examples.size} code example(s)"
156
+ puts
157
+
158
+ # Validate each example
159
+ all_valid = true
160
+ code_examples.each_with_index do |example, idx|
161
+ puts "Example #{idx + 1} (starting at line #{example[:start_line]}):"
162
+ puts '-' * 40
163
+
164
+ result = validate_code_against_schema(example[:code])
165
+
166
+ if result[:valid] && result[:warnings].empty?
167
+ Formatters::ProgressFormatter.success('Valid - No issues found')
168
+ elsif result[:valid]
169
+ Formatters::ProgressFormatter.success('Valid - With warnings')
170
+ result[:warnings].each do |warn|
171
+ line = example[:start_line] + (warn[:location] || 0)
172
+ puts " ⚠ Line #{line}: #{warn[:message]}"
173
+ end
174
+ else
175
+ Formatters::ProgressFormatter.error('Invalid - Violations detected')
176
+ result[:errors].each do |err|
177
+ line = example[:start_line] + (err[:location] || 0)
178
+ puts " ✗ Line #{line}: #{err[:message]}"
179
+ puts " Type: #{err[:type]}" if options[:verbose]
180
+ end
181
+ all_valid = false
182
+ end
183
+
184
+ puts
185
+ end
186
+
187
+ # Final summary
188
+ puts '=' * 60
189
+ if all_valid
190
+ Formatters::ProgressFormatter.success('All examples are valid')
191
+ exit 0
192
+ else
193
+ Formatters::ProgressFormatter.error('Validation failed')
194
+ puts
195
+ puts 'Fix the violations above and run validation again.'
196
+ exit 1
197
+ end
198
+ end
199
+ end
200
+
201
+ desc 'test-synthesis', 'Test agent synthesis from natural language instructions'
202
+ long_desc <<-DESC
203
+ Test the agent synthesis process by converting natural language instructions
204
+ into Ruby DSL code without creating an actual agent.
205
+
206
+ This command helps you validate your instructions and understand how the
207
+ synthesis engine interprets them. Use --dry-run to see the prompt that
208
+ would be sent to the LLM, or run without it to generate actual code.
209
+
210
+ Examples:
211
+ # Test with dry-run (show prompt only)
212
+ aictl system test-synthesis --instructions "Monitor GitHub issues daily" --dry-run
213
+
214
+ # Generate code from instructions
215
+ aictl system test-synthesis --instructions "Send daily reports to Slack"
216
+
217
+ # Specify custom agent name and tools
218
+ aictl system test-synthesis \\
219
+ --instructions "Process webhooks from GitHub" \\
220
+ --agent-name github-processor \\
221
+ --tools github,slack
222
+
223
+ # Specify available models
224
+ aictl system test-synthesis \\
225
+ --instructions "Analyze logs every hour" \\
226
+ --models gpt-4,claude-3-5-sonnet
227
+ DESC
228
+ option :instructions, type: :string, required: true, desc: 'Natural language instructions for the agent'
229
+ option :agent_name, type: :string, default: 'test-agent', desc: 'Name for the test agent'
230
+ option :tools, type: :string, desc: 'Comma-separated list of available tools'
231
+ option :models, type: :string, desc: 'Comma-separated list of available models'
232
+ option :dry_run, type: :boolean, default: false, desc: 'Show prompt without calling LLM'
233
+ def test_synthesis
234
+ handle_command_error('test synthesis') do
235
+ # Load synthesis template
236
+ template_content = load_bundled_template('agent')
237
+
238
+ # Detect temporal intent from instructions
239
+ temporal_intent = detect_temporal_intent(options[:instructions])
240
+
241
+ # Prepare template data
242
+ template_data = {
243
+ 'Instructions' => options[:instructions],
244
+ 'AgentName' => options[:agent_name],
245
+ 'ToolsList' => format_tools_list(options[:tools]),
246
+ 'ModelsList' => format_models_list(options[:models]),
247
+ 'TemporalIntent' => temporal_intent,
248
+ 'PersonaSection' => '',
249
+ 'ScheduleSection' => temporal_intent == 'scheduled' ? ' schedule "0 */1 * * *" # Example hourly schedule' : '',
250
+ 'ScheduleRules' => temporal_intent == 'scheduled' ? "\n2. Include schedule with cron expression\n3. Set mode to :scheduled\n4. " : "\n2. ",
251
+ 'ConstraintsSection' => '',
252
+ 'ErrorContext' => nil
253
+ }
254
+
255
+ # Render template (Go-style template syntax)
256
+ rendered_prompt = render_go_template(template_content, template_data)
257
+
258
+ if options[:dry_run]
259
+ # Show the prompt that would be sent
260
+ puts 'Synthesis Prompt Preview'
261
+ puts '=' * 80
262
+ puts
263
+ puts rendered_prompt
264
+ puts
265
+ puts '=' * 80
266
+ Formatters::ProgressFormatter.success('Dry-run complete - prompt displayed above')
267
+ return
268
+ end
269
+
270
+ # Call LLM to generate code
271
+ puts 'Generating agent code from instructions...'
272
+ puts
273
+
274
+ llm_response = call_llm_for_synthesis(rendered_prompt)
275
+
276
+ # Extract Ruby code from response
277
+ generated_code = extract_ruby_code(llm_response)
278
+
279
+ if generated_code.nil?
280
+ Formatters::ProgressFormatter.error('Failed to extract Ruby code from LLM response')
281
+ puts
282
+ puts 'LLM Response:'
283
+ puts llm_response
284
+ exit 1
285
+ end
286
+
287
+ # Display generated code
288
+ puts 'Generated Code:'
289
+ puts '=' * 80
290
+ puts generated_code
291
+ puts '=' * 80
292
+ puts
293
+
294
+ # Validate generated code
295
+ puts 'Validating generated code...'
296
+ validation_result = validate_code_against_schema(generated_code)
297
+
298
+ if validation_result[:valid] && validation_result[:warnings].empty?
299
+ Formatters::ProgressFormatter.success('✅ Code is valid - No issues found')
300
+ elsif validation_result[:valid]
301
+ Formatters::ProgressFormatter.success('✅ Code is valid - With warnings')
302
+ puts
303
+ validation_result[:warnings].each do |warn|
304
+ puts " ⚠ #{warn[:message]}"
305
+ end
306
+ else
307
+ Formatters::ProgressFormatter.error('❌ Code validation failed')
308
+ puts
309
+ validation_result[:errors].each do |err|
310
+ puts " ✗ #{err[:message]}"
311
+ end
312
+ end
313
+
314
+ puts
315
+ end
316
+ end
317
+
318
+ desc 'synthesis-template', 'Export synthesis templates for agent code generation'
319
+ long_desc <<-DESC
320
+ Export the synthesis templates used by the Language Operator to generate
321
+ agent code from natural language instructions.
322
+
323
+ These templates are used by the operator's synthesis engine to convert
324
+ user instructions into executable Ruby DSL code.
325
+
326
+ Examples:
327
+ # Export agent synthesis template (default)
328
+ aictl system synthesis-template
329
+
330
+ # Export persona distillation template
331
+ aictl system synthesis-template --type persona
332
+
333
+ # Export as JSON with schema included
334
+ aictl system synthesis-template --format json --with-schema
335
+
336
+ # Export as YAML
337
+ aictl system synthesis-template --format yaml
338
+
339
+ # Validate template syntax
340
+ aictl system synthesis-template --validate
341
+
342
+ # Save to file
343
+ aictl system synthesis-template > agent_synthesis.tmpl
344
+ DESC
345
+ option :format, type: :string, default: 'template', desc: 'Output format (template, json, yaml)'
346
+ option :type, type: :string, default: 'agent', desc: 'Template type (agent, persona)'
347
+ option :with_schema, type: :boolean, default: false, desc: 'Include DSL schema in output'
348
+ option :validate, type: :boolean, default: false, desc: 'Validate template syntax'
349
+ def synthesis_template
350
+ handle_command_error('load template') do
351
+ # Validate type
352
+ template_type = options[:type].downcase
353
+ unless %w[agent persona].include?(template_type)
354
+ Formatters::ProgressFormatter.error("Invalid template type: #{template_type}")
355
+ puts
356
+ puts 'Supported types: agent, persona'
357
+ exit 1
358
+ end
359
+
360
+ # Load template
361
+ template_content = load_template(template_type)
362
+
363
+ # Validate if requested
364
+ if options[:validate]
365
+ validation_result = validate_template_content(template_content, template_type)
366
+
367
+ # Display warnings if any
368
+ unless validation_result[:warnings].empty?
369
+ Formatters::ProgressFormatter.warn('Template validation warnings:')
370
+ validation_result[:warnings].each do |warning|
371
+ puts " ⚠ #{warning}"
372
+ end
373
+ puts
374
+ end
375
+
376
+ # Display errors and exit if validation failed
377
+ if validation_result[:valid]
378
+ Formatters::ProgressFormatter.success('Template validation passed')
379
+ return
380
+ else
381
+ Formatters::ProgressFormatter.error('Template validation failed:')
382
+ validation_result[:errors].each do |error|
383
+ puts " ✗ #{error}"
384
+ end
385
+ exit 1
386
+ end
387
+ end
388
+
389
+ # Generate output based on format
390
+ format = options[:format].downcase
391
+ case format
392
+ when 'template'
393
+ output_template_format(template_content)
394
+ when 'json'
395
+ output_json_format(template_content, template_type)
396
+ when 'yaml'
397
+ output_yaml_format(template_content, template_type)
398
+ else
399
+ Formatters::ProgressFormatter.error("Invalid format: #{format}")
400
+ puts
401
+ puts 'Supported formats: template, json, yaml'
402
+ exit 1
403
+ end
404
+ end
405
+ end
406
+
407
+ private
408
+
409
+ # Render Go-style template ({{.Variable}})
410
+ # Simplified implementation for basic variable substitution
411
+ def render_go_template(template, data)
412
+ result = template.dup
413
+
414
+ # Handle {{if .ErrorContext}} - remove this section for test-synthesis
415
+ result.gsub!(/{{if \.ErrorContext}}.*?{{else}}/m, '')
416
+ result.gsub!(/{{end}}/, '')
417
+
418
+ # Replace simple variables {{.Variable}}
419
+ data.each do |key, value|
420
+ result.gsub!("{{.#{key}}}", value.to_s)
421
+ end
422
+
423
+ result
424
+ end
425
+
426
+ # Detect temporal intent from instructions (scheduled vs autonomous)
427
+ def detect_temporal_intent(instructions)
428
+ temporal_keywords = {
429
+ scheduled: %w[daily weekly hourly monthly schedule cron every day week hour minute],
430
+ autonomous: %w[monitor watch continuously constantly always loop]
431
+ }
432
+
433
+ instructions_lower = instructions.downcase
434
+
435
+ # Check for scheduled keywords
436
+ scheduled_matches = temporal_keywords[:scheduled].count { |keyword| instructions_lower.include?(keyword) }
437
+ autonomous_matches = temporal_keywords[:autonomous].count { |keyword| instructions_lower.include?(keyword) }
438
+
439
+ scheduled_matches > autonomous_matches ? 'scheduled' : 'autonomous'
440
+ end
441
+
442
+ # Format tools list for template
443
+ def format_tools_list(tools_str)
444
+ return 'No tools specified' if tools_str.nil? || tools_str.strip.empty?
445
+
446
+ tools = tools_str.split(',').map(&:strip)
447
+ tools.map { |tool| "- #{tool}" }.join("\n")
448
+ end
449
+
450
+ # Format models list for template
451
+ def format_models_list(models_str)
452
+ # If not specified, try to detect from environment
453
+ if models_str.nil? || models_str.strip.empty?
454
+ models = detect_available_models
455
+ return models.map { |model| "- #{model}" }.join("\n") unless models.empty?
456
+
457
+ return 'No models specified (configure ANTHROPIC_API_KEY or OPENAI_API_KEY)'
458
+ end
459
+
460
+ models = models_str.split(',').map(&:strip)
461
+ models.map { |model| "- #{model}" }.join("\n")
462
+ end
463
+
464
+ # Detect available models from environment
465
+ def detect_available_models
466
+ models = []
467
+ models << 'claude-3-5-sonnet-20241022' if ENV['ANTHROPIC_API_KEY']
468
+ models << 'gpt-4-turbo' if ENV['OPENAI_API_KEY']
469
+ models
470
+ end
471
+
472
+ # Call LLM to generate code from synthesis prompt
473
+ def call_llm_for_synthesis(prompt)
474
+ require 'ruby_llm'
475
+
476
+ # Check for API keys
477
+ unless ENV['ANTHROPIC_API_KEY'] || ENV['OPENAI_API_KEY']
478
+ Formatters::ProgressFormatter.error('No LLM credentials found')
479
+ puts
480
+ puts 'Please set one of the following environment variables:'
481
+ puts ' - ANTHROPIC_API_KEY (for Claude models)'
482
+ puts ' - OPENAI_API_KEY (for GPT models)'
483
+ exit 1
484
+ end
485
+
486
+ # Prefer Anthropic if available
487
+ if ENV['ANTHROPIC_API_KEY']
488
+ provider = :anthropic
489
+ api_key = ENV['ANTHROPIC_API_KEY']
490
+ model = 'claude-3-5-sonnet-20241022'
491
+ else
492
+ provider = :openai
493
+ api_key = ENV.fetch('OPENAI_API_KEY', nil)
494
+ model = 'gpt-4-turbo'
495
+ end
496
+
497
+ # Create client and call LLM
498
+ client = RubyLLM.new(provider: provider, api_key: api_key)
499
+ messages = [{ role: 'user', content: prompt }]
500
+
501
+ response = client.chat(messages, model: model, max_tokens: 4000, temperature: 0.3)
502
+
503
+ # Extract content from response
504
+ if response.is_a?(Hash) && response.key?('content')
505
+ response['content']
506
+ elsif response.is_a?(String)
507
+ response
508
+ else
509
+ response.to_s
510
+ end
511
+ rescue StandardError => e
512
+ Formatters::ProgressFormatter.error("LLM call failed: #{e.message}")
513
+ exit 1
514
+ end
515
+
516
+ # Extract Ruby code from LLM response
517
+ # Looks for ```ruby ... ``` blocks
518
+ def extract_ruby_code(response)
519
+ # Match ```ruby ... ``` blocks
520
+ match = response.match(/```ruby\n(.*?)```/m)
521
+ return match[1].strip if match
522
+
523
+ # Try without language specifier
524
+ match = response.match(/```\n(.*?)```/m)
525
+ return match[1].strip if match
526
+
527
+ # If no code blocks, return nil
528
+ nil
529
+ end
530
+
531
+ # Load template from bundled gem or operator ConfigMap
532
+ def load_template(type)
533
+ # Try to fetch from operator ConfigMap first (if kubectl available)
534
+ template = fetch_from_operator(type)
535
+ return template if template
536
+
537
+ # Fall back to bundled template
538
+ load_bundled_template(type)
539
+ end
540
+
541
+ # Fetch template from operator ConfigMap via kubectl
542
+ def fetch_from_operator(type)
543
+ configmap_name = type == 'agent' ? 'agent-synthesis-template' : 'persona-distillation-template'
544
+ result = `kubectl get configmap #{configmap_name} -n language-operator-system -o jsonpath='{.data.template}' 2>/dev/null`
545
+ result.empty? ? nil : result
546
+ rescue StandardError
547
+ nil
548
+ end
549
+
550
+ # Load bundled template from gem
551
+ def load_bundled_template(type)
552
+ filename = type == 'agent' ? 'agent_synthesis.tmpl' : 'persona_distillation.tmpl'
553
+ template_path = File.join(__dir__, '..', '..', 'templates', 'examples', filename)
554
+ File.read(template_path)
555
+ end
556
+
557
+ # Validate template syntax and structure
558
+ def validate_template_content(content, type)
559
+ errors = []
560
+ warnings = []
561
+
562
+ # Check for required placeholders based on type
563
+ required_placeholders = if type == 'agent'
564
+ %w[
565
+ Instructions ToolsList ModelsList AgentName TemporalIntent
566
+ ]
567
+ else
568
+ %w[
569
+ PersonaName PersonaDescription PersonaSystemPrompt
570
+ AgentInstructions AgentTools
571
+ ]
572
+ end
573
+
574
+ required_placeholders.each do |placeholder|
575
+ errors << "Missing required placeholder: {{.#{placeholder}}}" unless content.include?("{{.#{placeholder}}}")
576
+ end
577
+
578
+ # Check for balanced braces
579
+ open_braces = content.scan(/{{/).count
580
+ close_braces = content.scan(/}}/).count
581
+ errors << "Unbalanced template braces ({{ vs }}): #{open_braces} open, #{close_braces} close" if open_braces != close_braces
582
+
583
+ # Extract and validate Ruby code blocks
584
+ code_examples = extract_code_examples(content)
585
+ code_examples.each do |example|
586
+ code_result = validate_code_against_schema(example[:code])
587
+ unless code_result[:valid]
588
+ code_result[:errors].each do |err|
589
+ # Adjust line numbers to be relative to template
590
+ line = example[:start_line] + (err[:location] || 0)
591
+ errors << "Line #{line}: #{err[:message]}"
592
+ end
593
+ end
594
+ code_result[:warnings].each do |warn|
595
+ line = example[:start_line] + (warn[:location] || 0)
596
+ warnings << "Line #{line}: #{warn[:message]}"
597
+ end
598
+ end
599
+
600
+ # Extract method calls and check if they're in the safe list
601
+ method_calls = extract_method_calls(content)
602
+ safe_methods = Dsl::Schema.safe_agent_methods +
603
+ Dsl::Schema.safe_tool_methods +
604
+ Dsl::Schema.safe_helper_methods
605
+ method_calls.each do |method|
606
+ next if safe_methods.include?(method)
607
+
608
+ warnings << "Method '#{method}' not in safe methods list (may be valid Ruby builtin)"
609
+ end
610
+
611
+ {
612
+ valid: errors.empty?,
613
+ errors: errors,
614
+ warnings: warnings
615
+ }
616
+ end
617
+
618
+ # Extract Ruby code examples from template
619
+ # Returns array of {code: String, start_line: Integer}
620
+ def extract_code_examples(template)
621
+ examples = []
622
+ lines = template.split("\n")
623
+ in_code_block = false
624
+ current_code = []
625
+ start_line = 0
626
+
627
+ lines.each_with_index do |line, idx|
628
+ if line.strip.start_with?('```ruby')
629
+ in_code_block = true
630
+ start_line = idx + 2 # idx is 0-based, we want line number (1-based) of first code line
631
+ current_code = []
632
+ elsif line.strip == '```' && in_code_block
633
+ in_code_block = false
634
+ examples << { code: current_code.join("\n"), start_line: start_line } unless current_code.empty?
635
+ elsif in_code_block
636
+ current_code << line
637
+ end
638
+ end
639
+
640
+ examples
641
+ end
642
+
643
+ # Extract method calls from template code
644
+ # Returns array of method name strings
645
+ def extract_method_calls(template)
646
+ require 'prism'
647
+
648
+ method_calls = []
649
+ code_examples = extract_code_examples(template)
650
+
651
+ code_examples.each do |example|
652
+ # Parse the code to find method calls
653
+ result = Prism.parse(example[:code])
654
+
655
+ # Walk the AST to find method calls
656
+ extract_methods_from_ast(result.value, method_calls) if result.success?
657
+ rescue Prism::ParseError
658
+ # Skip code with syntax errors - they'll be caught by validate_code_against_schema
659
+ next
660
+ end
661
+
662
+ method_calls.uniq
663
+ end
664
+
665
+ # Recursively extract method names from AST
666
+ def extract_methods_from_ast(node, methods)
667
+ return unless node
668
+
669
+ methods << node.name.to_s if node.is_a?(Prism::CallNode)
670
+
671
+ node.compact_child_nodes.each do |child|
672
+ extract_methods_from_ast(child, methods)
673
+ end
674
+ end
675
+
676
+ # Validate Ruby code against DSL schema
677
+ # Returns {valid: Boolean, errors: Array<Hash>, warnings: Array<Hash>}
678
+ def validate_code_against_schema(code)
679
+ require 'language_operator/agent/safety/ast_validator'
680
+
681
+ validator = LanguageOperator::Agent::Safety::ASTValidator.new
682
+ violations = validator.validate(code, '(template)')
683
+
684
+ errors = []
685
+ warnings = []
686
+
687
+ violations.each do |violation|
688
+ case violation[:type]
689
+ when :syntax_error
690
+ errors << {
691
+ type: :syntax_error,
692
+ location: 0,
693
+ message: violation[:message]
694
+ }
695
+ when :dangerous_method, :dangerous_constant, :dangerous_constant_access, :dangerous_global, :backtick_execution
696
+ errors << {
697
+ type: violation[:type],
698
+ location: violation[:location],
699
+ message: violation[:message]
700
+ }
701
+ else
702
+ warnings << {
703
+ type: violation[:type],
704
+ location: violation[:location] || 0,
705
+ message: violation[:message]
706
+ }
707
+ end
708
+ end
709
+
710
+ {
711
+ valid: errors.empty?,
712
+ errors: errors,
713
+ warnings: warnings
714
+ }
715
+ end
716
+
717
+ # Output raw template format
718
+ def output_template_format(content)
719
+ puts content
720
+ end
721
+
722
+ # Output JSON format with metadata
723
+ def output_json_format(content, type)
724
+ data = {
725
+ version: Dsl::Schema.version,
726
+ template_type: type,
727
+ template: content
728
+ }
729
+
730
+ if options[:with_schema]
731
+ data[:schema] = Dsl::Schema.to_json_schema
732
+ data[:safe_agent_methods] = Dsl::Schema.safe_agent_methods
733
+ data[:safe_tool_methods] = Dsl::Schema.safe_tool_methods
734
+ data[:safe_helper_methods] = Dsl::Schema.safe_helper_methods
735
+ end
736
+
737
+ puts JSON.pretty_generate(data)
738
+ end
739
+
740
+ # Output YAML format with metadata
741
+ def output_yaml_format(content, type)
742
+ data = {
743
+ 'version' => Dsl::Schema.version,
744
+ 'template_type' => type,
745
+ 'template' => content
746
+ }
747
+
748
+ if options[:with_schema]
749
+ data['schema'] = Dsl::Schema.to_json_schema.transform_keys(&:to_s)
750
+ data['safe_agent_methods'] = Dsl::Schema.safe_agent_methods
751
+ data['safe_tool_methods'] = Dsl::Schema.safe_tool_methods
752
+ data['safe_helper_methods'] = Dsl::Schema.safe_helper_methods
753
+ end
754
+
755
+ puts YAML.dump(data)
756
+ end
757
+ end
758
+ end
759
+ end
760
+ end