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
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'English'
5
+ require 'shellwords'
6
+ require 'tempfile'
7
+
8
+ module LanguageOperator
9
+ module CLI
10
+ module Commands
11
+ module Model
12
+ module Test
13
+ def self.included(base)
14
+ base.class_eval do
15
+ desc 'test NAME', 'Test model connectivity and functionality'
16
+ long_desc <<-DESC
17
+ Test that a model is operational by:
18
+ 1. Verifying the pod is running
19
+ 2. Testing the chat completion endpoint with a simple message
20
+
21
+ This command helps diagnose model deployment issues.
22
+ DESC
23
+ option :cluster, type: :string, desc: 'Override current cluster context'
24
+ option :timeout, type: :numeric, default: 30, desc: 'Timeout in seconds for endpoint test'
25
+ def test(name)
26
+ handle_command_error('test model') do
27
+ # 1. Get model resource
28
+ model = get_resource_or_exit(RESOURCE_MODEL, name)
29
+ model_name = model.dig('spec', 'modelName')
30
+
31
+ # 2. Check deployment status
32
+ deployment = check_deployment_status(name)
33
+
34
+ # 3. Check pod status
35
+ pod = check_pod_status(name, deployment)
36
+
37
+ # 4. Test chat completion endpoint
38
+ test_chat_completion(name, model_name, pod, options[:timeout])
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def check_deployment_status(name)
45
+ Formatters::ProgressFormatter.with_spinner('Verifying deployment') do
46
+ deployment = ctx.client.get_resource('Deployment', name, ctx.namespace)
47
+ replicas = deployment.dig('spec', 'replicas') || 1
48
+ ready_replicas = deployment.dig('status', 'readyReplicas') || 0
49
+
50
+ unless ready_replicas >= replicas
51
+ raise "Deployment not ready (#{ready_replicas}/#{replicas}). " \
52
+ "Run 'kubectl get deployment #{name} -n #{ctx.namespace}' for details."
53
+ end
54
+
55
+ deployment
56
+ end
57
+ rescue K8s::Error::NotFound
58
+ Formatters::ProgressFormatter.error("Deployment '#{name}' not found")
59
+ exit 1
60
+ end
61
+
62
+ def check_pod_status(name, deployment)
63
+ Formatters::ProgressFormatter.with_spinner('Verifying pod') do
64
+ labels = deployment.dig('spec', 'selector', 'matchLabels')
65
+
66
+ # Find matching pods using centralized utility
67
+ pods = CLI::Helpers::LabelUtils.find_pods_by_deployment_labels(ctx, name, labels)
68
+ raise "No pods found for model '#{name}'" if pods.empty?
69
+
70
+ # Find a running pod
71
+ running_pod = pods.find do |pod|
72
+ pod.dig('status', 'phase') == 'Running' &&
73
+ pod.dig('status', 'conditions')&.any? { |c| c['type'] == 'Ready' && c['status'] == 'True' }
74
+ end
75
+
76
+ if running_pod.nil?
77
+ pod_phases = pods.map { |p| p.dig('status', 'phase') }.join(', ')
78
+ # Get label selector for error message
79
+ labels_hash = labels.respond_to?(:to_h) ? labels.to_h : labels
80
+ label_selector = labels_hash.map { |k, v| "#{k}=#{v}" }.join(',')
81
+ raise "No running pods found. Pod phases: #{pod_phases}. " \
82
+ "Run 'kubectl get pods -l #{label_selector} -n #{ctx.namespace}' for details."
83
+ end
84
+
85
+ running_pod
86
+ end
87
+ end
88
+
89
+ def test_chat_completion(_name, model_name, pod, timeout)
90
+ Formatters::ProgressFormatter.with_spinner('Verifying chat completion requests') do
91
+ pod_name = pod.dig('metadata', 'name')
92
+
93
+ # Build the JSON payload
94
+ payload = JSON.generate({
95
+ model: model_name,
96
+ messages: [{ role: 'user', content: 'hello' }],
97
+ max_tokens: 10
98
+ })
99
+
100
+ # Create temporary file to avoid command injection
101
+ temp_file = nil
102
+ begin
103
+ temp_file = Tempfile.new(['model_test_payload', '.json'])
104
+ temp_file.write(payload)
105
+ temp_file.close
106
+
107
+ # Build secure curl command using temp file
108
+ # This eliminates shell injection vulnerability
109
+ curl_command = 'curl -s -X POST http://localhost:4000/v1/chat/completions ' \
110
+ "-H 'Content-Type: application/json' -d @#{temp_file.path} --max-time #{timeout.to_i}"
111
+
112
+ # Execute the curl command inside the pod
113
+ result = execute_in_pod(pod_name, curl_command)
114
+ ensure
115
+ # Clean up temporary file
116
+ temp_file&.unlink
117
+ end
118
+
119
+ # Parse the response
120
+ response = JSON.parse(result)
121
+
122
+ if response['error']
123
+ error_msg = response['error']['message'] || response['error']
124
+ raise error_msg
125
+ elsif !response['choices']
126
+ raise "Unexpected response format: #{result.lines.first.strip}"
127
+ end
128
+
129
+ response
130
+ rescue JSON::ParserError => e
131
+ raise "Failed to parse response: #{e.message}"
132
+ end
133
+ rescue StandardError => e
134
+ # Display error in bold red
135
+ puts
136
+ puts Formatters::ProgressFormatter.pastel.bold.red(e.message)
137
+ exit 1
138
+ end
139
+
140
+ def execute_in_pod(pod_name, command)
141
+ # Use kubectl exec to run command in pod
142
+ # command can be a string or array
143
+ kubectl_command = if command.is_a?(String)
144
+ "kubectl exec -n #{ctx.namespace} #{pod_name} -- sh -c #{Shellwords.escape(command)}"
145
+ else
146
+ Shellwords.join(['kubectl', 'exec', '-n', ctx.namespace, pod_name, '--'] + command)
147
+ end
148
+
149
+ output = `#{kubectl_command} 2>&1`
150
+ exit_code = $CHILD_STATUS.exitstatus
151
+
152
+ if exit_code != 0
153
+ Formatters::ProgressFormatter.error("Failed to execute command in pod: #{output}")
154
+ exit 1
155
+ end
156
+
157
+ output
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
@@ -1,31 +1,22 @@
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 '../helpers/resource_dependency_checker'
10
- require_relative '../helpers/editor_helper'
11
- require_relative '../helpers/pastel_helper'
12
- require_relative '../../config/cluster_config'
13
- require_relative '../../kubernetes/client'
14
- require_relative '../../kubernetes/resource_builder'
4
+ require_relative '../command_loader'
15
5
 
16
6
  module LanguageOperator
17
7
  module CLI
18
8
  module Commands
19
9
  # Persona management commands
20
10
  class Persona < BaseCommand
11
+ include Constants
21
12
  include Helpers::ClusterValidator
22
- include Helpers::PastelHelper
13
+ include Helpers::UxHelper
23
14
 
24
15
  desc 'list', 'List all personas in current cluster'
25
16
  option :cluster, type: :string, desc: 'Override current cluster context'
26
17
  def list
27
18
  handle_command_error('list personas') do
28
- personas = list_resources_or_empty('LanguagePersona') do
19
+ personas = list_resources_or_empty(RESOURCE_PERSONA) do
29
20
  puts
30
21
  puts 'Personas define the personality and capabilities of agents.'
31
22
  puts
@@ -36,7 +27,7 @@ module LanguageOperator
36
27
  return if personas.empty?
37
28
 
38
29
  # Get agents to count usage
39
- agents = ctx.client.list_resources('LanguageAgent', namespace: ctx.namespace)
30
+ agents = ctx.client.list_resources(RESOURCE_AGENT, namespace: ctx.namespace)
40
31
 
41
32
  table_data = personas.map do |persona|
42
33
  name = persona.dig('metadata', 'name')
@@ -58,7 +49,7 @@ module LanguageOperator
58
49
  option :cluster, type: :string, desc: 'Override current cluster context'
59
50
  def show(name)
60
51
  handle_command_error('show persona') do
61
- persona = get_resource_or_exit('LanguagePersona', name)
52
+ persona = get_resource_or_exit(RESOURCE_PERSONA, name)
62
53
 
63
54
  puts
64
55
  puts "Persona: #{pastel.cyan.bold(name)}"
@@ -123,7 +114,7 @@ module LanguageOperator
123
114
  handle_command_error('create persona') do
124
115
  # Check if persona already exists
125
116
  begin
126
- ctx.client.get_resource('LanguagePersona', name, ctx.namespace)
117
+ ctx.client.get_resource(RESOURCE_PERSONA, name, ctx.namespace)
127
118
  Formatters::ProgressFormatter.error("Persona '#{name}' already exists in cluster '#{ctx.name}'")
128
119
  puts
129
120
  puts 'Use a different name or delete the existing persona first:'
@@ -136,7 +127,7 @@ module LanguageOperator
136
127
  # If --from flag provided, copy from existing persona
137
128
  base_persona = nil
138
129
  if options[:from]
139
- base_persona = get_resource_or_exit('LanguagePersona', options[:from],
130
+ base_persona = get_resource_or_exit(RESOURCE_PERSONA, options[:from],
140
131
  error_message: "Source persona '#{options[:from]}' not found")
141
132
  Formatters::ProgressFormatter.info("Copying from persona '#{options[:from]}'")
142
133
  puts
@@ -144,7 +135,7 @@ module LanguageOperator
144
135
 
145
136
  # Interactive prompts
146
137
  require 'tty-prompt'
147
- prompt = TTY::Prompt.new
138
+ # prompt available via UxHelper
148
139
 
149
140
  puts
150
141
  puts '=' * 80
@@ -245,7 +236,7 @@ module LanguageOperator
245
236
  def edit(name)
246
237
  handle_command_error('edit persona') do
247
238
  # Get current persona
248
- persona = get_resource_or_exit('LanguagePersona', name)
239
+ persona = get_resource_or_exit(RESOURCE_PERSONA, name)
249
240
 
250
241
  # Open in editor
251
242
  original_yaml = YAML.dump(persona)
@@ -279,7 +270,7 @@ module LanguageOperator
279
270
  Formatters::ProgressFormatter.success("Persona '#{name}' updated successfully")
280
271
 
281
272
  # Check for agents using this persona
282
- agents = ctx.client.list_resources('LanguageAgent', namespace: ctx.namespace)
273
+ agents = ctx.client.list_resources(RESOURCE_AGENT, namespace: ctx.namespace)
283
274
  agents_using = Helpers::ResourceDependencyChecker.agents_using_persona(agents, name)
284
275
 
285
276
  if agents_using.any?
@@ -298,26 +289,17 @@ module LanguageOperator
298
289
  def delete(name)
299
290
  handle_command_error('delete persona') do
300
291
  # Get persona
301
- persona = get_resource_or_exit('LanguagePersona', name)
292
+ get_resource_or_exit(RESOURCE_PERSONA, name)
302
293
 
303
294
  # Check dependencies and get confirmation
304
295
  return unless check_dependencies_and_confirm('persona', name, force: options[:force])
305
296
 
306
297
  # Confirm deletion unless --force
307
- if confirm_deletion(
308
- 'persona', name, ctx.name,
309
- details: {
310
- 'Tone' => persona.dig('spec', 'tone'),
311
- 'Description' => persona.dig('spec', 'description')
312
- },
313
- force: options[:force]
314
- )
315
- # Delete persona
316
- Formatters::ProgressFormatter.with_spinner("Deleting persona '#{name}'") do
317
- ctx.client.delete_resource('LanguagePersona', name, ctx.namespace)
318
- end
298
+ return unless confirm_deletion_with_force('persona', name, ctx.name, force: options[:force])
319
299
 
320
- Formatters::ProgressFormatter.success("Persona '#{name}' deleted successfully")
300
+ # Delete persona
301
+ Formatters::ProgressFormatter.with_spinner("Deleting persona '#{name}'") do
302
+ ctx.client.delete_resource(RESOURCE_PERSONA, name, ctx.namespace)
321
303
  end
322
304
  end
323
305
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative '../base_command'
4
4
  require_relative '../formatters/progress_formatter'
5
- require_relative '../../ux/quickstart'
5
+ require_relative '../wizards/quickstart_wizard'
6
6
 
7
7
  module LanguageOperator
8
8
  module CLI
@@ -11,7 +11,8 @@ module LanguageOperator
11
11
  class Quickstart < BaseCommand
12
12
  desc 'start', 'Interactive setup wizard for first-time users'
13
13
  def start
14
- Ux::Quickstart.execute
14
+ wizard = Wizards::QuickstartWizard.new
15
+ wizard.run
15
16
  end
16
17
 
17
18
  default_task :start
@@ -1,20 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../base_command'
4
- require_relative '../formatters/progress_formatter'
5
- require_relative '../formatters/table_formatter'
6
- require_relative '../formatters/status_formatter'
7
- require_relative '../helpers/pastel_helper'
8
- require_relative '../../config/cluster_config'
9
- require_relative '../../kubernetes/client'
10
- require_relative '../helpers/cluster_validator'
3
+ require_relative '../command_loader'
11
4
 
12
5
  module LanguageOperator
13
6
  module CLI
14
7
  module Commands
15
8
  # System status and overview command
16
9
  class Status < BaseCommand
17
- include Helpers::PastelHelper
10
+ include Constants
11
+ include Helpers::UxHelper
18
12
 
19
13
  desc 'overview', 'Show system status and overview'
20
14
  def overview
@@ -26,21 +20,48 @@ module LanguageOperator
26
20
  if current_cluster
27
21
  cluster_config = Config::ClusterConfig.get_cluster(current_cluster)
28
22
 
29
- puts "\nCluster Details"
30
- puts '----------------'
31
- puts "Name: #{pastel.bold.white(current_cluster)}"
32
- puts "Namespace: #{pastel.bold.white(cluster_config[:namespace])}"
33
- puts
34
-
35
23
  # Check cluster health
36
24
  k8s = Helpers::ClusterValidator.kubernetes_client(current_cluster)
37
25
 
38
- # Operator status
26
+ # Try to get actual cluster resource from Kubernetes
27
+ cluster_resource = nil
39
28
  if k8s.operator_installed?
40
- version = k8s.operator_version || 'installed'
41
- Formatters::ProgressFormatter.success("Operator: #{version}")
29
+ begin
30
+ cluster_resource = k8s.get_resource('LanguageCluster', current_cluster, cluster_config[:namespace])
31
+ rescue StandardError
32
+ # Cluster resource might not exist yet
33
+ end
34
+ end
35
+
36
+ # Format cluster info using UxHelper
37
+ logo(title: 'cluster status')
38
+
39
+ if cluster_resource
40
+ # Use actual cluster resource data
41
+ status = cluster_resource.dig('status', 'phase') || 'Unknown'
42
+ domain = cluster_resource.dig('spec', 'domain')
43
+ created = cluster_resource.dig('metadata', 'creationTimestamp')
44
+
45
+ format_cluster_details(
46
+ name: current_cluster,
47
+ namespace: cluster_config[:namespace],
48
+ context: cluster_config[:context],
49
+ status: status,
50
+ domain: domain,
51
+ created: created
52
+ )
42
53
  else
43
- Formatters::ProgressFormatter.error('Operator: not installed')
54
+ # Fallback to local config and operator status
55
+ format_cluster_details(
56
+ name: current_cluster,
57
+ namespace: cluster_config[:namespace],
58
+ context: cluster_config[:context],
59
+ status: k8s.operator_installed? ? (k8s.operator_version || 'installed') : 'operator not installed'
60
+ )
61
+ end
62
+
63
+ # Early exit if operator not installed
64
+ unless k8s.operator_installed?
44
65
  puts
45
66
  puts 'Install the operator with:'
46
67
  puts ' aictl install'
@@ -48,54 +69,6 @@ module LanguageOperator
48
69
  return
49
70
  end
50
71
 
51
- # Agent statistics
52
- agents = k8s.list_resources('LanguageAgent', namespace: cluster_config[:namespace])
53
- agent_stats = categorize_by_status(agents)
54
-
55
- puts
56
- puts "Agents (#{agents.count} total):"
57
- agent_stats.each do |status, count|
58
- puts " #{format_status(status)}: #{count}"
59
- end
60
-
61
- # Tool statistics
62
- tools = k8s.list_resources('LanguageTool', namespace: cluster_config[:namespace])
63
- puts
64
- puts "Tools (#{tools.count} total):"
65
- if tools.any?
66
- tool_types = tools.group_by { |t| t.dig('spec', 'type') }
67
- tool_types.each do |type, items|
68
- puts " #{type}: #{items.count}"
69
- end
70
- else
71
- puts ' (none)'
72
- end
73
-
74
- # Model statistics
75
- models = k8s.list_resources('LanguageModel', namespace: cluster_config[:namespace])
76
- puts
77
- puts "Models (#{models.count} total):"
78
- if models.any?
79
- model_providers = models.group_by { |m| m.dig('spec', 'provider') }
80
- model_providers.each do |provider, items|
81
- puts " #{provider}: #{items.count}"
82
- end
83
- else
84
- puts ' (none)'
85
- end
86
-
87
- # Persona statistics
88
- personas = k8s.list_resources('LanguagePersona', namespace: cluster_config[:namespace])
89
- puts
90
- puts "Personas (#{personas.count} total):"
91
- if personas.any?
92
- personas.each do |persona|
93
- tone = persona.dig('spec', 'tone')
94
- puts " - #{persona.dig('metadata', 'name')} (#{tone})"
95
- end
96
- else
97
- puts ' (none)'
98
- end
99
72
  else
100
73
  Formatters::ProgressFormatter.warn('No cluster selected')
101
74
  puts
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'yaml'
5
+ require_relative '../../base_command'
6
+ require_relative '../../formatters/progress_formatter'
7
+ require_relative '../../../dsl/schema'
8
+
9
+ # Include all system subcommand modules
10
+ require_relative 'schema'
11
+ require_relative 'validate_template'
12
+ require_relative 'synthesize'
13
+ require_relative 'exec'
14
+ require_relative 'synthesis_template'
15
+
16
+ # Include helper modules
17
+ require_relative 'helpers/template_loader'
18
+ require_relative 'helpers/template_validator'
19
+ require_relative 'helpers/llm_synthesis'
20
+ require_relative 'helpers/pod_manager'
21
+
22
+ module LanguageOperator
23
+ module CLI
24
+ module Commands
25
+ module System
26
+ # Base system command class
27
+ class Base < BaseCommand
28
+ # Include all helper modules
29
+ include Helpers::TemplateLoader
30
+ include Helpers::TemplateValidator
31
+ include Helpers::LlmSynthesis
32
+ include Helpers::PodManager
33
+
34
+ # Include all subcommand modules
35
+ include Schema
36
+ include ValidateTemplate
37
+ include Synthesize
38
+ include Exec
39
+ include SynthesisTemplate
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LanguageOperator
4
+ module CLI
5
+ module Commands
6
+ module System
7
+ # Agent execution in test pod
8
+ module Exec
9
+ def self.included(base)
10
+ base.class_eval do
11
+ desc 'exec [AGENT_FILE]', 'Execute an agent file in a test pod on the cluster'
12
+ long_desc <<-DESC
13
+ Deploy and execute an agent file in a temporary test pod on the Kubernetes cluster.
14
+
15
+ This command creates a ConfigMap with the agent code, deploys a test pod,
16
+ streams the logs until completion, and cleans up all resources.
17
+
18
+ The agent code is mounted at /etc/agent/code/agent.rb as expected by the agent runtime.
19
+
20
+ Agent code can be provided either as a file path or via STDIN.
21
+ If no file path is provided, the command will read from STDIN.
22
+
23
+ Examples:
24
+ # Execute a synthesized agent file
25
+ aictl system exec agent.rb
26
+
27
+ # Execute with a custom agent name
28
+ aictl system exec agent.rb --agent-name my-test
29
+
30
+ # Keep the pod after execution for debugging
31
+ aictl system exec agent.rb --keep-pod
32
+
33
+ # Use a different agent image
34
+ aictl system exec agent.rb --image ghcr.io/language-operator/agent:v0.1.0
35
+
36
+ # Read agent code from STDIN
37
+ cat agent.rb | aictl system exec
38
+
39
+ # Pipe synthesized code directly to execution
40
+ cat agent.txt | aictl system synthesize | aictl system exec
41
+ DESC
42
+ option :agent_name, type: :string, default: 'test-agent', desc: 'Name for the test agent pod'
43
+ option :keep_pod, type: :boolean, default: false, desc: 'Keep the pod after execution (for debugging)'
44
+ option :image, type: :string, default: 'ghcr.io/language-operator/agent:latest', desc: 'Agent container image'
45
+ option :timeout, type: :numeric, default: 300, desc: 'Timeout in seconds for agent execution'
46
+ def exec(agent_file = nil)
47
+ handle_command_error('exec agent') do
48
+ # Verify cluster is selected
49
+ unless ctx.client
50
+ Formatters::ProgressFormatter.error('No cluster context available')
51
+ puts
52
+ puts 'Please configure kubectl with a valid cluster context:'
53
+ puts ' kubectl config get-contexts'
54
+ puts ' kubectl config use-context <context-name>'
55
+ exit 1
56
+ end
57
+
58
+ # Read agent code from file or STDIN
59
+ agent_code = if agent_file && !agent_file.strip.empty?
60
+ # Read from file
61
+ unless File.exist?(agent_file)
62
+ Formatters::ProgressFormatter.error("Agent file not found: #{agent_file}")
63
+ exit 1
64
+ end
65
+ File.read(agent_file)
66
+ elsif $stdin.tty?
67
+ # Read from STDIN
68
+ Formatters::ProgressFormatter.error('No agent code provided')
69
+ puts
70
+ puts 'Provide agent code either as a file or via STDIN:'
71
+ puts ' aictl system exec agent.rb'
72
+ puts ' cat agent.rb | aictl system exec'
73
+ exit 1
74
+ else
75
+ code = $stdin.read.strip
76
+ if code.empty?
77
+ Formatters::ProgressFormatter.error('No agent code provided')
78
+ puts
79
+ puts 'Provide agent code either as a file or via STDIN:'
80
+ puts ' aictl system exec agent.rb'
81
+ puts ' cat agent.rb | aictl system exec'
82
+ exit 1
83
+ end
84
+ code
85
+ end
86
+
87
+ # Generate unique names
88
+ timestamp = Time.now.to_i
89
+ configmap_name = "#{options[:agent_name]}-code-#{timestamp}"
90
+ pod_name = "#{options[:agent_name]}-#{timestamp}"
91
+
92
+ begin
93
+ # Create ConfigMap with agent code
94
+ Formatters::ProgressFormatter.with_spinner('Creating ConfigMap with agent code') do
95
+ create_agent_configmap(configmap_name, agent_code)
96
+ end
97
+
98
+ # Create test pod
99
+ Formatters::ProgressFormatter.with_spinner('Creating test pod') do
100
+ create_test_pod(pod_name, configmap_name, options[:image])
101
+ end
102
+
103
+ # Wait for pod to be ready or running
104
+ Formatters::ProgressFormatter.with_spinner('Waiting for pod to start') do
105
+ wait_for_pod_start(pod_name, timeout: 60)
106
+ end
107
+
108
+ # Stream logs until pod completes
109
+ stream_pod_logs(pod_name, timeout: options[:timeout])
110
+
111
+ # Wait for pod to fully terminate and get final status
112
+ exit_code = wait_for_pod_termination(pod_name)
113
+
114
+ if exit_code&.zero?
115
+ Formatters::ProgressFormatter.success('Agent completed successfully')
116
+ elsif exit_code
117
+ Formatters::ProgressFormatter.error("Agent failed with exit code: #{exit_code}")
118
+ else
119
+ Formatters::ProgressFormatter.warn('Unable to determine pod exit status')
120
+ end
121
+ ensure
122
+ # Clean up resources unless --keep-pod
123
+ puts
124
+ puts
125
+ if options[:keep_pod]
126
+ Formatters::ProgressFormatter.info('Resources kept for debugging:')
127
+ puts " Pod: #{pod_name}"
128
+ puts " ConfigMap: #{configmap_name}"
129
+ puts
130
+ puts "To view logs: kubectl logs -n #{ctx.namespace} #{pod_name}"
131
+ puts "To delete: kubectl delete pod,configmap -n #{ctx.namespace} #{pod_name} #{configmap_name}"
132
+ else
133
+ Formatters::ProgressFormatter.with_spinner('Cleaning up resources') do
134
+ delete_pod(pod_name)
135
+ delete_configmap(configmap_name)
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end