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,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require_relative '../helpers/pastel_helper'
|
|
3
|
+
require_relative '../helpers/ux_helper'
|
|
5
4
|
require_relative 'status_formatter'
|
|
6
5
|
|
|
7
6
|
module LanguageOperator
|
|
@@ -10,43 +9,57 @@ module LanguageOperator
|
|
|
10
9
|
# Table output for CLI list commands
|
|
11
10
|
class TableFormatter
|
|
12
11
|
class << self
|
|
13
|
-
include Helpers::
|
|
12
|
+
include Helpers::UxHelper
|
|
14
13
|
|
|
15
14
|
def clusters(clusters)
|
|
16
15
|
return ProgressFormatter.info('No clusters found') if clusters.empty?
|
|
17
16
|
|
|
18
|
-
headers =
|
|
17
|
+
headers = ['', 'NAME', 'NAMESPACE', 'STATUS', 'DOMAIN']
|
|
19
18
|
rows = clusters.map do |cluster|
|
|
19
|
+
# Extract asterisk for selected cluster
|
|
20
|
+
name = cluster[:name].to_s.gsub(' *', '')
|
|
21
|
+
is_current = cluster[:name].to_s.include?(' *')
|
|
22
|
+
|
|
23
|
+
# Apply bold yellow formatting if current cluster
|
|
24
|
+
formatted_name = is_current ? pastel.bold.yellow(name) : name
|
|
25
|
+
|
|
26
|
+
# Format domain - show empty cell if nil or empty, handle error states
|
|
27
|
+
domain_display = case cluster[:domain]
|
|
28
|
+
when nil, ''
|
|
29
|
+
''
|
|
30
|
+
when '?', '-'
|
|
31
|
+
cluster[:domain]
|
|
32
|
+
else
|
|
33
|
+
cluster[:domain]
|
|
34
|
+
end
|
|
35
|
+
|
|
20
36
|
[
|
|
21
|
-
cluster[:
|
|
37
|
+
StatusFormatter.dot(cluster[:status] || 'Unknown'),
|
|
38
|
+
formatted_name,
|
|
22
39
|
cluster[:namespace],
|
|
23
|
-
cluster[:
|
|
24
|
-
|
|
25
|
-
cluster[:models] || 0,
|
|
26
|
-
StatusFormatter.format(cluster[:status] || 'Unknown')
|
|
40
|
+
cluster[:status] || 'Unknown',
|
|
41
|
+
domain_display
|
|
27
42
|
]
|
|
28
43
|
end
|
|
29
44
|
|
|
30
|
-
table
|
|
31
|
-
puts table.render(:unicode, padding: [0, 1])
|
|
45
|
+
puts table(headers, rows)
|
|
32
46
|
end
|
|
33
47
|
|
|
34
48
|
def agents(agents)
|
|
35
49
|
return ProgressFormatter.info('No agents found') if agents.empty?
|
|
36
50
|
|
|
37
|
-
headers = ['
|
|
51
|
+
headers = ['', 'NAME', 'NAMESPACE', 'STATUS', 'MODE']
|
|
38
52
|
rows = agents.map do |agent|
|
|
39
53
|
[
|
|
54
|
+
StatusFormatter.dot(agent[:status]),
|
|
40
55
|
agent[:name],
|
|
41
|
-
agent[:
|
|
42
|
-
|
|
43
|
-
agent[:
|
|
44
|
-
agent[:executions] || 0
|
|
56
|
+
agent[:namespace],
|
|
57
|
+
agent[:status] || 'Unknown',
|
|
58
|
+
agent[:mode]
|
|
45
59
|
]
|
|
46
60
|
end
|
|
47
61
|
|
|
48
|
-
table
|
|
49
|
-
puts table.render(:unicode, padding: [0, 1])
|
|
62
|
+
puts table(headers, rows)
|
|
50
63
|
end
|
|
51
64
|
|
|
52
65
|
def all_agents(agents_by_cluster)
|
|
@@ -68,25 +81,23 @@ module LanguageOperator
|
|
|
68
81
|
end
|
|
69
82
|
end
|
|
70
83
|
|
|
71
|
-
table
|
|
72
|
-
puts table.render(:unicode, padding: [0, 1])
|
|
84
|
+
puts table(headers, rows)
|
|
73
85
|
end
|
|
74
86
|
|
|
75
87
|
def tools(tools)
|
|
76
88
|
return ProgressFormatter.info('No tools found') if tools.empty?
|
|
77
89
|
|
|
78
|
-
headers = ['
|
|
90
|
+
headers = ['', 'NAME', 'NAMESPACE', 'STATUS']
|
|
79
91
|
rows = tools.map do |tool|
|
|
80
92
|
[
|
|
93
|
+
StatusFormatter.dot(tool[:status]),
|
|
81
94
|
tool[:name],
|
|
82
|
-
tool[:
|
|
83
|
-
|
|
84
|
-
tool[:agents_using] || 0
|
|
95
|
+
tool[:namespace],
|
|
96
|
+
tool[:status] || 'Unknown'
|
|
85
97
|
]
|
|
86
98
|
end
|
|
87
99
|
|
|
88
|
-
table
|
|
89
|
-
puts table.render(:unicode, padding: [0, 1])
|
|
100
|
+
puts table(headers, rows)
|
|
90
101
|
end
|
|
91
102
|
|
|
92
103
|
def personas(personas)
|
|
@@ -102,25 +113,26 @@ module LanguageOperator
|
|
|
102
113
|
]
|
|
103
114
|
end
|
|
104
115
|
|
|
105
|
-
table
|
|
106
|
-
puts table.render(:unicode, padding: [0, 1])
|
|
116
|
+
puts table(headers, rows)
|
|
107
117
|
end
|
|
108
118
|
|
|
109
119
|
def models(models)
|
|
110
120
|
return ProgressFormatter.info('No models found') if models.empty?
|
|
111
121
|
|
|
112
|
-
headers =
|
|
122
|
+
headers = ['', 'NAME', 'NAMESPACE', 'STATUS', 'PROVIDER/MODEL']
|
|
113
123
|
rows = models.map do |model|
|
|
124
|
+
provider_model = "#{model[:provider]}/#{model[:model]}"
|
|
125
|
+
|
|
114
126
|
[
|
|
127
|
+
StatusFormatter.dot(model[:status]),
|
|
115
128
|
model[:name],
|
|
116
|
-
model[:
|
|
117
|
-
model[:
|
|
118
|
-
|
|
129
|
+
model[:namespace],
|
|
130
|
+
model[:status] || 'Unknown',
|
|
131
|
+
provider_model
|
|
119
132
|
]
|
|
120
133
|
end
|
|
121
134
|
|
|
122
|
-
table
|
|
123
|
-
puts table.render(:unicode, padding: [0, 1])
|
|
135
|
+
puts table(headers, rows)
|
|
124
136
|
end
|
|
125
137
|
|
|
126
138
|
def status_dashboard(cluster_summary, current_cluster: nil)
|
|
@@ -140,8 +152,7 @@ module LanguageOperator
|
|
|
140
152
|
]
|
|
141
153
|
end
|
|
142
154
|
|
|
143
|
-
table
|
|
144
|
-
puts table.render(:unicode, padding: [0, 1])
|
|
155
|
+
puts table(headers, rows)
|
|
145
156
|
|
|
146
157
|
return unless current_cluster
|
|
147
158
|
|
|
@@ -42,6 +42,42 @@ module LanguageOperator
|
|
|
42
42
|
end
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
# Format time elapsed since past time (opposite of time_until)
|
|
46
|
+
#
|
|
47
|
+
# @param past_time [Time] Past timestamp
|
|
48
|
+
# @return [String] Formatted string like "5m ago" or "2h 15m ago"
|
|
49
|
+
#
|
|
50
|
+
# @example
|
|
51
|
+
# ValueFormatter.time_ago(Time.now - 300) # => "5m ago"
|
|
52
|
+
def self.time_ago(past_time)
|
|
53
|
+
diff = Time.now - past_time
|
|
54
|
+
|
|
55
|
+
if diff < 0
|
|
56
|
+
'in the future' # Edge case for clock skew
|
|
57
|
+
elsif diff < SECONDS_PER_MINUTE
|
|
58
|
+
"#{diff.to_i}s ago"
|
|
59
|
+
elsif diff < SECONDS_PER_HOUR
|
|
60
|
+
minutes = (diff / SECONDS_PER_MINUTE).to_i
|
|
61
|
+
"#{minutes}m ago"
|
|
62
|
+
elsif diff < SECONDS_PER_DAY
|
|
63
|
+
hours = (diff / SECONDS_PER_HOUR).to_i
|
|
64
|
+
minutes = ((diff % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE).to_i
|
|
65
|
+
if minutes > 0
|
|
66
|
+
"#{hours}h #{minutes}m ago"
|
|
67
|
+
else
|
|
68
|
+
"#{hours}h ago"
|
|
69
|
+
end
|
|
70
|
+
else
|
|
71
|
+
days = (diff / SECONDS_PER_DAY).to_i
|
|
72
|
+
hours = ((diff % SECONDS_PER_DAY) / SECONDS_PER_HOUR).to_i
|
|
73
|
+
if hours > 0
|
|
74
|
+
"#{days}d #{hours}h ago"
|
|
75
|
+
else
|
|
76
|
+
"#{days}d ago"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
45
81
|
# Format a duration in seconds
|
|
46
82
|
#
|
|
47
83
|
# @param seconds [Numeric] Duration in seconds
|
|
@@ -107,6 +143,45 @@ module LanguageOperator
|
|
|
107
143
|
time.strftime('%Y-%m-%d %H:%M')
|
|
108
144
|
end
|
|
109
145
|
end
|
|
146
|
+
|
|
147
|
+
# Format Time object as HH:MM:SS for logs
|
|
148
|
+
#
|
|
149
|
+
# @param time [Time] The time to format
|
|
150
|
+
# @return [String] Formatted string like "14:30:25"
|
|
151
|
+
#
|
|
152
|
+
# @example
|
|
153
|
+
# ValueFormatter.log_time(Time.now) # => "14:30:25"
|
|
154
|
+
def self.log_time(time)
|
|
155
|
+
time.strftime('%H:%M:%S')
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Parse timestamp string and format as HH:MM:SS
|
|
159
|
+
#
|
|
160
|
+
# @param timestamp_str [String] ISO or text format timestamp
|
|
161
|
+
# @return [String] Formatted time or empty string on parse error
|
|
162
|
+
#
|
|
163
|
+
# @example
|
|
164
|
+
# ValueFormatter.parse_and_format_time("2025-01-15T14:30:25Z") # => "14:30:25"
|
|
165
|
+
def self.parse_and_format_time(timestamp_str)
|
|
166
|
+
return '' unless timestamp_str
|
|
167
|
+
|
|
168
|
+
time = Time.parse(timestamp_str)
|
|
169
|
+
log_time(time)
|
|
170
|
+
rescue StandardError
|
|
171
|
+
''
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Format time components as HH:MM for schedules
|
|
175
|
+
#
|
|
176
|
+
# @param hours [Integer] Hours (0-23)
|
|
177
|
+
# @param minutes [Integer] Minutes (0-59)
|
|
178
|
+
# @return [String] Formatted string like "14:30"
|
|
179
|
+
#
|
|
180
|
+
# @example
|
|
181
|
+
# ValueFormatter.schedule_time(14, 30) # => "14:30"
|
|
182
|
+
def self.schedule_time(hours, minutes)
|
|
183
|
+
format('%<hours>02d:%<minutes>02d', hours: hours, minutes: minutes)
|
|
184
|
+
end
|
|
110
185
|
end
|
|
111
186
|
end
|
|
112
187
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'shellwords'
|
|
3
4
|
require_relative 'cluster_validator'
|
|
4
5
|
|
|
5
6
|
module LanguageOperator
|
|
@@ -41,12 +42,13 @@ module LanguageOperator
|
|
|
41
42
|
end
|
|
42
43
|
|
|
43
44
|
# Build kubectl command args for this cluster context
|
|
45
|
+
# All user-controlled inputs are properly escaped to prevent command injection
|
|
44
46
|
# @return [Hash] kubectl arguments
|
|
45
47
|
def kubectl_args
|
|
46
48
|
{
|
|
47
|
-
kubeconfig: config[:kubeconfig] ? "--kubeconfig=#{config[:kubeconfig]}" : '',
|
|
48
|
-
context: config[:context] ? "--context=#{config[:context]}" : '',
|
|
49
|
-
namespace: "-n #{namespace}"
|
|
49
|
+
kubeconfig: config[:kubeconfig] ? "--kubeconfig=#{Shellwords.escape(config[:kubeconfig])}" : '',
|
|
50
|
+
context: config[:context] ? "--context=#{Shellwords.escape(config[:context])}" : '',
|
|
51
|
+
namespace: "-n #{Shellwords.escape(namespace)}"
|
|
50
52
|
}
|
|
51
53
|
end
|
|
52
54
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative '../formatters/progress_formatter'
|
|
4
4
|
require_relative '../../kubernetes/client'
|
|
5
|
+
require_relative '../../utils/secure_path'
|
|
5
6
|
|
|
6
7
|
module LanguageOperator
|
|
7
8
|
module CLI
|
|
@@ -59,7 +60,7 @@ module LanguageOperator
|
|
|
59
60
|
private
|
|
60
61
|
|
|
61
62
|
def default_kubeconfig_path
|
|
62
|
-
|
|
63
|
+
LanguageOperator::Utils::SecurePath.expand_home_path('.kube/config')
|
|
63
64
|
end
|
|
64
65
|
|
|
65
66
|
def kubeconfig_missing_message(path)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../constants/kubernetes_labels'
|
|
4
|
+
|
|
5
|
+
module LanguageOperator
|
|
6
|
+
module CLI
|
|
7
|
+
module Helpers
|
|
8
|
+
# Utilities for working with Kubernetes labels
|
|
9
|
+
module LabelUtils
|
|
10
|
+
# Normalize an agent name for use as a Kubernetes label value
|
|
11
|
+
#
|
|
12
|
+
# Kubernetes label values must follow DNS-1123 subdomain format:
|
|
13
|
+
# - contain only lowercase alphanumeric characters, '-' or '.'
|
|
14
|
+
# - start and end with an alphanumeric character
|
|
15
|
+
# - be at most 63 characters
|
|
16
|
+
#
|
|
17
|
+
# @param agent_name [String] The agent name to normalize
|
|
18
|
+
# @return [String] A valid label value
|
|
19
|
+
def self.normalize_agent_name(agent_name)
|
|
20
|
+
# Agent names should already be valid Kubernetes resource names,
|
|
21
|
+
# which are compatible with label values. Just ensure lowercase.
|
|
22
|
+
agent_name.to_s.downcase
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Build a label selector for finding agent pods
|
|
26
|
+
#
|
|
27
|
+
# @param agent_name [String] The agent name
|
|
28
|
+
# @return [String] A label selector string
|
|
29
|
+
def self.agent_pod_selector(agent_name)
|
|
30
|
+
normalized_name = normalize_agent_name(agent_name)
|
|
31
|
+
Constants::KubernetesLabels.agent_selector(normalized_name)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Validate that an agent name will work as a label value
|
|
35
|
+
#
|
|
36
|
+
# @param agent_name [String] The agent name to validate
|
|
37
|
+
# @return [Boolean] true if valid, false otherwise
|
|
38
|
+
def self.valid_label_value?(agent_name)
|
|
39
|
+
return false if agent_name.nil? || agent_name.empty?
|
|
40
|
+
|
|
41
|
+
# Check original string first (before normalization)
|
|
42
|
+
agent_str = agent_name.to_s
|
|
43
|
+
|
|
44
|
+
# Check DNS-1123 subdomain requirements:
|
|
45
|
+
# - 63 characters or less
|
|
46
|
+
# - lowercase letters, numbers, hyphens, and dots only
|
|
47
|
+
# - start and end with alphanumeric character
|
|
48
|
+
return false if agent_str.length > 63
|
|
49
|
+
return false unless agent_str.match?(/\A[a-z0-9]([a-z0-9\-.]*[a-z0-9])?\z/)
|
|
50
|
+
|
|
51
|
+
true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Get debugging information about label selector matching
|
|
55
|
+
#
|
|
56
|
+
# @param ctx [ClusterContext] Kubernetes context
|
|
57
|
+
# @param agent_name [String] Agent name being searched
|
|
58
|
+
# @return [Hash] Debug information about the search
|
|
59
|
+
def self.debug_pod_search(ctx, agent_name)
|
|
60
|
+
selector = agent_pod_selector(agent_name)
|
|
61
|
+
|
|
62
|
+
{
|
|
63
|
+
agent_name: agent_name,
|
|
64
|
+
normalized_name: normalize_agent_name(agent_name),
|
|
65
|
+
label_selector: selector,
|
|
66
|
+
namespace: ctx.namespace,
|
|
67
|
+
valid_label_value: valid_label_value?(agent_name)
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Convert deployment labels to pod selector and find matching pods
|
|
72
|
+
#
|
|
73
|
+
# This method handles the common pattern of extracting selector labels from a deployment,
|
|
74
|
+
# converting them to a label selector string, and finding matching pods.
|
|
75
|
+
#
|
|
76
|
+
# @param ctx [ClusterContext] Kubernetes context with client and namespace
|
|
77
|
+
# @param deployment_name [String] Name of the deployment (for error messages)
|
|
78
|
+
# @param labels [Hash, Object] Deployment selector labels (may be K8s::Resource or Hash)
|
|
79
|
+
# @return [Array] Array of pod resources matching the labels
|
|
80
|
+
# @raise [RuntimeError] If labels are nil, empty, or no pods found
|
|
81
|
+
def self.find_pods_by_deployment_labels(ctx, deployment_name, labels)
|
|
82
|
+
raise "Deployment '#{deployment_name}' has no selector labels" if labels.nil?
|
|
83
|
+
|
|
84
|
+
# Convert to hash if needed (K8s API may return K8s::Resource objects)
|
|
85
|
+
labels_hash = labels.respond_to?(:to_h) ? labels.to_h : labels
|
|
86
|
+
raise "Deployment '#{deployment_name}' has empty selector labels" if labels_hash.empty?
|
|
87
|
+
|
|
88
|
+
# Convert labels to Kubernetes label selector format
|
|
89
|
+
label_selector = labels_hash.map { |k, v| "#{k}=#{v}" }.join(',')
|
|
90
|
+
|
|
91
|
+
# Find matching pods
|
|
92
|
+
ctx.client.list_resources('Pod', namespace: ctx.namespace, label_selector: label_selector)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
data/lib/language_operator/{ux/concerns/provider_helpers.rb → cli/helpers/provider_helper.rb}
RENAMED
|
@@ -5,23 +5,21 @@ require 'json'
|
|
|
5
5
|
require 'uri'
|
|
6
6
|
|
|
7
7
|
module LanguageOperator
|
|
8
|
-
module
|
|
9
|
-
module
|
|
10
|
-
#
|
|
8
|
+
module CLI
|
|
9
|
+
module Helpers
|
|
10
|
+
# LLM provider operations for CLI wizards
|
|
11
11
|
#
|
|
12
|
-
# Provides helpers for testing provider connections
|
|
13
|
-
# and handling provider-specific configuration.
|
|
12
|
+
# Provides helpers for testing provider connections and fetching available models.
|
|
14
13
|
#
|
|
15
14
|
# @example
|
|
16
|
-
# class
|
|
17
|
-
# include
|
|
15
|
+
# class ModelWizard
|
|
16
|
+
# include Helpers::ProviderHelper
|
|
18
17
|
#
|
|
19
|
-
# def
|
|
20
|
-
#
|
|
21
|
-
# models = fetch_provider_models(:openai, api_key: 'sk-...')
|
|
18
|
+
# def test_anthropic_connection(api_key)
|
|
19
|
+
# test_provider_connection(:anthropic, api_key: api_key)
|
|
22
20
|
# end
|
|
23
21
|
# end
|
|
24
|
-
module
|
|
22
|
+
module ProviderHelper
|
|
25
23
|
# Test connection to an LLM provider
|
|
26
24
|
#
|
|
27
25
|
# @param provider [Symbol] Provider type (:anthropic, :openai, :openai_compatible)
|
|
@@ -65,27 +63,10 @@ module LanguageOperator
|
|
|
65
63
|
fetch_openai_compatible_models(endpoint, api_key)
|
|
66
64
|
end
|
|
67
65
|
rescue StandardError => e
|
|
68
|
-
|
|
66
|
+
Formatters::ProgressFormatter.warn("Could not fetch models: #{e.message}")
|
|
69
67
|
nil
|
|
70
68
|
end
|
|
71
69
|
|
|
72
|
-
# Get provider display information
|
|
73
|
-
#
|
|
74
|
-
# @param provider [Symbol] Provider type
|
|
75
|
-
# @return [Hash] Hash with :name, :docs_url keys
|
|
76
|
-
def provider_info(provider)
|
|
77
|
-
case provider
|
|
78
|
-
when :anthropic
|
|
79
|
-
{ name: 'Anthropic', docs_url: 'https://console.anthropic.com' }
|
|
80
|
-
when :openai
|
|
81
|
-
{ name: 'OpenAI', docs_url: 'https://platform.openai.com/api-keys' }
|
|
82
|
-
when :openai_compatible
|
|
83
|
-
{ name: 'OpenAI-Compatible', docs_url: nil }
|
|
84
|
-
else
|
|
85
|
-
{ name: provider.to_s.capitalize, docs_url: nil }
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
70
|
private
|
|
90
71
|
|
|
91
72
|
def test_anthropic(api_key)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../formatters/value_formatter'
|
|
4
|
+
|
|
3
5
|
module LanguageOperator
|
|
4
6
|
module CLI
|
|
5
7
|
module Helpers
|
|
@@ -45,6 +47,8 @@ module LanguageOperator
|
|
|
45
47
|
|
|
46
48
|
# Build a cron expression for interval-based execution
|
|
47
49
|
def interval_cron(interval, unit)
|
|
50
|
+
validate_interval(interval, unit)
|
|
51
|
+
|
|
48
52
|
case unit.downcase
|
|
49
53
|
when 'minutes', 'minute'
|
|
50
54
|
"*/#{interval} * * * *"
|
|
@@ -99,7 +103,23 @@ module LanguageOperator
|
|
|
99
103
|
end
|
|
100
104
|
|
|
101
105
|
def format_time(hours, minutes)
|
|
102
|
-
|
|
106
|
+
Formatters::ValueFormatter.schedule_time(hours, minutes)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def validate_interval(interval, unit)
|
|
110
|
+
raise ArgumentError, "Interval must be a positive integer, got: #{interval}" unless interval.is_a?(Integer) && interval.positive?
|
|
111
|
+
|
|
112
|
+
case unit.downcase
|
|
113
|
+
when 'minutes', 'minute'
|
|
114
|
+
raise ArgumentError, "Minutes interval must be between 1-59, got: #{interval}" if interval >= 60
|
|
115
|
+
when 'hours', 'hour'
|
|
116
|
+
raise ArgumentError, "Hours interval must be between 1-23, got: #{interval}" if interval >= 24
|
|
117
|
+
when 'days', 'day'
|
|
118
|
+
raise ArgumentError, "Days interval must be between 1-31, got: #{interval}" if interval >= 32
|
|
119
|
+
else
|
|
120
|
+
# Will be caught by the existing case statement
|
|
121
|
+
nil
|
|
122
|
+
end
|
|
103
123
|
end
|
|
104
124
|
end
|
|
105
125
|
end
|
|
@@ -49,18 +49,26 @@ module LanguageOperator
|
|
|
49
49
|
# @param options [Array<String>] Available options
|
|
50
50
|
# @return [String] Selected option
|
|
51
51
|
def self.select(prompt, options)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
loop do
|
|
53
|
+
puts prompt
|
|
54
|
+
options.each_with_index do |option, index|
|
|
55
|
+
puts " #{index + 1}. #{option}"
|
|
56
|
+
end
|
|
57
|
+
print "\nSelect (1-#{options.length}): "
|
|
58
|
+
|
|
59
|
+
input = $stdin.gets&.chomp || ''
|
|
60
|
+
|
|
61
|
+
# Allow user to quit/cancel
|
|
62
|
+
if input.downcase.match?(/^q(uit)?$/)
|
|
63
|
+
puts 'Selection cancelled'
|
|
64
|
+
exit 0
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
selection = input.to_i
|
|
68
|
+
return options[selection - 1] if selection.between?(1, options.length)
|
|
57
69
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
options[selection - 1]
|
|
61
|
-
else
|
|
62
|
-
puts 'Invalid selection'
|
|
63
|
-
exit 1
|
|
70
|
+
puts 'Invalid selection. Please try again.'
|
|
71
|
+
puts
|
|
64
72
|
end
|
|
65
73
|
end
|
|
66
74
|
end
|