language-operator 0.0.1 → 0.1.30

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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +125 -0
  3. data/CHANGELOG.md +53 -0
  4. data/Gemfile +8 -0
  5. data/Gemfile.lock +284 -0
  6. data/LICENSE +229 -21
  7. data/Makefile +77 -0
  8. data/README.md +3 -11
  9. data/Rakefile +34 -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/agent-reference.md +591 -0
  16. data/docs/dsl/best-practices.md +1078 -0
  17. data/docs/dsl/chat-endpoints.md +895 -0
  18. data/docs/dsl/constraints.md +671 -0
  19. data/docs/dsl/mcp-integration.md +1177 -0
  20. data/docs/dsl/webhooks.md +932 -0
  21. data/docs/dsl/workflows.md +744 -0
  22. data/examples/README.md +569 -0
  23. data/examples/agent_example.rb +86 -0
  24. data/examples/chat_endpoint_agent.rb +118 -0
  25. data/examples/github_webhook_agent.rb +171 -0
  26. data/examples/mcp_agent.rb +158 -0
  27. data/examples/oauth_callback_agent.rb +296 -0
  28. data/examples/stripe_webhook_agent.rb +219 -0
  29. data/examples/webhook_agent.rb +80 -0
  30. data/lib/language_operator/agent/base.rb +110 -0
  31. data/lib/language_operator/agent/executor.rb +440 -0
  32. data/lib/language_operator/agent/instrumentation.rb +54 -0
  33. data/lib/language_operator/agent/metrics_tracker.rb +183 -0
  34. data/lib/language_operator/agent/safety/ast_validator.rb +272 -0
  35. data/lib/language_operator/agent/safety/audit_logger.rb +104 -0
  36. data/lib/language_operator/agent/safety/budget_tracker.rb +175 -0
  37. data/lib/language_operator/agent/safety/content_filter.rb +93 -0
  38. data/lib/language_operator/agent/safety/manager.rb +207 -0
  39. data/lib/language_operator/agent/safety/rate_limiter.rb +150 -0
  40. data/lib/language_operator/agent/safety/safe_executor.rb +115 -0
  41. data/lib/language_operator/agent/scheduler.rb +183 -0
  42. data/lib/language_operator/agent/telemetry.rb +116 -0
  43. data/lib/language_operator/agent/web_server.rb +610 -0
  44. data/lib/language_operator/agent/webhook_authenticator.rb +226 -0
  45. data/lib/language_operator/agent.rb +149 -0
  46. data/lib/language_operator/cli/commands/agent.rb +1252 -0
  47. data/lib/language_operator/cli/commands/cluster.rb +335 -0
  48. data/lib/language_operator/cli/commands/install.rb +404 -0
  49. data/lib/language_operator/cli/commands/model.rb +266 -0
  50. data/lib/language_operator/cli/commands/persona.rb +396 -0
  51. data/lib/language_operator/cli/commands/quickstart.rb +22 -0
  52. data/lib/language_operator/cli/commands/status.rb +156 -0
  53. data/lib/language_operator/cli/commands/tool.rb +537 -0
  54. data/lib/language_operator/cli/commands/use.rb +47 -0
  55. data/lib/language_operator/cli/errors/handler.rb +180 -0
  56. data/lib/language_operator/cli/errors/suggestions.rb +176 -0
  57. data/lib/language_operator/cli/formatters/code_formatter.rb +81 -0
  58. data/lib/language_operator/cli/formatters/log_formatter.rb +290 -0
  59. data/lib/language_operator/cli/formatters/progress_formatter.rb +53 -0
  60. data/lib/language_operator/cli/formatters/table_formatter.rb +179 -0
  61. data/lib/language_operator/cli/formatters/value_formatter.rb +113 -0
  62. data/lib/language_operator/cli/helpers/cluster_context.rb +62 -0
  63. data/lib/language_operator/cli/helpers/cluster_validator.rb +101 -0
  64. data/lib/language_operator/cli/helpers/editor_helper.rb +58 -0
  65. data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +167 -0
  66. data/lib/language_operator/cli/helpers/resource_dependency_checker.rb +74 -0
  67. data/lib/language_operator/cli/helpers/schedule_builder.rb +108 -0
  68. data/lib/language_operator/cli/helpers/user_prompts.rb +69 -0
  69. data/lib/language_operator/cli/main.rb +232 -0
  70. data/lib/language_operator/cli/templates/tools/generic.yaml +66 -0
  71. data/lib/language_operator/cli/wizards/agent_wizard.rb +246 -0
  72. data/lib/language_operator/cli/wizards/quickstart_wizard.rb +588 -0
  73. data/lib/language_operator/client/base.rb +214 -0
  74. data/lib/language_operator/client/config.rb +136 -0
  75. data/lib/language_operator/client/cost_calculator.rb +37 -0
  76. data/lib/language_operator/client/mcp_connector.rb +123 -0
  77. data/lib/language_operator/client.rb +19 -0
  78. data/lib/language_operator/config/cluster_config.rb +101 -0
  79. data/lib/language_operator/config/tool_patterns.yaml +57 -0
  80. data/lib/language_operator/config/tool_registry.rb +96 -0
  81. data/lib/language_operator/config.rb +138 -0
  82. data/lib/language_operator/dsl/adapter.rb +124 -0
  83. data/lib/language_operator/dsl/agent_context.rb +90 -0
  84. data/lib/language_operator/dsl/agent_definition.rb +427 -0
  85. data/lib/language_operator/dsl/chat_endpoint_definition.rb +115 -0
  86. data/lib/language_operator/dsl/config.rb +119 -0
  87. data/lib/language_operator/dsl/context.rb +50 -0
  88. data/lib/language_operator/dsl/execution_context.rb +47 -0
  89. data/lib/language_operator/dsl/helpers.rb +109 -0
  90. data/lib/language_operator/dsl/http.rb +184 -0
  91. data/lib/language_operator/dsl/mcp_server_definition.rb +73 -0
  92. data/lib/language_operator/dsl/parameter_definition.rb +124 -0
  93. data/lib/language_operator/dsl/registry.rb +36 -0
  94. data/lib/language_operator/dsl/shell.rb +125 -0
  95. data/lib/language_operator/dsl/tool_definition.rb +112 -0
  96. data/lib/language_operator/dsl/webhook_authentication.rb +114 -0
  97. data/lib/language_operator/dsl/webhook_definition.rb +106 -0
  98. data/lib/language_operator/dsl/workflow_definition.rb +259 -0
  99. data/lib/language_operator/dsl.rb +160 -0
  100. data/lib/language_operator/errors.rb +60 -0
  101. data/lib/language_operator/kubernetes/client.rb +279 -0
  102. data/lib/language_operator/kubernetes/resource_builder.rb +194 -0
  103. data/lib/language_operator/loggable.rb +47 -0
  104. data/lib/language_operator/logger.rb +141 -0
  105. data/lib/language_operator/retry.rb +123 -0
  106. data/lib/language_operator/retryable.rb +132 -0
  107. data/lib/language_operator/tool_loader.rb +242 -0
  108. data/lib/language_operator/validators.rb +170 -0
  109. data/lib/language_operator/version.rb +1 -1
  110. data/lib/language_operator.rb +65 -3
  111. data/requirements/tasks/challenge.md +9 -0
  112. data/requirements/tasks/iterate.md +36 -0
  113. data/requirements/tasks/optimize.md +21 -0
  114. data/requirements/tasks/tag.md +5 -0
  115. data/test_agent_dsl.rb +108 -0
  116. metadata +503 -20
@@ -0,0 +1,335 @@
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
+ table_data = clusters.map do |cluster|
125
+ k8s = Helpers::ClusterValidator.kubernetes_client(cluster[:name])
126
+
127
+ # Get cluster stats
128
+ agents = k8s.list_resources('LanguageAgent', namespace: cluster[:namespace])
129
+ tools = k8s.list_resources('LanguageTool', namespace: cluster[:namespace])
130
+ models = k8s.list_resources('LanguageModel', namespace: cluster[:namespace])
131
+
132
+ # Get cluster status
133
+ cluster_resource = k8s.get_resource('LanguageCluster', cluster[:name], cluster[:namespace])
134
+ status = cluster_resource.dig('status', 'phase') || 'Unknown'
135
+
136
+ name_display = cluster[:name]
137
+ name_display += ' *' if cluster[:name] == current
138
+
139
+ {
140
+ name: name_display,
141
+ namespace: cluster[:namespace],
142
+ agents: agents.count,
143
+ tools: tools.count,
144
+ models: models.count,
145
+ status: status
146
+ }
147
+ rescue StandardError
148
+ {
149
+ name: cluster[:name],
150
+ namespace: cluster[:namespace],
151
+ agents: '?',
152
+ tools: '?',
153
+ models: '?',
154
+ status: 'Error'
155
+ }
156
+ end
157
+
158
+ Formatters::TableFormatter.clusters(table_data)
159
+
160
+ if current
161
+ puts "\nCurrent cluster: #{current} (*)"
162
+ else
163
+ puts "\nNo cluster selected. Use 'aictl use <cluster>' to select one."
164
+ end
165
+ rescue StandardError => e
166
+ Formatters::ProgressFormatter.error("Failed to list clusters: #{e.message}")
167
+ raise if ENV['DEBUG']
168
+
169
+ exit 1
170
+ end
171
+
172
+ desc 'current', 'Show current cluster context'
173
+ def current
174
+ cluster_name = Config::ClusterConfig.current_cluster
175
+
176
+ unless cluster_name
177
+ Formatters::ProgressFormatter.info('No cluster selected')
178
+ puts "\nSelect a cluster with:"
179
+ puts ' aictl use <cluster>'
180
+ return
181
+ end
182
+
183
+ cluster = Config::ClusterConfig.get_cluster(cluster_name)
184
+
185
+ unless cluster
186
+ Formatters::ProgressFormatter.error("Cluster '#{cluster_name}' not found in config")
187
+ exit 1
188
+ end
189
+
190
+ puts 'Current Cluster:'
191
+ puts " Name: #{cluster[:name]}"
192
+ puts " Namespace: #{cluster[:namespace]}"
193
+ puts " Context: #{cluster[:context] || 'default'}"
194
+ puts " Created: #{cluster[:created]}"
195
+
196
+ # Check cluster health
197
+ begin
198
+ k8s = Helpers::ClusterValidator.kubernetes_client(cluster_name)
199
+
200
+ if k8s.operator_installed?
201
+ version = k8s.operator_version
202
+ Formatters::ProgressFormatter.success("Operator: #{version || 'installed'}")
203
+ else
204
+ Formatters::ProgressFormatter.warn('Operator: not found')
205
+ end
206
+ rescue StandardError => e
207
+ Formatters::ProgressFormatter.error("Connection: #{e.message}")
208
+ end
209
+ rescue StandardError => e
210
+ Formatters::ProgressFormatter.error("Failed to show current cluster: #{e.message}")
211
+ raise if ENV['DEBUG']
212
+
213
+ exit 1
214
+ end
215
+
216
+ desc 'delete NAME', 'Delete a cluster'
217
+ option :force, type: :boolean, default: false, desc: 'Skip confirmation'
218
+ def delete(name)
219
+ unless Config::ClusterConfig.cluster_exists?(name)
220
+ Formatters::ProgressFormatter.error("Cluster '#{name}' not found")
221
+ exit 1
222
+ end
223
+
224
+ cluster = Config::ClusterConfig.get_cluster(name)
225
+
226
+ # Confirm deletion
227
+ unless options[:force]
228
+ pastel = Pastel.new
229
+ puts "This will delete cluster #{pastel.bold.red(name)} and all its resources (agents, models, tools, personas)."
230
+ puts
231
+ return unless Helpers::UserPrompts.confirm('Are you sure?')
232
+ end
233
+
234
+ # Delete LanguageCluster resource
235
+ begin
236
+ k8s = Helpers::ClusterValidator.kubernetes_client(name)
237
+
238
+ Formatters::ProgressFormatter.with_spinner('Deleting LanguageCluster resource') do
239
+ k8s.delete_resource('LanguageCluster', name, cluster[:namespace])
240
+ end
241
+ rescue StandardError => e
242
+ Formatters::ProgressFormatter.warn("Failed to delete cluster resource: #{e.message}")
243
+ end
244
+
245
+ # Remove from config
246
+ Formatters::ProgressFormatter.with_spinner('Removing cluster from configuration') do
247
+ Config::ClusterConfig.remove_cluster(name)
248
+ end
249
+
250
+ # Clear current cluster if this was it
251
+ Config::ClusterConfig.set_current_cluster(nil) if Config::ClusterConfig.current_cluster == name
252
+
253
+ Formatters::ProgressFormatter.success("Deleted cluster '#{name}'")
254
+ rescue StandardError => e
255
+ Formatters::ProgressFormatter.error("Failed to delete cluster: #{e.message}")
256
+ raise if ENV['DEBUG']
257
+
258
+ exit 1
259
+ end
260
+
261
+ desc 'inspect NAME', 'Show detailed cluster information'
262
+ def inspect(name)
263
+ unless Config::ClusterConfig.cluster_exists?(name)
264
+ Formatters::ProgressFormatter.error("Cluster '#{name}' not found")
265
+ exit 1
266
+ end
267
+
268
+ cluster = Config::ClusterConfig.get_cluster(name)
269
+
270
+ puts "Cluster: #{name}"
271
+ puts " Namespace: #{cluster[:namespace]}"
272
+ puts " Context: #{cluster[:context] || 'default'}"
273
+ puts " Created: #{cluster[:created]}"
274
+ puts
275
+
276
+ # Get detailed cluster info
277
+ begin
278
+ k8s = Helpers::ClusterValidator.kubernetes_client(name)
279
+
280
+ # Get cluster resource
281
+ cluster_resource = k8s.get_resource('LanguageCluster', name, cluster[:namespace])
282
+ status = cluster_resource.dig('status', 'phase') || 'Unknown'
283
+
284
+ puts "Status: #{status}"
285
+ puts
286
+
287
+ # Get agents
288
+ agents = k8s.list_resources('LanguageAgent', namespace: cluster[:namespace])
289
+ puts "Agents: #{agents.count}"
290
+ agents.each do |agent|
291
+ agent_status = agent.dig('status', 'phase') || 'Unknown'
292
+ puts " - #{agent.dig('metadata', 'name')} (#{agent_status})"
293
+ end
294
+ puts
295
+
296
+ # Get tools
297
+ tools = k8s.list_resources('LanguageTool', namespace: cluster[:namespace])
298
+ puts "Tools: #{tools.count}"
299
+ tools.each do |tool|
300
+ tool_type = tool.dig('spec', 'type')
301
+ puts " - #{tool.dig('metadata', 'name')} (#{tool_type})"
302
+ end
303
+ puts
304
+
305
+ # Get models
306
+ models = k8s.list_resources('LanguageModel', namespace: cluster[:namespace])
307
+ puts "Models: #{models.count}"
308
+ models.each do |model|
309
+ provider = model.dig('spec', 'provider')
310
+ model_name = model.dig('spec', 'modelName')
311
+ puts " - #{model.dig('metadata', 'name')} (#{provider}/#{model_name})"
312
+ end
313
+ puts
314
+
315
+ # Get personas
316
+ personas = k8s.list_resources('LanguagePersona', namespace: cluster[:namespace])
317
+ puts "Personas: #{personas.count}"
318
+ personas.each do |persona|
319
+ tone = persona.dig('spec', 'tone')
320
+ puts " - #{persona.dig('metadata', 'name')} (#{tone})"
321
+ end
322
+ rescue StandardError => e
323
+ Formatters::ProgressFormatter.error("Failed to get cluster details: #{e.message}")
324
+ raise if ENV['DEBUG']
325
+ end
326
+ rescue StandardError => e
327
+ Formatters::ProgressFormatter.error("Failed to inspect cluster: #{e.message}")
328
+ raise if ENV['DEBUG']
329
+
330
+ exit 1
331
+ end
332
+ end
333
+ end
334
+ end
335
+ end