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
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'English'
|
|
5
|
+
require 'shellwords'
|
|
6
|
+
require 'tempfile'
|
|
7
|
+
|
|
8
|
+
module LanguageOperator
|
|
9
|
+
module CLI
|
|
10
|
+
module Commands
|
|
11
|
+
module Model
|
|
12
|
+
module Test
|
|
13
|
+
def self.included(base)
|
|
14
|
+
base.class_eval do
|
|
15
|
+
desc 'test NAME', 'Test model connectivity and functionality'
|
|
16
|
+
long_desc <<-DESC
|
|
17
|
+
Test that a model is operational by:
|
|
18
|
+
1. Verifying the pod is running
|
|
19
|
+
2. Testing the chat completion endpoint with a simple message
|
|
20
|
+
|
|
21
|
+
This command helps diagnose model deployment issues.
|
|
22
|
+
DESC
|
|
23
|
+
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
24
|
+
option :timeout, type: :numeric, default: 30, desc: 'Timeout in seconds for endpoint test'
|
|
25
|
+
def test(name)
|
|
26
|
+
handle_command_error('test model') do
|
|
27
|
+
# 1. Get model resource
|
|
28
|
+
model = get_resource_or_exit(RESOURCE_MODEL, name)
|
|
29
|
+
model_name = model.dig('spec', 'modelName')
|
|
30
|
+
|
|
31
|
+
# 2. Check deployment status
|
|
32
|
+
deployment = check_deployment_status(name)
|
|
33
|
+
|
|
34
|
+
# 3. Check pod status
|
|
35
|
+
pod = check_pod_status(name, deployment)
|
|
36
|
+
|
|
37
|
+
# 4. Test chat completion endpoint
|
|
38
|
+
test_chat_completion(name, model_name, pod, options[:timeout])
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def check_deployment_status(name)
|
|
45
|
+
Formatters::ProgressFormatter.with_spinner('Verifying deployment') do
|
|
46
|
+
deployment = ctx.client.get_resource('Deployment', name, ctx.namespace)
|
|
47
|
+
replicas = deployment.dig('spec', 'replicas') || 1
|
|
48
|
+
ready_replicas = deployment.dig('status', 'readyReplicas') || 0
|
|
49
|
+
|
|
50
|
+
unless ready_replicas >= replicas
|
|
51
|
+
raise "Deployment not ready (#{ready_replicas}/#{replicas}). " \
|
|
52
|
+
"Run 'kubectl get deployment #{name} -n #{ctx.namespace}' for details."
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
deployment
|
|
56
|
+
end
|
|
57
|
+
rescue K8s::Error::NotFound
|
|
58
|
+
Formatters::ProgressFormatter.error("Deployment '#{name}' not found")
|
|
59
|
+
exit 1
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def check_pod_status(name, deployment)
|
|
63
|
+
Formatters::ProgressFormatter.with_spinner('Verifying pod') do
|
|
64
|
+
labels = deployment.dig('spec', 'selector', 'matchLabels')
|
|
65
|
+
|
|
66
|
+
# Find matching pods using centralized utility
|
|
67
|
+
pods = CLI::Helpers::LabelUtils.find_pods_by_deployment_labels(ctx, name, labels)
|
|
68
|
+
raise "No pods found for model '#{name}'" if pods.empty?
|
|
69
|
+
|
|
70
|
+
# Find a running pod
|
|
71
|
+
running_pod = pods.find do |pod|
|
|
72
|
+
pod.dig('status', 'phase') == 'Running' &&
|
|
73
|
+
pod.dig('status', 'conditions')&.any? { |c| c['type'] == 'Ready' && c['status'] == 'True' }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
if running_pod.nil?
|
|
77
|
+
pod_phases = pods.map { |p| p.dig('status', 'phase') }.join(', ')
|
|
78
|
+
# Get label selector for error message
|
|
79
|
+
labels_hash = labels.respond_to?(:to_h) ? labels.to_h : labels
|
|
80
|
+
label_selector = labels_hash.map { |k, v| "#{k}=#{v}" }.join(',')
|
|
81
|
+
raise "No running pods found. Pod phases: #{pod_phases}. " \
|
|
82
|
+
"Run 'kubectl get pods -l #{label_selector} -n #{ctx.namespace}' for details."
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
running_pod
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def test_chat_completion(_name, model_name, pod, timeout)
|
|
90
|
+
Formatters::ProgressFormatter.with_spinner('Verifying chat completion requests') do
|
|
91
|
+
pod_name = pod.dig('metadata', 'name')
|
|
92
|
+
|
|
93
|
+
# Build the JSON payload
|
|
94
|
+
payload = JSON.generate({
|
|
95
|
+
model: model_name,
|
|
96
|
+
messages: [{ role: 'user', content: 'hello' }],
|
|
97
|
+
max_tokens: 10
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
# Create temporary file to avoid command injection
|
|
101
|
+
temp_file = nil
|
|
102
|
+
begin
|
|
103
|
+
temp_file = Tempfile.new(['model_test_payload', '.json'])
|
|
104
|
+
temp_file.write(payload)
|
|
105
|
+
temp_file.close
|
|
106
|
+
|
|
107
|
+
# Build secure curl command using temp file
|
|
108
|
+
# This eliminates shell injection vulnerability
|
|
109
|
+
curl_command = 'curl -s -X POST http://localhost:4000/v1/chat/completions ' \
|
|
110
|
+
"-H 'Content-Type: application/json' -d @#{temp_file.path} --max-time #{timeout.to_i}"
|
|
111
|
+
|
|
112
|
+
# Execute the curl command inside the pod
|
|
113
|
+
result = execute_in_pod(pod_name, curl_command)
|
|
114
|
+
ensure
|
|
115
|
+
# Clean up temporary file
|
|
116
|
+
temp_file&.unlink
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Parse the response
|
|
120
|
+
response = JSON.parse(result)
|
|
121
|
+
|
|
122
|
+
if response['error']
|
|
123
|
+
error_msg = response['error']['message'] || response['error']
|
|
124
|
+
raise error_msg
|
|
125
|
+
elsif !response['choices']
|
|
126
|
+
raise "Unexpected response format: #{result.lines.first.strip}"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
response
|
|
130
|
+
rescue JSON::ParserError => e
|
|
131
|
+
raise "Failed to parse response: #{e.message}"
|
|
132
|
+
end
|
|
133
|
+
rescue StandardError => e
|
|
134
|
+
# Display error in bold red
|
|
135
|
+
puts
|
|
136
|
+
puts Formatters::ProgressFormatter.pastel.bold.red(e.message)
|
|
137
|
+
exit 1
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def execute_in_pod(pod_name, command)
|
|
141
|
+
# Use kubectl exec to run command in pod
|
|
142
|
+
# command can be a string or array
|
|
143
|
+
kubectl_command = if command.is_a?(String)
|
|
144
|
+
"kubectl exec -n #{ctx.namespace} #{pod_name} -- sh -c #{Shellwords.escape(command)}"
|
|
145
|
+
else
|
|
146
|
+
Shellwords.join(['kubectl', 'exec', '-n', ctx.namespace, pod_name, '--'] + command)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
output = `#{kubectl_command} 2>&1`
|
|
150
|
+
exit_code = $CHILD_STATUS.exitstatus
|
|
151
|
+
|
|
152
|
+
if exit_code != 0
|
|
153
|
+
Formatters::ProgressFormatter.error("Failed to execute command in pod: #{output}")
|
|
154
|
+
exit 1
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
output
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -1,31 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'yaml'
|
|
4
|
-
require_relative '../
|
|
5
|
-
require_relative '../formatters/progress_formatter'
|
|
6
|
-
require_relative '../formatters/table_formatter'
|
|
7
|
-
require_relative '../helpers/cluster_validator'
|
|
8
|
-
require_relative '../helpers/user_prompts'
|
|
9
|
-
require_relative '../helpers/resource_dependency_checker'
|
|
10
|
-
require_relative '../helpers/editor_helper'
|
|
11
|
-
require_relative '../helpers/pastel_helper'
|
|
12
|
-
require_relative '../../config/cluster_config'
|
|
13
|
-
require_relative '../../kubernetes/client'
|
|
14
|
-
require_relative '../../kubernetes/resource_builder'
|
|
4
|
+
require_relative '../command_loader'
|
|
15
5
|
|
|
16
6
|
module LanguageOperator
|
|
17
7
|
module CLI
|
|
18
8
|
module Commands
|
|
19
9
|
# Persona management commands
|
|
20
10
|
class Persona < BaseCommand
|
|
11
|
+
include Constants
|
|
21
12
|
include Helpers::ClusterValidator
|
|
22
|
-
include Helpers::
|
|
13
|
+
include Helpers::UxHelper
|
|
23
14
|
|
|
24
15
|
desc 'list', 'List all personas in current cluster'
|
|
25
16
|
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
26
17
|
def list
|
|
27
18
|
handle_command_error('list personas') do
|
|
28
|
-
personas = list_resources_or_empty(
|
|
19
|
+
personas = list_resources_or_empty(RESOURCE_PERSONA) do
|
|
29
20
|
puts
|
|
30
21
|
puts 'Personas define the personality and capabilities of agents.'
|
|
31
22
|
puts
|
|
@@ -36,7 +27,7 @@ module LanguageOperator
|
|
|
36
27
|
return if personas.empty?
|
|
37
28
|
|
|
38
29
|
# Get agents to count usage
|
|
39
|
-
agents = ctx.client.list_resources(
|
|
30
|
+
agents = ctx.client.list_resources(RESOURCE_AGENT, namespace: ctx.namespace)
|
|
40
31
|
|
|
41
32
|
table_data = personas.map do |persona|
|
|
42
33
|
name = persona.dig('metadata', 'name')
|
|
@@ -58,7 +49,7 @@ module LanguageOperator
|
|
|
58
49
|
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
59
50
|
def show(name)
|
|
60
51
|
handle_command_error('show persona') do
|
|
61
|
-
persona = get_resource_or_exit(
|
|
52
|
+
persona = get_resource_or_exit(RESOURCE_PERSONA, name)
|
|
62
53
|
|
|
63
54
|
puts
|
|
64
55
|
puts "Persona: #{pastel.cyan.bold(name)}"
|
|
@@ -123,7 +114,7 @@ module LanguageOperator
|
|
|
123
114
|
handle_command_error('create persona') do
|
|
124
115
|
# Check if persona already exists
|
|
125
116
|
begin
|
|
126
|
-
ctx.client.get_resource(
|
|
117
|
+
ctx.client.get_resource(RESOURCE_PERSONA, name, ctx.namespace)
|
|
127
118
|
Formatters::ProgressFormatter.error("Persona '#{name}' already exists in cluster '#{ctx.name}'")
|
|
128
119
|
puts
|
|
129
120
|
puts 'Use a different name or delete the existing persona first:'
|
|
@@ -136,7 +127,7 @@ module LanguageOperator
|
|
|
136
127
|
# If --from flag provided, copy from existing persona
|
|
137
128
|
base_persona = nil
|
|
138
129
|
if options[:from]
|
|
139
|
-
base_persona = get_resource_or_exit(
|
|
130
|
+
base_persona = get_resource_or_exit(RESOURCE_PERSONA, options[:from],
|
|
140
131
|
error_message: "Source persona '#{options[:from]}' not found")
|
|
141
132
|
Formatters::ProgressFormatter.info("Copying from persona '#{options[:from]}'")
|
|
142
133
|
puts
|
|
@@ -144,7 +135,7 @@ module LanguageOperator
|
|
|
144
135
|
|
|
145
136
|
# Interactive prompts
|
|
146
137
|
require 'tty-prompt'
|
|
147
|
-
prompt
|
|
138
|
+
# prompt available via UxHelper
|
|
148
139
|
|
|
149
140
|
puts
|
|
150
141
|
puts '=' * 80
|
|
@@ -245,7 +236,7 @@ module LanguageOperator
|
|
|
245
236
|
def edit(name)
|
|
246
237
|
handle_command_error('edit persona') do
|
|
247
238
|
# Get current persona
|
|
248
|
-
persona = get_resource_or_exit(
|
|
239
|
+
persona = get_resource_or_exit(RESOURCE_PERSONA, name)
|
|
249
240
|
|
|
250
241
|
# Open in editor
|
|
251
242
|
original_yaml = YAML.dump(persona)
|
|
@@ -279,7 +270,7 @@ module LanguageOperator
|
|
|
279
270
|
Formatters::ProgressFormatter.success("Persona '#{name}' updated successfully")
|
|
280
271
|
|
|
281
272
|
# Check for agents using this persona
|
|
282
|
-
agents = ctx.client.list_resources(
|
|
273
|
+
agents = ctx.client.list_resources(RESOURCE_AGENT, namespace: ctx.namespace)
|
|
283
274
|
agents_using = Helpers::ResourceDependencyChecker.agents_using_persona(agents, name)
|
|
284
275
|
|
|
285
276
|
if agents_using.any?
|
|
@@ -298,26 +289,17 @@ module LanguageOperator
|
|
|
298
289
|
def delete(name)
|
|
299
290
|
handle_command_error('delete persona') do
|
|
300
291
|
# Get persona
|
|
301
|
-
|
|
292
|
+
get_resource_or_exit(RESOURCE_PERSONA, name)
|
|
302
293
|
|
|
303
294
|
# Check dependencies and get confirmation
|
|
304
295
|
return unless check_dependencies_and_confirm('persona', name, force: options[:force])
|
|
305
296
|
|
|
306
297
|
# Confirm deletion unless --force
|
|
307
|
-
|
|
308
|
-
'persona', name, ctx.name,
|
|
309
|
-
details: {
|
|
310
|
-
'Tone' => persona.dig('spec', 'tone'),
|
|
311
|
-
'Description' => persona.dig('spec', 'description')
|
|
312
|
-
},
|
|
313
|
-
force: options[:force]
|
|
314
|
-
)
|
|
315
|
-
# Delete persona
|
|
316
|
-
Formatters::ProgressFormatter.with_spinner("Deleting persona '#{name}'") do
|
|
317
|
-
ctx.client.delete_resource('LanguagePersona', name, ctx.namespace)
|
|
318
|
-
end
|
|
298
|
+
return unless confirm_deletion_with_force('persona', name, ctx.name, force: options[:force])
|
|
319
299
|
|
|
320
|
-
|
|
300
|
+
# Delete persona
|
|
301
|
+
Formatters::ProgressFormatter.with_spinner("Deleting persona '#{name}'") do
|
|
302
|
+
ctx.client.delete_resource(RESOURCE_PERSONA, name, ctx.namespace)
|
|
321
303
|
end
|
|
322
304
|
end
|
|
323
305
|
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative '../base_command'
|
|
4
4
|
require_relative '../formatters/progress_formatter'
|
|
5
|
-
require_relative '
|
|
5
|
+
require_relative '../wizards/quickstart_wizard'
|
|
6
6
|
|
|
7
7
|
module LanguageOperator
|
|
8
8
|
module CLI
|
|
@@ -11,7 +11,8 @@ module LanguageOperator
|
|
|
11
11
|
class Quickstart < BaseCommand
|
|
12
12
|
desc 'start', 'Interactive setup wizard for first-time users'
|
|
13
13
|
def start
|
|
14
|
-
|
|
14
|
+
wizard = Wizards::QuickstartWizard.new
|
|
15
|
+
wizard.run
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
default_task :start
|
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '../
|
|
4
|
-
require_relative '../formatters/progress_formatter'
|
|
5
|
-
require_relative '../formatters/table_formatter'
|
|
6
|
-
require_relative '../formatters/status_formatter'
|
|
7
|
-
require_relative '../helpers/pastel_helper'
|
|
8
|
-
require_relative '../../config/cluster_config'
|
|
9
|
-
require_relative '../../kubernetes/client'
|
|
10
|
-
require_relative '../helpers/cluster_validator'
|
|
3
|
+
require_relative '../command_loader'
|
|
11
4
|
|
|
12
5
|
module LanguageOperator
|
|
13
6
|
module CLI
|
|
14
7
|
module Commands
|
|
15
8
|
# System status and overview command
|
|
16
9
|
class Status < BaseCommand
|
|
17
|
-
include
|
|
10
|
+
include Constants
|
|
11
|
+
include Helpers::UxHelper
|
|
18
12
|
|
|
19
13
|
desc 'overview', 'Show system status and overview'
|
|
20
14
|
def overview
|
|
@@ -26,21 +20,48 @@ module LanguageOperator
|
|
|
26
20
|
if current_cluster
|
|
27
21
|
cluster_config = Config::ClusterConfig.get_cluster(current_cluster)
|
|
28
22
|
|
|
29
|
-
puts "\nCluster Details"
|
|
30
|
-
puts '----------------'
|
|
31
|
-
puts "Name: #{pastel.bold.white(current_cluster)}"
|
|
32
|
-
puts "Namespace: #{pastel.bold.white(cluster_config[:namespace])}"
|
|
33
|
-
puts
|
|
34
|
-
|
|
35
23
|
# Check cluster health
|
|
36
24
|
k8s = Helpers::ClusterValidator.kubernetes_client(current_cluster)
|
|
37
25
|
|
|
38
|
-
#
|
|
26
|
+
# Try to get actual cluster resource from Kubernetes
|
|
27
|
+
cluster_resource = nil
|
|
39
28
|
if k8s.operator_installed?
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
begin
|
|
30
|
+
cluster_resource = k8s.get_resource('LanguageCluster', current_cluster, cluster_config[:namespace])
|
|
31
|
+
rescue StandardError
|
|
32
|
+
# Cluster resource might not exist yet
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Format cluster info using UxHelper
|
|
37
|
+
logo(title: 'cluster status')
|
|
38
|
+
|
|
39
|
+
if cluster_resource
|
|
40
|
+
# Use actual cluster resource data
|
|
41
|
+
status = cluster_resource.dig('status', 'phase') || 'Unknown'
|
|
42
|
+
domain = cluster_resource.dig('spec', 'domain')
|
|
43
|
+
created = cluster_resource.dig('metadata', 'creationTimestamp')
|
|
44
|
+
|
|
45
|
+
format_cluster_details(
|
|
46
|
+
name: current_cluster,
|
|
47
|
+
namespace: cluster_config[:namespace],
|
|
48
|
+
context: cluster_config[:context],
|
|
49
|
+
status: status,
|
|
50
|
+
domain: domain,
|
|
51
|
+
created: created
|
|
52
|
+
)
|
|
42
53
|
else
|
|
43
|
-
|
|
54
|
+
# Fallback to local config and operator status
|
|
55
|
+
format_cluster_details(
|
|
56
|
+
name: current_cluster,
|
|
57
|
+
namespace: cluster_config[:namespace],
|
|
58
|
+
context: cluster_config[:context],
|
|
59
|
+
status: k8s.operator_installed? ? (k8s.operator_version || 'installed') : 'operator not installed'
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Early exit if operator not installed
|
|
64
|
+
unless k8s.operator_installed?
|
|
44
65
|
puts
|
|
45
66
|
puts 'Install the operator with:'
|
|
46
67
|
puts ' aictl install'
|
|
@@ -48,54 +69,6 @@ module LanguageOperator
|
|
|
48
69
|
return
|
|
49
70
|
end
|
|
50
71
|
|
|
51
|
-
# Agent statistics
|
|
52
|
-
agents = k8s.list_resources('LanguageAgent', namespace: cluster_config[:namespace])
|
|
53
|
-
agent_stats = categorize_by_status(agents)
|
|
54
|
-
|
|
55
|
-
puts
|
|
56
|
-
puts "Agents (#{agents.count} total):"
|
|
57
|
-
agent_stats.each do |status, count|
|
|
58
|
-
puts " #{format_status(status)}: #{count}"
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# Tool statistics
|
|
62
|
-
tools = k8s.list_resources('LanguageTool', namespace: cluster_config[:namespace])
|
|
63
|
-
puts
|
|
64
|
-
puts "Tools (#{tools.count} total):"
|
|
65
|
-
if tools.any?
|
|
66
|
-
tool_types = tools.group_by { |t| t.dig('spec', 'type') }
|
|
67
|
-
tool_types.each do |type, items|
|
|
68
|
-
puts " #{type}: #{items.count}"
|
|
69
|
-
end
|
|
70
|
-
else
|
|
71
|
-
puts ' (none)'
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# Model statistics
|
|
75
|
-
models = k8s.list_resources('LanguageModel', namespace: cluster_config[:namespace])
|
|
76
|
-
puts
|
|
77
|
-
puts "Models (#{models.count} total):"
|
|
78
|
-
if models.any?
|
|
79
|
-
model_providers = models.group_by { |m| m.dig('spec', 'provider') }
|
|
80
|
-
model_providers.each do |provider, items|
|
|
81
|
-
puts " #{provider}: #{items.count}"
|
|
82
|
-
end
|
|
83
|
-
else
|
|
84
|
-
puts ' (none)'
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# Persona statistics
|
|
88
|
-
personas = k8s.list_resources('LanguagePersona', namespace: cluster_config[:namespace])
|
|
89
|
-
puts
|
|
90
|
-
puts "Personas (#{personas.count} total):"
|
|
91
|
-
if personas.any?
|
|
92
|
-
personas.each do |persona|
|
|
93
|
-
tone = persona.dig('spec', 'tone')
|
|
94
|
-
puts " - #{persona.dig('metadata', 'name')} (#{tone})"
|
|
95
|
-
end
|
|
96
|
-
else
|
|
97
|
-
puts ' (none)'
|
|
98
|
-
end
|
|
99
72
|
else
|
|
100
73
|
Formatters::ProgressFormatter.warn('No cluster selected')
|
|
101
74
|
puts
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
require_relative '../../base_command'
|
|
6
|
+
require_relative '../../formatters/progress_formatter'
|
|
7
|
+
require_relative '../../../dsl/schema'
|
|
8
|
+
|
|
9
|
+
# Include all system subcommand modules
|
|
10
|
+
require_relative 'schema'
|
|
11
|
+
require_relative 'validate_template'
|
|
12
|
+
require_relative 'synthesize'
|
|
13
|
+
require_relative 'exec'
|
|
14
|
+
require_relative 'synthesis_template'
|
|
15
|
+
|
|
16
|
+
# Include helper modules
|
|
17
|
+
require_relative 'helpers/template_loader'
|
|
18
|
+
require_relative 'helpers/template_validator'
|
|
19
|
+
require_relative 'helpers/llm_synthesis'
|
|
20
|
+
require_relative 'helpers/pod_manager'
|
|
21
|
+
|
|
22
|
+
module LanguageOperator
|
|
23
|
+
module CLI
|
|
24
|
+
module Commands
|
|
25
|
+
module System
|
|
26
|
+
# Base system command class
|
|
27
|
+
class Base < BaseCommand
|
|
28
|
+
# Include all helper modules
|
|
29
|
+
include Helpers::TemplateLoader
|
|
30
|
+
include Helpers::TemplateValidator
|
|
31
|
+
include Helpers::LlmSynthesis
|
|
32
|
+
include Helpers::PodManager
|
|
33
|
+
|
|
34
|
+
# Include all subcommand modules
|
|
35
|
+
include Schema
|
|
36
|
+
include ValidateTemplate
|
|
37
|
+
include Synthesize
|
|
38
|
+
include Exec
|
|
39
|
+
include SynthesisTemplate
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LanguageOperator
|
|
4
|
+
module CLI
|
|
5
|
+
module Commands
|
|
6
|
+
module System
|
|
7
|
+
# Agent execution in test pod
|
|
8
|
+
module Exec
|
|
9
|
+
def self.included(base)
|
|
10
|
+
base.class_eval do
|
|
11
|
+
desc 'exec [AGENT_FILE]', 'Execute an agent file in a test pod on the cluster'
|
|
12
|
+
long_desc <<-DESC
|
|
13
|
+
Deploy and execute an agent file in a temporary test pod on the Kubernetes cluster.
|
|
14
|
+
|
|
15
|
+
This command creates a ConfigMap with the agent code, deploys a test pod,
|
|
16
|
+
streams the logs until completion, and cleans up all resources.
|
|
17
|
+
|
|
18
|
+
The agent code is mounted at /etc/agent/code/agent.rb as expected by the agent runtime.
|
|
19
|
+
|
|
20
|
+
Agent code can be provided either as a file path or via STDIN.
|
|
21
|
+
If no file path is provided, the command will read from STDIN.
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
# Execute a synthesized agent file
|
|
25
|
+
aictl system exec agent.rb
|
|
26
|
+
|
|
27
|
+
# Execute with a custom agent name
|
|
28
|
+
aictl system exec agent.rb --agent-name my-test
|
|
29
|
+
|
|
30
|
+
# Keep the pod after execution for debugging
|
|
31
|
+
aictl system exec agent.rb --keep-pod
|
|
32
|
+
|
|
33
|
+
# Use a different agent image
|
|
34
|
+
aictl system exec agent.rb --image ghcr.io/language-operator/agent:v0.1.0
|
|
35
|
+
|
|
36
|
+
# Read agent code from STDIN
|
|
37
|
+
cat agent.rb | aictl system exec
|
|
38
|
+
|
|
39
|
+
# Pipe synthesized code directly to execution
|
|
40
|
+
cat agent.txt | aictl system synthesize | aictl system exec
|
|
41
|
+
DESC
|
|
42
|
+
option :agent_name, type: :string, default: 'test-agent', desc: 'Name for the test agent pod'
|
|
43
|
+
option :keep_pod, type: :boolean, default: false, desc: 'Keep the pod after execution (for debugging)'
|
|
44
|
+
option :image, type: :string, default: 'ghcr.io/language-operator/agent:latest', desc: 'Agent container image'
|
|
45
|
+
option :timeout, type: :numeric, default: 300, desc: 'Timeout in seconds for agent execution'
|
|
46
|
+
def exec(agent_file = nil)
|
|
47
|
+
handle_command_error('exec agent') do
|
|
48
|
+
# Verify cluster is selected
|
|
49
|
+
unless ctx.client
|
|
50
|
+
Formatters::ProgressFormatter.error('No cluster context available')
|
|
51
|
+
puts
|
|
52
|
+
puts 'Please configure kubectl with a valid cluster context:'
|
|
53
|
+
puts ' kubectl config get-contexts'
|
|
54
|
+
puts ' kubectl config use-context <context-name>'
|
|
55
|
+
exit 1
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Read agent code from file or STDIN
|
|
59
|
+
agent_code = if agent_file && !agent_file.strip.empty?
|
|
60
|
+
# Read from file
|
|
61
|
+
unless File.exist?(agent_file)
|
|
62
|
+
Formatters::ProgressFormatter.error("Agent file not found: #{agent_file}")
|
|
63
|
+
exit 1
|
|
64
|
+
end
|
|
65
|
+
File.read(agent_file)
|
|
66
|
+
elsif $stdin.tty?
|
|
67
|
+
# Read from STDIN
|
|
68
|
+
Formatters::ProgressFormatter.error('No agent code provided')
|
|
69
|
+
puts
|
|
70
|
+
puts 'Provide agent code either as a file or via STDIN:'
|
|
71
|
+
puts ' aictl system exec agent.rb'
|
|
72
|
+
puts ' cat agent.rb | aictl system exec'
|
|
73
|
+
exit 1
|
|
74
|
+
else
|
|
75
|
+
code = $stdin.read.strip
|
|
76
|
+
if code.empty?
|
|
77
|
+
Formatters::ProgressFormatter.error('No agent code provided')
|
|
78
|
+
puts
|
|
79
|
+
puts 'Provide agent code either as a file or via STDIN:'
|
|
80
|
+
puts ' aictl system exec agent.rb'
|
|
81
|
+
puts ' cat agent.rb | aictl system exec'
|
|
82
|
+
exit 1
|
|
83
|
+
end
|
|
84
|
+
code
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Generate unique names
|
|
88
|
+
timestamp = Time.now.to_i
|
|
89
|
+
configmap_name = "#{options[:agent_name]}-code-#{timestamp}"
|
|
90
|
+
pod_name = "#{options[:agent_name]}-#{timestamp}"
|
|
91
|
+
|
|
92
|
+
begin
|
|
93
|
+
# Create ConfigMap with agent code
|
|
94
|
+
Formatters::ProgressFormatter.with_spinner('Creating ConfigMap with agent code') do
|
|
95
|
+
create_agent_configmap(configmap_name, agent_code)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Create test pod
|
|
99
|
+
Formatters::ProgressFormatter.with_spinner('Creating test pod') do
|
|
100
|
+
create_test_pod(pod_name, configmap_name, options[:image])
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Wait for pod to be ready or running
|
|
104
|
+
Formatters::ProgressFormatter.with_spinner('Waiting for pod to start') do
|
|
105
|
+
wait_for_pod_start(pod_name, timeout: 60)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Stream logs until pod completes
|
|
109
|
+
stream_pod_logs(pod_name, timeout: options[:timeout])
|
|
110
|
+
|
|
111
|
+
# Wait for pod to fully terminate and get final status
|
|
112
|
+
exit_code = wait_for_pod_termination(pod_name)
|
|
113
|
+
|
|
114
|
+
if exit_code&.zero?
|
|
115
|
+
Formatters::ProgressFormatter.success('Agent completed successfully')
|
|
116
|
+
elsif exit_code
|
|
117
|
+
Formatters::ProgressFormatter.error("Agent failed with exit code: #{exit_code}")
|
|
118
|
+
else
|
|
119
|
+
Formatters::ProgressFormatter.warn('Unable to determine pod exit status')
|
|
120
|
+
end
|
|
121
|
+
ensure
|
|
122
|
+
# Clean up resources unless --keep-pod
|
|
123
|
+
puts
|
|
124
|
+
puts
|
|
125
|
+
if options[:keep_pod]
|
|
126
|
+
Formatters::ProgressFormatter.info('Resources kept for debugging:')
|
|
127
|
+
puts " Pod: #{pod_name}"
|
|
128
|
+
puts " ConfigMap: #{configmap_name}"
|
|
129
|
+
puts
|
|
130
|
+
puts "To view logs: kubectl logs -n #{ctx.namespace} #{pod_name}"
|
|
131
|
+
puts "To delete: kubectl delete pod,configmap -n #{ctx.namespace} #{pod_name} #{configmap_name}"
|
|
132
|
+
else
|
|
133
|
+
Formatters::ProgressFormatter.with_spinner('Cleaning up resources') do
|
|
134
|
+
delete_pod(pod_name)
|
|
135
|
+
delete_configmap(configmap_name)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|