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.
- checksums.yaml +4 -4
- data/.claude/commands/persona.md +9 -0
- data/.claude/commands/task.md +46 -1
- data/.rubocop.yml +13 -0
- data/.rubocop_custom/use_ux_helper.rb +44 -0
- data/CHANGELOG.md +8 -0
- data/Gemfile.lock +12 -1
- data/Makefile +26 -7
- data/Makefile.common +50 -0
- data/bin/aictl +8 -1
- data/components/agent/Gemfile +1 -1
- data/components/agent/bin/langop-agent +7 -0
- data/docs/README.md +58 -0
- data/docs/{dsl/best-practices.md → best-practices.md} +4 -4
- data/docs/cli-reference.md +274 -0
- data/docs/{dsl/constraints.md → constraints.md} +5 -5
- data/docs/how-agents-work.md +156 -0
- data/docs/installation.md +218 -0
- data/docs/quickstart.md +299 -0
- data/docs/understanding-generated-code.md +265 -0
- data/docs/using-tools.md +457 -0
- data/docs/webhooks.md +509 -0
- data/examples/ux_helpers_demo.rb +296 -0
- data/lib/language_operator/agent/base.rb +11 -1
- data/lib/language_operator/agent/executor.rb +23 -6
- data/lib/language_operator/agent/safety/safe_executor.rb +41 -39
- data/lib/language_operator/agent/task_executor.rb +346 -63
- data/lib/language_operator/agent/web_server.rb +110 -14
- data/lib/language_operator/agent/webhook_authenticator.rb +39 -5
- data/lib/language_operator/agent.rb +88 -2
- data/lib/language_operator/cli/base_command.rb +17 -11
- data/lib/language_operator/cli/command_loader.rb +72 -0
- data/lib/language_operator/cli/commands/agent/base.rb +837 -0
- data/lib/language_operator/cli/commands/agent/code_operations.rb +102 -0
- data/lib/language_operator/cli/commands/agent/helpers/cluster_llm_client.rb +116 -0
- data/lib/language_operator/cli/commands/agent/helpers/code_parser.rb +115 -0
- data/lib/language_operator/cli/commands/agent/helpers/synthesis_watcher.rb +96 -0
- data/lib/language_operator/cli/commands/agent/learning.rb +289 -0
- data/lib/language_operator/cli/commands/agent/lifecycle.rb +102 -0
- data/lib/language_operator/cli/commands/agent/logs.rb +125 -0
- data/lib/language_operator/cli/commands/agent/workspace.rb +327 -0
- data/lib/language_operator/cli/commands/cluster.rb +129 -84
- data/lib/language_operator/cli/commands/install.rb +1 -1
- data/lib/language_operator/cli/commands/model/base.rb +215 -0
- data/lib/language_operator/cli/commands/model/test.rb +165 -0
- data/lib/language_operator/cli/commands/persona.rb +16 -34
- data/lib/language_operator/cli/commands/quickstart.rb +3 -2
- data/lib/language_operator/cli/commands/status.rb +40 -67
- data/lib/language_operator/cli/commands/system/base.rb +44 -0
- data/lib/language_operator/cli/commands/system/exec.rb +147 -0
- data/lib/language_operator/cli/commands/system/helpers/llm_synthesis.rb +183 -0
- data/lib/language_operator/cli/commands/system/helpers/pod_manager.rb +212 -0
- data/lib/language_operator/cli/commands/system/helpers/template_loader.rb +57 -0
- data/lib/language_operator/cli/commands/system/helpers/template_validator.rb +174 -0
- data/lib/language_operator/cli/commands/system/schema.rb +92 -0
- data/lib/language_operator/cli/commands/system/synthesis_template.rb +151 -0
- data/lib/language_operator/cli/commands/system/synthesize.rb +224 -0
- data/lib/language_operator/cli/commands/system/validate_template.rb +130 -0
- data/lib/language_operator/cli/commands/tool/base.rb +271 -0
- data/lib/language_operator/cli/commands/tool/install.rb +255 -0
- data/lib/language_operator/cli/commands/tool/search.rb +69 -0
- data/lib/language_operator/cli/commands/tool/test.rb +115 -0
- data/lib/language_operator/cli/commands/use.rb +29 -6
- data/lib/language_operator/cli/errors/handler.rb +20 -17
- data/lib/language_operator/cli/errors/suggestions.rb +3 -5
- data/lib/language_operator/cli/errors/thor_errors.rb +55 -0
- data/lib/language_operator/cli/formatters/code_formatter.rb +4 -11
- data/lib/language_operator/cli/formatters/log_formatter.rb +8 -15
- data/lib/language_operator/cli/formatters/progress_formatter.rb +6 -8
- data/lib/language_operator/cli/formatters/status_formatter.rb +26 -7
- data/lib/language_operator/cli/formatters/table_formatter.rb +47 -36
- data/lib/language_operator/cli/formatters/value_formatter.rb +75 -0
- data/lib/language_operator/cli/helpers/cluster_context.rb +5 -3
- data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +2 -1
- data/lib/language_operator/cli/helpers/label_utils.rb +97 -0
- data/lib/language_operator/{ux/concerns/provider_helpers.rb → cli/helpers/provider_helper.rb} +10 -29
- data/lib/language_operator/cli/helpers/schedule_builder.rb +21 -1
- data/lib/language_operator/cli/helpers/user_prompts.rb +19 -11
- data/lib/language_operator/cli/helpers/ux_helper.rb +538 -0
- data/lib/language_operator/{ux/concerns/input_validation.rb → cli/helpers/validation_helper.rb} +13 -66
- data/lib/language_operator/cli/main.rb +50 -40
- data/lib/language_operator/cli/templates/tools/generic.yaml +3 -0
- data/lib/language_operator/cli/wizards/agent_wizard.rb +12 -20
- data/lib/language_operator/cli/wizards/model_wizard.rb +271 -0
- data/lib/language_operator/cli/wizards/quickstart_wizard.rb +8 -34
- data/lib/language_operator/client/base.rb +28 -0
- data/lib/language_operator/client/config.rb +4 -1
- data/lib/language_operator/client/mcp_connector.rb +1 -1
- data/lib/language_operator/config/cluster_config.rb +3 -2
- data/lib/language_operator/config.rb +38 -11
- data/lib/language_operator/constants/kubernetes_labels.rb +80 -0
- data/lib/language_operator/constants.rb +13 -0
- data/lib/language_operator/dsl/http.rb +127 -10
- data/lib/language_operator/dsl.rb +153 -6
- data/lib/language_operator/errors.rb +50 -0
- data/lib/language_operator/kubernetes/client.rb +11 -6
- data/lib/language_operator/kubernetes/resource_builder.rb +58 -84
- data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
- data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
- data/lib/language_operator/type_coercion.rb +118 -34
- data/lib/language_operator/utils/secure_path.rb +74 -0
- data/lib/language_operator/utils.rb +7 -0
- data/lib/language_operator/validators.rb +54 -2
- data/lib/language_operator/version.rb +1 -1
- data/synth/001/Makefile +10 -2
- data/synth/001/agent.rb +16 -15
- data/synth/001/output.log +27 -10
- data/synth/002/Makefile +10 -2
- data/synth/003/Makefile +1 -1
- data/synth/003/README.md +205 -133
- data/synth/003/agent.optimized.rb +66 -0
- data/synth/003/agent.synthesized.rb +41 -0
- metadata +111 -35
- data/docs/dsl/agent-reference.md +0 -604
- data/docs/dsl/mcp-integration.md +0 -1177
- data/docs/dsl/webhooks.md +0 -932
- data/docs/dsl/workflows.md +0 -744
- data/lib/language_operator/cli/commands/agent.rb +0 -1712
- data/lib/language_operator/cli/commands/model.rb +0 -366
- data/lib/language_operator/cli/commands/system.rb +0 -1259
- data/lib/language_operator/cli/commands/tool.rb +0 -654
- data/lib/language_operator/cli/formatters/optimization_formatter.rb +0 -226
- data/lib/language_operator/cli/helpers/pastel_helper.rb +0 -24
- data/lib/language_operator/learning/adapters/base_adapter.rb +0 -149
- data/lib/language_operator/learning/adapters/jaeger_adapter.rb +0 -221
- data/lib/language_operator/learning/adapters/signoz_adapter.rb +0 -435
- data/lib/language_operator/learning/adapters/tempo_adapter.rb +0 -239
- data/lib/language_operator/learning/optimizer.rb +0 -319
- data/lib/language_operator/learning/pattern_detector.rb +0 -260
- data/lib/language_operator/learning/task_synthesizer.rb +0 -288
- data/lib/language_operator/learning/trace_analyzer.rb +0 -285
- data/lib/language_operator/templates/task_synthesis.tmpl +0 -98
- data/lib/language_operator/ux/base.rb +0 -81
- data/lib/language_operator/ux/concerns/README.md +0 -155
- data/lib/language_operator/ux/concerns/headings.rb +0 -90
- data/lib/language_operator/ux/create_agent.rb +0 -255
- data/lib/language_operator/ux/create_model.rb +0 -267
- data/lib/language_operator/ux/quickstart.rb +0 -594
- data/synth/003/agent.rb +0 -41
- data/synth/003/output.log +0 -68
- /data/docs/{architecture/agent-runtime.md → agent-internals.md} +0 -0
- /data/docs/{dsl/chat-endpoints.md → chat-endpoints.md} +0 -0
- /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 '../
|
|
5
|
-
require_relative '
|
|
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
|
|
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
|
-
|
|
66
|
-
|
|
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',
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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(
|
|
129
|
-
tools = k8s.list_resources(
|
|
130
|
-
models = k8s.list_resources(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
k8s.
|
|
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
|
-
|
|
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(
|
|
313
|
-
|
|
314
|
-
|
|
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(
|
|
322
|
-
|
|
323
|
-
|
|
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(
|
|
331
|
-
|
|
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
|
-
|
|
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
|
-
|
|
342
|
-
|
|
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
|