language-operator 0.0.1 → 0.1.31
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 +125 -0
- data/CHANGELOG.md +88 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +284 -0
- data/LICENSE +229 -21
- data/Makefile +82 -0
- data/README.md +3 -11
- data/Rakefile +63 -0
- data/bin/aictl +7 -0
- data/completions/_aictl +232 -0
- data/completions/aictl.bash +121 -0
- data/completions/aictl.fish +114 -0
- data/docs/architecture/agent-runtime.md +585 -0
- data/docs/dsl/SCHEMA_VERSION.md +250 -0
- data/docs/dsl/agent-reference.md +604 -0
- data/docs/dsl/best-practices.md +1078 -0
- data/docs/dsl/chat-endpoints.md +895 -0
- data/docs/dsl/constraints.md +671 -0
- data/docs/dsl/mcp-integration.md +1177 -0
- data/docs/dsl/webhooks.md +932 -0
- data/docs/dsl/workflows.md +744 -0
- data/lib/language_operator/agent/base.rb +110 -0
- data/lib/language_operator/agent/executor.rb +440 -0
- data/lib/language_operator/agent/instrumentation.rb +54 -0
- data/lib/language_operator/agent/metrics_tracker.rb +183 -0
- data/lib/language_operator/agent/safety/ast_validator.rb +272 -0
- data/lib/language_operator/agent/safety/audit_logger.rb +104 -0
- data/lib/language_operator/agent/safety/budget_tracker.rb +175 -0
- data/lib/language_operator/agent/safety/content_filter.rb +93 -0
- data/lib/language_operator/agent/safety/manager.rb +207 -0
- data/lib/language_operator/agent/safety/rate_limiter.rb +150 -0
- data/lib/language_operator/agent/safety/safe_executor.rb +127 -0
- data/lib/language_operator/agent/scheduler.rb +183 -0
- data/lib/language_operator/agent/telemetry.rb +116 -0
- data/lib/language_operator/agent/web_server.rb +610 -0
- data/lib/language_operator/agent/webhook_authenticator.rb +226 -0
- data/lib/language_operator/agent.rb +149 -0
- data/lib/language_operator/cli/commands/agent.rb +1205 -0
- data/lib/language_operator/cli/commands/cluster.rb +371 -0
- data/lib/language_operator/cli/commands/install.rb +404 -0
- data/lib/language_operator/cli/commands/model.rb +266 -0
- data/lib/language_operator/cli/commands/persona.rb +393 -0
- data/lib/language_operator/cli/commands/quickstart.rb +22 -0
- data/lib/language_operator/cli/commands/status.rb +143 -0
- data/lib/language_operator/cli/commands/system.rb +772 -0
- data/lib/language_operator/cli/commands/tool.rb +537 -0
- data/lib/language_operator/cli/commands/use.rb +47 -0
- data/lib/language_operator/cli/errors/handler.rb +180 -0
- data/lib/language_operator/cli/errors/suggestions.rb +176 -0
- data/lib/language_operator/cli/formatters/code_formatter.rb +77 -0
- data/lib/language_operator/cli/formatters/log_formatter.rb +288 -0
- data/lib/language_operator/cli/formatters/progress_formatter.rb +49 -0
- data/lib/language_operator/cli/formatters/status_formatter.rb +37 -0
- data/lib/language_operator/cli/formatters/table_formatter.rb +163 -0
- data/lib/language_operator/cli/formatters/value_formatter.rb +113 -0
- data/lib/language_operator/cli/helpers/cluster_context.rb +62 -0
- data/lib/language_operator/cli/helpers/cluster_validator.rb +101 -0
- data/lib/language_operator/cli/helpers/editor_helper.rb +58 -0
- data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +167 -0
- data/lib/language_operator/cli/helpers/pastel_helper.rb +24 -0
- data/lib/language_operator/cli/helpers/resource_dependency_checker.rb +74 -0
- data/lib/language_operator/cli/helpers/schedule_builder.rb +108 -0
- data/lib/language_operator/cli/helpers/user_prompts.rb +69 -0
- data/lib/language_operator/cli/main.rb +236 -0
- data/lib/language_operator/cli/templates/tools/generic.yaml +66 -0
- data/lib/language_operator/cli/wizards/agent_wizard.rb +246 -0
- data/lib/language_operator/cli/wizards/quickstart_wizard.rb +588 -0
- data/lib/language_operator/client/base.rb +214 -0
- data/lib/language_operator/client/config.rb +136 -0
- data/lib/language_operator/client/cost_calculator.rb +37 -0
- data/lib/language_operator/client/mcp_connector.rb +123 -0
- data/lib/language_operator/client.rb +19 -0
- data/lib/language_operator/config/cluster_config.rb +101 -0
- data/lib/language_operator/config/tool_patterns.yaml +57 -0
- data/lib/language_operator/config/tool_registry.rb +96 -0
- data/lib/language_operator/config.rb +138 -0
- data/lib/language_operator/dsl/adapter.rb +124 -0
- data/lib/language_operator/dsl/agent_context.rb +90 -0
- data/lib/language_operator/dsl/agent_definition.rb +427 -0
- data/lib/language_operator/dsl/chat_endpoint_definition.rb +115 -0
- data/lib/language_operator/dsl/config.rb +119 -0
- data/lib/language_operator/dsl/context.rb +50 -0
- data/lib/language_operator/dsl/execution_context.rb +47 -0
- data/lib/language_operator/dsl/helpers.rb +109 -0
- data/lib/language_operator/dsl/http.rb +184 -0
- data/lib/language_operator/dsl/mcp_server_definition.rb +73 -0
- data/lib/language_operator/dsl/parameter_definition.rb +124 -0
- data/lib/language_operator/dsl/registry.rb +36 -0
- data/lib/language_operator/dsl/schema.rb +1102 -0
- data/lib/language_operator/dsl/shell.rb +125 -0
- data/lib/language_operator/dsl/tool_definition.rb +112 -0
- data/lib/language_operator/dsl/webhook_authentication.rb +114 -0
- data/lib/language_operator/dsl/webhook_definition.rb +106 -0
- data/lib/language_operator/dsl/workflow_definition.rb +259 -0
- data/lib/language_operator/dsl.rb +161 -0
- data/lib/language_operator/errors.rb +60 -0
- data/lib/language_operator/kubernetes/client.rb +279 -0
- data/lib/language_operator/kubernetes/resource_builder.rb +194 -0
- data/lib/language_operator/loggable.rb +47 -0
- data/lib/language_operator/logger.rb +141 -0
- data/lib/language_operator/retry.rb +123 -0
- data/lib/language_operator/retryable.rb +132 -0
- data/lib/language_operator/templates/README.md +23 -0
- data/lib/language_operator/templates/examples/agent_synthesis.tmpl +115 -0
- data/lib/language_operator/templates/examples/persona_distillation.tmpl +19 -0
- data/lib/language_operator/templates/schema/.gitkeep +0 -0
- data/lib/language_operator/templates/schema/CHANGELOG.md +93 -0
- data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +306 -0
- data/lib/language_operator/templates/schema/agent_dsl_schema.json +452 -0
- data/lib/language_operator/tool_loader.rb +242 -0
- data/lib/language_operator/validators.rb +170 -0
- data/lib/language_operator/version.rb +1 -1
- data/lib/language_operator.rb +65 -3
- data/requirements/tasks/challenge.md +9 -0
- data/requirements/tasks/iterate.md +36 -0
- data/requirements/tasks/optimize.md +21 -0
- data/requirements/tasks/tag.md +5 -0
- data/test_agent_dsl.rb +108 -0
- metadata +507 -20
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'cluster_validator'
|
|
4
|
+
|
|
5
|
+
module LanguageOperator
|
|
6
|
+
module CLI
|
|
7
|
+
module Helpers
|
|
8
|
+
# Encapsulates cluster context (name, config, client) to reduce boilerplate
|
|
9
|
+
# in command implementations.
|
|
10
|
+
#
|
|
11
|
+
# Instead of repeating:
|
|
12
|
+
# cluster = ClusterValidator.get_cluster(options[:cluster])
|
|
13
|
+
# cluster_config = ClusterValidator.get_cluster_config(cluster)
|
|
14
|
+
# k8s = ClusterValidator.kubernetes_client(options[:cluster])
|
|
15
|
+
#
|
|
16
|
+
# Use:
|
|
17
|
+
# ctx = ClusterContext.from_options(options)
|
|
18
|
+
# # Access: ctx.name, ctx.config, ctx.client, ctx.namespace
|
|
19
|
+
class ClusterContext
|
|
20
|
+
attr_reader :name, :config, :client, :namespace
|
|
21
|
+
|
|
22
|
+
# Create ClusterContext from command options hash
|
|
23
|
+
# @param options [Hash] Thor command options (expects :cluster key)
|
|
24
|
+
# @return [ClusterContext] Initialized context
|
|
25
|
+
def self.from_options(options)
|
|
26
|
+
name = ClusterValidator.get_cluster(options[:cluster])
|
|
27
|
+
config = ClusterValidator.get_cluster_config(name)
|
|
28
|
+
client = ClusterValidator.kubernetes_client(options[:cluster])
|
|
29
|
+
new(name, config, client)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Initialize with cluster details
|
|
33
|
+
# @param name [String] Cluster name
|
|
34
|
+
# @param config [Hash] Cluster configuration
|
|
35
|
+
# @param client [LanguageOperator::Kubernetes::Client] K8s client
|
|
36
|
+
def initialize(name, config, client)
|
|
37
|
+
@name = name
|
|
38
|
+
@config = config
|
|
39
|
+
@client = client
|
|
40
|
+
@namespace = config[:namespace]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Build kubectl command args for this cluster context
|
|
44
|
+
# @return [Hash] kubectl arguments
|
|
45
|
+
def kubectl_args
|
|
46
|
+
{
|
|
47
|
+
kubeconfig: config[:kubeconfig] ? "--kubeconfig=#{config[:kubeconfig]}" : '',
|
|
48
|
+
context: config[:context] ? "--context=#{config[:context]}" : '',
|
|
49
|
+
namespace: "-n #{namespace}"
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Build kubectl command prefix string
|
|
54
|
+
# @return [String] kubectl command prefix
|
|
55
|
+
def kubectl_prefix
|
|
56
|
+
args = kubectl_args
|
|
57
|
+
"kubectl #{args[:kubeconfig]} #{args[:context]} #{args[:namespace]}".strip.squeeze(' ')
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../formatters/progress_formatter'
|
|
4
|
+
require_relative '../errors/handler'
|
|
5
|
+
require_relative 'kubeconfig_validator'
|
|
6
|
+
require_relative '../../config/cluster_config'
|
|
7
|
+
|
|
8
|
+
module LanguageOperator
|
|
9
|
+
module CLI
|
|
10
|
+
module Helpers
|
|
11
|
+
# Validates that a cluster is selected before executing commands
|
|
12
|
+
module ClusterValidator
|
|
13
|
+
class << self
|
|
14
|
+
# Ensure a cluster is selected, exit with helpful message if not
|
|
15
|
+
def ensure_cluster_selected!
|
|
16
|
+
return current_cluster if current_cluster
|
|
17
|
+
|
|
18
|
+
Errors::Handler.handle_no_cluster_selected
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Get current cluster, or allow override via --cluster flag
|
|
22
|
+
def get_cluster(cluster_override = nil)
|
|
23
|
+
if cluster_override
|
|
24
|
+
validate_cluster_exists!(cluster_override)
|
|
25
|
+
cluster_override
|
|
26
|
+
else
|
|
27
|
+
ensure_cluster_selected!
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Validate that a specific cluster exists
|
|
32
|
+
def validate_cluster_exists!(name)
|
|
33
|
+
return if Config::ClusterConfig.cluster_exists?(name)
|
|
34
|
+
|
|
35
|
+
# Build context with available clusters for fuzzy matching
|
|
36
|
+
clusters = Config::ClusterConfig.list_clusters
|
|
37
|
+
available_names = clusters.map { |c| c[:name] }
|
|
38
|
+
|
|
39
|
+
# Use error handler with fuzzy matching
|
|
40
|
+
error = K8s::Error::NotFound.new(404, 'Not Found', 'cluster')
|
|
41
|
+
Errors::Handler.handle_not_found(error,
|
|
42
|
+
resource_type: 'cluster',
|
|
43
|
+
resource_name: name,
|
|
44
|
+
available_resources: available_names)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Get current cluster name
|
|
48
|
+
def current_cluster
|
|
49
|
+
Config::ClusterConfig.current_cluster
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Get current cluster config
|
|
53
|
+
def current_cluster_config
|
|
54
|
+
cluster_name = ensure_cluster_selected!
|
|
55
|
+
Config::ClusterConfig.get_cluster(cluster_name)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Get cluster config by name (with validation)
|
|
59
|
+
def get_cluster_config(name)
|
|
60
|
+
validate_cluster_exists!(name)
|
|
61
|
+
config = Config::ClusterConfig.get_cluster(name)
|
|
62
|
+
|
|
63
|
+
# Validate kubeconfig exists and is accessible
|
|
64
|
+
validate_kubeconfig!(config)
|
|
65
|
+
|
|
66
|
+
config
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Validate kubeconfig for the given cluster config
|
|
70
|
+
def validate_kubeconfig!(cluster_config)
|
|
71
|
+
# Check if kubeconfig file exists
|
|
72
|
+
kubeconfig_path = cluster_config[:kubeconfig]
|
|
73
|
+
return if kubeconfig_path && File.exist?(kubeconfig_path)
|
|
74
|
+
|
|
75
|
+
Formatters::ProgressFormatter.error("Kubeconfig not found: #{kubeconfig_path}")
|
|
76
|
+
puts
|
|
77
|
+
puts 'The kubeconfig file for this cluster does not exist.'
|
|
78
|
+
puts
|
|
79
|
+
puts 'To fix this issue:'
|
|
80
|
+
puts ' 1. Verify the kubeconfig path in ~/.aictl/config.yaml'
|
|
81
|
+
puts ' 2. Re-create the cluster configuration:'
|
|
82
|
+
puts " aictl cluster create #{cluster_config[:name]}"
|
|
83
|
+
exit 1
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Create a Kubernetes client for the given cluster
|
|
87
|
+
def kubernetes_client(cluster_override = nil)
|
|
88
|
+
cluster = get_cluster(cluster_override)
|
|
89
|
+
cluster_config = get_cluster_config(cluster)
|
|
90
|
+
|
|
91
|
+
require_relative '../../kubernetes/client'
|
|
92
|
+
Kubernetes::Client.new(
|
|
93
|
+
kubeconfig: cluster_config[:kubeconfig],
|
|
94
|
+
context: cluster_config[:context]
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tempfile'
|
|
4
|
+
|
|
5
|
+
module LanguageOperator
|
|
6
|
+
module CLI
|
|
7
|
+
module Helpers
|
|
8
|
+
# Helper module for editing content in the user's preferred editor.
|
|
9
|
+
# Handles tempfile creation, cleanup, and editor invocation.
|
|
10
|
+
module EditorHelper
|
|
11
|
+
# Edit content in the user's preferred editor
|
|
12
|
+
#
|
|
13
|
+
# @param content [String] Content to edit
|
|
14
|
+
# @param filename_prefix [String] Prefix for the temp file name
|
|
15
|
+
# @param extension [String] File extension (default: '.txt')
|
|
16
|
+
# @param default_editor [String] Editor to use if $EDITOR not set (default: 'vi')
|
|
17
|
+
# @return [String] The edited content
|
|
18
|
+
# @raise [RuntimeError] If editor command fails
|
|
19
|
+
#
|
|
20
|
+
# @example Edit agent instructions
|
|
21
|
+
# new_content = EditorHelper.edit_content(
|
|
22
|
+
# current_instructions,
|
|
23
|
+
# 'agent-instructions-',
|
|
24
|
+
# '.txt'
|
|
25
|
+
# )
|
|
26
|
+
#
|
|
27
|
+
# @example Edit YAML configuration
|
|
28
|
+
# new_yaml = EditorHelper.edit_content(
|
|
29
|
+
# model.to_yaml,
|
|
30
|
+
# 'model-',
|
|
31
|
+
# '.yaml',
|
|
32
|
+
# default_editor: 'vim'
|
|
33
|
+
# )
|
|
34
|
+
def self.edit_content(content, filename_prefix, extension = '.txt', default_editor: 'vi')
|
|
35
|
+
editor = ENV['EDITOR'] || default_editor
|
|
36
|
+
tempfile = Tempfile.new([filename_prefix, extension])
|
|
37
|
+
|
|
38
|
+
begin
|
|
39
|
+
# Write content and flush to ensure it's on disk
|
|
40
|
+
tempfile.write(content)
|
|
41
|
+
tempfile.flush
|
|
42
|
+
tempfile.close
|
|
43
|
+
|
|
44
|
+
# Open in editor
|
|
45
|
+
success = system("#{editor} #{tempfile.path}")
|
|
46
|
+
raise "Editor command failed: #{editor}" unless success
|
|
47
|
+
|
|
48
|
+
# Read edited content
|
|
49
|
+
File.read(tempfile.path)
|
|
50
|
+
ensure
|
|
51
|
+
# Clean up temp file
|
|
52
|
+
tempfile.unlink
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../formatters/progress_formatter'
|
|
4
|
+
require_relative '../../kubernetes/client'
|
|
5
|
+
|
|
6
|
+
module LanguageOperator
|
|
7
|
+
module CLI
|
|
8
|
+
module Helpers
|
|
9
|
+
# Validates kubeconfig and cluster connectivity
|
|
10
|
+
class KubeconfigValidator
|
|
11
|
+
class << self
|
|
12
|
+
# Validate kubeconfig exists and cluster is accessible
|
|
13
|
+
# Returns [valid, error_message]
|
|
14
|
+
def validate
|
|
15
|
+
# Check if kubeconfig file exists
|
|
16
|
+
kubeconfig_path = detect_kubeconfig
|
|
17
|
+
return [false, kubeconfig_missing_message(kubeconfig_path)] unless kubeconfig_path && File.exist?(kubeconfig_path)
|
|
18
|
+
|
|
19
|
+
# Try to connect to cluster
|
|
20
|
+
begin
|
|
21
|
+
k8s = Kubernetes::Client.new(kubeconfig: kubeconfig_path)
|
|
22
|
+
|
|
23
|
+
# Test connectivity by listing namespaces
|
|
24
|
+
k8s.client.api('v1').resource('namespaces').list
|
|
25
|
+
|
|
26
|
+
# Check if operator is installed
|
|
27
|
+
return [false, operator_missing_message] unless k8s.operator_installed?
|
|
28
|
+
|
|
29
|
+
[true, nil]
|
|
30
|
+
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT => e
|
|
31
|
+
[false, connection_failed_message(e)]
|
|
32
|
+
rescue K8s::Error::Unauthorized => e
|
|
33
|
+
[false, auth_failed_message(e)]
|
|
34
|
+
rescue StandardError => e
|
|
35
|
+
[false, generic_error_message(e)]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Validate and exit with error if invalid
|
|
40
|
+
def validate!
|
|
41
|
+
valid, error_message = validate
|
|
42
|
+
return if valid
|
|
43
|
+
|
|
44
|
+
Formatters::ProgressFormatter.error(error_message)
|
|
45
|
+
exit 1
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Detect kubeconfig path
|
|
49
|
+
def detect_kubeconfig
|
|
50
|
+
ENV.fetch('KUBECONFIG', nil) || default_kubeconfig_path
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Check if kubeconfig exists
|
|
54
|
+
def kubeconfig_exists?
|
|
55
|
+
path = detect_kubeconfig
|
|
56
|
+
path && File.exist?(path)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def default_kubeconfig_path
|
|
62
|
+
File.expand_path('~/.kube/config')
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def kubeconfig_missing_message(path)
|
|
66
|
+
<<~MSG
|
|
67
|
+
Kubeconfig file not found
|
|
68
|
+
|
|
69
|
+
Expected location: #{path || default_kubeconfig_path}
|
|
70
|
+
|
|
71
|
+
To fix this issue:
|
|
72
|
+
1. Ensure you have a Kubernetes cluster configured
|
|
73
|
+
2. Set KUBECONFIG environment variable to point to your kubeconfig file:
|
|
74
|
+
export KUBECONFIG=/path/to/your/kubeconfig
|
|
75
|
+
|
|
76
|
+
Or place your kubeconfig at: ~/.kube/config
|
|
77
|
+
|
|
78
|
+
For local development, you can use:
|
|
79
|
+
- kind (https://kind.sigs.k8s.io/)
|
|
80
|
+
- k3d (https://k3d.io/)
|
|
81
|
+
- minikube (https://minikube.sigs.k8s.io/)
|
|
82
|
+
MSG
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def connection_failed_message(error)
|
|
86
|
+
<<~MSG
|
|
87
|
+
Failed to connect to Kubernetes cluster
|
|
88
|
+
|
|
89
|
+
Error: #{error.message}
|
|
90
|
+
|
|
91
|
+
To fix this issue:
|
|
92
|
+
1. Check if your cluster is running:
|
|
93
|
+
kubectl cluster-info
|
|
94
|
+
|
|
95
|
+
2. Verify your kubeconfig is correct:
|
|
96
|
+
kubectl config view
|
|
97
|
+
|
|
98
|
+
3. Check your cluster context:
|
|
99
|
+
kubectl config current-context
|
|
100
|
+
|
|
101
|
+
4. Test basic connectivity:
|
|
102
|
+
kubectl get namespaces
|
|
103
|
+
MSG
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def auth_failed_message(error)
|
|
107
|
+
<<~MSG
|
|
108
|
+
Kubernetes authentication failed
|
|
109
|
+
|
|
110
|
+
Error: #{error.message}
|
|
111
|
+
|
|
112
|
+
To fix this issue:
|
|
113
|
+
1. Verify your credentials are valid:
|
|
114
|
+
kubectl config view
|
|
115
|
+
|
|
116
|
+
2. Check if your authentication token/certificate is expired
|
|
117
|
+
|
|
118
|
+
3. Re-authenticate with your cluster provider
|
|
119
|
+
|
|
120
|
+
4. Test authentication:
|
|
121
|
+
kubectl get namespaces
|
|
122
|
+
MSG
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def operator_missing_message
|
|
126
|
+
<<~MSG
|
|
127
|
+
Language Operator is not installed in the cluster
|
|
128
|
+
|
|
129
|
+
The Language Operator CRDs were not found in the cluster.
|
|
130
|
+
|
|
131
|
+
To install the operator:
|
|
132
|
+
|
|
133
|
+
1. Using aictl (recommended):
|
|
134
|
+
aictl install
|
|
135
|
+
|
|
136
|
+
2. Or manually with Helm:
|
|
137
|
+
helm repo add language-operator https://language-operator.github.io/charts
|
|
138
|
+
helm repo update
|
|
139
|
+
helm install language-operator language-operator/language-operator
|
|
140
|
+
|
|
141
|
+
3. Verify installation:
|
|
142
|
+
kubectl get deployment -n language-operator-system language-operator
|
|
143
|
+
|
|
144
|
+
For more information, visit: https://github.com/language-operator/language-operator
|
|
145
|
+
MSG
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def generic_error_message(error)
|
|
149
|
+
<<~MSG
|
|
150
|
+
Unexpected error validating cluster connection
|
|
151
|
+
|
|
152
|
+
Error: #{error.class}: #{error.message}
|
|
153
|
+
|
|
154
|
+
Please check:
|
|
155
|
+
1. Your kubeconfig file is valid
|
|
156
|
+
2. Your cluster is accessible
|
|
157
|
+
3. You have appropriate permissions
|
|
158
|
+
|
|
159
|
+
For debugging, run with DEBUG=1:
|
|
160
|
+
DEBUG=1 aictl <command>
|
|
161
|
+
MSG
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pastel'
|
|
4
|
+
|
|
5
|
+
module LanguageOperator
|
|
6
|
+
module CLI
|
|
7
|
+
module Helpers
|
|
8
|
+
# Shared module providing Pastel color functionality
|
|
9
|
+
# to CLI commands, formatters, and helpers.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# include PastelHelper
|
|
13
|
+
# puts pastel.green("Success!")
|
|
14
|
+
module PastelHelper
|
|
15
|
+
# Returns a memoized Pastel instance for colorizing terminal output
|
|
16
|
+
#
|
|
17
|
+
# @return [Pastel] Pastel instance
|
|
18
|
+
def pastel
|
|
19
|
+
@pastel ||= Pastel.new
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LanguageOperator
|
|
4
|
+
module CLI
|
|
5
|
+
module Helpers
|
|
6
|
+
# Helper module for checking which agents depend on specific resources
|
|
7
|
+
# (tools, models, personas). Used by CLI commands to warn before deletion.
|
|
8
|
+
module ResourceDependencyChecker
|
|
9
|
+
# Find agents that use a specific tool
|
|
10
|
+
#
|
|
11
|
+
# @param agents [Array<Hash>] Array of agent resources from kubectl
|
|
12
|
+
# @param tool_name [String] Name of the tool to check
|
|
13
|
+
# @return [Array<Hash>] Agents that reference this tool
|
|
14
|
+
def self.agents_using_tool(agents, tool_name)
|
|
15
|
+
agents.select do |agent|
|
|
16
|
+
agent_tools = agent.dig('spec', 'tools') || []
|
|
17
|
+
agent_tools.include?(tool_name)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Find agents that use a specific language model
|
|
22
|
+
#
|
|
23
|
+
# @param agents [Array<Hash>] Array of agent resources from kubectl
|
|
24
|
+
# @param model_name [String] Name of the model to check
|
|
25
|
+
# @return [Array<Hash>] Agents that reference this model
|
|
26
|
+
def self.agents_using_model(agents, model_name)
|
|
27
|
+
agents.select do |agent|
|
|
28
|
+
agent_model_refs = agent.dig('spec', 'modelRefs') || []
|
|
29
|
+
agent_models = agent_model_refs.map { |ref| ref['name'] }
|
|
30
|
+
agent_models.include?(model_name)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Find agents that use a specific persona
|
|
35
|
+
#
|
|
36
|
+
# @param agents [Array<Hash>] Array of agent resources from kubectl
|
|
37
|
+
# @param persona_name [String] Name of the persona to check
|
|
38
|
+
# @return [Array<Hash>] Agents that reference this persona
|
|
39
|
+
def self.agents_using_persona(agents, persona_name)
|
|
40
|
+
agents.select do |agent|
|
|
41
|
+
agent.dig('spec', 'persona') == persona_name
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Count how many agents use a specific tool
|
|
46
|
+
#
|
|
47
|
+
# @param agents [Array<Hash>] Array of agent resources
|
|
48
|
+
# @param tool_name [String] Name of the tool
|
|
49
|
+
# @return [Integer] Count of agents using this tool
|
|
50
|
+
def self.tool_usage_count(agents, tool_name)
|
|
51
|
+
agents_using_tool(agents, tool_name).size
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Count how many agents use a specific model
|
|
55
|
+
#
|
|
56
|
+
# @param agents [Array<Hash>] Array of agent resources
|
|
57
|
+
# @param model_name [String] Name of the model
|
|
58
|
+
# @return [Integer] Count of agents using this model
|
|
59
|
+
def self.model_usage_count(agents, model_name)
|
|
60
|
+
agents_using_model(agents, model_name).size
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Count how many agents use a specific persona
|
|
64
|
+
#
|
|
65
|
+
# @param agents [Array<Hash>] Array of agent resources
|
|
66
|
+
# @param persona_name [String] Name of the persona
|
|
67
|
+
# @return [Integer] Count of agents using this persona
|
|
68
|
+
def self.persona_usage_count(agents, persona_name)
|
|
69
|
+
agents_using_persona(agents, persona_name).size
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LanguageOperator
|
|
4
|
+
module CLI
|
|
5
|
+
module Helpers
|
|
6
|
+
# Helper for building schedules from natural language inputs
|
|
7
|
+
class ScheduleBuilder
|
|
8
|
+
class << self
|
|
9
|
+
# Parse natural language time input and return 24-hour format
|
|
10
|
+
# Examples: "4pm" -> "16:00", "9:30am" -> "09:30", "16:00" -> "16:00"
|
|
11
|
+
def parse_time(input)
|
|
12
|
+
input = input.strip.downcase
|
|
13
|
+
|
|
14
|
+
# Handle 24-hour format (e.g., "16:00", "9:30")
|
|
15
|
+
if input.match?(/^\d{1,2}:\d{2}$/)
|
|
16
|
+
hours, minutes = input.split(':').map(&:to_i)
|
|
17
|
+
return format_time(hours, minutes) if valid_time?(hours, minutes)
|
|
18
|
+
|
|
19
|
+
raise ArgumentError, "Invalid time: #{input}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Handle 12-hour format with am/pm (e.g., "4pm", "9:30am")
|
|
23
|
+
match = input.match(/^(\d{1,2})(?::(\d{2}))?\s*(am|pm)$/i)
|
|
24
|
+
raise ArgumentError, "Invalid time format: #{input}" unless match
|
|
25
|
+
|
|
26
|
+
hours = match[1].to_i
|
|
27
|
+
minutes = match[2].to_i
|
|
28
|
+
period = match[3].downcase
|
|
29
|
+
|
|
30
|
+
# Convert to 24-hour format
|
|
31
|
+
hours = 0 if hours == 12 && period == 'am'
|
|
32
|
+
hours = 12 if hours == 12 && period == 'pm'
|
|
33
|
+
hours += 12 if period == 'pm' && hours != 12
|
|
34
|
+
|
|
35
|
+
raise ArgumentError, "Invalid time: #{input}" unless valid_time?(hours, minutes)
|
|
36
|
+
|
|
37
|
+
format_time(hours, minutes)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Build a cron expression for daily execution at a specific time
|
|
41
|
+
def daily_cron(time_string)
|
|
42
|
+
hours, minutes = time_string.split(':').map(&:to_i)
|
|
43
|
+
"#{minutes} #{hours} * * *"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Build a cron expression for interval-based execution
|
|
47
|
+
def interval_cron(interval, unit)
|
|
48
|
+
case unit.downcase
|
|
49
|
+
when 'minutes', 'minute'
|
|
50
|
+
"*/#{interval} * * * *"
|
|
51
|
+
when 'hours', 'hour'
|
|
52
|
+
"0 */#{interval} * * *"
|
|
53
|
+
when 'days', 'day'
|
|
54
|
+
"0 0 */#{interval} * *"
|
|
55
|
+
else
|
|
56
|
+
raise ArgumentError, "Invalid unit: #{unit}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Convert cron expression to human-readable format
|
|
61
|
+
def cron_to_human(cron_expr)
|
|
62
|
+
parts = cron_expr.split
|
|
63
|
+
return cron_expr if parts.length != 5
|
|
64
|
+
|
|
65
|
+
minute, hour, day, month, weekday = parts
|
|
66
|
+
|
|
67
|
+
# Daily at specific time
|
|
68
|
+
if minute =~ /^\d+$/ && hour =~ /^\d+$/ && day == '*' && month == '*' && weekday == '*'
|
|
69
|
+
time_str = format_time(hour.to_i, minute.to_i)
|
|
70
|
+
return "Daily at #{time_str}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Every N minutes
|
|
74
|
+
if minute.start_with?('*/') && hour == '*'
|
|
75
|
+
interval = minute[2..].to_i
|
|
76
|
+
return "Every #{interval} minute#{'s' if interval > 1}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Every N hours
|
|
80
|
+
if minute == '0' && hour.start_with?('*/')
|
|
81
|
+
interval = hour[2..].to_i
|
|
82
|
+
return "Every #{interval} hour#{'s' if interval > 1}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Every N days
|
|
86
|
+
if minute == '0' && hour == '0' && day.start_with?('*/')
|
|
87
|
+
interval = day[2..].to_i
|
|
88
|
+
return "Every #{interval} day#{'s' if interval > 1}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Fallback to cron expression
|
|
92
|
+
cron_expr
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def valid_time?(hours, minutes)
|
|
98
|
+
hours >= 0 && hours < 24 && minutes >= 0 && minutes < 60
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def format_time(hours, minutes)
|
|
102
|
+
format('%<hours>02d:%<minutes>02d', hours: hours, minutes: minutes)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LanguageOperator
|
|
4
|
+
module CLI
|
|
5
|
+
module Helpers
|
|
6
|
+
# Helper module for user confirmation prompts and interactive input.
|
|
7
|
+
# Consolidates the repeated confirmation pattern used throughout commands.
|
|
8
|
+
module UserPrompts
|
|
9
|
+
# Ask user for confirmation
|
|
10
|
+
# @param message [String] The confirmation message
|
|
11
|
+
# @param force [Boolean] Skip prompt if true (default: false)
|
|
12
|
+
# @return [Boolean] true if confirmed, false otherwise
|
|
13
|
+
# rubocop:disable Naming/PredicateMethod
|
|
14
|
+
def self.confirm(message, force: false)
|
|
15
|
+
return true if force
|
|
16
|
+
|
|
17
|
+
print "#{message} (y/N): "
|
|
18
|
+
response = $stdin.gets&.chomp || ''
|
|
19
|
+
puts
|
|
20
|
+
response.downcase == 'y'
|
|
21
|
+
end
|
|
22
|
+
# rubocop:enable Naming/PredicateMethod
|
|
23
|
+
|
|
24
|
+
# Ask user for confirmation and exit if not confirmed
|
|
25
|
+
# @param message [String] The confirmation message
|
|
26
|
+
# @param force [Boolean] Skip prompt if true (default: false)
|
|
27
|
+
# @param cancel_message [String] Message to display on cancellation
|
|
28
|
+
# @return [void] Returns if confirmed, exits otherwise
|
|
29
|
+
def self.confirm!(message, force: false, cancel_message: 'Operation cancelled')
|
|
30
|
+
return if confirm(message, force: force)
|
|
31
|
+
|
|
32
|
+
puts cancel_message
|
|
33
|
+
exit 0
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Ask user for text input
|
|
37
|
+
# @param prompt [String] The prompt message
|
|
38
|
+
# @param default [String, nil] Default value if user enters nothing
|
|
39
|
+
# @return [String] User input
|
|
40
|
+
def self.ask(prompt, default: nil)
|
|
41
|
+
prompt_text = default ? "#{prompt} [#{default}]" : prompt
|
|
42
|
+
print "#{prompt_text}: "
|
|
43
|
+
response = $stdin.gets&.chomp || ''
|
|
44
|
+
response.empty? && default ? default : response
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Ask user to select from options
|
|
48
|
+
# @param prompt [String] The prompt message
|
|
49
|
+
# @param options [Array<String>] Available options
|
|
50
|
+
# @return [String] Selected option
|
|
51
|
+
def self.select(prompt, options)
|
|
52
|
+
puts prompt
|
|
53
|
+
options.each_with_index do |option, index|
|
|
54
|
+
puts " #{index + 1}. #{option}"
|
|
55
|
+
end
|
|
56
|
+
print "\nSelect (1-#{options.length}): "
|
|
57
|
+
|
|
58
|
+
selection = $stdin.gets&.chomp.to_i
|
|
59
|
+
if selection.between?(1, options.length)
|
|
60
|
+
options[selection - 1]
|
|
61
|
+
else
|
|
62
|
+
puts 'Invalid selection'
|
|
63
|
+
exit 1
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|