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,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LanguageOperator
|
|
4
|
+
module CLI
|
|
5
|
+
module Commands
|
|
6
|
+
module System
|
|
7
|
+
module Helpers
|
|
8
|
+
# LLM synthesis utilities
|
|
9
|
+
module LlmSynthesis
|
|
10
|
+
# Call LLM to generate code from synthesis prompt using cluster model
|
|
11
|
+
def call_llm_for_synthesis(prompt, model_name)
|
|
12
|
+
require 'json'
|
|
13
|
+
require 'faraday'
|
|
14
|
+
|
|
15
|
+
# Get model resource
|
|
16
|
+
model = get_resource_or_exit('LanguageModel', model_name)
|
|
17
|
+
model_id = model.dig('spec', 'modelName')
|
|
18
|
+
|
|
19
|
+
# Get the model's pod
|
|
20
|
+
pod = get_model_pod(model_name)
|
|
21
|
+
pod_name = pod.dig('metadata', 'name')
|
|
22
|
+
|
|
23
|
+
# Set up port-forward to access the model pod
|
|
24
|
+
port_forward_pid = nil
|
|
25
|
+
local_port = find_available_port
|
|
26
|
+
|
|
27
|
+
begin
|
|
28
|
+
# Start kubectl port-forward in background
|
|
29
|
+
port_forward_pid = start_port_forward(pod_name, local_port, 4000)
|
|
30
|
+
|
|
31
|
+
# Wait for port-forward to be ready
|
|
32
|
+
wait_for_port(local_port)
|
|
33
|
+
|
|
34
|
+
# Build the JSON payload for the chat completion request
|
|
35
|
+
payload = {
|
|
36
|
+
model: model_id,
|
|
37
|
+
messages: [{ role: 'user', content: prompt }],
|
|
38
|
+
max_tokens: 4000,
|
|
39
|
+
temperature: 0.3
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Make HTTP request using Faraday
|
|
43
|
+
conn = Faraday.new(url: "http://localhost:#{local_port}") do |f|
|
|
44
|
+
f.request :json
|
|
45
|
+
f.response :json
|
|
46
|
+
f.adapter Faraday.default_adapter
|
|
47
|
+
f.options.timeout = 120
|
|
48
|
+
f.options.open_timeout = 10
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
response = conn.post('/v1/chat/completions', payload)
|
|
52
|
+
|
|
53
|
+
# Parse response
|
|
54
|
+
result = response.body
|
|
55
|
+
|
|
56
|
+
if result['error']
|
|
57
|
+
error_msg = result['error']['message'] || result['error']
|
|
58
|
+
raise "Model error: #{error_msg}"
|
|
59
|
+
elsif !result['choices'] || result['choices'].empty?
|
|
60
|
+
raise "Unexpected response format: #{result.inspect}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Extract the content from the first choice
|
|
64
|
+
result.dig('choices', 0, 'message', 'content')
|
|
65
|
+
rescue Faraday::TimeoutError
|
|
66
|
+
raise 'LLM request timed out after 120 seconds'
|
|
67
|
+
rescue Faraday::ConnectionFailed => e
|
|
68
|
+
raise "Failed to connect to model: #{e.message}"
|
|
69
|
+
rescue StandardError => e
|
|
70
|
+
Formatters::ProgressFormatter.error("LLM call failed: #{e.message}")
|
|
71
|
+
puts
|
|
72
|
+
puts "Make sure the model '#{model_name}' is running: kubectl get pods -n #{ctx.namespace}"
|
|
73
|
+
exit 1
|
|
74
|
+
ensure
|
|
75
|
+
# Clean up port-forward process
|
|
76
|
+
cleanup_port_forward(port_forward_pid) if port_forward_pid
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Get the pod for a model
|
|
81
|
+
def get_model_pod(model_name)
|
|
82
|
+
# Get the deployment for the model
|
|
83
|
+
deployment = ctx.client.get_resource('Deployment', model_name, ctx.namespace)
|
|
84
|
+
labels = deployment.dig('spec', 'selector', 'matchLabels')
|
|
85
|
+
|
|
86
|
+
# Find matching pods using centralized utility
|
|
87
|
+
pods = CLI::Helpers::LabelUtils.find_pods_by_deployment_labels(ctx, model_name, labels)
|
|
88
|
+
raise "No pods found for model '#{model_name}'" if pods.empty?
|
|
89
|
+
|
|
90
|
+
running_pod = pods.find do |pod|
|
|
91
|
+
pod.dig('status', 'phase') == 'Running' &&
|
|
92
|
+
pod.dig('status', 'conditions')&.any? { |c| c['type'] == 'Ready' && c['status'] == 'True' }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
if running_pod.nil?
|
|
96
|
+
pod_phases = pods.map { |p| p.dig('status', 'phase') }.join(', ')
|
|
97
|
+
raise "No running pods found. Pod phases: #{pod_phases}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
running_pod
|
|
101
|
+
rescue K8s::Error::NotFound
|
|
102
|
+
raise "Model deployment '#{model_name}' not found"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Find an available local port for port-forwarding
|
|
106
|
+
def find_available_port
|
|
107
|
+
require 'socket'
|
|
108
|
+
|
|
109
|
+
# Try ports in the range 14000-14999
|
|
110
|
+
(14_000..14_999).each do |port|
|
|
111
|
+
server = TCPServer.new('127.0.0.1', port)
|
|
112
|
+
server.close
|
|
113
|
+
return port
|
|
114
|
+
rescue Errno::EADDRINUSE
|
|
115
|
+
# Port in use, try next
|
|
116
|
+
next
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
raise 'No available ports found in range 14000-14999'
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Start kubectl port-forward in background
|
|
123
|
+
def start_port_forward(pod_name, local_port, remote_port)
|
|
124
|
+
require 'English'
|
|
125
|
+
|
|
126
|
+
cmd = "kubectl port-forward -n #{ctx.namespace} #{pod_name} #{local_port}:#{remote_port}"
|
|
127
|
+
pid = spawn(cmd, out: '/dev/null', err: '/dev/null')
|
|
128
|
+
|
|
129
|
+
# Detach so it runs in background
|
|
130
|
+
Process.detach(pid)
|
|
131
|
+
|
|
132
|
+
pid
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Wait for port-forward to be ready
|
|
136
|
+
def wait_for_port(port, max_attempts: 30)
|
|
137
|
+
require 'socket'
|
|
138
|
+
|
|
139
|
+
max_attempts.times do
|
|
140
|
+
socket = TCPSocket.new('127.0.0.1', port)
|
|
141
|
+
socket.close
|
|
142
|
+
return true
|
|
143
|
+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
|
|
144
|
+
sleep 0.1
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
raise "Port-forward to localhost:#{port} failed to become ready after #{max_attempts} attempts"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Clean up port-forward process
|
|
151
|
+
def cleanup_port_forward(pid)
|
|
152
|
+
return unless pid
|
|
153
|
+
|
|
154
|
+
begin
|
|
155
|
+
Process.kill('TERM', pid)
|
|
156
|
+
Process.wait(pid, Process::WNOHANG)
|
|
157
|
+
rescue Errno::ESRCH
|
|
158
|
+
# Process already gone
|
|
159
|
+
rescue Errno::ECHILD
|
|
160
|
+
# Process already reaped
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Extract Ruby code from LLM response
|
|
165
|
+
# Looks for ```ruby ... ``` blocks
|
|
166
|
+
def extract_ruby_code(response)
|
|
167
|
+
# Match ```ruby ... ``` blocks
|
|
168
|
+
match = response.match(/```ruby\n(.*?)```/m)
|
|
169
|
+
return match[1].strip if match
|
|
170
|
+
|
|
171
|
+
# Try without language specifier
|
|
172
|
+
match = response.match(/```\n(.*?)```/m)
|
|
173
|
+
return match[1].strip if match
|
|
174
|
+
|
|
175
|
+
# If no code blocks, return nil
|
|
176
|
+
nil
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../../../constants/kubernetes_labels'
|
|
4
|
+
|
|
5
|
+
module LanguageOperator
|
|
6
|
+
module CLI
|
|
7
|
+
module Commands
|
|
8
|
+
module System
|
|
9
|
+
module Helpers
|
|
10
|
+
# Pod management utilities for exec command
|
|
11
|
+
module PodManager
|
|
12
|
+
# Create a ConfigMap with agent code
|
|
13
|
+
def create_agent_configmap(name, code)
|
|
14
|
+
configmap = {
|
|
15
|
+
'apiVersion' => 'v1',
|
|
16
|
+
'kind' => 'ConfigMap',
|
|
17
|
+
'metadata' => {
|
|
18
|
+
'name' => name,
|
|
19
|
+
'namespace' => ctx.namespace
|
|
20
|
+
},
|
|
21
|
+
'data' => {
|
|
22
|
+
'agent.rb' => code
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
ctx.client.create_resource(configmap)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Create a test pod for running the agent
|
|
30
|
+
def create_test_pod(name, configmap_name, image)
|
|
31
|
+
# Detect available models in the cluster
|
|
32
|
+
model_env = detect_model_config
|
|
33
|
+
|
|
34
|
+
if model_env.nil?
|
|
35
|
+
Formatters::ProgressFormatter.warn('Could not detect model configuration from cluster')
|
|
36
|
+
Formatters::ProgressFormatter.warn('Agent may fail without MODEL_ENDPOINTS configured')
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
env_vars = [
|
|
40
|
+
{ 'name' => 'AGENT_NAME', 'value' => name },
|
|
41
|
+
{ 'name' => 'AGENT_MODE', 'value' => 'autonomous' },
|
|
42
|
+
{ 'name' => 'AGENT_CODE_PATH', 'value' => '/etc/agent/code/agent.rb' },
|
|
43
|
+
{ 'name' => 'CONFIG_PATH', 'value' => '/nonexistent/config.yaml' }
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
# Add model configuration if available
|
|
47
|
+
env_vars += model_env if model_env
|
|
48
|
+
|
|
49
|
+
pod = {
|
|
50
|
+
'apiVersion' => 'v1',
|
|
51
|
+
'kind' => 'Pod',
|
|
52
|
+
'metadata' => {
|
|
53
|
+
'name' => name,
|
|
54
|
+
'namespace' => ctx.namespace,
|
|
55
|
+
'labels' => Constants::KubernetesLabels.test_agent_labels(name).merge(
|
|
56
|
+
Constants::KubernetesLabels::KIND_LABEL => 'LanguageAgent'
|
|
57
|
+
)
|
|
58
|
+
},
|
|
59
|
+
'spec' => {
|
|
60
|
+
'restartPolicy' => 'Never',
|
|
61
|
+
'containers' => [
|
|
62
|
+
{
|
|
63
|
+
'name' => 'agent',
|
|
64
|
+
'image' => image,
|
|
65
|
+
'imagePullPolicy' => 'Always',
|
|
66
|
+
'env' => env_vars,
|
|
67
|
+
'volumeMounts' => [
|
|
68
|
+
{
|
|
69
|
+
'name' => 'agent-code',
|
|
70
|
+
'mountPath' => '/etc/agent/code',
|
|
71
|
+
'readOnly' => true
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
'volumes' => [
|
|
77
|
+
{
|
|
78
|
+
'name' => 'agent-code',
|
|
79
|
+
'configMap' => {
|
|
80
|
+
'name' => configmap_name
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
ctx.client.create_resource(pod)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Detect model configuration from the cluster
|
|
91
|
+
def detect_model_config
|
|
92
|
+
models = ctx.client.list_resources('LanguageModel', namespace: ctx.namespace)
|
|
93
|
+
return nil if models.empty?
|
|
94
|
+
|
|
95
|
+
# Use first available model
|
|
96
|
+
model = models.first
|
|
97
|
+
model_name = model.dig('metadata', 'name')
|
|
98
|
+
model_id = model.dig('spec', 'modelName')
|
|
99
|
+
|
|
100
|
+
# Build endpoint URL (port 8000 is the model service port)
|
|
101
|
+
endpoint = "http://#{model_name}.#{ctx.namespace}.svc.cluster.local:8000"
|
|
102
|
+
|
|
103
|
+
[
|
|
104
|
+
{ 'name' => 'MODEL_ENDPOINTS', 'value' => endpoint },
|
|
105
|
+
{ 'name' => 'LLM_MODEL', 'value' => model_id },
|
|
106
|
+
{ 'name' => 'OPENAI_API_KEY', 'value' => 'sk-dummy-key-for-local-proxy' }
|
|
107
|
+
]
|
|
108
|
+
rescue StandardError => e
|
|
109
|
+
Formatters::ProgressFormatter.error("Failed to detect model configuration: #{e.message}")
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Wait for pod to start (running or terminated)
|
|
114
|
+
def wait_for_pod_start(name, timeout: 60)
|
|
115
|
+
start_time = Time.now
|
|
116
|
+
loop do
|
|
117
|
+
pod = ctx.client.get_resource('Pod', name, ctx.namespace)
|
|
118
|
+
phase = pod.dig('status', 'phase')
|
|
119
|
+
|
|
120
|
+
return if %w[Running Succeeded Failed].include?(phase)
|
|
121
|
+
|
|
122
|
+
raise "Pod #{name} did not start within #{timeout} seconds" if Time.now - start_time > timeout
|
|
123
|
+
|
|
124
|
+
sleep 1
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Stream pod logs until completion
|
|
129
|
+
def stream_pod_logs(name, timeout: 300)
|
|
130
|
+
require 'open3'
|
|
131
|
+
|
|
132
|
+
cmd = "kubectl logs -f -n #{ctx.namespace} #{name} 2>&1"
|
|
133
|
+
Open3.popen3(cmd) do |_stdin, stdout, _stderr, wait_thr|
|
|
134
|
+
# Set up timeout
|
|
135
|
+
start_time = Time.now
|
|
136
|
+
|
|
137
|
+
# Stream logs
|
|
138
|
+
stdout.each_line do |line|
|
|
139
|
+
puts line
|
|
140
|
+
|
|
141
|
+
# Check timeout
|
|
142
|
+
if Time.now - start_time > timeout
|
|
143
|
+
Process.kill('TERM', wait_thr.pid)
|
|
144
|
+
raise "Log streaming timed out after #{timeout} seconds"
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Wait for process to complete
|
|
149
|
+
wait_thr.value
|
|
150
|
+
end
|
|
151
|
+
rescue Errno::EPIPE
|
|
152
|
+
# Pod terminated, logs finished
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Wait for pod to terminate and get exit code
|
|
156
|
+
def wait_for_pod_termination(name, timeout: 10)
|
|
157
|
+
# Give the pod a moment to fully transition after logs complete
|
|
158
|
+
sleep 2
|
|
159
|
+
|
|
160
|
+
start_time = Time.now
|
|
161
|
+
loop do
|
|
162
|
+
pod = ctx.client.get_resource('Pod', name, ctx.namespace)
|
|
163
|
+
phase = pod.dig('status', 'phase')
|
|
164
|
+
container_status = pod.dig('status', 'containerStatuses', 0)
|
|
165
|
+
|
|
166
|
+
# Pod completed successfully or failed
|
|
167
|
+
if %w[Succeeded Failed].include?(phase) && container_status && (terminated = container_status.dig('state', 'terminated'))
|
|
168
|
+
return terminated['exitCode']
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Check timeout
|
|
172
|
+
if Time.now - start_time > timeout
|
|
173
|
+
# Try one last time
|
|
174
|
+
if container_status && (terminated = container_status.dig('state', 'terminated'))
|
|
175
|
+
return terminated['exitCode']
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
return nil
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
sleep 0.5
|
|
182
|
+
rescue K8s::Error::NotFound
|
|
183
|
+
# Pod was deleted before we could get status
|
|
184
|
+
return nil
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Get pod status
|
|
189
|
+
def get_pod_status(name)
|
|
190
|
+
pod = ctx.client.get_resource('Pod', name, ctx.namespace)
|
|
191
|
+
pod.to_h.fetch('status', {})
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Delete a pod
|
|
195
|
+
def delete_pod(name)
|
|
196
|
+
ctx.client.delete_resource('Pod', name, ctx.namespace)
|
|
197
|
+
rescue K8s::Error::NotFound
|
|
198
|
+
# Already deleted
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Delete a ConfigMap
|
|
202
|
+
def delete_configmap(name)
|
|
203
|
+
ctx.client.delete_resource('ConfigMap', name, ctx.namespace)
|
|
204
|
+
rescue K8s::Error::NotFound
|
|
205
|
+
# Already deleted
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LanguageOperator
|
|
4
|
+
module CLI
|
|
5
|
+
module Commands
|
|
6
|
+
module System
|
|
7
|
+
module Helpers
|
|
8
|
+
# Template loading utilities
|
|
9
|
+
module TemplateLoader
|
|
10
|
+
# Load template from bundled gem or operator ConfigMap
|
|
11
|
+
def load_template(type)
|
|
12
|
+
# Try to fetch from operator ConfigMap first (if kubectl available)
|
|
13
|
+
template = fetch_from_operator(type)
|
|
14
|
+
return template if template
|
|
15
|
+
|
|
16
|
+
# Fall back to bundled template
|
|
17
|
+
load_bundled_template(type)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Fetch template from operator ConfigMap via kubectl
|
|
21
|
+
def fetch_from_operator(type)
|
|
22
|
+
configmap_name = type == 'agent' ? 'agent-synthesis-template' : 'persona-distillation-template'
|
|
23
|
+
result = `kubectl get configmap #{configmap_name} -n language-operator-system -o jsonpath='{.data.template}' 2>/dev/null`
|
|
24
|
+
result.empty? ? nil : result
|
|
25
|
+
rescue StandardError
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Load bundled template from gem
|
|
30
|
+
def load_bundled_template(type)
|
|
31
|
+
filename = type == 'agent' ? 'agent_synthesis.tmpl' : 'persona_distillation.tmpl'
|
|
32
|
+
template_path = File.join(__dir__, '..', '..', '..', 'templates', filename)
|
|
33
|
+
File.read(template_path)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Render Go-style template ({{.Variable}})
|
|
37
|
+
# Simplified implementation for basic variable substitution
|
|
38
|
+
def render_go_template(template, data)
|
|
39
|
+
result = template.dup
|
|
40
|
+
|
|
41
|
+
# Handle {{if .ErrorContext}} - remove this section for test-synthesis
|
|
42
|
+
result.gsub!(/{{if \.ErrorContext}}.*?{{else}}/m, '')
|
|
43
|
+
result.gsub!(/{{end}}/, '')
|
|
44
|
+
|
|
45
|
+
# Replace simple variables {{.Variable}}
|
|
46
|
+
data.each do |key, value|
|
|
47
|
+
result.gsub!("{{.#{key}}}", value.to_s)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
result
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LanguageOperator
|
|
4
|
+
module CLI
|
|
5
|
+
module Commands
|
|
6
|
+
module System
|
|
7
|
+
module Helpers
|
|
8
|
+
# Template validation utilities
|
|
9
|
+
module TemplateValidator
|
|
10
|
+
# Validate template syntax and structure
|
|
11
|
+
def validate_template_content(content, type)
|
|
12
|
+
errors = []
|
|
13
|
+
warnings = []
|
|
14
|
+
|
|
15
|
+
# Check for required placeholders based on type
|
|
16
|
+
required_placeholders = if type == 'agent'
|
|
17
|
+
%w[
|
|
18
|
+
Instructions ToolsList ModelsList AgentName TemporalIntent
|
|
19
|
+
]
|
|
20
|
+
else
|
|
21
|
+
%w[
|
|
22
|
+
PersonaName PersonaDescription PersonaSystemPrompt
|
|
23
|
+
AgentInstructions AgentTools
|
|
24
|
+
]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
required_placeholders.each do |placeholder|
|
|
28
|
+
errors << "Missing required placeholder: {{.#{placeholder}}}" unless content.include?("{{.#{placeholder}}}")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Check for balanced braces
|
|
32
|
+
open_braces = content.scan(/{{/).count
|
|
33
|
+
close_braces = content.scan(/}}/).count
|
|
34
|
+
errors << "Unbalanced template braces ({{ vs }}): #{open_braces} open, #{close_braces} close" if open_braces != close_braces
|
|
35
|
+
|
|
36
|
+
# Extract and validate Ruby code blocks
|
|
37
|
+
code_examples = extract_code_examples(content)
|
|
38
|
+
code_examples.each do |example|
|
|
39
|
+
code_result = validate_code_against_schema(example[:code])
|
|
40
|
+
unless code_result[:valid]
|
|
41
|
+
code_result[:errors].each do |err|
|
|
42
|
+
# Adjust line numbers to be relative to template
|
|
43
|
+
line = example[:start_line] + (err[:location] || 0)
|
|
44
|
+
errors << "Line #{line}: #{err[:message]}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
code_result[:warnings].each do |warn|
|
|
48
|
+
line = example[:start_line] + (warn[:location] || 0)
|
|
49
|
+
warnings << "Line #{line}: #{warn[:message]}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Extract method calls and check if they're in the safe list
|
|
54
|
+
method_calls = extract_method_calls(content)
|
|
55
|
+
safe_methods = Dsl::Schema.safe_agent_methods +
|
|
56
|
+
Dsl::Schema.safe_tool_methods +
|
|
57
|
+
Dsl::Schema.safe_helper_methods
|
|
58
|
+
method_calls.each do |method|
|
|
59
|
+
next if safe_methods.include?(method)
|
|
60
|
+
|
|
61
|
+
warnings << "Method '#{method}' not in safe methods list (may be valid Ruby builtin)"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
{
|
|
65
|
+
valid: errors.empty?,
|
|
66
|
+
errors: errors,
|
|
67
|
+
warnings: warnings
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Extract Ruby code examples from template
|
|
72
|
+
# Returns array of {code: String, start_line: Integer}
|
|
73
|
+
def extract_code_examples(template)
|
|
74
|
+
examples = []
|
|
75
|
+
lines = template.split("\n")
|
|
76
|
+
in_code_block = false
|
|
77
|
+
current_code = []
|
|
78
|
+
start_line = 0
|
|
79
|
+
|
|
80
|
+
lines.each_with_index do |line, idx|
|
|
81
|
+
if line.strip.start_with?('```ruby')
|
|
82
|
+
in_code_block = true
|
|
83
|
+
start_line = idx + 2 # idx is 0-based, we want line number (1-based) of first code line
|
|
84
|
+
current_code = []
|
|
85
|
+
elsif line.strip == '```' && in_code_block
|
|
86
|
+
in_code_block = false
|
|
87
|
+
examples << { code: current_code.join("\n"), start_line: start_line } unless current_code.empty?
|
|
88
|
+
elsif in_code_block
|
|
89
|
+
current_code << line
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
examples
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Extract method calls from template code
|
|
97
|
+
# Returns array of method name strings
|
|
98
|
+
def extract_method_calls(template)
|
|
99
|
+
require 'prism'
|
|
100
|
+
|
|
101
|
+
method_calls = []
|
|
102
|
+
code_examples = extract_code_examples(template)
|
|
103
|
+
|
|
104
|
+
code_examples.each do |example|
|
|
105
|
+
# Parse the code to find method calls
|
|
106
|
+
result = Prism.parse(example[:code])
|
|
107
|
+
|
|
108
|
+
# Walk the AST to find method calls
|
|
109
|
+
extract_methods_from_ast(result.value, method_calls) if result.success?
|
|
110
|
+
rescue Prism::ParseError
|
|
111
|
+
# Skip code with syntax errors - they'll be caught by validate_code_against_schema
|
|
112
|
+
next
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
method_calls.uniq
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Recursively extract method names from AST
|
|
119
|
+
def extract_methods_from_ast(node, methods)
|
|
120
|
+
return unless node
|
|
121
|
+
|
|
122
|
+
methods << node.name.to_s if node.is_a?(Prism::CallNode)
|
|
123
|
+
|
|
124
|
+
node.compact_child_nodes.each do |child|
|
|
125
|
+
extract_methods_from_ast(child, methods)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Validate Ruby code against DSL schema
|
|
130
|
+
# Returns {valid: Boolean, errors: Array<Hash>, warnings: Array<Hash>}
|
|
131
|
+
def validate_code_against_schema(code)
|
|
132
|
+
require 'language_operator/agent/safety/ast_validator'
|
|
133
|
+
|
|
134
|
+
validator = LanguageOperator::Agent::Safety::ASTValidator.new
|
|
135
|
+
violations = validator.validate(code, '(template)')
|
|
136
|
+
|
|
137
|
+
errors = []
|
|
138
|
+
warnings = []
|
|
139
|
+
|
|
140
|
+
violations.each do |violation|
|
|
141
|
+
case violation[:type]
|
|
142
|
+
when :syntax_error
|
|
143
|
+
errors << {
|
|
144
|
+
type: :syntax_error,
|
|
145
|
+
location: 0,
|
|
146
|
+
message: violation[:message]
|
|
147
|
+
}
|
|
148
|
+
when :dangerous_method, :dangerous_constant, :dangerous_constant_access, :dangerous_global, :backtick_execution
|
|
149
|
+
errors << {
|
|
150
|
+
type: violation[:type],
|
|
151
|
+
location: violation[:location],
|
|
152
|
+
message: violation[:message]
|
|
153
|
+
}
|
|
154
|
+
else
|
|
155
|
+
warnings << {
|
|
156
|
+
type: violation[:type],
|
|
157
|
+
location: violation[:location] || 0,
|
|
158
|
+
message: violation[:message]
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
{
|
|
164
|
+
valid: errors.empty?,
|
|
165
|
+
errors: errors,
|
|
166
|
+
warnings: warnings
|
|
167
|
+
}
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|