language-operator 0.1.63 → 0.1.65
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/.plan.md +127 -0
- data/.rspec +3 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +4 -1
- data/Makefile +34 -80
- data/components/agent/Gemfile +1 -1
- data/docs/cheat-sheet.md +173 -0
- data/lib/language_operator/agent/base.rb +10 -1
- data/lib/language_operator/agent/event_config.rb +172 -0
- data/lib/language_operator/agent/safety/ast_validator.rb +1 -1
- data/lib/language_operator/agent/safety/safe_executor.rb +5 -1
- data/lib/language_operator/agent/task_executor.rb +87 -7
- data/lib/language_operator/agent/telemetry.rb +25 -3
- data/lib/language_operator/agent/web_server.rb +6 -9
- data/lib/language_operator/cli/commands/agent/base.rb +15 -17
- data/lib/language_operator/cli/commands/agent/learning.rb +156 -37
- data/lib/language_operator/cli/commands/cluster.rb +2 -2
- data/lib/language_operator/cli/commands/status.rb +2 -2
- data/lib/language_operator/cli/commands/system/synthesize.rb +1 -1
- data/lib/language_operator/cli/formatters/value_formatter.rb +1 -1
- data/lib/language_operator/cli/helpers/ux_helper.rb +3 -4
- data/lib/language_operator/config.rb +3 -3
- data/lib/language_operator/constants/kubernetes_labels.rb +2 -2
- data/lib/language_operator/dsl/task_definition.rb +18 -7
- data/lib/language_operator/instrumentation/task_tracer.rb +44 -3
- data/lib/language_operator/kubernetes/client.rb +111 -0
- data/lib/language_operator/templates/schema/CHANGELOG.md +28 -0
- 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 +22 -8
- data/lib/language_operator/version.rb +1 -1
- data/synth/002/agent.rb +23 -12
- data/synth/002/output.log +88 -15
- data/synth/003/Makefile +5 -2
- data/synth/004/Makefile +54 -0
- data/synth/004/README.md +281 -0
- data/synth/004/instructions.txt +1 -0
- metadata +8 -1
|
@@ -81,7 +81,8 @@ module LanguageOperator
|
|
|
81
81
|
agent = ctx.client.get_resource(RESOURCE_AGENT, name, ctx.namespace)
|
|
82
82
|
|
|
83
83
|
# Check current status
|
|
84
|
-
annotations = agent.dig('metadata', 'annotations')
|
|
84
|
+
annotations = agent.dig('metadata', 'annotations')
|
|
85
|
+
annotations = annotations.respond_to?(:to_h) ? annotations.to_h : (annotations || {})
|
|
85
86
|
disabled_annotation = Constants::KubernetesLabels::LEARNING_DISABLED_LABEL
|
|
86
87
|
|
|
87
88
|
unless annotations.key?(disabled_annotation)
|
|
@@ -117,7 +118,8 @@ module LanguageOperator
|
|
|
117
118
|
agent = ctx.client.get_resource(RESOURCE_AGENT, name, ctx.namespace)
|
|
118
119
|
|
|
119
120
|
# Check current status
|
|
120
|
-
annotations = agent.dig('metadata', 'annotations')
|
|
121
|
+
annotations = agent.dig('metadata', 'annotations')
|
|
122
|
+
annotations = annotations.respond_to?(:to_h) ? annotations.to_h : (annotations || {})
|
|
121
123
|
disabled_annotation = Constants::KubernetesLabels::LEARNING_DISABLED_LABEL
|
|
122
124
|
|
|
123
125
|
if annotations.key?(disabled_annotation)
|
|
@@ -147,45 +149,27 @@ module LanguageOperator
|
|
|
147
149
|
end
|
|
148
150
|
|
|
149
151
|
def display_learning_status(agent, learning_status, cluster_name)
|
|
150
|
-
|
|
151
|
-
annotations = agent.dig('metadata', 'annotations')
|
|
152
|
+
agent.dig('metadata', 'name')
|
|
153
|
+
annotations = agent.dig('metadata', 'annotations')
|
|
154
|
+
annotations = annotations.respond_to?(:to_h) ? annotations.to_h : (annotations || {})
|
|
152
155
|
|
|
153
156
|
puts
|
|
154
157
|
|
|
155
|
-
#
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
status_text = learning_enabled ? 'Enabled' : 'Disabled'
|
|
158
|
+
# Display Agent Status box
|
|
159
|
+
display_agent_status_box(agent, cluster_name)
|
|
160
|
+
puts
|
|
159
161
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
rows: {
|
|
163
|
-
'Agent' => pastel.white.bold(agent_name),
|
|
164
|
-
'Cluster' => cluster_name,
|
|
165
|
-
'Learning' => pastel.send(status_color).bold(status_text),
|
|
166
|
-
'Last Updated' => agent.dig('metadata', 'resourceVersion') || 'Unknown'
|
|
167
|
-
}
|
|
168
|
-
)
|
|
162
|
+
# Display Learning Status box
|
|
163
|
+
display_learning_status_box(agent, learning_status, annotations)
|
|
169
164
|
puts
|
|
170
165
|
|
|
171
166
|
# If learning status ConfigMap exists, show detailed information
|
|
172
167
|
if learning_status
|
|
173
168
|
display_detailed_learning_status(learning_status)
|
|
174
169
|
else
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
puts
|
|
170
|
+
learning_enabled = !annotations.key?(Constants::KubernetesLabels::LEARNING_DISABLED_LABEL)
|
|
171
|
+
display_learning_explanation(learning_enabled)
|
|
178
172
|
end
|
|
179
|
-
|
|
180
|
-
# Show next steps
|
|
181
|
-
puts pastel.white.bold('Available Commands:')
|
|
182
|
-
if learning_enabled
|
|
183
|
-
puts pastel.dim(" aictl agent learning disable #{agent_name}")
|
|
184
|
-
else
|
|
185
|
-
puts pastel.dim(" aictl agent learning enable #{agent_name}")
|
|
186
|
-
end
|
|
187
|
-
puts pastel.dim(" aictl agent versions #{agent_name}")
|
|
188
|
-
puts pastel.dim(" aictl agent inspect #{agent_name}")
|
|
189
173
|
end
|
|
190
174
|
|
|
191
175
|
def display_detailed_learning_status(learning_status)
|
|
@@ -206,11 +190,7 @@ module LanguageOperator
|
|
|
206
190
|
executions = task_info['executions'] || 0
|
|
207
191
|
status = task_info['status'] || 'neural'
|
|
208
192
|
|
|
209
|
-
confidence_color =
|
|
210
|
-
:green
|
|
211
|
-
else
|
|
212
|
-
confidence >= 70 ? :yellow : :red
|
|
213
|
-
end
|
|
193
|
+
confidence_color = determine_confidence_color(confidence)
|
|
214
194
|
|
|
215
195
|
puts " #{pastel.cyan(task_name)}"
|
|
216
196
|
puts " Status: #{format_task_status(status)}"
|
|
@@ -261,7 +241,8 @@ module LanguageOperator
|
|
|
261
241
|
agent = client.get_resource(RESOURCE_AGENT, name, namespace)
|
|
262
242
|
|
|
263
243
|
# Add annotation
|
|
264
|
-
annotations = agent.dig('metadata', 'annotations')
|
|
244
|
+
annotations = agent.dig('metadata', 'annotations')
|
|
245
|
+
annotations = annotations.respond_to?(:to_h) ? annotations.to_h : (annotations || {})
|
|
265
246
|
annotations[annotation_key] = annotation_value
|
|
266
247
|
agent['metadata']['annotations'] = annotations
|
|
267
248
|
|
|
@@ -274,13 +255,151 @@ module LanguageOperator
|
|
|
274
255
|
agent = client.get_resource(RESOURCE_AGENT, name, namespace)
|
|
275
256
|
|
|
276
257
|
# Remove annotation
|
|
277
|
-
annotations = agent.dig('metadata', 'annotations')
|
|
258
|
+
annotations = agent.dig('metadata', 'annotations')
|
|
259
|
+
annotations = annotations.respond_to?(:to_h) ? annotations.to_h : (annotations || {})
|
|
278
260
|
annotations.delete(annotation_key)
|
|
279
261
|
agent['metadata']['annotations'] = annotations
|
|
280
262
|
|
|
281
263
|
# Update the agent
|
|
282
264
|
client.update_resource(agent)
|
|
283
265
|
end
|
|
266
|
+
|
|
267
|
+
def format_agent_timestamp(agent)
|
|
268
|
+
created_time = agent.dig('metadata', 'creationTimestamp')
|
|
269
|
+
return 'Unknown' unless created_time
|
|
270
|
+
|
|
271
|
+
begin
|
|
272
|
+
Time.parse(created_time).strftime('%Y-%m-%d %H:%M:%S UTC')
|
|
273
|
+
rescue StandardError
|
|
274
|
+
'Unknown'
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def display_agent_status_box(agent, cluster_name)
|
|
279
|
+
agent_name = agent.dig('metadata', 'name')
|
|
280
|
+
timestamp = format_agent_timestamp(agent)
|
|
281
|
+
|
|
282
|
+
# Get agent operational status
|
|
283
|
+
status = agent['status']
|
|
284
|
+
if status && status['conditions']
|
|
285
|
+
ready_condition = status['conditions'].find { |c| c['type'] == 'Ready' }
|
|
286
|
+
if ready_condition
|
|
287
|
+
begin
|
|
288
|
+
last_activity = Time.parse(ready_condition['lastTransitionTime']).strftime('%Y-%m-%d %H:%M:%S UTC')
|
|
289
|
+
status_text = ready_condition['status'] == 'True' ? 'Ready' : 'Not Ready'
|
|
290
|
+
status_colored = ready_condition['status'] == 'True' ? pastel.green(status_text) : pastel.yellow(status_text)
|
|
291
|
+
rescue StandardError
|
|
292
|
+
last_activity = 'Unknown'
|
|
293
|
+
status_colored = pastel.dim('Unknown')
|
|
294
|
+
end
|
|
295
|
+
else
|
|
296
|
+
last_activity = 'Unknown'
|
|
297
|
+
status_colored = pastel.dim('Unknown')
|
|
298
|
+
end
|
|
299
|
+
else
|
|
300
|
+
last_activity = 'Unknown'
|
|
301
|
+
status_colored = pastel.dim('Unknown')
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
highlighted_box(
|
|
305
|
+
title: 'Agent Status',
|
|
306
|
+
color: :yellow,
|
|
307
|
+
rows: {
|
|
308
|
+
'Name' => pastel.white.bold(agent_name),
|
|
309
|
+
'Cluster' => cluster_name,
|
|
310
|
+
'Created' => timestamp,
|
|
311
|
+
'Last Activity' => last_activity,
|
|
312
|
+
'Status' => status_colored
|
|
313
|
+
}
|
|
314
|
+
)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def display_learning_status_box(_agent, learning_status, annotations)
|
|
318
|
+
learning_enabled = !annotations.key?(Constants::KubernetesLabels::LEARNING_DISABLED_LABEL)
|
|
319
|
+
status_color = learning_enabled ? :green : :yellow
|
|
320
|
+
status_text = learning_enabled ? 'Enabled' : 'Disabled'
|
|
321
|
+
|
|
322
|
+
# Parse execution summary from ConfigMap if available
|
|
323
|
+
execution_summary = parse_execution_summary(learning_status)
|
|
324
|
+
|
|
325
|
+
if execution_summary
|
|
326
|
+
total_executions = execution_summary['totalExecutions'] || 0
|
|
327
|
+
learning_threshold = execution_summary['learningThreshold'] || 10
|
|
328
|
+
execution_summary['successRate'] || 0.0
|
|
329
|
+
last_execution = execution_summary['lastExecution']
|
|
330
|
+
|
|
331
|
+
runs_processed = "#{total_executions}/#{learning_threshold}"
|
|
332
|
+
progress_percent = [(total_executions.to_f / learning_threshold * 100).round, 100].min
|
|
333
|
+
progress = "#{progress_percent}% toward learning threshold"
|
|
334
|
+
|
|
335
|
+
last_run = if last_execution && !last_execution.empty?
|
|
336
|
+
begin
|
|
337
|
+
Time.parse(last_execution).strftime('%Y-%m-%d %H:%M:%S UTC')
|
|
338
|
+
rescue StandardError
|
|
339
|
+
'Unknown'
|
|
340
|
+
end
|
|
341
|
+
else
|
|
342
|
+
'No executions yet'
|
|
343
|
+
end
|
|
344
|
+
else
|
|
345
|
+
runs_processed = 'No data'
|
|
346
|
+
progress = 'Waiting for agent executions'
|
|
347
|
+
last_run = 'No executions yet'
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
highlighted_box(
|
|
351
|
+
title: 'Learning Status',
|
|
352
|
+
color: :cyan,
|
|
353
|
+
rows: {
|
|
354
|
+
'Learning' => pastel.send(status_color).bold(status_text),
|
|
355
|
+
'Threshold' => "#{pastel.cyan('10 successful runs')} (auto-learning trigger)",
|
|
356
|
+
'Confidence Target' => "#{pastel.cyan('85%')} (pattern detection)",
|
|
357
|
+
'Runs Processed' => runs_processed,
|
|
358
|
+
'Progress' => progress,
|
|
359
|
+
'Last Execution' => last_run
|
|
360
|
+
}
|
|
361
|
+
)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def display_learning_explanation(learning_enabled)
|
|
365
|
+
if learning_enabled
|
|
366
|
+
puts pastel.dim('Learning is enabled and will begin automatically after the agent completes 10 successful runs.')
|
|
367
|
+
puts pastel.dim('Neural tasks will be analyzed for patterns and converted to symbolic implementations.')
|
|
368
|
+
else
|
|
369
|
+
puts pastel.yellow('Learning is disabled for this agent.')
|
|
370
|
+
puts pastel.dim('Enable learning to allow automatic task optimization after sufficient executions.')
|
|
371
|
+
end
|
|
372
|
+
puts
|
|
373
|
+
puts pastel.dim('Note: Execution metrics will be available once the agent starts running and')
|
|
374
|
+
puts pastel.dim('the operator begins collecting telemetry data.')
|
|
375
|
+
puts
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def determine_confidence_color(confidence)
|
|
379
|
+
if confidence >= 85
|
|
380
|
+
:green
|
|
381
|
+
elsif confidence >= 70
|
|
382
|
+
:yellow
|
|
383
|
+
else
|
|
384
|
+
:red
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def parse_execution_summary(learning_status)
|
|
389
|
+
return nil unless learning_status
|
|
390
|
+
|
|
391
|
+
data = learning_status['data']
|
|
392
|
+
return nil unless data
|
|
393
|
+
|
|
394
|
+
execution_summary_json = data['execution-summary']
|
|
395
|
+
return nil unless execution_summary_json
|
|
396
|
+
|
|
397
|
+
begin
|
|
398
|
+
JSON.parse(execution_summary_json)
|
|
399
|
+
rescue StandardError
|
|
400
|
+
nil
|
|
401
|
+
end
|
|
402
|
+
end
|
|
284
403
|
end
|
|
285
404
|
end
|
|
286
405
|
end
|
|
@@ -58,8 +58,8 @@ module LanguageOperator
|
|
|
58
58
|
unless k8s.namespace_exists?(namespace)
|
|
59
59
|
Formatters::ProgressFormatter.with_spinner("Creating namespace '#{namespace}'") do
|
|
60
60
|
k8s.create_namespace(namespace, labels: Constants::KubernetesLabels.cluster_management_labels.merge(
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
Constants::KubernetesLabels::CLUSTER_LABEL => name
|
|
62
|
+
))
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
|
|
@@ -35,13 +35,13 @@ module LanguageOperator
|
|
|
35
35
|
|
|
36
36
|
# Format cluster info using UxHelper
|
|
37
37
|
logo(title: 'cluster status')
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
if cluster_resource
|
|
40
40
|
# Use actual cluster resource data
|
|
41
41
|
status = cluster_resource.dig('status', 'phase') || 'Unknown'
|
|
42
42
|
domain = cluster_resource.dig('spec', 'domain')
|
|
43
43
|
created = cluster_resource.dig('metadata', 'creationTimestamp')
|
|
44
|
-
|
|
44
|
+
|
|
45
45
|
format_cluster_details(
|
|
46
46
|
name: current_cluster,
|
|
47
47
|
namespace: cluster_config[:namespace],
|
|
@@ -55,7 +55,7 @@ module LanguageOperator
|
|
|
55
55
|
option :model, type: :string, desc: 'Model to use for synthesis (defaults to first available in cluster)'
|
|
56
56
|
option :dry_run, type: :boolean, default: false, desc: 'Show prompt without calling LLM'
|
|
57
57
|
option :raw, type: :boolean, default: false, desc: 'Output only the raw code without formatting'
|
|
58
|
-
|
|
58
|
+
|
|
59
59
|
def synthesize(instructions = nil)
|
|
60
60
|
handle_command_error('synthesize agent') do
|
|
61
61
|
# Read instructions from STDIN if not provided as argument
|
|
@@ -139,8 +139,7 @@ module LanguageOperator
|
|
|
139
139
|
# @example
|
|
140
140
|
# puts highlight_ruby_code("puts 'Hello, world!'")
|
|
141
141
|
def highlight_ruby_code(code_content)
|
|
142
|
-
|
|
143
|
-
highlighted
|
|
142
|
+
rouge_formatter.format(rouge_lexer.lex(code_content))
|
|
144
143
|
end
|
|
145
144
|
|
|
146
145
|
def logo(title: nil, sparkle: false)
|
|
@@ -409,7 +408,7 @@ module LanguageOperator
|
|
|
409
408
|
def format_resource_details(type:, name:, common_fields: {}, optional_fields: {})
|
|
410
409
|
rows = { 'Name' => pastel.white.bold(name) }
|
|
411
410
|
rows.merge!(common_fields)
|
|
412
|
-
|
|
411
|
+
|
|
413
412
|
optional_fields.each do |key, value|
|
|
414
413
|
case key
|
|
415
414
|
when 'Domain'
|
|
@@ -418,7 +417,7 @@ module LanguageOperator
|
|
|
418
417
|
rows[key] = value if value
|
|
419
418
|
end
|
|
420
419
|
end
|
|
421
|
-
|
|
420
|
+
|
|
422
421
|
highlighted_box(
|
|
423
422
|
title: "Language#{type}",
|
|
424
423
|
rows: rows.compact
|
|
@@ -191,7 +191,7 @@ module LanguageOperator
|
|
|
191
191
|
# Config.get_int('MAX_WORKERS', default: 4)
|
|
192
192
|
def self.get_int(*keys, default: nil)
|
|
193
193
|
keys.each do |key|
|
|
194
|
-
value = ENV
|
|
194
|
+
value = ENV.fetch(key.to_s, nil)
|
|
195
195
|
next unless value
|
|
196
196
|
|
|
197
197
|
begin
|
|
@@ -220,7 +220,7 @@ module LanguageOperator
|
|
|
220
220
|
# Config.get_bool('USE_TLS', 'ENABLE_TLS', default: true)
|
|
221
221
|
def self.get_bool(*keys, default: false)
|
|
222
222
|
keys.each do |key|
|
|
223
|
-
value = ENV
|
|
223
|
+
value = ENV.fetch(key.to_s, nil)
|
|
224
224
|
next unless value
|
|
225
225
|
|
|
226
226
|
return %w[true 1 yes on].include?(value.to_s.downcase)
|
|
@@ -240,7 +240,7 @@ module LanguageOperator
|
|
|
240
240
|
# Config.get_array('ALLOWED_HOSTS', separator: ',')
|
|
241
241
|
def self.get_array(*keys, default: [], separator: ',')
|
|
242
242
|
keys.each do |key|
|
|
243
|
-
value = ENV
|
|
243
|
+
value = ENV.fetch(key.to_s, nil)
|
|
244
244
|
next unless value
|
|
245
245
|
next if value.empty?
|
|
246
246
|
|
|
@@ -61,7 +61,7 @@ module LanguageOperator
|
|
|
61
61
|
# Build a label selector string for finding tool pods
|
|
62
62
|
#
|
|
63
63
|
# @param tool_name [String] The tool name
|
|
64
|
-
# @return [String] Label selector string for kubectl commands
|
|
64
|
+
# @return [String] Label selector string for kubectl commands
|
|
65
65
|
def tool_selector(tool_name)
|
|
66
66
|
"#{TOOL_LABEL}=#{tool_name}"
|
|
67
67
|
end
|
|
@@ -77,4 +77,4 @@ module LanguageOperator
|
|
|
77
77
|
end
|
|
78
78
|
end
|
|
79
79
|
end
|
|
80
|
-
end
|
|
80
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative '../loggable'
|
|
4
4
|
require_relative '../type_coercion'
|
|
5
|
+
require_relative '../agent/task_executor'
|
|
5
6
|
|
|
6
7
|
module LanguageOperator
|
|
7
8
|
module Dsl
|
|
@@ -148,7 +149,7 @@ module LanguageOperator
|
|
|
148
149
|
#
|
|
149
150
|
# @param params [Hash] Input parameters
|
|
150
151
|
# @return [Hash] Validated and coerced parameters
|
|
151
|
-
# @raise [
|
|
152
|
+
# @raise [LanguageOperator::Agent::TaskValidationError] If validation fails
|
|
152
153
|
def validate_inputs(params)
|
|
153
154
|
params = params.transform_keys(&:to_sym)
|
|
154
155
|
validated = {}
|
|
@@ -157,7 +158,12 @@ module LanguageOperator
|
|
|
157
158
|
key_sym = key.to_sym
|
|
158
159
|
value = params[key_sym]
|
|
159
160
|
|
|
160
|
-
|
|
161
|
+
if value.nil?
|
|
162
|
+
original_error = ArgumentError.new("Missing required input parameter: #{key}")
|
|
163
|
+
raise LanguageOperator::Agent::TaskValidationError.new(@name,
|
|
164
|
+
"Missing required input parameter: #{key}",
|
|
165
|
+
original_error)
|
|
166
|
+
end
|
|
161
167
|
|
|
162
168
|
validated[key_sym] = coerce_value(value, type, "input parameter '#{key}'")
|
|
163
169
|
end
|
|
@@ -173,7 +179,7 @@ module LanguageOperator
|
|
|
173
179
|
#
|
|
174
180
|
# @param result [Hash] Output values
|
|
175
181
|
# @return [Hash] Validated and coerced outputs
|
|
176
|
-
# @raise [
|
|
182
|
+
# @raise [LanguageOperator::Agent::TaskValidationError] If validation fails
|
|
177
183
|
def validate_outputs(result)
|
|
178
184
|
return result if @outputs_schema.empty? # No schema = no validation
|
|
179
185
|
|
|
@@ -184,7 +190,12 @@ module LanguageOperator
|
|
|
184
190
|
key_sym = key.to_sym
|
|
185
191
|
value = result[key_sym]
|
|
186
192
|
|
|
187
|
-
|
|
193
|
+
if value.nil?
|
|
194
|
+
original_error = ArgumentError.new("Missing required output field: #{key}")
|
|
195
|
+
raise LanguageOperator::Agent::TaskValidationError.new(@name,
|
|
196
|
+
"Missing required output field: #{key}",
|
|
197
|
+
original_error)
|
|
198
|
+
end
|
|
188
199
|
|
|
189
200
|
validated[key_sym] = coerce_value(value, type, "output field '#{key}'")
|
|
190
201
|
end
|
|
@@ -275,12 +286,12 @@ module LanguageOperator
|
|
|
275
286
|
# @param type [String] Target type
|
|
276
287
|
# @param context [String] Context for error messages
|
|
277
288
|
# @return [Object] Coerced value
|
|
278
|
-
# @raise [
|
|
289
|
+
# @raise [LanguageOperator::Agent::TaskValidationError] If coercion fails
|
|
279
290
|
def coerce_value(value, type, context)
|
|
280
291
|
TypeCoercion.coerce(value, type)
|
|
281
292
|
rescue ArgumentError => e
|
|
282
|
-
# Re-raise with context added
|
|
283
|
-
raise
|
|
293
|
+
# Re-raise as TaskValidationError with context added
|
|
294
|
+
raise LanguageOperator::Agent::TaskValidationError.new(@name, "#{e.message} for #{context}", e)
|
|
284
295
|
end
|
|
285
296
|
|
|
286
297
|
# Convert schema hash to JSON Schema format
|
|
@@ -87,13 +87,19 @@ module LanguageOperator
|
|
|
87
87
|
# @param prompt [String] The generated prompt
|
|
88
88
|
# @param validated_inputs [Hash] Validated input parameters
|
|
89
89
|
# @return [Hash] Span attributes
|
|
90
|
-
def neural_task_attributes(
|
|
90
|
+
def neural_task_attributes(task, prompt, validated_inputs)
|
|
91
91
|
attributes = {
|
|
92
92
|
'gen_ai.operation.name' => 'chat',
|
|
93
93
|
'gen_ai.system' => determine_genai_system,
|
|
94
94
|
'gen_ai.prompt.size' => prompt.bytesize
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
# Add agent context (CRITICAL for learning system)
|
|
98
|
+
add_agent_context_attributes(attributes)
|
|
99
|
+
|
|
100
|
+
# Add task identification
|
|
101
|
+
attributes['task.name'] = task.name.to_s if task.respond_to?(:name) && task.name
|
|
102
|
+
|
|
97
103
|
# Add model if available
|
|
98
104
|
if @agent.respond_to?(:config) && @agent.config
|
|
99
105
|
model = @agent.config.dig('llm', 'model') || @agent.config['model']
|
|
@@ -130,10 +136,19 @@ module LanguageOperator
|
|
|
130
136
|
# @param task [TaskDefinition] The task definition
|
|
131
137
|
# @return [Hash] Span attributes
|
|
132
138
|
def symbolic_task_attributes(task)
|
|
133
|
-
{
|
|
139
|
+
attributes = {
|
|
134
140
|
'task.execution.type' => 'symbolic',
|
|
135
|
-
'task.execution.has_block' => task.execute_block ? 'true' : 'false'
|
|
141
|
+
'task.execution.has_block' => task.execute_block ? 'true' : 'false',
|
|
142
|
+
'gen_ai.operation.name' => 'execute_task'
|
|
136
143
|
}
|
|
144
|
+
|
|
145
|
+
# Add agent context (CRITICAL for learning system)
|
|
146
|
+
add_agent_context_attributes(attributes)
|
|
147
|
+
|
|
148
|
+
# Add task identification
|
|
149
|
+
attributes['task.name'] = task.name.to_s if task.respond_to?(:name) && task.name
|
|
150
|
+
|
|
151
|
+
attributes
|
|
137
152
|
end
|
|
138
153
|
|
|
139
154
|
# Record token usage from LLM response on span
|
|
@@ -265,6 +280,9 @@ module LanguageOperator
|
|
|
265
280
|
'gen_ai.tool.name' => extract_tool_name(tool_call)
|
|
266
281
|
}
|
|
267
282
|
|
|
283
|
+
# Add agent context (CRITICAL for learning system)
|
|
284
|
+
add_agent_context_attributes(attributes)
|
|
285
|
+
|
|
268
286
|
# Add tool call ID if available
|
|
269
287
|
attributes['gen_ai.tool.call.id'] = tool_call.id.to_s if tool_call.respond_to?(:id) && tool_call.id
|
|
270
288
|
|
|
@@ -341,6 +359,29 @@ module LanguageOperator
|
|
|
341
359
|
rescue StandardError => e
|
|
342
360
|
logger&.warn('Failed to record output metadata', error: e.message)
|
|
343
361
|
end
|
|
362
|
+
|
|
363
|
+
# Add agent context attributes to span attributes hash
|
|
364
|
+
#
|
|
365
|
+
# Ensures all spans include agent identification required for learning system.
|
|
366
|
+
# This is redundant with resource attributes but provides explicit visibility.
|
|
367
|
+
#
|
|
368
|
+
# @param attributes [Hash] Span attributes hash to modify
|
|
369
|
+
def add_agent_context_attributes(attributes)
|
|
370
|
+
# Agent name is CRITICAL for learning controller to track executions
|
|
371
|
+
if (agent_name = ENV.fetch('AGENT_NAME', nil))
|
|
372
|
+
attributes['agent.name'] = agent_name
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Add agent mode for better traceability
|
|
376
|
+
if (agent_mode = ENV.fetch('AGENT_MODE', nil))
|
|
377
|
+
attributes['agent.mode'] = agent_mode
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Add cluster context if available
|
|
381
|
+
if (cluster_name = ENV.fetch('AGENT_CLUSTER', nil))
|
|
382
|
+
attributes['agent.cluster'] = cluster_name
|
|
383
|
+
end
|
|
384
|
+
end
|
|
344
385
|
end
|
|
345
386
|
# rubocop:enable Metrics/ModuleLength
|
|
346
387
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'k8s-ruby'
|
|
4
4
|
require 'yaml'
|
|
5
5
|
require_relative '../utils/secure_path'
|
|
6
|
+
require_relative '../agent/event_config'
|
|
6
7
|
|
|
7
8
|
module LanguageOperator
|
|
8
9
|
module Kubernetes
|
|
@@ -163,6 +164,72 @@ module LanguageOperator
|
|
|
163
164
|
create_resource(resource)
|
|
164
165
|
end
|
|
165
166
|
|
|
167
|
+
# Create a Kubernetes Event
|
|
168
|
+
# @param event [Hash] Event resource hash
|
|
169
|
+
# @return [K8s::Resource] Created event resource
|
|
170
|
+
def create_event(event)
|
|
171
|
+
# Ensure proper apiVersion and kind for events
|
|
172
|
+
event = event.merge({
|
|
173
|
+
'apiVersion' => 'v1',
|
|
174
|
+
'kind' => 'Event'
|
|
175
|
+
})
|
|
176
|
+
create_resource(event)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Emit an execution event for agent task completion
|
|
180
|
+
# @param task_name [String] Name of the executed task
|
|
181
|
+
# @param success [Boolean] Whether task succeeded
|
|
182
|
+
# @param duration_ms [Float] Execution duration in milliseconds
|
|
183
|
+
# @param metadata [Hash] Additional metadata to include
|
|
184
|
+
# @return [K8s::Resource, nil] Created event resource or nil if disabled
|
|
185
|
+
def emit_execution_event(task_name, success:, duration_ms:, metadata: {})
|
|
186
|
+
# Check if events are enabled based on configuration
|
|
187
|
+
event_config = Agent::EventConfig.load
|
|
188
|
+
event_type = success ? :success : :failure
|
|
189
|
+
return nil unless Agent::EventConfig.should_emit?(event_type, event_config)
|
|
190
|
+
|
|
191
|
+
agent_name = ENV.fetch('AGENT_NAME', nil)
|
|
192
|
+
agent_namespace = ENV.fetch('AGENT_NAMESPACE', current_namespace)
|
|
193
|
+
|
|
194
|
+
return nil unless agent_name && agent_namespace
|
|
195
|
+
|
|
196
|
+
timestamp = Time.now.to_i
|
|
197
|
+
event_name = "#{agent_name}-task-#{task_name}-#{timestamp}"
|
|
198
|
+
|
|
199
|
+
event = {
|
|
200
|
+
'metadata' => {
|
|
201
|
+
'name' => event_name,
|
|
202
|
+
'namespace' => agent_namespace,
|
|
203
|
+
'labels' => {
|
|
204
|
+
'app.kubernetes.io/name' => 'language-operator',
|
|
205
|
+
'app.kubernetes.io/component' => 'agent',
|
|
206
|
+
'langop.io/agent-name' => agent_name,
|
|
207
|
+
'langop.io/task-name' => task_name.to_s
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
'involvedObject' => {
|
|
211
|
+
'kind' => 'LanguageAgent',
|
|
212
|
+
'name' => agent_name,
|
|
213
|
+
'namespace' => agent_namespace,
|
|
214
|
+
'apiVersion' => 'langop.io/v1alpha1'
|
|
215
|
+
},
|
|
216
|
+
'reason' => success ? 'TaskCompleted' : 'TaskFailed',
|
|
217
|
+
'message' => build_event_message(task_name, success, duration_ms, metadata, event_config),
|
|
218
|
+
'type' => success ? 'Normal' : 'Warning',
|
|
219
|
+
'firstTimestamp' => Time.now.utc.iso8601,
|
|
220
|
+
'lastTimestamp' => Time.now.utc.iso8601,
|
|
221
|
+
'source' => {
|
|
222
|
+
'component' => 'language-operator-agent'
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
create_event(event)
|
|
227
|
+
rescue StandardError => e
|
|
228
|
+
# Log error but don't fail task execution
|
|
229
|
+
warn "Failed to emit execution event: #{e.message}"
|
|
230
|
+
nil
|
|
231
|
+
end
|
|
232
|
+
|
|
166
233
|
# Check if operator is installed
|
|
167
234
|
def operator_installed?
|
|
168
235
|
# Check if LanguageCluster CRD exists
|
|
@@ -184,6 +251,48 @@ module LanguageOperator
|
|
|
184
251
|
|
|
185
252
|
private
|
|
186
253
|
|
|
254
|
+
# Build event message for task execution
|
|
255
|
+
# @param task_name [String] Task name
|
|
256
|
+
# @param success [Boolean] Whether task succeeded
|
|
257
|
+
# @param duration_ms [Float] Execution duration in milliseconds
|
|
258
|
+
# @param metadata [Hash] Additional metadata
|
|
259
|
+
# @param event_config [Hash] Event configuration
|
|
260
|
+
# @return [String] Formatted event message
|
|
261
|
+
def build_event_message(task_name, success, duration_ms, metadata = {}, event_config = nil)
|
|
262
|
+
event_config ||= Agent::EventConfig.load
|
|
263
|
+
content_config = Agent::EventConfig.content_config(event_config)
|
|
264
|
+
|
|
265
|
+
status = success ? 'completed successfully' : 'failed'
|
|
266
|
+
message = "Task '#{task_name}' #{status} in #{duration_ms.round(2)}ms"
|
|
267
|
+
|
|
268
|
+
# Include metadata if configured
|
|
269
|
+
if content_config[:include_task_metadata] && metadata.any?
|
|
270
|
+
# Filter metadata based on configuration
|
|
271
|
+
filtered_metadata = metadata.dup
|
|
272
|
+
|
|
273
|
+
# Remove error details if not configured to include them
|
|
274
|
+
unless content_config[:include_error_details]
|
|
275
|
+
filtered_metadata.delete('error_type')
|
|
276
|
+
filtered_metadata.delete('error_category')
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
if filtered_metadata.any?
|
|
280
|
+
details = filtered_metadata.map { |k, v| "#{k}: #{v}" }.join(', ')
|
|
281
|
+
message += " (#{details})"
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Truncate if configured and message is too long
|
|
286
|
+
if content_config[:truncate_long_messages] &&
|
|
287
|
+
message.length > content_config[:max_message_length]
|
|
288
|
+
max_length = content_config[:max_message_length]
|
|
289
|
+
truncated_length = message.length - max_length
|
|
290
|
+
message = message[0...max_length] + "... (truncated #{truncated_length} chars)"
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
message
|
|
294
|
+
end
|
|
295
|
+
|
|
187
296
|
# Build singleton instance with automatic config detection
|
|
188
297
|
def self.build_singleton
|
|
189
298
|
if in_cluster?
|
|
@@ -266,6 +375,8 @@ module LanguageOperator
|
|
|
266
375
|
'statefulsets'
|
|
267
376
|
when 'cronjob'
|
|
268
377
|
'cronjobs'
|
|
378
|
+
when 'event'
|
|
379
|
+
'events'
|
|
269
380
|
else
|
|
270
381
|
# Generic pluralization - add 's'
|
|
271
382
|
"#{kind.downcase}s"
|