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.
- checksums.yaml +4 -4
- data/.rubocop.yml +7 -8
- data/CHANGELOG.md +14 -0
- data/CI_STATUS.md +56 -0
- data/Gemfile.lock +2 -2
- data/Makefile +22 -6
- data/lib/language_operator/agent/base.rb +10 -6
- data/lib/language_operator/agent/executor.rb +19 -97
- data/lib/language_operator/agent/safety/ast_validator.rb +62 -43
- data/lib/language_operator/agent/safety/safe_executor.rb +27 -2
- data/lib/language_operator/agent/scheduler.rb +60 -0
- data/lib/language_operator/agent/task_executor.rb +548 -0
- data/lib/language_operator/agent.rb +90 -27
- data/lib/language_operator/cli/base_command.rb +117 -0
- data/lib/language_operator/cli/commands/agent.rb +339 -407
- data/lib/language_operator/cli/commands/cluster.rb +274 -290
- data/lib/language_operator/cli/commands/install.rb +110 -119
- data/lib/language_operator/cli/commands/model.rb +284 -184
- data/lib/language_operator/cli/commands/persona.rb +218 -284
- data/lib/language_operator/cli/commands/quickstart.rb +4 -5
- data/lib/language_operator/cli/commands/status.rb +31 -35
- data/lib/language_operator/cli/commands/system.rb +221 -233
- data/lib/language_operator/cli/commands/tool.rb +356 -422
- data/lib/language_operator/cli/commands/use.rb +19 -22
- data/lib/language_operator/cli/helpers/resource_dependency_checker.rb +0 -18
- data/lib/language_operator/cli/wizards/quickstart_wizard.rb +0 -1
- data/lib/language_operator/client/config.rb +20 -21
- data/lib/language_operator/config.rb +115 -3
- data/lib/language_operator/constants.rb +54 -0
- data/lib/language_operator/dsl/agent_context.rb +7 -7
- data/lib/language_operator/dsl/agent_definition.rb +111 -26
- data/lib/language_operator/dsl/config.rb +30 -66
- data/lib/language_operator/dsl/main_definition.rb +114 -0
- data/lib/language_operator/dsl/schema.rb +84 -43
- data/lib/language_operator/dsl/task_definition.rb +315 -0
- data/lib/language_operator/dsl.rb +0 -1
- data/lib/language_operator/instrumentation/task_tracer.rb +285 -0
- data/lib/language_operator/logger.rb +4 -4
- data/lib/language_operator/synthesis_test_harness.rb +324 -0
- data/lib/language_operator/templates/examples/agent_synthesis.tmpl +26 -8
- data/lib/language_operator/templates/schema/CHANGELOG.md +26 -0
- data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
- data/lib/language_operator/templates/schema/agent_dsl_schema.json +84 -42
- data/lib/language_operator/type_coercion.rb +250 -0
- data/lib/language_operator/ux/base.rb +81 -0
- data/lib/language_operator/ux/concerns/README.md +155 -0
- data/lib/language_operator/ux/concerns/headings.rb +90 -0
- data/lib/language_operator/ux/concerns/input_validation.rb +146 -0
- data/lib/language_operator/ux/concerns/provider_helpers.rb +167 -0
- data/lib/language_operator/ux/create_agent.rb +252 -0
- data/lib/language_operator/ux/create_model.rb +267 -0
- data/lib/language_operator/ux/quickstart.rb +594 -0
- data/lib/language_operator/version.rb +1 -1
- data/lib/language_operator.rb +2 -0
- data/requirements/ARCHITECTURE.md +1 -0
- data/requirements/SCRATCH.md +153 -0
- data/requirements/dsl.md +0 -0
- data/requirements/features +1 -0
- data/requirements/personas +1 -0
- data/requirements/proposals +1 -0
- data/requirements/tasks/iterate.md +14 -15
- data/requirements/tasks/optimize.md +13 -4
- data/synth/001/Makefile +90 -0
- data/synth/001/agent.rb +26 -0
- data/synth/001/agent.yaml +7 -0
- data/synth/001/output.log +44 -0
- data/synth/Makefile +39 -0
- data/synth/README.md +342 -0
- metadata +37 -10
- data/lib/language_operator/dsl/workflow_definition.rb +0 -259
- 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 <
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
46
|
+
{
|
|
47
|
+
name: name,
|
|
48
|
+
provider: provider,
|
|
49
|
+
model: model_name,
|
|
50
|
+
status: status
|
|
51
|
+
}
|
|
52
|
+
end
|
|
58
53
|
|
|
59
|
-
|
|
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:
|
|
72
|
-
option :model, type: :string, required:
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
agents_using.
|
|
152
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|