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.
Files changed (120) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +125 -0
  3. data/CHANGELOG.md +88 -0
  4. data/Gemfile +8 -0
  5. data/Gemfile.lock +284 -0
  6. data/LICENSE +229 -21
  7. data/Makefile +82 -0
  8. data/README.md +3 -11
  9. data/Rakefile +63 -0
  10. data/bin/aictl +7 -0
  11. data/completions/_aictl +232 -0
  12. data/completions/aictl.bash +121 -0
  13. data/completions/aictl.fish +114 -0
  14. data/docs/architecture/agent-runtime.md +585 -0
  15. data/docs/dsl/SCHEMA_VERSION.md +250 -0
  16. data/docs/dsl/agent-reference.md +604 -0
  17. data/docs/dsl/best-practices.md +1078 -0
  18. data/docs/dsl/chat-endpoints.md +895 -0
  19. data/docs/dsl/constraints.md +671 -0
  20. data/docs/dsl/mcp-integration.md +1177 -0
  21. data/docs/dsl/webhooks.md +932 -0
  22. data/docs/dsl/workflows.md +744 -0
  23. data/lib/language_operator/agent/base.rb +110 -0
  24. data/lib/language_operator/agent/executor.rb +440 -0
  25. data/lib/language_operator/agent/instrumentation.rb +54 -0
  26. data/lib/language_operator/agent/metrics_tracker.rb +183 -0
  27. data/lib/language_operator/agent/safety/ast_validator.rb +272 -0
  28. data/lib/language_operator/agent/safety/audit_logger.rb +104 -0
  29. data/lib/language_operator/agent/safety/budget_tracker.rb +175 -0
  30. data/lib/language_operator/agent/safety/content_filter.rb +93 -0
  31. data/lib/language_operator/agent/safety/manager.rb +207 -0
  32. data/lib/language_operator/agent/safety/rate_limiter.rb +150 -0
  33. data/lib/language_operator/agent/safety/safe_executor.rb +127 -0
  34. data/lib/language_operator/agent/scheduler.rb +183 -0
  35. data/lib/language_operator/agent/telemetry.rb +116 -0
  36. data/lib/language_operator/agent/web_server.rb +610 -0
  37. data/lib/language_operator/agent/webhook_authenticator.rb +226 -0
  38. data/lib/language_operator/agent.rb +149 -0
  39. data/lib/language_operator/cli/commands/agent.rb +1205 -0
  40. data/lib/language_operator/cli/commands/cluster.rb +371 -0
  41. data/lib/language_operator/cli/commands/install.rb +404 -0
  42. data/lib/language_operator/cli/commands/model.rb +266 -0
  43. data/lib/language_operator/cli/commands/persona.rb +393 -0
  44. data/lib/language_operator/cli/commands/quickstart.rb +22 -0
  45. data/lib/language_operator/cli/commands/status.rb +143 -0
  46. data/lib/language_operator/cli/commands/system.rb +772 -0
  47. data/lib/language_operator/cli/commands/tool.rb +537 -0
  48. data/lib/language_operator/cli/commands/use.rb +47 -0
  49. data/lib/language_operator/cli/errors/handler.rb +180 -0
  50. data/lib/language_operator/cli/errors/suggestions.rb +176 -0
  51. data/lib/language_operator/cli/formatters/code_formatter.rb +77 -0
  52. data/lib/language_operator/cli/formatters/log_formatter.rb +288 -0
  53. data/lib/language_operator/cli/formatters/progress_formatter.rb +49 -0
  54. data/lib/language_operator/cli/formatters/status_formatter.rb +37 -0
  55. data/lib/language_operator/cli/formatters/table_formatter.rb +163 -0
  56. data/lib/language_operator/cli/formatters/value_formatter.rb +113 -0
  57. data/lib/language_operator/cli/helpers/cluster_context.rb +62 -0
  58. data/lib/language_operator/cli/helpers/cluster_validator.rb +101 -0
  59. data/lib/language_operator/cli/helpers/editor_helper.rb +58 -0
  60. data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +167 -0
  61. data/lib/language_operator/cli/helpers/pastel_helper.rb +24 -0
  62. data/lib/language_operator/cli/helpers/resource_dependency_checker.rb +74 -0
  63. data/lib/language_operator/cli/helpers/schedule_builder.rb +108 -0
  64. data/lib/language_operator/cli/helpers/user_prompts.rb +69 -0
  65. data/lib/language_operator/cli/main.rb +236 -0
  66. data/lib/language_operator/cli/templates/tools/generic.yaml +66 -0
  67. data/lib/language_operator/cli/wizards/agent_wizard.rb +246 -0
  68. data/lib/language_operator/cli/wizards/quickstart_wizard.rb +588 -0
  69. data/lib/language_operator/client/base.rb +214 -0
  70. data/lib/language_operator/client/config.rb +136 -0
  71. data/lib/language_operator/client/cost_calculator.rb +37 -0
  72. data/lib/language_operator/client/mcp_connector.rb +123 -0
  73. data/lib/language_operator/client.rb +19 -0
  74. data/lib/language_operator/config/cluster_config.rb +101 -0
  75. data/lib/language_operator/config/tool_patterns.yaml +57 -0
  76. data/lib/language_operator/config/tool_registry.rb +96 -0
  77. data/lib/language_operator/config.rb +138 -0
  78. data/lib/language_operator/dsl/adapter.rb +124 -0
  79. data/lib/language_operator/dsl/agent_context.rb +90 -0
  80. data/lib/language_operator/dsl/agent_definition.rb +427 -0
  81. data/lib/language_operator/dsl/chat_endpoint_definition.rb +115 -0
  82. data/lib/language_operator/dsl/config.rb +119 -0
  83. data/lib/language_operator/dsl/context.rb +50 -0
  84. data/lib/language_operator/dsl/execution_context.rb +47 -0
  85. data/lib/language_operator/dsl/helpers.rb +109 -0
  86. data/lib/language_operator/dsl/http.rb +184 -0
  87. data/lib/language_operator/dsl/mcp_server_definition.rb +73 -0
  88. data/lib/language_operator/dsl/parameter_definition.rb +124 -0
  89. data/lib/language_operator/dsl/registry.rb +36 -0
  90. data/lib/language_operator/dsl/schema.rb +1102 -0
  91. data/lib/language_operator/dsl/shell.rb +125 -0
  92. data/lib/language_operator/dsl/tool_definition.rb +112 -0
  93. data/lib/language_operator/dsl/webhook_authentication.rb +114 -0
  94. data/lib/language_operator/dsl/webhook_definition.rb +106 -0
  95. data/lib/language_operator/dsl/workflow_definition.rb +259 -0
  96. data/lib/language_operator/dsl.rb +161 -0
  97. data/lib/language_operator/errors.rb +60 -0
  98. data/lib/language_operator/kubernetes/client.rb +279 -0
  99. data/lib/language_operator/kubernetes/resource_builder.rb +194 -0
  100. data/lib/language_operator/loggable.rb +47 -0
  101. data/lib/language_operator/logger.rb +141 -0
  102. data/lib/language_operator/retry.rb +123 -0
  103. data/lib/language_operator/retryable.rb +132 -0
  104. data/lib/language_operator/templates/README.md +23 -0
  105. data/lib/language_operator/templates/examples/agent_synthesis.tmpl +115 -0
  106. data/lib/language_operator/templates/examples/persona_distillation.tmpl +19 -0
  107. data/lib/language_operator/templates/schema/.gitkeep +0 -0
  108. data/lib/language_operator/templates/schema/CHANGELOG.md +93 -0
  109. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +306 -0
  110. data/lib/language_operator/templates/schema/agent_dsl_schema.json +452 -0
  111. data/lib/language_operator/tool_loader.rb +242 -0
  112. data/lib/language_operator/validators.rb +170 -0
  113. data/lib/language_operator/version.rb +1 -1
  114. data/lib/language_operator.rb +65 -3
  115. data/requirements/tasks/challenge.md +9 -0
  116. data/requirements/tasks/iterate.md +36 -0
  117. data/requirements/tasks/optimize.md +21 -0
  118. data/requirements/tasks/tag.md +5 -0
  119. data/test_agent_dsl.rb +108 -0
  120. metadata +507 -20
@@ -0,0 +1,371 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'yaml'
5
+ require_relative '../formatters/progress_formatter'
6
+ require_relative '../formatters/table_formatter'
7
+ require_relative '../helpers/cluster_validator'
8
+ require_relative '../helpers/user_prompts'
9
+ require_relative '../../config/cluster_config'
10
+ require_relative '../../kubernetes/client'
11
+ require_relative '../../kubernetes/resource_builder'
12
+
13
+ module LanguageOperator
14
+ module CLI
15
+ module Commands
16
+ # Cluster management commands
17
+ class Cluster < Thor
18
+ desc 'create NAME', 'Create a new language cluster'
19
+ option :namespace, type: :string, desc: 'Kubernetes namespace (defaults to current context namespace)'
20
+ option :kubeconfig, type: :string, desc: 'Path to kubeconfig file'
21
+ option :context, type: :string, desc: 'Kubernetes context to use'
22
+ option :switch, type: :boolean, default: true, desc: 'Switch to new cluster context'
23
+ option :dry_run, type: :boolean, default: false, desc: 'Output the manifest without creating'
24
+ def create(name)
25
+ kubeconfig = options[:kubeconfig]
26
+ context = options[:context]
27
+
28
+ # Handle dry-run: output manifest and exit early
29
+ if options[:dry_run]
30
+ namespace = options[:namespace] || 'default'
31
+ resource = Kubernetes::ResourceBuilder.language_cluster(name, namespace: namespace)
32
+ puts resource.to_yaml
33
+ return
34
+ end
35
+
36
+ # Check if cluster already exists
37
+ if Config::ClusterConfig.cluster_exists?(name)
38
+ Formatters::ProgressFormatter.error("Cluster '#{name}' already exists")
39
+ exit 1
40
+ end
41
+
42
+ # Create Kubernetes client
43
+ k8s = Formatters::ProgressFormatter.with_spinner('Connecting to Kubernetes cluster') do
44
+ Kubernetes::Client.new(kubeconfig: kubeconfig, context: context)
45
+ end
46
+
47
+ # Determine namespace: use --namespace flag, or current context namespace, or 'default'
48
+ namespace = options[:namespace] || k8s.current_namespace || 'default'
49
+
50
+ # Check if operator is installed
51
+ unless k8s.operator_installed?
52
+ Formatters::ProgressFormatter.error('Language Operator not found in cluster')
53
+ puts "\nInstall the operator first:"
54
+ puts ' aictl install'
55
+ exit 1
56
+ end
57
+
58
+ # Create namespace if it doesn't exist
59
+ unless k8s.namespace_exists?(namespace)
60
+ Formatters::ProgressFormatter.with_spinner("Creating namespace '#{namespace}'") do
61
+ k8s.create_namespace(namespace, labels: {
62
+ 'app.kubernetes.io/managed-by' => 'aictl',
63
+ 'langop.io/cluster' => name
64
+ })
65
+ end
66
+ end
67
+
68
+ # Create LanguageCluster resource
69
+ Formatters::ProgressFormatter.with_spinner('Creating LanguageCluster resource') do
70
+ resource = Kubernetes::ResourceBuilder.language_cluster(name, namespace: namespace)
71
+ k8s.apply_resource(resource)
72
+ resource
73
+ end
74
+
75
+ # Get the actual Kubernetes context being used
76
+ actual_context = k8s.current_context
77
+
78
+ # Save cluster to config
79
+ Formatters::ProgressFormatter.with_spinner('Saving cluster configuration') do
80
+ Config::ClusterConfig.add_cluster(
81
+ name,
82
+ namespace,
83
+ kubeconfig || ENV.fetch('KUBECONFIG', File.expand_path('~/.kube/config')),
84
+ actual_context
85
+ )
86
+ end
87
+
88
+ # Switch to new cluster if requested
89
+ if options[:switch]
90
+ Config::ClusterConfig.set_current_cluster(name)
91
+ Formatters::ProgressFormatter.success("Created and switched to cluster '#{name}'")
92
+ else
93
+ Formatters::ProgressFormatter.success("Created cluster '#{name}'")
94
+ puts "\nSwitch to this cluster with:"
95
+ puts " aictl use #{name}"
96
+ end
97
+
98
+ pastel = Pastel.new
99
+ puts "\nCluster Details"
100
+ puts '----------------'
101
+ puts "Name: #{pastel.bold.white(name)}"
102
+ puts "Namespace: #{pastel.bold.white(namespace)}"
103
+ rescue StandardError => e
104
+ Formatters::ProgressFormatter.error("Failed to create cluster: #{e.message}")
105
+ raise if ENV['DEBUG']
106
+
107
+ exit 1
108
+ end
109
+
110
+ desc 'list', 'List all clusters'
111
+ option :all, type: :boolean, default: false, desc: 'Show all clusters including inactive'
112
+ def list
113
+ clusters = Config::ClusterConfig.list_clusters
114
+ current = Config::ClusterConfig.current_cluster
115
+
116
+ if clusters.empty?
117
+ Formatters::ProgressFormatter.info('No clusters found')
118
+ puts "\nCreate a cluster with:"
119
+ puts ' aictl cluster create <name>'
120
+ return
121
+ end
122
+
123
+ # Build table data
124
+ # rubocop:disable Metrics/BlockLength
125
+ table_data = clusters.map do |cluster|
126
+ k8s = Helpers::ClusterValidator.kubernetes_client(cluster[:name])
127
+
128
+ # Get cluster stats
129
+ agents = k8s.list_resources('LanguageAgent', namespace: cluster[:namespace])
130
+ tools = k8s.list_resources('LanguageTool', namespace: cluster[:namespace])
131
+ models = k8s.list_resources('LanguageModel', namespace: cluster[:namespace])
132
+
133
+ # Get cluster status
134
+ cluster_resource = k8s.get_resource('LanguageCluster', cluster[:name], cluster[:namespace])
135
+ status = cluster_resource.dig('status', 'phase') || 'Unknown'
136
+
137
+ name_display = cluster[:name]
138
+ name_display += ' *' if cluster[:name] == current
139
+
140
+ {
141
+ name: name_display,
142
+ namespace: cluster[:namespace],
143
+ agents: agents.count,
144
+ tools: tools.count,
145
+ models: models.count,
146
+ status: status
147
+ }
148
+ rescue K8s::Error::NotFound
149
+ # Cluster exists in local config but not in Kubernetes
150
+ name_display = cluster[:name]
151
+ name_display += ' *' if cluster[:name] == current
152
+
153
+ {
154
+ name: name_display,
155
+ namespace: cluster[:namespace],
156
+ agents: '-',
157
+ tools: '-',
158
+ models: '-',
159
+ status: 'Not Found'
160
+ }
161
+ rescue StandardError
162
+ # Other errors (connection issues, auth problems, etc.)
163
+ name_display = cluster[:name]
164
+ name_display += ' *' if cluster[:name] == current
165
+
166
+ {
167
+ name: name_display,
168
+ namespace: cluster[:namespace],
169
+ agents: '?',
170
+ tools: '?',
171
+ models: '?',
172
+ status: 'Error'
173
+ }
174
+ end
175
+ # rubocop:enable Metrics/BlockLength
176
+
177
+ Formatters::TableFormatter.clusters(table_data)
178
+
179
+ if current
180
+ puts "\nCurrent cluster: #{current} (*)"
181
+ else
182
+ puts "\nNo cluster selected. Use 'aictl use <cluster>' to select one."
183
+ end
184
+
185
+ # Show helpful message if any clusters are not found
186
+ not_found_clusters = table_data.select { |c| c[:status] == 'Not Found' }
187
+ if not_found_clusters.any?
188
+ puts
189
+ Formatters::ProgressFormatter.warn('Some clusters exist in local config but not in Kubernetes')
190
+ puts
191
+ puts 'Clusters with "Not Found" status are defined in ~/.aictl/config.yaml'
192
+ puts 'but the corresponding LanguageCluster resource does not exist in Kubernetes.'
193
+ puts
194
+ puts 'To fix this:'
195
+ not_found_clusters.each do |cluster|
196
+ cluster_name = cluster[:name].gsub(' *', '')
197
+ puts " • Remove from config: aictl cluster delete #{cluster_name}"
198
+ puts " • Or recreate: aictl cluster create #{cluster_name}"
199
+ end
200
+ end
201
+ rescue StandardError => e
202
+ Formatters::ProgressFormatter.error("Failed to list clusters: #{e.message}")
203
+ raise if ENV['DEBUG']
204
+
205
+ exit 1
206
+ end
207
+
208
+ desc 'current', 'Show current cluster context'
209
+ def current
210
+ cluster_name = Config::ClusterConfig.current_cluster
211
+
212
+ unless cluster_name
213
+ Formatters::ProgressFormatter.info('No cluster selected')
214
+ puts "\nSelect a cluster with:"
215
+ puts ' aictl use <cluster>'
216
+ return
217
+ end
218
+
219
+ cluster = Config::ClusterConfig.get_cluster(cluster_name)
220
+
221
+ unless cluster
222
+ Formatters::ProgressFormatter.error("Cluster '#{cluster_name}' not found in config")
223
+ exit 1
224
+ end
225
+
226
+ puts 'Current Cluster:'
227
+ puts " Name: #{cluster[:name]}"
228
+ puts " Namespace: #{cluster[:namespace]}"
229
+ puts " Context: #{cluster[:context] || 'default'}"
230
+ puts " Created: #{cluster[:created]}"
231
+
232
+ # Check cluster health
233
+ begin
234
+ k8s = Helpers::ClusterValidator.kubernetes_client(cluster_name)
235
+
236
+ if k8s.operator_installed?
237
+ version = k8s.operator_version
238
+ Formatters::ProgressFormatter.success("Operator: #{version || 'installed'}")
239
+ else
240
+ Formatters::ProgressFormatter.warn('Operator: not found')
241
+ end
242
+ rescue StandardError => e
243
+ Formatters::ProgressFormatter.error("Connection: #{e.message}")
244
+ end
245
+ rescue StandardError => e
246
+ Formatters::ProgressFormatter.error("Failed to show current cluster: #{e.message}")
247
+ raise if ENV['DEBUG']
248
+
249
+ exit 1
250
+ end
251
+
252
+ desc 'delete NAME', 'Delete a cluster'
253
+ option :force, type: :boolean, default: false, desc: 'Skip confirmation'
254
+ def delete(name)
255
+ unless Config::ClusterConfig.cluster_exists?(name)
256
+ Formatters::ProgressFormatter.error("Cluster '#{name}' not found")
257
+ exit 1
258
+ end
259
+
260
+ cluster = Config::ClusterConfig.get_cluster(name)
261
+
262
+ # Confirm deletion
263
+ unless options[:force]
264
+ pastel = Pastel.new
265
+ puts "This will delete cluster #{pastel.bold.red(name)} and all its resources (agents, models, tools, personas)."
266
+ puts
267
+ return unless Helpers::UserPrompts.confirm('Are you sure?')
268
+ end
269
+
270
+ # Delete LanguageCluster resource
271
+ begin
272
+ k8s = Helpers::ClusterValidator.kubernetes_client(name)
273
+
274
+ Formatters::ProgressFormatter.with_spinner('Deleting LanguageCluster resource') do
275
+ k8s.delete_resource('LanguageCluster', name, cluster[:namespace])
276
+ end
277
+ rescue StandardError => e
278
+ Formatters::ProgressFormatter.warn("Failed to delete cluster resource: #{e.message}")
279
+ end
280
+
281
+ # Remove from config
282
+ Formatters::ProgressFormatter.with_spinner('Removing cluster from configuration') do
283
+ Config::ClusterConfig.remove_cluster(name)
284
+ end
285
+
286
+ # Clear current cluster if this was it
287
+ Config::ClusterConfig.set_current_cluster(nil) if Config::ClusterConfig.current_cluster == name
288
+
289
+ Formatters::ProgressFormatter.success("Deleted cluster '#{name}'")
290
+ rescue StandardError => e
291
+ Formatters::ProgressFormatter.error("Failed to delete cluster: #{e.message}")
292
+ raise if ENV['DEBUG']
293
+
294
+ exit 1
295
+ end
296
+
297
+ desc 'inspect NAME', 'Show detailed cluster information'
298
+ def inspect(name)
299
+ unless Config::ClusterConfig.cluster_exists?(name)
300
+ Formatters::ProgressFormatter.error("Cluster '#{name}' not found")
301
+ exit 1
302
+ end
303
+
304
+ cluster = Config::ClusterConfig.get_cluster(name)
305
+
306
+ puts "Cluster: #{name}"
307
+ puts " Namespace: #{cluster[:namespace]}"
308
+ puts " Context: #{cluster[:context] || 'default'}"
309
+ puts " Created: #{cluster[:created]}"
310
+ puts
311
+
312
+ # Get detailed cluster info
313
+ begin
314
+ k8s = Helpers::ClusterValidator.kubernetes_client(name)
315
+
316
+ # Get cluster resource
317
+ cluster_resource = k8s.get_resource('LanguageCluster', name, cluster[:namespace])
318
+ status = cluster_resource.dig('status', 'phase') || 'Unknown'
319
+
320
+ puts "Status: #{status}"
321
+ puts
322
+
323
+ # Get agents
324
+ agents = k8s.list_resources('LanguageAgent', namespace: cluster[:namespace])
325
+ puts "Agents: #{agents.count}"
326
+ agents.each do |agent|
327
+ agent_status = agent.dig('status', 'phase') || 'Unknown'
328
+ puts " - #{agent.dig('metadata', 'name')} (#{agent_status})"
329
+ end
330
+ puts
331
+
332
+ # Get tools
333
+ tools = k8s.list_resources('LanguageTool', namespace: cluster[:namespace])
334
+ puts "Tools: #{tools.count}"
335
+ tools.each do |tool|
336
+ tool_type = tool.dig('spec', 'type')
337
+ puts " - #{tool.dig('metadata', 'name')} (#{tool_type})"
338
+ end
339
+ puts
340
+
341
+ # Get models
342
+ models = k8s.list_resources('LanguageModel', namespace: cluster[:namespace])
343
+ puts "Models: #{models.count}"
344
+ models.each do |model|
345
+ provider = model.dig('spec', 'provider')
346
+ model_name = model.dig('spec', 'modelName')
347
+ puts " - #{model.dig('metadata', 'name')} (#{provider}/#{model_name})"
348
+ end
349
+ puts
350
+
351
+ # Get personas
352
+ personas = k8s.list_resources('LanguagePersona', namespace: cluster[:namespace])
353
+ puts "Personas: #{personas.count}"
354
+ personas.each do |persona|
355
+ tone = persona.dig('spec', 'tone')
356
+ puts " - #{persona.dig('metadata', 'name')} (#{tone})"
357
+ end
358
+ rescue StandardError => e
359
+ Formatters::ProgressFormatter.error("Failed to get cluster details: #{e.message}")
360
+ raise if ENV['DEBUG']
361
+ end
362
+ rescue StandardError => e
363
+ Formatters::ProgressFormatter.error("Failed to inspect cluster: #{e.message}")
364
+ raise if ENV['DEBUG']
365
+
366
+ exit 1
367
+ end
368
+ end
369
+ end
370
+ end
371
+ end