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