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
|
@@ -2,19 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
require 'thor'
|
|
4
4
|
require 'fileutils'
|
|
5
|
+
require_relative '../utils/secure_path'
|
|
5
6
|
require_relative 'commands/cluster'
|
|
6
7
|
require_relative 'commands/use'
|
|
7
|
-
require_relative 'commands/agent'
|
|
8
|
+
require_relative 'commands/agent/base'
|
|
8
9
|
require_relative 'commands/status'
|
|
9
10
|
require_relative 'commands/persona'
|
|
10
|
-
require_relative 'commands/tool'
|
|
11
|
-
require_relative 'commands/model'
|
|
11
|
+
require_relative 'commands/tool/base'
|
|
12
|
+
require_relative 'commands/model/base'
|
|
12
13
|
require_relative 'commands/quickstart'
|
|
13
14
|
require_relative 'commands/install'
|
|
14
|
-
require_relative 'commands/system'
|
|
15
|
+
require_relative 'commands/system/base'
|
|
15
16
|
require_relative 'formatters/progress_formatter'
|
|
17
|
+
require_relative 'helpers/ux_helper'
|
|
16
18
|
require_relative '../config/cluster_config'
|
|
17
19
|
require_relative '../kubernetes/client'
|
|
20
|
+
require_relative 'errors/thor_errors'
|
|
18
21
|
|
|
19
22
|
module LanguageOperator
|
|
20
23
|
module CLI
|
|
@@ -22,6 +25,8 @@ module LanguageOperator
|
|
|
22
25
|
#
|
|
23
26
|
# Provides commands for creating, running, and managing language-operator resources.
|
|
24
27
|
class Main < Thor
|
|
28
|
+
include Helpers::UxHelper
|
|
29
|
+
|
|
25
30
|
def self.exit_on_failure?
|
|
26
31
|
true
|
|
27
32
|
end
|
|
@@ -33,11 +38,11 @@ module LanguageOperator
|
|
|
33
38
|
|
|
34
39
|
desc 'version', 'Show aictl and operator version'
|
|
35
40
|
def version
|
|
36
|
-
|
|
37
|
-
puts
|
|
38
|
-
|
|
39
|
-
# Try to get operator version from current cluster
|
|
41
|
+
# Check operator installation status first
|
|
40
42
|
current_cluster = Config::ClusterConfig.current_cluster
|
|
43
|
+
operator_version = nil
|
|
44
|
+
operator_installed = false
|
|
45
|
+
|
|
41
46
|
if current_cluster
|
|
42
47
|
cluster_config = Config::ClusterConfig.get_cluster(current_cluster)
|
|
43
48
|
begin
|
|
@@ -47,52 +52,46 @@ module LanguageOperator
|
|
|
47
52
|
)
|
|
48
53
|
|
|
49
54
|
if k8s.operator_installed?
|
|
55
|
+
operator_installed = true
|
|
50
56
|
operator_version = k8s.operator_version || 'unknown'
|
|
51
|
-
puts "Operator: v#{operator_version}"
|
|
52
|
-
puts "Cluster: #{current_cluster}"
|
|
53
|
-
|
|
54
|
-
# Check compatibility (simple version check)
|
|
55
|
-
# In the future, this could be more sophisticated
|
|
56
|
-
puts
|
|
57
|
-
Formatters::ProgressFormatter.success('Versions are compatible')
|
|
58
|
-
else
|
|
59
|
-
Formatters::ProgressFormatter.warn("Operator not installed in cluster '#{current_cluster}'")
|
|
60
57
|
end
|
|
61
|
-
rescue StandardError
|
|
62
|
-
|
|
58
|
+
rescue StandardError
|
|
59
|
+
# Silently handle connection errors - we'll show appropriate message below
|
|
63
60
|
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Show sparkly logo with appropriate subtitle
|
|
64
|
+
if operator_installed
|
|
65
|
+
logo(sparkle: true, title: "kubernetes language-operator detected (v#{operator_version})")
|
|
64
66
|
else
|
|
65
|
-
|
|
66
|
-
puts
|
|
67
|
-
puts 'Select a cluster to check operator version:'
|
|
68
|
-
puts ' aictl use <cluster>'
|
|
67
|
+
logo(sparkle: true, title: 'kubernetes language-operator not found')
|
|
69
68
|
end
|
|
70
69
|
end
|
|
71
70
|
|
|
72
|
-
desc 'cluster SUBCOMMAND ...ARGS', 'Manage
|
|
71
|
+
desc 'cluster SUBCOMMAND ...ARGS', 'Manage clusters'
|
|
73
72
|
subcommand 'cluster', Commands::Cluster
|
|
74
73
|
|
|
75
|
-
desc 'use CLUSTER', 'Switch to a different cluster
|
|
74
|
+
desc 'use CLUSTER', 'Switch to a different cluster'
|
|
76
75
|
def use(cluster_name)
|
|
77
76
|
Commands::Use.new.switch(cluster_name)
|
|
78
77
|
end
|
|
79
78
|
|
|
80
|
-
desc 'agent SUBCOMMAND ...ARGS', 'Manage
|
|
81
|
-
subcommand 'agent', Commands::Agent
|
|
79
|
+
desc 'agent SUBCOMMAND ...ARGS', 'Manage agents'
|
|
80
|
+
subcommand 'agent', Commands::Agent::Base
|
|
82
81
|
|
|
83
|
-
desc 'persona SUBCOMMAND ...ARGS', 'Manage
|
|
82
|
+
desc 'persona SUBCOMMAND ...ARGS', 'Manage personas'
|
|
84
83
|
subcommand 'persona', Commands::Persona
|
|
85
84
|
|
|
86
|
-
desc 'tool SUBCOMMAND ...ARGS', 'Manage
|
|
87
|
-
subcommand 'tool', Commands::Tool
|
|
85
|
+
desc 'tool SUBCOMMAND ...ARGS', 'Manage tools'
|
|
86
|
+
subcommand 'tool', Commands::Tool::Base
|
|
88
87
|
|
|
89
|
-
desc 'model SUBCOMMAND ...ARGS', 'Manage
|
|
90
|
-
subcommand 'model', Commands::Model
|
|
88
|
+
desc 'model SUBCOMMAND ...ARGS', 'Manage models'
|
|
89
|
+
subcommand 'model', Commands::Model::Base
|
|
91
90
|
|
|
92
|
-
desc 'system SUBCOMMAND ...ARGS', 'System
|
|
93
|
-
subcommand 'system', Commands::System
|
|
91
|
+
desc 'system SUBCOMMAND ...ARGS', 'System utilities'
|
|
92
|
+
subcommand 'system', Commands::System::Base
|
|
94
93
|
|
|
95
|
-
desc 'quickstart', '
|
|
94
|
+
desc 'quickstart', 'Wizard for first-time users'
|
|
96
95
|
def quickstart
|
|
97
96
|
Commands::Quickstart.new.invoke(:start)
|
|
98
97
|
end
|
|
@@ -157,13 +156,24 @@ module LanguageOperator
|
|
|
157
156
|
when 'fish'
|
|
158
157
|
install_fish_completion
|
|
159
158
|
else
|
|
160
|
-
|
|
159
|
+
message = "Unsupported shell: #{shell}"
|
|
160
|
+
Formatters::ProgressFormatter.error(message)
|
|
161
161
|
puts
|
|
162
162
|
puts 'Supported shells: bash, zsh, fish'
|
|
163
|
-
|
|
163
|
+
raise Errors::ValidationError, message
|
|
164
164
|
end
|
|
165
165
|
end
|
|
166
166
|
|
|
167
|
+
def help(command = nil, subcommand = false)
|
|
168
|
+
if command.nil? && !subcommand
|
|
169
|
+
# Show logo when displaying general help (no specific command)
|
|
170
|
+
logo
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Delegate to Thor's original help method
|
|
174
|
+
super
|
|
175
|
+
end
|
|
176
|
+
|
|
167
177
|
private
|
|
168
178
|
|
|
169
179
|
def install_bash_completion
|
|
@@ -174,7 +184,7 @@ module LanguageOperator
|
|
|
174
184
|
return
|
|
175
185
|
end
|
|
176
186
|
|
|
177
|
-
target =
|
|
187
|
+
target = LanguageOperator::Utils::SecurePath.expand_home_path('.bash_completion.d/aictl')
|
|
178
188
|
FileUtils.mkdir_p(File.dirname(target))
|
|
179
189
|
FileUtils.cp(completion_file, target)
|
|
180
190
|
|
|
@@ -196,7 +206,7 @@ module LanguageOperator
|
|
|
196
206
|
end
|
|
197
207
|
|
|
198
208
|
# Check if user has a custom fpath directory
|
|
199
|
-
fpath_dir =
|
|
209
|
+
fpath_dir = LanguageOperator::Utils::SecurePath.expand_home_path('.zsh/completions')
|
|
200
210
|
FileUtils.mkdir_p(fpath_dir)
|
|
201
211
|
|
|
202
212
|
target = File.join(fpath_dir, '_aictl')
|
|
@@ -220,7 +230,7 @@ module LanguageOperator
|
|
|
220
230
|
return
|
|
221
231
|
end
|
|
222
232
|
|
|
223
|
-
target =
|
|
233
|
+
target = LanguageOperator::Utils::SecurePath.expand_home_path('.config/fish/completions/aictl.fish')
|
|
224
234
|
FileUtils.mkdir_p(File.dirname(target))
|
|
225
235
|
FileUtils.cp(completion_file, target)
|
|
226
236
|
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'tty-prompt'
|
|
4
|
-
require 'pastel'
|
|
5
3
|
require_relative '../helpers/schedule_builder'
|
|
4
|
+
require_relative '../helpers/ux_helper'
|
|
6
5
|
|
|
7
6
|
module LanguageOperator
|
|
8
7
|
module CLI
|
|
9
8
|
module Wizards
|
|
10
9
|
# Interactive wizard for creating agents
|
|
11
10
|
class AgentWizard
|
|
12
|
-
|
|
11
|
+
include Helpers::UxHelper
|
|
13
12
|
|
|
14
13
|
def initialize
|
|
15
|
-
|
|
16
|
-
@pastel = Pastel.new
|
|
14
|
+
# UxHelper provides pastel and prompt methods
|
|
17
15
|
end
|
|
18
16
|
|
|
19
17
|
# Run the wizard and return the generated description
|
|
@@ -42,11 +40,7 @@ module LanguageOperator
|
|
|
42
40
|
private
|
|
43
41
|
|
|
44
42
|
def show_welcome
|
|
45
|
-
|
|
46
|
-
puts pastel.cyan('╭────────────────────────────────────────╮')
|
|
47
|
-
puts pastel.cyan('│ Let\'s create your agent! 🤖 │')
|
|
48
|
-
puts pastel.cyan('╰────────────────────────────────────────╯')
|
|
49
|
-
puts
|
|
43
|
+
logo(title: 'create an agent')
|
|
50
44
|
end
|
|
51
45
|
|
|
52
46
|
def ask_task_description
|
|
@@ -216,23 +210,21 @@ module LanguageOperator
|
|
|
216
210
|
|
|
217
211
|
def show_preview(description, schedule_info, tools_config)
|
|
218
212
|
puts
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
213
|
+
|
|
214
|
+
content = []
|
|
215
|
+
content << "#{pastel.bold('Task:')} #{description}"
|
|
222
216
|
|
|
223
217
|
if schedule_info[:type] == :manual
|
|
224
|
-
|
|
218
|
+
content << "#{pastel.bold('Mode:')} Manual trigger"
|
|
225
219
|
else
|
|
226
220
|
schedule_text = schedule_info[:description] || 'on demand'
|
|
227
|
-
|
|
221
|
+
content << "#{pastel.bold('Schedule:')} #{schedule_text}"
|
|
228
222
|
end
|
|
229
223
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
puts "│ #{pastel.bold('Tools:')} #{tools_config[:tools].join(', ')}" if tools_config[:tools]&.any?
|
|
224
|
+
content << "#{pastel.bold('Cron:')} #{pastel.dim(schedule_info[:cron])}" if schedule_info[:cron]
|
|
225
|
+
content << "#{pastel.bold('Tools:')} #{tools_config[:tools].join(', ')}" if tools_config[:tools]&.any?
|
|
233
226
|
|
|
234
|
-
puts '
|
|
235
|
-
puts pastel.cyan('╰────────────────────────────────────────╯')
|
|
227
|
+
puts box(content.join("\n"), title: 'Preview')
|
|
236
228
|
puts
|
|
237
229
|
end
|
|
238
230
|
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../helpers/ux_helper'
|
|
4
|
+
require_relative '../helpers/validation_helper'
|
|
5
|
+
require_relative '../helpers/provider_helper'
|
|
6
|
+
require_relative '../formatters/progress_formatter'
|
|
7
|
+
require_relative '../../kubernetes/client'
|
|
8
|
+
require_relative '../../kubernetes/resource_builder'
|
|
9
|
+
|
|
10
|
+
module LanguageOperator
|
|
11
|
+
module CLI
|
|
12
|
+
module Wizards
|
|
13
|
+
# Interactive wizard for creating language models
|
|
14
|
+
#
|
|
15
|
+
# Guides users through provider selection, credential input,
|
|
16
|
+
# model selection, and resource creation.
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# wizard = Wizards::ModelWizard.new(ctx)
|
|
20
|
+
# wizard.run
|
|
21
|
+
class ModelWizard
|
|
22
|
+
include Helpers::UxHelper
|
|
23
|
+
include Helpers::ValidationHelper
|
|
24
|
+
include Helpers::ProviderHelper
|
|
25
|
+
|
|
26
|
+
attr_reader :ctx
|
|
27
|
+
|
|
28
|
+
def initialize(ctx)
|
|
29
|
+
@ctx = ctx
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Run the wizard and create the model
|
|
33
|
+
#
|
|
34
|
+
# @return [Boolean] true if model was created successfully
|
|
35
|
+
def run
|
|
36
|
+
show_welcome
|
|
37
|
+
|
|
38
|
+
# Step 1: Provider selection
|
|
39
|
+
provider_info = select_provider
|
|
40
|
+
return false unless provider_info
|
|
41
|
+
|
|
42
|
+
# Step 2: Get credentials
|
|
43
|
+
credentials = get_credentials(provider_info)
|
|
44
|
+
return false unless credentials
|
|
45
|
+
|
|
46
|
+
# Step 3: Test connection
|
|
47
|
+
test_result = test_connection(provider_info, credentials)
|
|
48
|
+
return false unless test_result[:success]
|
|
49
|
+
|
|
50
|
+
# Step 4: Select model
|
|
51
|
+
model_id = select_model(provider_info, credentials)
|
|
52
|
+
return false unless model_id
|
|
53
|
+
|
|
54
|
+
# Step 5: Get display name
|
|
55
|
+
model_name = get_model_name(model_id)
|
|
56
|
+
return false unless model_name
|
|
57
|
+
|
|
58
|
+
# Create resources
|
|
59
|
+
success = create_model_resource(model_name, provider_info, credentials, model_id)
|
|
60
|
+
return false unless success
|
|
61
|
+
|
|
62
|
+
# Show success
|
|
63
|
+
show_success(model_name, model_id, provider_info)
|
|
64
|
+
|
|
65
|
+
true
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def show_welcome
|
|
71
|
+
logo(title: "add a model to cluster '#{ctx.name}'")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def select_provider
|
|
75
|
+
provider = prompt.select('Select a provider:') do |menu|
|
|
76
|
+
menu.choice 'Anthropic', :anthropic
|
|
77
|
+
menu.choice 'OpenAI', :openai
|
|
78
|
+
menu.choice 'Other (OpenAI-compatible)', :openai_compatible
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
case provider
|
|
82
|
+
when :anthropic
|
|
83
|
+
{ provider: :anthropic, provider_key: 'anthropic', display_name: 'Anthropic' }
|
|
84
|
+
when :openai
|
|
85
|
+
{ provider: :openai, provider_key: 'openai', display_name: 'OpenAI' }
|
|
86
|
+
when :openai_compatible
|
|
87
|
+
endpoint = ask_url('API endpoint URL (e.g., http://localhost:11434):')
|
|
88
|
+
return nil unless endpoint
|
|
89
|
+
|
|
90
|
+
{ provider: :openai_compatible, provider_key: 'openai-compatible',
|
|
91
|
+
display_name: 'OpenAI-Compatible', endpoint: endpoint }
|
|
92
|
+
end
|
|
93
|
+
rescue TTY::Reader::InputInterrupt
|
|
94
|
+
Formatters::ProgressFormatter.error('Cancelled')
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def get_credentials(provider_info)
|
|
99
|
+
case provider_info[:provider]
|
|
100
|
+
when :anthropic
|
|
101
|
+
show_credential_help('Anthropic', 'https://console.anthropic.com')
|
|
102
|
+
api_key = ask_secret('Enter your Anthropic API key:')
|
|
103
|
+
return nil unless api_key
|
|
104
|
+
|
|
105
|
+
{ api_key: api_key }
|
|
106
|
+
when :openai
|
|
107
|
+
show_credential_help('OpenAI', 'https://platform.openai.com/api-keys')
|
|
108
|
+
api_key = ask_secret('Enter your OpenAI API key:')
|
|
109
|
+
return nil unless api_key
|
|
110
|
+
|
|
111
|
+
{ api_key: api_key }
|
|
112
|
+
when :openai_compatible
|
|
113
|
+
needs_auth = ask_yes_no('Does this endpoint require authentication?', default: false)
|
|
114
|
+
return nil if needs_auth.nil?
|
|
115
|
+
|
|
116
|
+
api_key = needs_auth ? ask_secret('Enter API key:') : nil
|
|
117
|
+
{ api_key: api_key, endpoint: provider_info[:endpoint] }
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def show_credential_help(_provider_name, url)
|
|
122
|
+
puts "If you need to, get your API key at #{pastel.cyan(url)}."
|
|
123
|
+
puts
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def test_connection(provider_info, credentials)
|
|
127
|
+
puts
|
|
128
|
+
test_result = Formatters::ProgressFormatter.with_spinner('Testing connection') do
|
|
129
|
+
test_provider_connection(
|
|
130
|
+
provider_info[:provider],
|
|
131
|
+
api_key: credentials[:api_key],
|
|
132
|
+
endpoint: provider_info[:endpoint]
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
unless test_result[:success]
|
|
137
|
+
Formatters::ProgressFormatter.error("Connection failed: #{test_result[:error]}")
|
|
138
|
+
puts
|
|
139
|
+
retry_choice = ask_yes_no('Try again with different credentials?', default: false)
|
|
140
|
+
if retry_choice
|
|
141
|
+
new_credentials = get_credentials(provider_info)
|
|
142
|
+
return { success: false } unless new_credentials
|
|
143
|
+
|
|
144
|
+
return test_connection(provider_info, new_credentials)
|
|
145
|
+
end
|
|
146
|
+
return { success: false }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
puts
|
|
150
|
+
{ success: true }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def select_model(provider_info, credentials)
|
|
154
|
+
available_models = fetch_provider_models(
|
|
155
|
+
provider_info[:provider],
|
|
156
|
+
api_key: credentials[:api_key],
|
|
157
|
+
endpoint: provider_info[:endpoint]
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if available_models.nil? || available_models.empty?
|
|
161
|
+
Formatters::ProgressFormatter.warn('Could not fetch available models')
|
|
162
|
+
puts
|
|
163
|
+
model_id = prompt.ask('Enter model identifier manually:') do |q|
|
|
164
|
+
q.required true
|
|
165
|
+
end
|
|
166
|
+
return model_id
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
ask_select('Select a model:', available_models, per_page: 10)
|
|
170
|
+
rescue TTY::Reader::InputInterrupt
|
|
171
|
+
Formatters::ProgressFormatter.error('Cancelled')
|
|
172
|
+
nil
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def get_model_name(model_id)
|
|
176
|
+
# Generate smart default from model_id
|
|
177
|
+
# Examples: "gpt-4-turbo" -> "gpt-4-turbo", "claude-3-opus-20240229" -> "claude-3-opus-20240229"
|
|
178
|
+
# "mistralai/magistral-small-2509" -> "magistral-small-2509"
|
|
179
|
+
default_name = model_id.split('/').last.downcase.gsub(/[^0-9a-z]/i, '-').gsub(/-+/, '-')
|
|
180
|
+
default_name = default_name[0..62] if default_name.length > 63 # K8s limit is 63 chars
|
|
181
|
+
|
|
182
|
+
ask_k8s_name('Your name for this model:', default: default_name)
|
|
183
|
+
rescue TTY::Reader::InputInterrupt
|
|
184
|
+
Formatters::ProgressFormatter.error('Cancelled')
|
|
185
|
+
nil
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def create_model_resource(model_name, provider_info, credentials, model_id)
|
|
189
|
+
# Check if model already exists
|
|
190
|
+
begin
|
|
191
|
+
ctx.client.get_resource('LanguageModel', model_name, ctx.namespace)
|
|
192
|
+
Formatters::ProgressFormatter.error("Model '#{model_name}' already exists in cluster '#{ctx.name}'")
|
|
193
|
+
puts
|
|
194
|
+
puts "Use 'aictl model inspect #{model_name}' to view details"
|
|
195
|
+
puts "Use 'aictl model edit #{model_name}' to modify it"
|
|
196
|
+
return false
|
|
197
|
+
rescue K8s::Error::NotFound
|
|
198
|
+
# Expected - model doesn't exist yet
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
puts
|
|
202
|
+
Formatters::ProgressFormatter.with_spinner("Creating model '#{model_name}'") do
|
|
203
|
+
# Create API key secret if provided
|
|
204
|
+
if credentials[:api_key]
|
|
205
|
+
secret_name = "#{model_name}-api-key"
|
|
206
|
+
secret = {
|
|
207
|
+
'apiVersion' => 'v1',
|
|
208
|
+
'kind' => 'Secret',
|
|
209
|
+
'metadata' => {
|
|
210
|
+
'name' => secret_name,
|
|
211
|
+
'namespace' => ctx.namespace
|
|
212
|
+
},
|
|
213
|
+
'type' => 'Opaque',
|
|
214
|
+
'stringData' => {
|
|
215
|
+
'api-key' => credentials[:api_key]
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
ctx.client.apply_resource(secret)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Create LanguageModel resource
|
|
222
|
+
resource = Kubernetes::ResourceBuilder.language_model(
|
|
223
|
+
model_name,
|
|
224
|
+
provider: provider_info[:provider_key],
|
|
225
|
+
model: model_id,
|
|
226
|
+
endpoint: provider_info[:endpoint],
|
|
227
|
+
cluster: ctx.namespace
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Add API key reference if secret was created
|
|
231
|
+
if credentials[:api_key]
|
|
232
|
+
resource['spec']['apiKeySecret'] = {
|
|
233
|
+
'name' => "#{model_name}-api-key",
|
|
234
|
+
'key' => 'api-key'
|
|
235
|
+
}
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
ctx.client.apply_resource(resource)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
true
|
|
242
|
+
rescue StandardError => e
|
|
243
|
+
Formatters::ProgressFormatter.error("Failed to create model: #{e.message}")
|
|
244
|
+
false
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def show_success(model_name, model_id, provider_info)
|
|
248
|
+
puts
|
|
249
|
+
highlighted_box(
|
|
250
|
+
title: 'LanguageModel Details',
|
|
251
|
+
rows: {
|
|
252
|
+
'Name' => model_name,
|
|
253
|
+
'Provider' => provider_info[:display_name],
|
|
254
|
+
'Model' => model_id,
|
|
255
|
+
'Endpoint' => provider_info[:endpoint],
|
|
256
|
+
'Cluster' => ctx.name
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
puts
|
|
260
|
+
puts 'Next steps:'
|
|
261
|
+
puts
|
|
262
|
+
puts '1. Use this model in an agent:'
|
|
263
|
+
puts " #{pastel.dim("aictl agent create --model #{model_name}")}"
|
|
264
|
+
puts '2. View model details:'
|
|
265
|
+
puts " #{pastel.dim("aictl model inspect #{model_name}")}"
|
|
266
|
+
puts
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'tty-prompt'
|
|
4
|
-
require 'pastel'
|
|
5
3
|
require 'k8s-ruby'
|
|
6
4
|
require_relative '../formatters/progress_formatter'
|
|
5
|
+
require_relative '../helpers/ux_helper'
|
|
7
6
|
require_relative '../../config/cluster_config'
|
|
8
7
|
require_relative '../../kubernetes/client'
|
|
9
8
|
require_relative '../../kubernetes/resource_builder'
|
|
9
|
+
require_relative '../../utils/secure_path'
|
|
10
10
|
|
|
11
11
|
module LanguageOperator
|
|
12
12
|
module CLI
|
|
13
13
|
module Wizards
|
|
14
14
|
# Interactive quickstart wizard for first-time setup
|
|
15
15
|
class QuickstartWizard
|
|
16
|
+
include Helpers::UxHelper
|
|
17
|
+
|
|
16
18
|
def initialize
|
|
17
|
-
|
|
18
|
-
@pastel = Pastel.new
|
|
19
|
+
# UxHelper provides pastel and prompt methods
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def run
|
|
@@ -38,31 +39,11 @@ module LanguageOperator
|
|
|
38
39
|
|
|
39
40
|
private
|
|
40
41
|
|
|
41
|
-
attr_reader :prompt, :pastel
|
|
42
|
-
|
|
43
42
|
def show_welcome
|
|
44
|
-
|
|
45
|
-
puts pastel.cyan('╭────────────────────────────────────────────────╮')
|
|
46
|
-
puts "#{pastel.cyan('│')} Welcome to Language Operator! 🎉 #{pastel.cyan('│')}"
|
|
47
|
-
puts "#{pastel.cyan('│')} Let's get you set up (takes ~5 minutes) #{pastel.cyan('│')}"
|
|
48
|
-
puts pastel.cyan('╰────────────────────────────────────────────────╯')
|
|
49
|
-
puts
|
|
50
|
-
puts 'This wizard will help you:'
|
|
51
|
-
puts ' 1. Connect to your Kubernetes cluster'
|
|
52
|
-
puts ' 2. Configure a language model'
|
|
53
|
-
puts ' 3. Create your first autonomous agent'
|
|
54
|
-
puts
|
|
55
|
-
puts pastel.dim('Press Enter to begin...')
|
|
56
|
-
$stdin.gets
|
|
43
|
+
logo(title: 'cluster setup quick start')
|
|
57
44
|
end
|
|
58
45
|
|
|
59
46
|
def setup_cluster
|
|
60
|
-
puts
|
|
61
|
-
puts '─' * 50
|
|
62
|
-
puts pastel.cyan('Step 1/3: Connect to Kubernetes')
|
|
63
|
-
puts '─' * 50
|
|
64
|
-
puts
|
|
65
|
-
|
|
66
47
|
# Check if user has kubectl configured
|
|
67
48
|
has_kubectl = prompt.yes?('Do you have kubectl configured?')
|
|
68
49
|
|
|
@@ -99,7 +80,7 @@ module LanguageOperator
|
|
|
99
80
|
end
|
|
100
81
|
|
|
101
82
|
def get_kubectl_contexts
|
|
102
|
-
kubeconfig_path = ENV.fetch('KUBECONFIG',
|
|
83
|
+
kubeconfig_path = ENV.fetch('KUBECONFIG', LanguageOperator::Utils::SecurePath.expand_home_path('.kube/config'))
|
|
103
84
|
|
|
104
85
|
return [] unless File.exist?(kubeconfig_path)
|
|
105
86
|
|
|
@@ -111,7 +92,7 @@ module LanguageOperator
|
|
|
111
92
|
end
|
|
112
93
|
|
|
113
94
|
def create_cluster_config(name, context)
|
|
114
|
-
kubeconfig_path = ENV.fetch('KUBECONFIG',
|
|
95
|
+
kubeconfig_path = ENV.fetch('KUBECONFIG', LanguageOperator::Utils::SecurePath.expand_home_path('.kube/config'))
|
|
115
96
|
|
|
116
97
|
Formatters::ProgressFormatter.with_spinner("Creating cluster '#{name}'") do
|
|
117
98
|
# Create Kubernetes client to verify connection
|
|
@@ -138,13 +119,6 @@ module LanguageOperator
|
|
|
138
119
|
|
|
139
120
|
{ name: name, namespace: namespace, kubeconfig: kubeconfig_path, context: context, k8s: k8s }
|
|
140
121
|
end
|
|
141
|
-
|
|
142
|
-
{
|
|
143
|
-
name: name,
|
|
144
|
-
namespace: (kubeconfig_path && K8s::Config.load_file(kubeconfig_path).context(context).namespace) || 'default',
|
|
145
|
-
kubeconfig: kubeconfig_path,
|
|
146
|
-
context: context
|
|
147
|
-
}
|
|
148
122
|
rescue StandardError => e
|
|
149
123
|
puts
|
|
150
124
|
Formatters::ProgressFormatter.error("Failed to connect: #{e.message}")
|
|
@@ -168,6 +168,34 @@ module LanguageOperator
|
|
|
168
168
|
@debug
|
|
169
169
|
end
|
|
170
170
|
|
|
171
|
+
# Cleanup all MCP connections and reset client state
|
|
172
|
+
#
|
|
173
|
+
# This method properly closes all MCP client connections and clears
|
|
174
|
+
# the @clients array to prevent resource leaks. Should be called
|
|
175
|
+
# when the client is no longer needed.
|
|
176
|
+
#
|
|
177
|
+
# @return [void]
|
|
178
|
+
def cleanup_connections
|
|
179
|
+
return if @clients.empty?
|
|
180
|
+
|
|
181
|
+
logger.debug('Cleaning up MCP connections', client_count: @clients.length)
|
|
182
|
+
|
|
183
|
+
@clients.each do |client|
|
|
184
|
+
# Close the client connection if it responds to close
|
|
185
|
+
client.close if client.respond_to?(:close)
|
|
186
|
+
rescue StandardError => e
|
|
187
|
+
logger.warn('Error closing MCP client connection',
|
|
188
|
+
client: client.class.name,
|
|
189
|
+
error: e.message)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Clear the clients array and chat session
|
|
193
|
+
@clients.clear
|
|
194
|
+
@chat = nil
|
|
195
|
+
|
|
196
|
+
logger.debug('MCP connections cleanup completed')
|
|
197
|
+
end
|
|
198
|
+
|
|
171
199
|
private
|
|
172
200
|
|
|
173
201
|
def logger_component
|
|
@@ -24,7 +24,10 @@ module LanguageOperator
|
|
|
24
24
|
# @return [Hash] Configuration hash
|
|
25
25
|
# @raise [Errno::ENOENT] If file doesn't exist
|
|
26
26
|
def self.load(path)
|
|
27
|
-
YAML.
|
|
27
|
+
config = YAML.safe_load_file(path, permitted_classes: [Symbol], aliases: true)
|
|
28
|
+
# Normalize structure to ensure defaults
|
|
29
|
+
config['mcp_servers'] ||= []
|
|
30
|
+
config
|
|
28
31
|
end
|
|
29
32
|
|
|
30
33
|
# Load configuration from environment variables
|
|
@@ -10,7 +10,7 @@ module LanguageOperator
|
|
|
10
10
|
#
|
|
11
11
|
# @return [void]
|
|
12
12
|
def connect_mcp_servers
|
|
13
|
-
enabled_servers = @config['mcp_servers'].select { |s| s['enabled'] }
|
|
13
|
+
enabled_servers = (@config['mcp_servers'] || []).select { |s| s['enabled'] }
|
|
14
14
|
|
|
15
15
|
if enabled_servers.empty?
|
|
16
16
|
logger.info('No MCP servers configured, agent will run without tools')
|