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.
Files changed (143) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/persona.md +9 -0
  3. data/.claude/commands/task.md +46 -1
  4. data/.rubocop.yml +13 -0
  5. data/.rubocop_custom/use_ux_helper.rb +44 -0
  6. data/CHANGELOG.md +8 -0
  7. data/Gemfile.lock +12 -1
  8. data/Makefile +26 -7
  9. data/Makefile.common +50 -0
  10. data/bin/aictl +8 -1
  11. data/components/agent/Gemfile +1 -1
  12. data/components/agent/bin/langop-agent +7 -0
  13. data/docs/README.md +58 -0
  14. data/docs/{dsl/best-practices.md → best-practices.md} +4 -4
  15. data/docs/cli-reference.md +274 -0
  16. data/docs/{dsl/constraints.md → constraints.md} +5 -5
  17. data/docs/how-agents-work.md +156 -0
  18. data/docs/installation.md +218 -0
  19. data/docs/quickstart.md +299 -0
  20. data/docs/understanding-generated-code.md +265 -0
  21. data/docs/using-tools.md +457 -0
  22. data/docs/webhooks.md +509 -0
  23. data/examples/ux_helpers_demo.rb +296 -0
  24. data/lib/language_operator/agent/base.rb +11 -1
  25. data/lib/language_operator/agent/executor.rb +23 -6
  26. data/lib/language_operator/agent/safety/safe_executor.rb +41 -39
  27. data/lib/language_operator/agent/task_executor.rb +346 -63
  28. data/lib/language_operator/agent/web_server.rb +110 -14
  29. data/lib/language_operator/agent/webhook_authenticator.rb +39 -5
  30. data/lib/language_operator/agent.rb +88 -2
  31. data/lib/language_operator/cli/base_command.rb +17 -11
  32. data/lib/language_operator/cli/command_loader.rb +72 -0
  33. data/lib/language_operator/cli/commands/agent/base.rb +837 -0
  34. data/lib/language_operator/cli/commands/agent/code_operations.rb +102 -0
  35. data/lib/language_operator/cli/commands/agent/helpers/cluster_llm_client.rb +116 -0
  36. data/lib/language_operator/cli/commands/agent/helpers/code_parser.rb +115 -0
  37. data/lib/language_operator/cli/commands/agent/helpers/synthesis_watcher.rb +96 -0
  38. data/lib/language_operator/cli/commands/agent/learning.rb +289 -0
  39. data/lib/language_operator/cli/commands/agent/lifecycle.rb +102 -0
  40. data/lib/language_operator/cli/commands/agent/logs.rb +125 -0
  41. data/lib/language_operator/cli/commands/agent/workspace.rb +327 -0
  42. data/lib/language_operator/cli/commands/cluster.rb +129 -84
  43. data/lib/language_operator/cli/commands/install.rb +1 -1
  44. data/lib/language_operator/cli/commands/model/base.rb +215 -0
  45. data/lib/language_operator/cli/commands/model/test.rb +165 -0
  46. data/lib/language_operator/cli/commands/persona.rb +16 -34
  47. data/lib/language_operator/cli/commands/quickstart.rb +3 -2
  48. data/lib/language_operator/cli/commands/status.rb +40 -67
  49. data/lib/language_operator/cli/commands/system/base.rb +44 -0
  50. data/lib/language_operator/cli/commands/system/exec.rb +147 -0
  51. data/lib/language_operator/cli/commands/system/helpers/llm_synthesis.rb +183 -0
  52. data/lib/language_operator/cli/commands/system/helpers/pod_manager.rb +212 -0
  53. data/lib/language_operator/cli/commands/system/helpers/template_loader.rb +57 -0
  54. data/lib/language_operator/cli/commands/system/helpers/template_validator.rb +174 -0
  55. data/lib/language_operator/cli/commands/system/schema.rb +92 -0
  56. data/lib/language_operator/cli/commands/system/synthesis_template.rb +151 -0
  57. data/lib/language_operator/cli/commands/system/synthesize.rb +224 -0
  58. data/lib/language_operator/cli/commands/system/validate_template.rb +130 -0
  59. data/lib/language_operator/cli/commands/tool/base.rb +271 -0
  60. data/lib/language_operator/cli/commands/tool/install.rb +255 -0
  61. data/lib/language_operator/cli/commands/tool/search.rb +69 -0
  62. data/lib/language_operator/cli/commands/tool/test.rb +115 -0
  63. data/lib/language_operator/cli/commands/use.rb +29 -6
  64. data/lib/language_operator/cli/errors/handler.rb +20 -17
  65. data/lib/language_operator/cli/errors/suggestions.rb +3 -5
  66. data/lib/language_operator/cli/errors/thor_errors.rb +55 -0
  67. data/lib/language_operator/cli/formatters/code_formatter.rb +4 -11
  68. data/lib/language_operator/cli/formatters/log_formatter.rb +8 -15
  69. data/lib/language_operator/cli/formatters/progress_formatter.rb +6 -8
  70. data/lib/language_operator/cli/formatters/status_formatter.rb +26 -7
  71. data/lib/language_operator/cli/formatters/table_formatter.rb +47 -36
  72. data/lib/language_operator/cli/formatters/value_formatter.rb +75 -0
  73. data/lib/language_operator/cli/helpers/cluster_context.rb +5 -3
  74. data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +2 -1
  75. data/lib/language_operator/cli/helpers/label_utils.rb +97 -0
  76. data/lib/language_operator/{ux/concerns/provider_helpers.rb → cli/helpers/provider_helper.rb} +10 -29
  77. data/lib/language_operator/cli/helpers/schedule_builder.rb +21 -1
  78. data/lib/language_operator/cli/helpers/user_prompts.rb +19 -11
  79. data/lib/language_operator/cli/helpers/ux_helper.rb +538 -0
  80. data/lib/language_operator/{ux/concerns/input_validation.rb → cli/helpers/validation_helper.rb} +13 -66
  81. data/lib/language_operator/cli/main.rb +50 -40
  82. data/lib/language_operator/cli/templates/tools/generic.yaml +3 -0
  83. data/lib/language_operator/cli/wizards/agent_wizard.rb +12 -20
  84. data/lib/language_operator/cli/wizards/model_wizard.rb +271 -0
  85. data/lib/language_operator/cli/wizards/quickstart_wizard.rb +8 -34
  86. data/lib/language_operator/client/base.rb +28 -0
  87. data/lib/language_operator/client/config.rb +4 -1
  88. data/lib/language_operator/client/mcp_connector.rb +1 -1
  89. data/lib/language_operator/config/cluster_config.rb +3 -2
  90. data/lib/language_operator/config.rb +38 -11
  91. data/lib/language_operator/constants/kubernetes_labels.rb +80 -0
  92. data/lib/language_operator/constants.rb +13 -0
  93. data/lib/language_operator/dsl/http.rb +127 -10
  94. data/lib/language_operator/dsl.rb +153 -6
  95. data/lib/language_operator/errors.rb +50 -0
  96. data/lib/language_operator/kubernetes/client.rb +11 -6
  97. data/lib/language_operator/kubernetes/resource_builder.rb +58 -84
  98. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
  99. data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
  100. data/lib/language_operator/type_coercion.rb +118 -34
  101. data/lib/language_operator/utils/secure_path.rb +74 -0
  102. data/lib/language_operator/utils.rb +7 -0
  103. data/lib/language_operator/validators.rb +54 -2
  104. data/lib/language_operator/version.rb +1 -1
  105. data/synth/001/Makefile +10 -2
  106. data/synth/001/agent.rb +16 -15
  107. data/synth/001/output.log +27 -10
  108. data/synth/002/Makefile +10 -2
  109. data/synth/003/Makefile +1 -1
  110. data/synth/003/README.md +205 -133
  111. data/synth/003/agent.optimized.rb +66 -0
  112. data/synth/003/agent.synthesized.rb +41 -0
  113. metadata +111 -35
  114. data/docs/dsl/agent-reference.md +0 -604
  115. data/docs/dsl/mcp-integration.md +0 -1177
  116. data/docs/dsl/webhooks.md +0 -932
  117. data/docs/dsl/workflows.md +0 -744
  118. data/lib/language_operator/cli/commands/agent.rb +0 -1712
  119. data/lib/language_operator/cli/commands/model.rb +0 -366
  120. data/lib/language_operator/cli/commands/system.rb +0 -1259
  121. data/lib/language_operator/cli/commands/tool.rb +0 -654
  122. data/lib/language_operator/cli/formatters/optimization_formatter.rb +0 -226
  123. data/lib/language_operator/cli/helpers/pastel_helper.rb +0 -24
  124. data/lib/language_operator/learning/adapters/base_adapter.rb +0 -149
  125. data/lib/language_operator/learning/adapters/jaeger_adapter.rb +0 -221
  126. data/lib/language_operator/learning/adapters/signoz_adapter.rb +0 -435
  127. data/lib/language_operator/learning/adapters/tempo_adapter.rb +0 -239
  128. data/lib/language_operator/learning/optimizer.rb +0 -319
  129. data/lib/language_operator/learning/pattern_detector.rb +0 -260
  130. data/lib/language_operator/learning/task_synthesizer.rb +0 -288
  131. data/lib/language_operator/learning/trace_analyzer.rb +0 -285
  132. data/lib/language_operator/templates/task_synthesis.tmpl +0 -98
  133. data/lib/language_operator/ux/base.rb +0 -81
  134. data/lib/language_operator/ux/concerns/README.md +0 -155
  135. data/lib/language_operator/ux/concerns/headings.rb +0 -90
  136. data/lib/language_operator/ux/create_agent.rb +0 -255
  137. data/lib/language_operator/ux/create_model.rb +0 -267
  138. data/lib/language_operator/ux/quickstart.rb +0 -594
  139. data/synth/003/agent.rb +0 -41
  140. data/synth/003/output.log +0 -68
  141. /data/docs/{architecture/agent-runtime.md → agent-internals.md} +0 -0
  142. /data/docs/{dsl/chat-endpoints.md → chat-endpoints.md} +0 -0
  143. /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