language-operator 0.1.61 → 0.1.62

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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/persona.md +9 -0
  3. data/.claude/commands/task.md +46 -1
  4. data/.rubocop.yml +13 -0
  5. data/.rubocop_custom/use_ux_helper.rb +44 -0
  6. data/CHANGELOG.md +8 -0
  7. data/Gemfile.lock +12 -1
  8. data/Makefile +26 -7
  9. data/Makefile.common +50 -0
  10. data/bin/aictl +8 -1
  11. data/components/agent/Gemfile +1 -1
  12. data/components/agent/bin/langop-agent +7 -0
  13. data/docs/README.md +58 -0
  14. data/docs/{dsl/best-practices.md → best-practices.md} +4 -4
  15. data/docs/cli-reference.md +274 -0
  16. data/docs/{dsl/constraints.md → constraints.md} +5 -5
  17. data/docs/how-agents-work.md +156 -0
  18. data/docs/installation.md +218 -0
  19. data/docs/quickstart.md +299 -0
  20. data/docs/understanding-generated-code.md +265 -0
  21. data/docs/using-tools.md +457 -0
  22. data/docs/webhooks.md +509 -0
  23. data/examples/ux_helpers_demo.rb +296 -0
  24. data/lib/language_operator/agent/base.rb +11 -1
  25. data/lib/language_operator/agent/executor.rb +23 -6
  26. data/lib/language_operator/agent/safety/safe_executor.rb +41 -39
  27. data/lib/language_operator/agent/task_executor.rb +346 -63
  28. data/lib/language_operator/agent/web_server.rb +110 -14
  29. data/lib/language_operator/agent/webhook_authenticator.rb +39 -5
  30. data/lib/language_operator/agent.rb +88 -2
  31. data/lib/language_operator/cli/base_command.rb +17 -11
  32. data/lib/language_operator/cli/command_loader.rb +72 -0
  33. data/lib/language_operator/cli/commands/agent/base.rb +837 -0
  34. data/lib/language_operator/cli/commands/agent/code_operations.rb +102 -0
  35. data/lib/language_operator/cli/commands/agent/helpers/cluster_llm_client.rb +116 -0
  36. data/lib/language_operator/cli/commands/agent/helpers/code_parser.rb +115 -0
  37. data/lib/language_operator/cli/commands/agent/helpers/synthesis_watcher.rb +96 -0
  38. data/lib/language_operator/cli/commands/agent/learning.rb +289 -0
  39. data/lib/language_operator/cli/commands/agent/lifecycle.rb +102 -0
  40. data/lib/language_operator/cli/commands/agent/logs.rb +125 -0
  41. data/lib/language_operator/cli/commands/agent/workspace.rb +327 -0
  42. data/lib/language_operator/cli/commands/cluster.rb +129 -84
  43. data/lib/language_operator/cli/commands/install.rb +1 -1
  44. data/lib/language_operator/cli/commands/model/base.rb +215 -0
  45. data/lib/language_operator/cli/commands/model/test.rb +165 -0
  46. data/lib/language_operator/cli/commands/persona.rb +16 -34
  47. data/lib/language_operator/cli/commands/quickstart.rb +3 -2
  48. data/lib/language_operator/cli/commands/status.rb +40 -67
  49. data/lib/language_operator/cli/commands/system/base.rb +44 -0
  50. data/lib/language_operator/cli/commands/system/exec.rb +147 -0
  51. data/lib/language_operator/cli/commands/system/helpers/llm_synthesis.rb +183 -0
  52. data/lib/language_operator/cli/commands/system/helpers/pod_manager.rb +212 -0
  53. data/lib/language_operator/cli/commands/system/helpers/template_loader.rb +57 -0
  54. data/lib/language_operator/cli/commands/system/helpers/template_validator.rb +174 -0
  55. data/lib/language_operator/cli/commands/system/schema.rb +92 -0
  56. data/lib/language_operator/cli/commands/system/synthesis_template.rb +151 -0
  57. data/lib/language_operator/cli/commands/system/synthesize.rb +224 -0
  58. data/lib/language_operator/cli/commands/system/validate_template.rb +130 -0
  59. data/lib/language_operator/cli/commands/tool/base.rb +271 -0
  60. data/lib/language_operator/cli/commands/tool/install.rb +255 -0
  61. data/lib/language_operator/cli/commands/tool/search.rb +69 -0
  62. data/lib/language_operator/cli/commands/tool/test.rb +115 -0
  63. data/lib/language_operator/cli/commands/use.rb +29 -6
  64. data/lib/language_operator/cli/errors/handler.rb +20 -17
  65. data/lib/language_operator/cli/errors/suggestions.rb +3 -5
  66. data/lib/language_operator/cli/errors/thor_errors.rb +55 -0
  67. data/lib/language_operator/cli/formatters/code_formatter.rb +4 -11
  68. data/lib/language_operator/cli/formatters/log_formatter.rb +8 -15
  69. data/lib/language_operator/cli/formatters/progress_formatter.rb +6 -8
  70. data/lib/language_operator/cli/formatters/status_formatter.rb +26 -7
  71. data/lib/language_operator/cli/formatters/table_formatter.rb +47 -36
  72. data/lib/language_operator/cli/formatters/value_formatter.rb +75 -0
  73. data/lib/language_operator/cli/helpers/cluster_context.rb +5 -3
  74. data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +2 -1
  75. data/lib/language_operator/cli/helpers/label_utils.rb +97 -0
  76. data/lib/language_operator/{ux/concerns/provider_helpers.rb → cli/helpers/provider_helper.rb} +10 -29
  77. data/lib/language_operator/cli/helpers/schedule_builder.rb +21 -1
  78. data/lib/language_operator/cli/helpers/user_prompts.rb +19 -11
  79. data/lib/language_operator/cli/helpers/ux_helper.rb +538 -0
  80. data/lib/language_operator/{ux/concerns/input_validation.rb → cli/helpers/validation_helper.rb} +13 -66
  81. data/lib/language_operator/cli/main.rb +50 -40
  82. data/lib/language_operator/cli/templates/tools/generic.yaml +3 -0
  83. data/lib/language_operator/cli/wizards/agent_wizard.rb +12 -20
  84. data/lib/language_operator/cli/wizards/model_wizard.rb +271 -0
  85. data/lib/language_operator/cli/wizards/quickstart_wizard.rb +8 -34
  86. data/lib/language_operator/client/base.rb +28 -0
  87. data/lib/language_operator/client/config.rb +4 -1
  88. data/lib/language_operator/client/mcp_connector.rb +1 -1
  89. data/lib/language_operator/config/cluster_config.rb +3 -2
  90. data/lib/language_operator/config.rb +38 -11
  91. data/lib/language_operator/constants/kubernetes_labels.rb +80 -0
  92. data/lib/language_operator/constants.rb +13 -0
  93. data/lib/language_operator/dsl/http.rb +127 -10
  94. data/lib/language_operator/dsl.rb +153 -6
  95. data/lib/language_operator/errors.rb +50 -0
  96. data/lib/language_operator/kubernetes/client.rb +11 -6
  97. data/lib/language_operator/kubernetes/resource_builder.rb +58 -84
  98. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
  99. data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
  100. data/lib/language_operator/type_coercion.rb +118 -34
  101. data/lib/language_operator/utils/secure_path.rb +74 -0
  102. data/lib/language_operator/utils.rb +7 -0
  103. data/lib/language_operator/validators.rb +54 -2
  104. data/lib/language_operator/version.rb +1 -1
  105. data/synth/001/Makefile +10 -2
  106. data/synth/001/agent.rb +16 -15
  107. data/synth/001/output.log +27 -10
  108. data/synth/002/Makefile +10 -2
  109. data/synth/003/Makefile +1 -1
  110. data/synth/003/README.md +205 -133
  111. data/synth/003/agent.optimized.rb +66 -0
  112. data/synth/003/agent.synthesized.rb +41 -0
  113. metadata +111 -35
  114. data/docs/dsl/agent-reference.md +0 -604
  115. data/docs/dsl/mcp-integration.md +0 -1177
  116. data/docs/dsl/webhooks.md +0 -932
  117. data/docs/dsl/workflows.md +0 -744
  118. data/lib/language_operator/cli/commands/agent.rb +0 -1712
  119. data/lib/language_operator/cli/commands/model.rb +0 -366
  120. data/lib/language_operator/cli/commands/system.rb +0 -1259
  121. data/lib/language_operator/cli/commands/tool.rb +0 -654
  122. data/lib/language_operator/cli/formatters/optimization_formatter.rb +0 -226
  123. data/lib/language_operator/cli/helpers/pastel_helper.rb +0 -24
  124. data/lib/language_operator/learning/adapters/base_adapter.rb +0 -149
  125. data/lib/language_operator/learning/adapters/jaeger_adapter.rb +0 -221
  126. data/lib/language_operator/learning/adapters/signoz_adapter.rb +0 -435
  127. data/lib/language_operator/learning/adapters/tempo_adapter.rb +0 -239
  128. data/lib/language_operator/learning/optimizer.rb +0 -319
  129. data/lib/language_operator/learning/pattern_detector.rb +0 -260
  130. data/lib/language_operator/learning/task_synthesizer.rb +0 -288
  131. data/lib/language_operator/learning/trace_analyzer.rb +0 -285
  132. data/lib/language_operator/templates/task_synthesis.tmpl +0 -98
  133. data/lib/language_operator/ux/base.rb +0 -81
  134. data/lib/language_operator/ux/concerns/README.md +0 -155
  135. data/lib/language_operator/ux/concerns/headings.rb +0 -90
  136. data/lib/language_operator/ux/create_agent.rb +0 -255
  137. data/lib/language_operator/ux/create_model.rb +0 -267
  138. data/lib/language_operator/ux/quickstart.rb +0 -594
  139. data/synth/003/agent.rb +0 -41
  140. data/synth/003/output.log +0 -68
  141. /data/docs/{architecture/agent-runtime.md → agent-internals.md} +0 -0
  142. /data/docs/{dsl/chat-endpoints.md → chat-endpoints.md} +0 -0
  143. /data/docs/{dsl/SCHEMA_VERSION.md → schema-versioning.md} +0 -0
@@ -1,21 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'yaml'
4
- require_relative '../base_command'
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'
4
+ require_relative '../command_loader'
5
+ require_relative '../../utils/secure_path'
12
6
 
13
7
  module LanguageOperator
14
8
  module CLI
15
9
  module Commands
16
10
  # Cluster management commands
17
11
  class Cluster < BaseCommand
18
- include Helpers::PastelHelper
12
+ include Constants
13
+ include Helpers::UxHelper
19
14
 
20
15
  desc 'create NAME', 'Create a new language cluster'
21
16
  option :namespace, type: :string, desc: 'Kubernetes namespace (defaults to current context namespace)'
@@ -23,6 +18,7 @@ module LanguageOperator
23
18
  option :context, type: :string, desc: 'Kubernetes context to use'
24
19
  option :switch, type: :boolean, default: true, desc: 'Switch to new cluster context'
25
20
  option :dry_run, type: :boolean, default: false, desc: 'Output the manifest without creating'
21
+ option :domain, type: :string, desc: 'Base domain for webhook routing (e.g., example.com)'
26
22
  def create(name)
27
23
  handle_command_error('create cluster') do
28
24
  kubeconfig = options[:kubeconfig]
@@ -31,7 +27,7 @@ module LanguageOperator
31
27
  # Handle dry-run: output manifest and exit early
32
28
  if options[:dry_run]
33
29
  namespace = options[:namespace] || 'default'
34
- resource = Kubernetes::ResourceBuilder.language_cluster(name, namespace: namespace)
30
+ resource = Kubernetes::ResourceBuilder.language_cluster(name, namespace: namespace, domain: options[:domain])
35
31
  puts resource.to_yaml
36
32
  return
37
33
  end
@@ -61,16 +57,15 @@ module LanguageOperator
61
57
  # Create namespace if it doesn't exist
62
58
  unless k8s.namespace_exists?(namespace)
63
59
  Formatters::ProgressFormatter.with_spinner("Creating namespace '#{namespace}'") do
64
- k8s.create_namespace(namespace, labels: {
65
- 'app.kubernetes.io/managed-by' => 'aictl',
66
- 'langop.io/cluster' => name
67
- })
60
+ k8s.create_namespace(namespace, labels: Constants::KubernetesLabels.cluster_management_labels.merge(
61
+ Constants::KubernetesLabels::CLUSTER_LABEL => name
62
+ ))
68
63
  end
69
64
  end
70
65
 
71
66
  # Create LanguageCluster resource
72
67
  Formatters::ProgressFormatter.with_spinner('Creating LanguageCluster resource') do
73
- resource = Kubernetes::ResourceBuilder.language_cluster(name, namespace: namespace)
68
+ resource = Kubernetes::ResourceBuilder.language_cluster(name, namespace: namespace, domain: options[:domain])
74
69
  k8s.apply_resource(resource)
75
70
  resource
76
71
  end
@@ -83,26 +78,30 @@ module LanguageOperator
83
78
  Config::ClusterConfig.add_cluster(
84
79
  name,
85
80
  namespace,
86
- kubeconfig || ENV.fetch('KUBECONFIG', File.expand_path('~/.kube/config')),
81
+ kubeconfig || ENV.fetch('KUBECONFIG', LanguageOperator::Utils::SecurePath.expand_home_path('.kube/config')),
87
82
  actual_context
88
83
  )
89
84
  end
90
85
 
91
86
  # Switch to new cluster if requested
92
- if options[:switch]
93
- Config::ClusterConfig.set_current_cluster(name)
94
- Formatters::ProgressFormatter.success("Created and switched to cluster '#{name}'")
95
- else
96
- Formatters::ProgressFormatter.success("Created cluster '#{name}'")
97
- puts "\nSwitch to this cluster with:"
98
- puts " aictl use #{name}"
99
- end
87
+ Config::ClusterConfig.set_current_cluster(name) if options[:switch]
100
88
 
101
- puts "\nCluster Details"
102
- puts '---------------'
103
- puts "Name: #{pastel.bold.white(name)}"
104
- puts "Namespace: #{pastel.bold.white(namespace)}"
105
- puts "Context: #{pastel.bold.white(actual_context)}"
89
+ puts
90
+ format_cluster_details(
91
+ name: name,
92
+ namespace: namespace,
93
+ context: actual_context,
94
+ domain: options[:domain],
95
+ status: 'Ready',
96
+ created: Time.now.strftime('%Y-%m-%dT%H:%M:%SZ')
97
+ )
98
+
99
+ # Show usage instructions if not auto-switched
100
+ unless options[:switch]
101
+ puts
102
+ puts 'Switch to this cluster with:'
103
+ puts pastel.dim(" aictl use #{name}")
104
+ end
106
105
  end
107
106
  end
108
107
 
@@ -120,21 +119,54 @@ module LanguageOperator
120
119
  return
121
120
  end
122
121
 
122
+ # Cache clients by kubeconfig:context to prevent resource leaks
123
+ clients_cache = {}
124
+
123
125
  # Build table data
124
126
  table_data = clusters.map do |cluster|
125
- k8s = Helpers::ClusterValidator.kubernetes_client(cluster[:name])
127
+ begin
128
+ # Get cluster config for cache key and reuse clients
129
+ cluster_config = Config::ClusterConfig.get_cluster(cluster[:name])
130
+ cache_key = "#{cluster_config[:kubeconfig]}:#{cluster_config[:context]}"
131
+
132
+ # Reuse existing client or create new one
133
+ k8s = clients_cache[cache_key] ||= begin
134
+ # Validate kubeconfig exists before creating client
135
+ Helpers::ClusterValidator.validate_kubeconfig!(cluster_config)
136
+ require_relative '../../kubernetes/client'
137
+ Kubernetes::Client.new(
138
+ kubeconfig: cluster_config[:kubeconfig],
139
+ context: cluster_config[:context]
140
+ )
141
+ end
142
+ rescue StandardError
143
+ # Handle cluster config or client creation errors
144
+ name_display = cluster[:name]
145
+ name_display += ' *' if cluster[:name] == current
146
+
147
+ next {
148
+ name: name_display,
149
+ namespace: cluster[:namespace],
150
+ agents: '?',
151
+ tools: '?',
152
+ models: '?',
153
+ status: 'Config Error',
154
+ domain: '?'
155
+ }
156
+ end
126
157
 
127
158
  # 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])
159
+ agents = k8s.list_resources(RESOURCE_AGENT, namespace: cluster[:namespace])
160
+ tools = k8s.list_resources(RESOURCE_TOOL, namespace: cluster[:namespace])
161
+ models = k8s.list_resources(RESOURCE_MODEL, namespace: cluster[:namespace])
131
162
 
132
- # Get cluster status
163
+ # Get cluster status and domain
133
164
  cluster_resource = k8s.get_resource('LanguageCluster', cluster[:name], cluster[:namespace])
134
165
  status = cluster_resource.dig('status', 'phase') || 'Unknown'
166
+ domain = cluster_resource.dig('spec', 'domain')
135
167
 
136
168
  name_display = cluster[:name]
137
- name_display += ' *' if cluster[:name] == current
169
+ name_display = "#{pastel.bold(cluster[:name])} (selected)" if cluster[:name] == current
138
170
 
139
171
  {
140
172
  name: name_display,
@@ -142,7 +174,8 @@ module LanguageOperator
142
174
  agents: agents.count,
143
175
  tools: tools.count,
144
176
  models: models.count,
145
- status: status
177
+ status: status,
178
+ domain: domain
146
179
  }
147
180
  rescue K8s::Error::NotFound
148
181
  # Cluster exists in local config but not in Kubernetes
@@ -155,7 +188,8 @@ module LanguageOperator
155
188
  agents: '-',
156
189
  tools: '-',
157
190
  models: '-',
158
- status: 'Not Found'
191
+ status: 'Not Found',
192
+ domain: '-'
159
193
  }
160
194
  rescue StandardError
161
195
  # Other errors (connection issues, auth problems, etc.)
@@ -168,17 +202,14 @@ module LanguageOperator
168
202
  agents: '?',
169
203
  tools: '?',
170
204
  models: '?',
171
- status: 'Error'
205
+ status: 'Error',
206
+ domain: '?'
172
207
  }
173
208
  end
174
209
 
175
210
  Formatters::TableFormatter.clusters(table_data)
176
211
 
177
- if current
178
- puts "\nCurrent cluster: #{current} (*)"
179
- else
180
- puts "\nNo cluster selected. Use 'aictl use <cluster>' to select one."
181
- end
212
+ puts "\nNo cluster selected. Use 'aictl use <cluster>' to select one." unless current
182
213
 
183
214
  # Show helpful message if any clusters are not found
184
215
  not_found_clusters = table_data.select { |c| c[:status] == 'Not Found' }
@@ -242,6 +273,7 @@ module LanguageOperator
242
273
 
243
274
  desc 'delete NAME', 'Delete a cluster'
244
275
  option :force, type: :boolean, default: false, desc: 'Skip confirmation'
276
+ option :force_local, type: :boolean, default: false, desc: 'Force removal from local config only (skip Kubernetes deletion)'
245
277
  def delete(name)
246
278
  handle_command_error('delete cluster') do
247
279
  unless Config::ClusterConfig.cluster_exists?(name)
@@ -252,32 +284,40 @@ module LanguageOperator
252
284
  cluster = Config::ClusterConfig.get_cluster(name)
253
285
 
254
286
  # Confirm deletion
255
- unless options[:force]
256
- puts "This will delete cluster #{pastel.bold.red(name)} and all its resources (agents, models, tools, personas)."
257
- puts
258
- return unless Helpers::UserPrompts.confirm('Are you sure?')
259
- end
287
+ return if !options[:force] && !confirm_deletion('cluster', name, name)
260
288
 
261
- # Delete LanguageCluster resource
262
- begin
263
- k8s = Helpers::ClusterValidator.kubernetes_client(name)
264
-
265
- Formatters::ProgressFormatter.with_spinner('Deleting LanguageCluster resource') do
266
- k8s.delete_resource('LanguageCluster', name, cluster[:namespace])
289
+ # Delete LanguageCluster resource from Kubernetes (unless --force-local)
290
+ if options[:force_local]
291
+ Formatters::ProgressFormatter.warn('Skipping Kubernetes resource deletion (--force-local specified)')
292
+ else
293
+ begin
294
+ k8s = Helpers::ClusterValidator.kubernetes_client(name)
295
+
296
+ Formatters::ProgressFormatter.with_spinner('Deleting LanguageCluster resource') do
297
+ k8s.delete_resource('LanguageCluster', name, cluster[:namespace])
298
+ end
299
+ rescue StandardError => e
300
+ Formatters::ProgressFormatter.error("Failed to delete cluster resource: #{e.message}")
301
+ puts
302
+ puts 'Cluster deletion failed. The LanguageCluster resource could not be removed from Kubernetes.'
303
+ puts 'This may be due to:'
304
+ puts ' • Network connectivity issues'
305
+ puts ' • Insufficient permissions'
306
+ puts ' • The cluster resource no longer exists'
307
+ puts
308
+ puts 'To force removal from local configuration only, use:'
309
+ puts pastel.dim(" aictl cluster delete #{name} --force-local")
310
+ exit 1
267
311
  end
268
- rescue StandardError => e
269
- Formatters::ProgressFormatter.warn("Failed to delete cluster resource: #{e.message}")
270
312
  end
271
313
 
272
- # Remove from config
314
+ # Remove from config only after successful Kubernetes deletion
273
315
  Formatters::ProgressFormatter.with_spinner('Removing cluster from configuration') do
274
316
  Config::ClusterConfig.remove_cluster(name)
275
317
  end
276
318
 
277
319
  # Clear current cluster if this was it
278
320
  Config::ClusterConfig.set_current_cluster(nil) if Config::ClusterConfig.current_cluster == name
279
-
280
- Formatters::ProgressFormatter.success("Deleted cluster '#{name}'")
281
321
  end
282
322
  end
283
323
 
@@ -291,12 +331,6 @@ module LanguageOperator
291
331
 
292
332
  cluster = Config::ClusterConfig.get_cluster(name)
293
333
 
294
- puts "Cluster: #{name}"
295
- puts " Namespace: #{cluster[:namespace]}"
296
- puts " Context: #{cluster[:context] || 'default'}"
297
- puts " Created: #{cluster[:created]}"
298
- puts
299
-
300
334
  # Get detailed cluster info
301
335
  begin
302
336
  k8s = Helpers::ClusterValidator.kubernetes_client(name)
@@ -304,45 +338,56 @@ module LanguageOperator
304
338
  # Get cluster resource
305
339
  cluster_resource = k8s.get_resource('LanguageCluster', name, cluster[:namespace])
306
340
  status = cluster_resource.dig('status', 'phase') || 'Unknown'
341
+ domain = cluster_resource.dig('spec', 'domain')
307
342
 
308
- puts "Status: #{status}"
343
+ # Main cluster information
344
+ puts
345
+ highlighted_box(
346
+ title: 'LanguageCluster',
347
+ rows: {
348
+ 'Name' => pastel.white.bold(name),
349
+ 'Namespace' => cluster[:namespace],
350
+ 'Cluster' => name,
351
+ 'Context' => cluster[:context] || 'default',
352
+ 'Domain' => domain,
353
+ 'Status' => status,
354
+ 'Created' => cluster[:created]
355
+ }.compact
356
+ )
309
357
  puts
310
358
 
311
359
  # Get agents
312
- agents = k8s.list_resources('LanguageAgent', namespace: cluster[:namespace])
313
- puts "Agents: #{agents.count}"
314
- agents.each do |agent|
315
- agent_status = agent.dig('status', 'phase') || 'Unknown'
316
- puts " - #{agent.dig('metadata', 'name')} (#{agent_status})"
360
+ agents = k8s.list_resources(RESOURCE_AGENT, namespace: cluster[:namespace])
361
+ agent_items = agents.map do |agent|
362
+ { name: agent.dig('metadata', 'name'), status: agent.dig('status', 'phase') || 'Unknown' }
317
363
  end
364
+ list_box(title: 'Agents', items: agent_items, style: :detailed)
318
365
  puts
319
366
 
320
367
  # Get tools
321
- tools = k8s.list_resources('LanguageTool', namespace: cluster[:namespace])
322
- puts "Tools: #{tools.count}"
323
- tools.each do |tool|
324
- tool_type = tool.dig('spec', 'type')
325
- puts " - #{tool.dig('metadata', 'name')} (#{tool_type})"
368
+ tools = k8s.list_resources(RESOURCE_TOOL, namespace: cluster[:namespace])
369
+ tool_items = tools.map do |tool|
370
+ { name: tool.dig('metadata', 'name') }
326
371
  end
372
+ list_box(title: 'Tools', items: tool_items, style: :detailed)
327
373
  puts
328
374
 
329
375
  # Get models
330
- models = k8s.list_resources('LanguageModel', namespace: cluster[:namespace])
331
- puts "Models: #{models.count}"
332
- models.each do |model|
376
+ models = k8s.list_resources(RESOURCE_MODEL, namespace: cluster[:namespace])
377
+ model_items = models.map do |model|
333
378
  provider = model.dig('spec', 'provider')
334
379
  model_name = model.dig('spec', 'modelName')
335
- puts " - #{model.dig('metadata', 'name')} (#{provider}/#{model_name})"
380
+ { name: model.dig('metadata', 'name'), meta: "#{provider}/#{model_name}" }
336
381
  end
382
+ list_box(title: 'Models', items: model_items, style: :detailed)
337
383
  puts
338
384
 
339
385
  # Get personas
340
386
  personas = k8s.list_resources('LanguagePersona', namespace: cluster[:namespace])
341
- puts "Personas: #{personas.count}"
342
- personas.each do |persona|
343
- tone = persona.dig('spec', 'tone')
344
- puts " - #{persona.dig('metadata', 'name')} (#{tone})"
387
+ persona_items = personas.map do |persona|
388
+ { name: persona.dig('metadata', 'name'), meta: persona.dig('spec', 'tone') }
345
389
  end
390
+ list_box(title: 'Personas', items: persona_items, style: :detailed)
346
391
  rescue StandardError => e
347
392
  Formatters::ProgressFormatter.error("Failed to get cluster details: #{e.message}")
348
393
  raise if ENV['DEBUG']
@@ -275,7 +275,7 @@ module LanguageOperator
275
275
  puts ' - LanguageModel resources'
276
276
  puts ' - LanguagePersona resources'
277
277
  puts
278
- return unless Helpers::UserPrompts.confirm('Continue with uninstall?')
278
+ return unless CLI::Helpers::UserPrompts.confirm('Continue with uninstall?')
279
279
  end
280
280
 
281
281
  # Build helm uninstall command
@@ -0,0 +1,215 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require_relative '../../command_loader'
5
+ require_relative '../../wizards/model_wizard'
6
+ require_relative 'test'
7
+
8
+ module LanguageOperator
9
+ module CLI
10
+ module Commands
11
+ module Model
12
+ # Model management commands
13
+ class Base < BaseCommand
14
+ include Constants
15
+ include Helpers::ClusterValidator
16
+ include Test
17
+
18
+ desc 'list', 'List all models in current cluster'
19
+ option :cluster, type: :string, desc: 'Override current cluster context'
20
+ def list
21
+ handle_command_error('list models') do
22
+ models = list_resources_or_empty(RESOURCE_MODEL, resource_name: 'models') do
23
+ puts
24
+ puts 'Create a model with:'
25
+ puts ' aictl model create <name> --provider <provider> --model <model>'
26
+ end
27
+
28
+ return if models.empty?
29
+
30
+ table_data = models.map do |model|
31
+ name = model.dig('metadata', 'name')
32
+ provider = model.dig('spec', 'provider') || 'unknown'
33
+ model_name = model.dig('spec', 'modelName') || 'unknown'
34
+ status = model.dig('status', 'phase') || 'Unknown'
35
+
36
+ {
37
+ name: name,
38
+ namespace: model.dig('metadata', 'namespace') || ctx.namespace,
39
+ provider: provider,
40
+ model: model_name,
41
+ status: status
42
+ }
43
+ end
44
+
45
+ Formatters::TableFormatter.models(table_data)
46
+ end
47
+ end
48
+
49
+ desc 'create [NAME]', 'Create a new model'
50
+ long_desc <<-DESC
51
+ Create a new LanguageModel resource in the cluster.
52
+
53
+ If NAME is omitted and no options are provided, an interactive wizard will guide you.
54
+
55
+ Examples:
56
+ aictl model create # Launch interactive wizard
57
+ aictl model create gpt4 --provider openai --model gpt-4-turbo
58
+ aictl model create claude --provider anthropic --model claude-3-opus-20240229
59
+ aictl model create local --provider openai_compatible --model llama-3 --endpoint http://localhost:8080
60
+ DESC
61
+ option :provider, type: :string, required: false, desc: 'LLM provider (e.g., openai, anthropic, openai_compatible)'
62
+ option :model, type: :string, required: false, desc: 'Model identifier (e.g., gpt-4, claude-3-opus)'
63
+ option :endpoint, type: :string, desc: 'Custom endpoint URL (for openai_compatible or self-hosted)'
64
+ option :cluster, type: :string, desc: 'Override current cluster context'
65
+ option :dry_run, type: :boolean, default: false, desc: 'Output the manifest without creating'
66
+ def create(name = nil)
67
+ handle_command_error('create model') do
68
+ # Launch interactive wizard if no arguments provided
69
+ if name.nil? && options[:provider].nil? && options[:model].nil?
70
+ wizard = Wizards::ModelWizard.new(ctx)
71
+ wizard.run
72
+ return
73
+ end
74
+
75
+ # Validate required options for non-interactive mode
76
+ if options[:provider].nil? || options[:model].nil?
77
+ Formatters::ProgressFormatter.error(
78
+ 'Must provide both --provider and --model, or use interactive mode (run without arguments)'
79
+ )
80
+ exit 1
81
+ end
82
+
83
+ # Build LanguageModel resource
84
+ resource = Kubernetes::ResourceBuilder.language_model(
85
+ name,
86
+ provider: options[:provider],
87
+ model: options[:model],
88
+ endpoint: options[:endpoint],
89
+ cluster: ctx.namespace,
90
+ cluster_ref: ctx.name
91
+ )
92
+
93
+ # Handle dry-run: output manifest and exit
94
+ if options[:dry_run]
95
+ puts resource.to_yaml
96
+ return
97
+ end
98
+
99
+ # Check if model already exists
100
+ begin
101
+ ctx.client.get_resource(RESOURCE_MODEL, name, ctx.namespace)
102
+ Formatters::ProgressFormatter.error("Model '#{name}' already exists in cluster '#{ctx.name}'")
103
+ exit 1
104
+ rescue K8s::Error::NotFound
105
+ # Model doesn't exist, proceed with creation
106
+ end
107
+
108
+ # Create model
109
+ Formatters::ProgressFormatter.with_spinner("Creating model '#{name}'") do
110
+ ctx.client.apply_resource(resource)
111
+ end
112
+
113
+ Formatters::ProgressFormatter.success("Model '#{name}' created successfully")
114
+ puts
115
+ format_model_details(
116
+ name: name,
117
+ namespace: ctx.namespace,
118
+ cluster: ctx.name,
119
+ status: 'Ready',
120
+ provider: options[:provider],
121
+ model: options[:model],
122
+ endpoint: options[:endpoint],
123
+ created: Time.now.strftime('%Y-%m-%dT%H:%M:%SZ')
124
+ )
125
+ end
126
+ end
127
+
128
+ desc 'inspect NAME', 'Show detailed model information'
129
+ option :cluster, type: :string, desc: 'Override current cluster context'
130
+ def inspect(name)
131
+ handle_command_error('inspect model') do
132
+ model = get_resource_or_exit(RESOURCE_MODEL, name)
133
+
134
+ puts
135
+ format_model_details(
136
+ name: name,
137
+ namespace: ctx.namespace,
138
+ cluster: ctx.name,
139
+ status: model.dig('status', 'phase') || 'Unknown',
140
+ provider: model.dig('spec', 'provider'),
141
+ model: model.dig('spec', 'modelName'),
142
+ endpoint: model.dig('spec', 'endpoint'),
143
+ created: model.dig('metadata', 'creationTimestamp')
144
+ )
145
+ puts
146
+
147
+ # Get agents using this model
148
+ agents = ctx.client.list_resources(RESOURCE_AGENT, namespace: ctx.namespace)
149
+ agents_using = Helpers::ResourceDependencyChecker.agents_using_model(agents, name)
150
+ agent_names = agents_using.map { |agent| agent.dig('metadata', 'name') }
151
+
152
+ list_box(
153
+ title: 'Agents using this model',
154
+ items: agent_names,
155
+ empty_message: 'No agents using this model'
156
+ )
157
+
158
+ puts
159
+ labels = model.dig('metadata', 'labels') || {}
160
+ list_box(
161
+ title: 'Labels',
162
+ items: labels,
163
+ style: :key_value
164
+ )
165
+ end
166
+ end
167
+
168
+ desc 'delete NAME', 'Delete a model'
169
+ option :cluster, type: :string, desc: 'Override current cluster context'
170
+ option :force, type: :boolean, default: false, desc: 'Skip confirmation'
171
+ def delete(name)
172
+ handle_command_error('delete model') do
173
+ get_resource_or_exit(RESOURCE_MODEL, name)
174
+
175
+ # Check dependencies and get confirmation
176
+ return unless check_dependencies_and_confirm('model', name, force: options[:force])
177
+
178
+ # Confirm deletion unless --force
179
+ return unless confirm_deletion_with_force('model', name, ctx.name, force: options[:force])
180
+
181
+ # Delete model
182
+ Formatters::ProgressFormatter.with_spinner("Deleting model '#{name}'") do
183
+ ctx.client.delete_resource(RESOURCE_MODEL, name, ctx.namespace)
184
+ end
185
+ end
186
+ end
187
+
188
+ desc 'edit NAME', 'Edit model configuration'
189
+ option :cluster, type: :string, desc: 'Override current cluster context'
190
+ def edit(name)
191
+ handle_command_error('edit model') do
192
+ model = get_resource_or_exit(RESOURCE_MODEL, name)
193
+
194
+ # Edit model YAML in user's editor
195
+ edited_yaml = Helpers::EditorHelper.edit_content(
196
+ model.to_yaml,
197
+ 'model-',
198
+ '.yaml',
199
+ default_editor: 'vim'
200
+ )
201
+ edited_model = YAML.safe_load(edited_yaml)
202
+
203
+ # Apply changes
204
+ Formatters::ProgressFormatter.with_spinner("Updating model '#{name}'") do
205
+ ctx.client.apply_resource(edited_model)
206
+ end
207
+
208
+ Formatters::ProgressFormatter.success("Model '#{name}' updated successfully")
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
215
+ end