language-operator 0.1.31 → 0.1.35

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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -8
  3. data/CHANGELOG.md +14 -0
  4. data/CI_STATUS.md +56 -0
  5. data/Gemfile.lock +2 -2
  6. data/Makefile +22 -6
  7. data/lib/language_operator/agent/base.rb +10 -6
  8. data/lib/language_operator/agent/executor.rb +19 -97
  9. data/lib/language_operator/agent/safety/ast_validator.rb +62 -43
  10. data/lib/language_operator/agent/safety/safe_executor.rb +27 -2
  11. data/lib/language_operator/agent/scheduler.rb +60 -0
  12. data/lib/language_operator/agent/task_executor.rb +548 -0
  13. data/lib/language_operator/agent.rb +90 -27
  14. data/lib/language_operator/cli/base_command.rb +117 -0
  15. data/lib/language_operator/cli/commands/agent.rb +339 -407
  16. data/lib/language_operator/cli/commands/cluster.rb +274 -290
  17. data/lib/language_operator/cli/commands/install.rb +110 -119
  18. data/lib/language_operator/cli/commands/model.rb +284 -184
  19. data/lib/language_operator/cli/commands/persona.rb +218 -284
  20. data/lib/language_operator/cli/commands/quickstart.rb +4 -5
  21. data/lib/language_operator/cli/commands/status.rb +31 -35
  22. data/lib/language_operator/cli/commands/system.rb +221 -233
  23. data/lib/language_operator/cli/commands/tool.rb +356 -422
  24. data/lib/language_operator/cli/commands/use.rb +19 -22
  25. data/lib/language_operator/cli/helpers/resource_dependency_checker.rb +0 -18
  26. data/lib/language_operator/cli/wizards/quickstart_wizard.rb +0 -1
  27. data/lib/language_operator/client/config.rb +20 -21
  28. data/lib/language_operator/config.rb +115 -3
  29. data/lib/language_operator/constants.rb +54 -0
  30. data/lib/language_operator/dsl/agent_context.rb +7 -7
  31. data/lib/language_operator/dsl/agent_definition.rb +111 -26
  32. data/lib/language_operator/dsl/config.rb +30 -66
  33. data/lib/language_operator/dsl/main_definition.rb +114 -0
  34. data/lib/language_operator/dsl/schema.rb +84 -43
  35. data/lib/language_operator/dsl/task_definition.rb +315 -0
  36. data/lib/language_operator/dsl.rb +0 -1
  37. data/lib/language_operator/instrumentation/task_tracer.rb +285 -0
  38. data/lib/language_operator/logger.rb +4 -4
  39. data/lib/language_operator/synthesis_test_harness.rb +324 -0
  40. data/lib/language_operator/templates/examples/agent_synthesis.tmpl +26 -8
  41. data/lib/language_operator/templates/schema/CHANGELOG.md +26 -0
  42. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
  43. data/lib/language_operator/templates/schema/agent_dsl_schema.json +84 -42
  44. data/lib/language_operator/type_coercion.rb +250 -0
  45. data/lib/language_operator/ux/base.rb +81 -0
  46. data/lib/language_operator/ux/concerns/README.md +155 -0
  47. data/lib/language_operator/ux/concerns/headings.rb +90 -0
  48. data/lib/language_operator/ux/concerns/input_validation.rb +146 -0
  49. data/lib/language_operator/ux/concerns/provider_helpers.rb +167 -0
  50. data/lib/language_operator/ux/create_agent.rb +252 -0
  51. data/lib/language_operator/ux/create_model.rb +267 -0
  52. data/lib/language_operator/ux/quickstart.rb +594 -0
  53. data/lib/language_operator/version.rb +1 -1
  54. data/lib/language_operator.rb +2 -0
  55. data/requirements/ARCHITECTURE.md +1 -0
  56. data/requirements/SCRATCH.md +153 -0
  57. data/requirements/dsl.md +0 -0
  58. data/requirements/features +1 -0
  59. data/requirements/personas +1 -0
  60. data/requirements/proposals +1 -0
  61. data/requirements/tasks/iterate.md +14 -15
  62. data/requirements/tasks/optimize.md +13 -4
  63. data/synth/001/Makefile +90 -0
  64. data/synth/001/agent.rb +26 -0
  65. data/synth/001/agent.yaml +7 -0
  66. data/synth/001/output.log +44 -0
  67. data/synth/Makefile +39 -0
  68. data/synth/README.md +342 -0
  69. metadata +37 -10
  70. data/lib/language_operator/dsl/workflow_definition.rb +0 -259
  71. data/test_agent_dsl.rb +0 -108
@@ -1,265 +1,365 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'thor'
4
3
  require 'yaml'
4
+ require 'json'
5
+ require 'English'
6
+ require 'shellwords'
7
+ require_relative '../base_command'
5
8
  require_relative '../formatters/progress_formatter'
6
9
  require_relative '../formatters/table_formatter'
7
10
  require_relative '../helpers/cluster_validator'
8
- require_relative '../helpers/cluster_context'
9
11
  require_relative '../helpers/user_prompts'
10
12
  require_relative '../helpers/resource_dependency_checker'
11
13
  require_relative '../helpers/editor_helper'
12
14
  require_relative '../../config/cluster_config'
13
15
  require_relative '../../kubernetes/client'
14
16
  require_relative '../../kubernetes/resource_builder'
17
+ require_relative '../../ux/create_model'
15
18
 
16
19
  module LanguageOperator
17
20
  module CLI
18
21
  module Commands
19
22
  # Model management commands
20
- class Model < Thor
23
+ class Model < BaseCommand
21
24
  include Helpers::ClusterValidator
22
25
 
23
26
  desc 'list', 'List all models in current cluster'
24
27
  option :cluster, type: :string, desc: 'Override current cluster context'
25
28
  def list
26
- ctx = Helpers::ClusterContext.from_options(options)
27
-
28
- models = ctx.client.list_resources('LanguageModel', namespace: ctx.namespace)
29
+ handle_command_error('list models') do
30
+ models = list_resources_or_empty('LanguageModel') do
31
+ puts
32
+ puts 'Models define LLM configurations for agents.'
33
+ puts
34
+ puts 'Create a model with:'
35
+ puts ' aictl model create <name> --provider <provider> --model <model>'
36
+ end
29
37
 
30
- if models.empty?
31
- Formatters::ProgressFormatter.info("No models found in cluster '#{ctx.name}'")
32
- puts
33
- puts 'Models define LLM configurations for agents.'
34
- puts
35
- puts 'Create a model with:'
36
- puts ' aictl model create <name> --provider <provider> --model <model>'
37
- return
38
- end
38
+ return if models.empty?
39
39
 
40
- table_data = models.map do |model|
41
- name = model.dig('metadata', 'name')
42
- provider = model.dig('spec', 'provider') || 'unknown'
43
- model_name = model.dig('spec', 'modelName') || 'unknown'
44
- status = model.dig('status', 'phase') || 'Unknown'
45
-
46
- {
47
- name: name,
48
- provider: provider,
49
- model: model_name,
50
- status: status
51
- }
52
- end
40
+ table_data = models.map do |model|
41
+ name = model.dig('metadata', 'name')
42
+ provider = model.dig('spec', 'provider') || 'unknown'
43
+ model_name = model.dig('spec', 'modelName') || 'unknown'
44
+ status = model.dig('status', 'phase') || 'Unknown'
53
45
 
54
- Formatters::TableFormatter.models(table_data)
55
- rescue StandardError => e
56
- Formatters::ProgressFormatter.error("Failed to list models: #{e.message}")
57
- raise if ENV['DEBUG']
46
+ {
47
+ name: name,
48
+ provider: provider,
49
+ model: model_name,
50
+ status: status
51
+ }
52
+ end
58
53
 
59
- exit 1
54
+ Formatters::TableFormatter.models(table_data)
55
+ end
60
56
  end
61
57
 
62
- desc 'create NAME', 'Create a new model'
58
+ desc 'create [NAME]', 'Create a new model'
63
59
  long_desc <<-DESC
64
60
  Create a new LanguageModel resource in the cluster.
65
61
 
62
+ If NAME is omitted and no options are provided, an interactive wizard will guide you.
63
+
66
64
  Examples:
65
+ aictl model create # Launch interactive wizard
67
66
  aictl model create gpt4 --provider openai --model gpt-4-turbo
68
67
  aictl model create claude --provider anthropic --model claude-3-opus-20240229
69
68
  aictl model create local --provider openai_compatible --model llama-3 --endpoint http://localhost:8080
70
69
  DESC
71
- option :provider, type: :string, required: true, desc: 'LLM provider (e.g., openai, anthropic, openai_compatible)'
72
- option :model, type: :string, required: true, desc: 'Model identifier (e.g., gpt-4, claude-3-opus)'
70
+ option :provider, type: :string, required: false, desc: 'LLM provider (e.g., openai, anthropic, openai_compatible)'
71
+ option :model, type: :string, required: false, desc: 'Model identifier (e.g., gpt-4, claude-3-opus)'
73
72
  option :endpoint, type: :string, desc: 'Custom endpoint URL (for openai_compatible or self-hosted)'
74
73
  option :cluster, type: :string, desc: 'Override current cluster context'
75
74
  option :dry_run, type: :boolean, default: false, desc: 'Output the manifest without creating'
76
- def create(name)
77
- ctx = Helpers::ClusterContext.from_options(options)
78
-
79
- # Build LanguageModel resource
80
- resource = Kubernetes::ResourceBuilder.language_model(
81
- name,
82
- provider: options[:provider],
83
- model: options[:model],
84
- endpoint: options[:endpoint],
85
- cluster: ctx.namespace
86
- )
87
-
88
- # Handle dry-run: output manifest and exit
89
- if options[:dry_run]
90
- puts resource.to_yaml
91
- return
92
- end
75
+ def create(name = nil)
76
+ handle_command_error('create model') do
77
+ # Launch interactive wizard if no arguments provided
78
+ if name.nil? && options[:provider].nil? && options[:model].nil?
79
+ Ux::CreateModel.execute(ctx)
80
+ return
81
+ end
93
82
 
94
- # Check if model already exists
95
- begin
96
- ctx.client.get_resource('LanguageModel', name, ctx.namespace)
97
- Formatters::ProgressFormatter.error("Model '#{name}' already exists in cluster '#{ctx.name}'")
98
- exit 1
99
- rescue K8s::Error::NotFound
100
- # Model doesn't exist, proceed with creation
101
- end
83
+ # Validate required options for non-interactive mode
84
+ if options[:provider].nil? || options[:model].nil?
85
+ Formatters::ProgressFormatter.error(
86
+ 'Must provide both --provider and --model, or use interactive mode (run without arguments)'
87
+ )
88
+ exit 1
89
+ end
102
90
 
103
- # Create model
104
- Formatters::ProgressFormatter.with_spinner("Creating model '#{name}'") do
105
- ctx.client.apply_resource(resource)
106
- end
91
+ # Build LanguageModel resource
92
+ resource = Kubernetes::ResourceBuilder.language_model(
93
+ name,
94
+ provider: options[:provider],
95
+ model: options[:model],
96
+ endpoint: options[:endpoint],
97
+ cluster: ctx.namespace
98
+ )
99
+
100
+ # Handle dry-run: output manifest and exit
101
+ if options[:dry_run]
102
+ puts resource.to_yaml
103
+ return
104
+ end
107
105
 
108
- Formatters::ProgressFormatter.success("Model '#{name}' created successfully")
109
- puts
110
- puts 'Model Details:'
111
- puts " Name: #{name}"
112
- puts " Provider: #{options[:provider]}"
113
- puts " Model: #{options[:model]}"
114
- puts " Endpoint: #{options[:endpoint]}" if options[:endpoint]
115
- puts " Cluster: #{ctx.name}"
116
- rescue StandardError => e
117
- Formatters::ProgressFormatter.error("Failed to create model: #{e.message}")
118
- raise if ENV['DEBUG']
106
+ # Check if model already exists
107
+ begin
108
+ ctx.client.get_resource('LanguageModel', name, ctx.namespace)
109
+ Formatters::ProgressFormatter.error("Model '#{name}' already exists in cluster '#{ctx.name}'")
110
+ exit 1
111
+ rescue K8s::Error::NotFound
112
+ # Model doesn't exist, proceed with creation
113
+ end
119
114
 
120
- exit 1
115
+ # Create model
116
+ Formatters::ProgressFormatter.with_spinner("Creating model '#{name}'") do
117
+ ctx.client.apply_resource(resource)
118
+ end
119
+
120
+ Formatters::ProgressFormatter.success("Model '#{name}' created successfully")
121
+ puts
122
+ puts 'Model Details:'
123
+ puts " Name: #{name}"
124
+ puts " Provider: #{options[:provider]}"
125
+ puts " Model: #{options[:model]}"
126
+ puts " Endpoint: #{options[:endpoint]}" if options[:endpoint]
127
+ puts " Cluster: #{ctx.name}"
128
+ end
121
129
  end
122
130
 
123
131
  desc 'inspect NAME', 'Show detailed model information'
124
132
  option :cluster, type: :string, desc: 'Override current cluster context'
125
133
  def inspect(name)
126
- ctx = Helpers::ClusterContext.from_options(options)
127
-
128
- # Get model
129
- begin
130
- model = ctx.client.get_resource('LanguageModel', name, ctx.namespace)
131
- rescue K8s::Error::NotFound
132
- Formatters::ProgressFormatter.error("Model '#{name}' not found in cluster '#{ctx.name}'")
133
- exit 1
134
- end
135
-
136
- puts "Model: #{name}"
137
- puts " Cluster: #{ctx.name}"
138
- puts " Namespace: #{ctx.namespace}"
139
- puts " Provider: #{model.dig('spec', 'provider')}"
140
- puts " Model: #{model.dig('spec', 'modelName')}"
141
- puts " Endpoint: #{model.dig('spec', 'endpoint')}" if model.dig('spec', 'endpoint')
142
- puts " Status: #{model.dig('status', 'phase') || 'Unknown'}"
143
- puts
144
-
145
- # Get agents using this model
146
- agents = ctx.client.list_resources('LanguageAgent', namespace: ctx.namespace)
147
- agents_using = Helpers::ResourceDependencyChecker.agents_using_model(agents, name)
134
+ handle_command_error('inspect model') do
135
+ model = get_resource_or_exit('LanguageModel', name)
136
+
137
+ puts "Model: #{name}"
138
+ puts " Cluster: #{ctx.name}"
139
+ puts " Namespace: #{ctx.namespace}"
140
+ puts " Provider: #{model.dig('spec', 'provider')}"
141
+ puts " Model: #{model.dig('spec', 'modelName')}"
142
+ puts " Endpoint: #{model.dig('spec', 'endpoint')}" if model.dig('spec', 'endpoint')
143
+ puts " Status: #{model.dig('status', 'phase') || 'Unknown'}"
144
+ puts
148
145
 
149
- if agents_using.any?
150
- puts "Agents using this model (#{agents_using.count}):"
151
- agents_using.each do |agent|
152
- puts " - #{agent.dig('metadata', 'name')}"
146
+ # Get agents using this model
147
+ agents = ctx.client.list_resources('LanguageAgent', namespace: ctx.namespace)
148
+ agents_using = Helpers::ResourceDependencyChecker.agents_using_model(agents, name)
149
+
150
+ if agents_using.any?
151
+ puts "Agents using this model (#{agents_using.count}):"
152
+ agents_using.each do |agent|
153
+ puts " - #{agent.dig('metadata', 'name')}"
154
+ end
155
+ else
156
+ puts 'No agents using this model'
153
157
  end
154
- else
155
- puts 'No agents using this model'
156
- end
157
158
 
158
- puts
159
- puts 'Labels:'
160
- labels = model.dig('metadata', 'labels') || {}
161
- if labels.empty?
162
- puts ' (none)'
163
- else
164
- labels.each do |key, value|
165
- puts " #{key}: #{value}"
159
+ puts
160
+ puts 'Labels:'
161
+ labels = model.dig('metadata', 'labels') || {}
162
+ if labels.empty?
163
+ puts ' (none)'
164
+ else
165
+ labels.each do |key, value|
166
+ puts " #{key}: #{value}"
167
+ end
166
168
  end
167
169
  end
168
- rescue StandardError => e
169
- Formatters::ProgressFormatter.error("Failed to inspect model: #{e.message}")
170
- raise if ENV['DEBUG']
171
-
172
- exit 1
173
170
  end
174
171
 
175
172
  desc 'delete NAME', 'Delete a model'
176
173
  option :cluster, type: :string, desc: 'Override current cluster context'
177
174
  option :force, type: :boolean, default: false, desc: 'Skip confirmation'
178
175
  def delete(name)
179
- ctx = Helpers::ClusterContext.from_options(options)
180
-
181
- # Get model
182
- begin
183
- model = ctx.client.get_resource('LanguageModel', name, ctx.namespace)
184
- rescue K8s::Error::NotFound
185
- Formatters::ProgressFormatter.error("Model '#{name}' not found in cluster '#{ctx.name}'")
186
- exit 1
176
+ handle_command_error('delete model') do
177
+ model = get_resource_or_exit('LanguageModel', name)
178
+
179
+ # Check dependencies and get confirmation
180
+ return unless check_dependencies_and_confirm('model', name, force: options[:force])
181
+
182
+ # Confirm deletion unless --force
183
+ if confirm_deletion(
184
+ 'model', name, ctx.name,
185
+ details: {
186
+ 'Provider' => model.dig('spec', 'provider'),
187
+ 'Model' => model.dig('spec', 'modelName'),
188
+ 'Status' => model.dig('status', 'phase')
189
+ },
190
+ force: options[:force]
191
+ )
192
+ # Delete model
193
+ Formatters::ProgressFormatter.with_spinner("Deleting model '#{name}'") do
194
+ ctx.client.delete_resource('LanguageModel', name, ctx.namespace)
195
+ end
196
+
197
+ Formatters::ProgressFormatter.success("Model '#{name}' deleted successfully")
198
+ end
187
199
  end
200
+ end
188
201
 
189
- # Check for agents using this model
190
- agents = ctx.client.list_resources('LanguageAgent', namespace: ctx.namespace)
191
- agents_using = Helpers::ResourceDependencyChecker.agents_using_model(agents, name)
192
-
193
- if agents_using.any? && !options[:force]
194
- Formatters::ProgressFormatter.warn("Model '#{name}' is in use by #{agents_using.count} agent(s)")
195
- puts
196
- puts 'Agents using this model:'
197
- agents_using.each do |agent|
198
- puts " - #{agent.dig('metadata', 'name')}"
202
+ desc 'edit NAME', 'Edit model configuration'
203
+ option :cluster, type: :string, desc: 'Override current cluster context'
204
+ def edit(name)
205
+ handle_command_error('edit model') do
206
+ model = get_resource_or_exit('LanguageModel', name)
207
+
208
+ # Edit model YAML in user's editor
209
+ edited_yaml = Helpers::EditorHelper.edit_content(
210
+ model.to_yaml,
211
+ 'model-',
212
+ '.yaml',
213
+ default_editor: 'vim'
214
+ )
215
+ edited_model = YAML.safe_load(edited_yaml)
216
+
217
+ # Apply changes
218
+ Formatters::ProgressFormatter.with_spinner("Updating model '#{name}'") do
219
+ ctx.client.apply_resource(edited_model)
199
220
  end
200
- puts
201
- puts 'Delete these agents first, or use --force to delete anyway.'
202
- puts
203
- return unless Helpers::UserPrompts.confirm('Are you sure?')
204
- end
205
221
 
206
- # Confirm deletion unless --force
207
- unless options[:force] || agents_using.any?
208
- puts "This will delete model '#{name}' from cluster '#{ctx.name}':"
209
- puts " Provider: #{model.dig('spec', 'provider')}"
210
- puts " Model: #{model.dig('spec', 'modelName')}"
211
- puts " Status: #{model.dig('status', 'phase')}"
212
- puts
213
- return unless Helpers::UserPrompts.confirm('Are you sure?')
222
+ Formatters::ProgressFormatter.success("Model '#{name}' updated successfully")
214
223
  end
224
+ end
225
+
226
+ desc 'test NAME', 'Test model connectivity and functionality'
227
+ long_desc <<-DESC
228
+ Test that a model is operational by:
229
+ 1. Verifying the pod is running
230
+ 2. Testing the chat completion endpoint with a simple message
215
231
 
216
- # Delete model
217
- Formatters::ProgressFormatter.with_spinner("Deleting model '#{name}'") do
218
- ctx.client.delete_resource('LanguageModel', name, ctx.namespace)
232
+ This command helps diagnose model deployment issues.
233
+ DESC
234
+ option :cluster, type: :string, desc: 'Override current cluster context'
235
+ option :timeout, type: :numeric, default: 30, desc: 'Timeout in seconds for endpoint test'
236
+ def test(name)
237
+ handle_command_error('test model') do
238
+ # 1. Get model resource
239
+ model = get_resource_or_exit('LanguageModel', name)
240
+ model_name = model.dig('spec', 'modelName')
241
+ model.dig('spec', 'provider')
242
+
243
+ # 2. Check deployment status
244
+ deployment = check_deployment_status(name)
245
+
246
+ # 3. Check pod status
247
+ pod = check_pod_status(name, deployment)
248
+
249
+ # 4. Test chat completion endpoint
250
+ test_chat_completion(name, model_name, pod, options[:timeout])
219
251
  end
252
+ end
220
253
 
221
- Formatters::ProgressFormatter.success("Model '#{name}' deleted successfully")
222
- rescue StandardError => e
223
- Formatters::ProgressFormatter.error("Failed to delete model: #{e.message}")
224
- raise if ENV['DEBUG']
254
+ private
255
+
256
+ def check_deployment_status(name)
257
+ Formatters::ProgressFormatter.with_spinner('Verifying deployment') do
258
+ deployment = ctx.client.get_resource('Deployment', name, ctx.namespace)
259
+ replicas = deployment.dig('spec', 'replicas') || 1
260
+ ready_replicas = deployment.dig('status', 'readyReplicas') || 0
225
261
 
262
+ unless ready_replicas >= replicas
263
+ raise "Deployment not ready (#{ready_replicas}/#{replicas}). " \
264
+ "Run 'kubectl get deployment #{name} -n #{ctx.namespace}' for details."
265
+ end
266
+
267
+ deployment
268
+ end
269
+ rescue K8s::Error::NotFound
270
+ Formatters::ProgressFormatter.error("Deployment '#{name}' not found")
226
271
  exit 1
227
272
  end
228
273
 
229
- desc 'edit NAME', 'Edit model configuration'
230
- option :cluster, type: :string, desc: 'Override current cluster context'
231
- def edit(name)
232
- ctx = Helpers::ClusterContext.from_options(options)
274
+ def check_pod_status(name, deployment)
275
+ Formatters::ProgressFormatter.with_spinner('Verifying pod') do
276
+ labels = deployment.dig('spec', 'selector', 'matchLabels')
277
+ raise "Deployment '#{name}' has no selector labels" if labels.nil?
233
278
 
234
- # Get current model
235
- begin
236
- model = ctx.client.get_resource('LanguageModel', name, ctx.namespace)
237
- rescue K8s::Error::NotFound
238
- Formatters::ProgressFormatter.error("Model '#{name}' not found in cluster '#{ctx.name}'")
239
- exit 1
240
- end
279
+ # Convert K8s::Resource to hash if needed
280
+ labels_hash = labels.respond_to?(:to_h) ? labels.to_h : labels
281
+ raise "Deployment '#{name}' has empty selector labels" if labels_hash.empty?
282
+
283
+ label_selector = labels_hash.map { |k, v| "#{k}=#{v}" }.join(',')
284
+
285
+ pods = ctx.client.list_resources('Pod', namespace: ctx.namespace, label_selector: label_selector)
286
+ raise "No pods found for model '#{name}'" if pods.empty?
287
+
288
+ # Find a running pod
289
+ running_pod = pods.find do |pod|
290
+ pod.dig('status', 'phase') == 'Running' &&
291
+ pod.dig('status', 'conditions')&.any? { |c| c['type'] == 'Ready' && c['status'] == 'True' }
292
+ end
241
293
 
242
- # Edit model YAML in user's editor
243
- edited_yaml = Helpers::EditorHelper.edit_content(
244
- model.to_yaml,
245
- 'model-',
246
- '.yaml',
247
- default_editor: 'vim'
248
- )
249
- edited_model = YAML.safe_load(edited_yaml)
250
-
251
- # Apply changes
252
- Formatters::ProgressFormatter.with_spinner("Updating model '#{name}'") do
253
- ctx.client.apply_resource(edited_model)
294
+ if running_pod.nil?
295
+ pod_phases = pods.map { |p| p.dig('status', 'phase') }.join(', ')
296
+ raise "No running pods found. Pod phases: #{pod_phases}. " \
297
+ "Run 'kubectl get pods -l #{label_selector} -n #{ctx.namespace}' for details."
298
+ end
299
+
300
+ running_pod
254
301
  end
302
+ end
255
303
 
256
- Formatters::ProgressFormatter.success("Model '#{name}' updated successfully")
257
- rescue StandardError => e
258
- Formatters::ProgressFormatter.error("Failed to edit model: #{e.message}")
259
- raise if ENV['DEBUG']
304
+ def test_chat_completion(_name, model_name, pod, timeout)
305
+ Formatters::ProgressFormatter.with_spinner('Verifying chat completion requests') do
306
+ pod_name = pod.dig('metadata', 'name')
307
+
308
+ # Build the JSON payload
309
+ payload = JSON.generate({
310
+ model: model_name,
311
+ messages: [{ role: 'user', content: 'hello' }],
312
+ max_tokens: 10
313
+ })
314
+
315
+ # Build the curl command using echo to pipe JSON
316
+ # This avoids shell escaping issues with -d flag
317
+ curl_command = "echo '#{payload}' | curl -s -X POST http://localhost:4000/v1/chat/completions " \
318
+ "-H 'Content-Type: application/json' -d @- --max-time #{timeout}"
319
+
320
+ # Execute the curl command inside the pod
321
+ result = execute_in_pod(pod_name, curl_command)
322
+
323
+ # Parse the response
324
+ response = JSON.parse(result)
325
+
326
+ if response['error']
327
+ error_msg = response['error']['message'] || response['error']
328
+ raise error_msg
329
+ elsif !response['choices']
330
+ raise "Unexpected response format: #{result.lines.first.strip}"
331
+ end
260
332
 
333
+ response
334
+ rescue JSON::ParserError => e
335
+ raise "Failed to parse response: #{e.message}"
336
+ end
337
+ rescue StandardError => e
338
+ # Display error in bold red
339
+ puts
340
+ puts Formatters::ProgressFormatter.pastel.bold.red(e.message)
261
341
  exit 1
262
342
  end
343
+
344
+ def execute_in_pod(pod_name, command)
345
+ # Use kubectl exec to run command in pod
346
+ # command can be a string or array
347
+ kubectl_command = if command.is_a?(String)
348
+ "kubectl exec -n #{ctx.namespace} #{pod_name} -- sh -c #{Shellwords.escape(command)}"
349
+ else
350
+ (['kubectl', 'exec', '-n', ctx.namespace, pod_name, '--'] + command).join(' ')
351
+ end
352
+
353
+ output = `#{kubectl_command} 2>&1`
354
+ exit_code = $CHILD_STATUS.exitstatus
355
+
356
+ if exit_code != 0
357
+ Formatters::ProgressFormatter.error("Failed to execute command in pod: #{output}")
358
+ exit 1
359
+ end
360
+
361
+ output
362
+ end
263
363
  end
264
364
  end
265
365
  end