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,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LanguageOperator
|
|
4
|
+
module CLI
|
|
5
|
+
module Commands
|
|
6
|
+
module Agent
|
|
7
|
+
# Code viewing and editing for agents
|
|
8
|
+
module CodeOperations
|
|
9
|
+
def self.included(base)
|
|
10
|
+
base.class_eval do
|
|
11
|
+
desc 'code NAME', 'Display synthesized agent code'
|
|
12
|
+
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
13
|
+
option :raw, type: :boolean, default: false, desc: 'Output raw code without formatting'
|
|
14
|
+
def code(name)
|
|
15
|
+
handle_command_error('get code') do
|
|
16
|
+
require_relative '../../formatters/code_formatter'
|
|
17
|
+
|
|
18
|
+
ctx = CLI::Helpers::ClusterContext.from_options(options)
|
|
19
|
+
|
|
20
|
+
# Get the code ConfigMap for this agent
|
|
21
|
+
configmap_name = "#{name}-code"
|
|
22
|
+
begin
|
|
23
|
+
configmap = ctx.client.get_resource('ConfigMap', configmap_name, ctx.namespace)
|
|
24
|
+
rescue K8s::Error::NotFound
|
|
25
|
+
Formatters::ProgressFormatter.error("Synthesized code not found for agent '#{name}'")
|
|
26
|
+
puts
|
|
27
|
+
puts 'Possible reasons:'
|
|
28
|
+
puts ' - Agent synthesis not yet complete'
|
|
29
|
+
puts ' - Agent synthesis failed'
|
|
30
|
+
puts
|
|
31
|
+
puts 'Check synthesis status with:'
|
|
32
|
+
puts " aictl agent inspect #{name}"
|
|
33
|
+
exit 1
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Get the agent.rb code from the ConfigMap
|
|
37
|
+
code_content = configmap.dig('data', 'agent.rb')
|
|
38
|
+
unless code_content
|
|
39
|
+
Formatters::ProgressFormatter.error('Code content not found in ConfigMap')
|
|
40
|
+
exit 1
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Raw output mode - just print the code
|
|
44
|
+
if options[:raw]
|
|
45
|
+
puts code_content
|
|
46
|
+
return
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Display with syntax highlighting
|
|
50
|
+
Formatters::CodeFormatter.display_ruby_code(
|
|
51
|
+
code_content,
|
|
52
|
+
title: "Synthesized Code for Agent: #{name}"
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
desc 'edit NAME', 'Edit agent instructions'
|
|
58
|
+
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
59
|
+
def edit(name)
|
|
60
|
+
handle_command_error('edit agent') do
|
|
61
|
+
ctx = CLI::Helpers::ClusterContext.from_options(options)
|
|
62
|
+
|
|
63
|
+
# Get current agent
|
|
64
|
+
agent = get_resource_or_exit(LanguageOperator::Constants::RESOURCE_AGENT, name)
|
|
65
|
+
|
|
66
|
+
current_instructions = agent.dig('spec', 'instructions')
|
|
67
|
+
|
|
68
|
+
# Edit instructions in user's editor
|
|
69
|
+
new_instructions = Helpers::EditorHelper.edit_content(
|
|
70
|
+
current_instructions,
|
|
71
|
+
'agent-instructions-',
|
|
72
|
+
'.txt'
|
|
73
|
+
).strip
|
|
74
|
+
|
|
75
|
+
# Check if changed
|
|
76
|
+
if new_instructions == current_instructions
|
|
77
|
+
Formatters::ProgressFormatter.info('No changes made')
|
|
78
|
+
return
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Update agent resource
|
|
82
|
+
agent['spec']['instructions'] = new_instructions
|
|
83
|
+
|
|
84
|
+
Formatters::ProgressFormatter.with_spinner('Updating agent instructions') do
|
|
85
|
+
ctx.client.apply_resource(agent)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
Formatters::ProgressFormatter.success('Agent instructions updated')
|
|
89
|
+
puts
|
|
90
|
+
puts 'The operator will automatically re-synthesize the agent code.'
|
|
91
|
+
puts
|
|
92
|
+
puts 'Watch synthesis progress with:'
|
|
93
|
+
puts " aictl agent inspect #{name}"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'socket'
|
|
4
|
+
require 'faraday'
|
|
5
|
+
require 'json'
|
|
6
|
+
|
|
7
|
+
module LanguageOperator
|
|
8
|
+
module CLI
|
|
9
|
+
module Commands
|
|
10
|
+
module Agent
|
|
11
|
+
module Helpers
|
|
12
|
+
# LLM client that uses port-forwarding to cluster model deployments (LiteLLM proxy)
|
|
13
|
+
class ClusterLLMClient
|
|
14
|
+
def initialize(ctx:, model_name:, model_id:, agent_command:)
|
|
15
|
+
@ctx = ctx
|
|
16
|
+
@model_name = model_name
|
|
17
|
+
@model_id = model_id
|
|
18
|
+
@agent_command = agent_command
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def chat(prompt)
|
|
22
|
+
pod = get_model_pod
|
|
23
|
+
pod_name = pod.dig('metadata', 'name')
|
|
24
|
+
|
|
25
|
+
local_port = find_available_port
|
|
26
|
+
port_forward_pid = nil
|
|
27
|
+
|
|
28
|
+
begin
|
|
29
|
+
port_forward_pid = start_port_forward(pod_name, local_port, 4000)
|
|
30
|
+
wait_for_port(local_port)
|
|
31
|
+
|
|
32
|
+
conn = Faraday.new(url: "http://localhost:#{local_port}") do |f|
|
|
33
|
+
f.request :json
|
|
34
|
+
f.response :json
|
|
35
|
+
f.adapter Faraday.default_adapter
|
|
36
|
+
f.options.timeout = 120
|
|
37
|
+
f.options.open_timeout = 10
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
payload = {
|
|
41
|
+
model: @model_id,
|
|
42
|
+
messages: [{ role: 'user', content: prompt }],
|
|
43
|
+
max_tokens: 4000,
|
|
44
|
+
temperature: 0.3
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
response = conn.post('/v1/chat/completions', payload)
|
|
48
|
+
result = response.body
|
|
49
|
+
|
|
50
|
+
raise "LLM error: #{result['error']['message'] || result['error']}" if result['error']
|
|
51
|
+
|
|
52
|
+
result.dig('choices', 0, 'message', 'content')
|
|
53
|
+
ensure
|
|
54
|
+
cleanup_port_forward(port_forward_pid) if port_forward_pid
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def get_model_pod
|
|
61
|
+
# Get the deployment for the model
|
|
62
|
+
deployment = @ctx.client.get_resource('Deployment', @model_name, @ctx.namespace)
|
|
63
|
+
raise "Deployment '#{@model_name}' not found in namespace '#{@ctx.namespace}'" if deployment.nil?
|
|
64
|
+
|
|
65
|
+
labels = deployment.dig('spec', 'selector', 'matchLabels')
|
|
66
|
+
|
|
67
|
+
# Find matching pods using centralized utility
|
|
68
|
+
pods = CLI::Helpers::LabelUtils.find_pods_by_deployment_labels(@ctx, @model_name, labels)
|
|
69
|
+
raise "No pods found for model '#{@model_name}'" if pods.empty?
|
|
70
|
+
|
|
71
|
+
running_pods = pods.select { |p| p.dig('status', 'phase') == 'Running' }
|
|
72
|
+
raise "No running pods found for model '#{@model_name}'" if running_pods.empty?
|
|
73
|
+
|
|
74
|
+
running_pods.first
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def find_available_port
|
|
78
|
+
server = TCPServer.new('127.0.0.1', 0)
|
|
79
|
+
port = server.addr[1]
|
|
80
|
+
server.close
|
|
81
|
+
port
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def start_port_forward(pod_name, local_port, remote_port)
|
|
85
|
+
pid = spawn(
|
|
86
|
+
'kubectl', 'port-forward',
|
|
87
|
+
'-n', @ctx.namespace,
|
|
88
|
+
"pod/#{pod_name}",
|
|
89
|
+
"#{local_port}:#{remote_port}",
|
|
90
|
+
%i[out err] => '/dev/null'
|
|
91
|
+
)
|
|
92
|
+
Process.detach(pid)
|
|
93
|
+
pid
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def wait_for_port(port, max_attempts: 30)
|
|
97
|
+
max_attempts.times do
|
|
98
|
+
TCPSocket.new('127.0.0.1', port).close
|
|
99
|
+
return true
|
|
100
|
+
rescue Errno::ECONNREFUSED
|
|
101
|
+
sleep 0.1
|
|
102
|
+
end
|
|
103
|
+
raise "Port #{port} not available after #{max_attempts} attempts"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def cleanup_port_forward(pid)
|
|
107
|
+
Process.kill('TERM', pid)
|
|
108
|
+
rescue Errno::ESRCH
|
|
109
|
+
# Process already gone
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LanguageOperator
|
|
4
|
+
module CLI
|
|
5
|
+
module Commands
|
|
6
|
+
module Agent
|
|
7
|
+
module Helpers
|
|
8
|
+
# Helper methods for parsing agent code from ConfigMaps
|
|
9
|
+
module CodeParser
|
|
10
|
+
# Load agent definition from ConfigMap
|
|
11
|
+
#
|
|
12
|
+
# @param ctx [ClusterContext] Cluster context
|
|
13
|
+
# @param agent_name [String] Name of the agent
|
|
14
|
+
# @return [Object, nil] Agent definition or nil if not found
|
|
15
|
+
def load_agent_definition(ctx, agent_name)
|
|
16
|
+
# Try to get the agent code ConfigMap
|
|
17
|
+
configmap_name = "#{agent_name}-code"
|
|
18
|
+
begin
|
|
19
|
+
configmap = ctx.client.get_resource('ConfigMap', configmap_name, ctx.namespace)
|
|
20
|
+
code_content = configmap.dig('data', 'agent.rb')
|
|
21
|
+
|
|
22
|
+
return nil unless code_content
|
|
23
|
+
|
|
24
|
+
# Parse the code to extract agent definition
|
|
25
|
+
# For now, we'll create a mock definition with the task structure
|
|
26
|
+
# In a full implementation, this would eval the code safely
|
|
27
|
+
parse_agent_code(code_content)
|
|
28
|
+
rescue K8s::Error::NotFound
|
|
29
|
+
nil
|
|
30
|
+
rescue StandardError => e
|
|
31
|
+
@logger&.error("Failed to load agent definition: #{e.message}")
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Parse agent code to extract definition
|
|
37
|
+
#
|
|
38
|
+
# @param code [String] Ruby agent code
|
|
39
|
+
# @return [Object] Agent definition structure
|
|
40
|
+
def parse_agent_code(code)
|
|
41
|
+
require_relative '../../../../dsl/agent_definition'
|
|
42
|
+
|
|
43
|
+
# Create a minimal agent definition structure
|
|
44
|
+
agent_def = Struct.new(:tasks, :name, :mcp_servers) do
|
|
45
|
+
def initialize
|
|
46
|
+
super({}, 'agent', {})
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
agent = agent_def.new
|
|
51
|
+
|
|
52
|
+
# Parse tasks from code - extract full task definitions
|
|
53
|
+
code.scan(/task\s+:(\w+),?\s*(.*?)(?=\n\s*(?:task\s+:|main\s+do|end\s*$))/m) do |match|
|
|
54
|
+
task_name = match[0].to_sym
|
|
55
|
+
task_block = match[1]
|
|
56
|
+
|
|
57
|
+
# Check if neural (has instructions but no do block) or symbolic
|
|
58
|
+
is_neural = task_block.include?('instructions:') && !task_block.match?(/\bdo\s*\|/)
|
|
59
|
+
|
|
60
|
+
# Extract instructions
|
|
61
|
+
instructions = extract_string_value(task_block, 'instructions')
|
|
62
|
+
|
|
63
|
+
# Extract inputs hash
|
|
64
|
+
inputs = extract_hash_value(task_block, 'inputs')
|
|
65
|
+
|
|
66
|
+
# Extract outputs hash
|
|
67
|
+
outputs = extract_hash_value(task_block, 'outputs')
|
|
68
|
+
|
|
69
|
+
task = Struct.new(:name, :neural?, :instructions, :inputs, :outputs).new(
|
|
70
|
+
task_name, is_neural, instructions, inputs, outputs
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
agent.tasks[task_name] = task
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
agent
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Extract a string value from DSL code (e.g., instructions: "...")
|
|
80
|
+
#
|
|
81
|
+
# @param code [String] Code snippet
|
|
82
|
+
# @param key [String] Key to extract
|
|
83
|
+
# @return [String] Extracted value or empty string
|
|
84
|
+
def extract_string_value(code, key)
|
|
85
|
+
# Match both single and double quoted strings, including multi-line
|
|
86
|
+
match = code.match(/#{key}:\s*(['"])(.*?)\1/m) ||
|
|
87
|
+
code.match(/#{key}:\s*(['"])(.+?)\1/m)
|
|
88
|
+
match ? match[2] : ''
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Extract a hash value from DSL code (e.g., inputs: { foo: 'bar' })
|
|
92
|
+
#
|
|
93
|
+
# @param code [String] Code snippet
|
|
94
|
+
# @param key [String] Key to extract
|
|
95
|
+
# @return [Hash] Extracted hash or empty hash
|
|
96
|
+
def extract_hash_value(code, key)
|
|
97
|
+
match = code.match(/#{key}:\s*\{([^}]*)\}/)
|
|
98
|
+
return {} unless match
|
|
99
|
+
|
|
100
|
+
hash_content = match[1].strip
|
|
101
|
+
return {} if hash_content.empty?
|
|
102
|
+
|
|
103
|
+
# Parse simple key: 'value' or key: "value" pairs
|
|
104
|
+
result = {}
|
|
105
|
+
hash_content.scan(/(\w+):\s*(['"])([^'"]*)\2/) do |k, _quote, v|
|
|
106
|
+
result[k.to_sym] = v
|
|
107
|
+
end
|
|
108
|
+
result
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LanguageOperator
|
|
4
|
+
module CLI
|
|
5
|
+
module Commands
|
|
6
|
+
module Agent
|
|
7
|
+
module Helpers
|
|
8
|
+
# Helper methods for watching agent synthesis status
|
|
9
|
+
module SynthesisWatcher
|
|
10
|
+
# Watch synthesis status with progress spinner
|
|
11
|
+
#
|
|
12
|
+
# @param k8s [Kubernetes::Client] Kubernetes client
|
|
13
|
+
# @param agent_name [String] Agent name
|
|
14
|
+
# @param namespace [String] Namespace
|
|
15
|
+
# @return [Hash] Synthesis result with success status and metadata
|
|
16
|
+
def watch_synthesis_status(k8s, agent_name, namespace)
|
|
17
|
+
max_wait = 600 # Wait up to 10 minutes (local models can be slow)
|
|
18
|
+
interval = 2 # Check every 2 seconds
|
|
19
|
+
elapsed = 0
|
|
20
|
+
start_time = Time.now
|
|
21
|
+
synthesis_data = {}
|
|
22
|
+
|
|
23
|
+
Formatters::ProgressFormatter.with_spinner('Synthesizing code from instructions') do
|
|
24
|
+
synthesis_result = nil
|
|
25
|
+
loop do
|
|
26
|
+
status = check_synthesis_status(k8s, agent_name, namespace, synthesis_data, start_time)
|
|
27
|
+
if status
|
|
28
|
+
synthesis_result = status
|
|
29
|
+
break
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Timeout check
|
|
33
|
+
if elapsed >= max_wait
|
|
34
|
+
Formatters::ProgressFormatter.warn('Synthesis taking longer than expected, continuing in background...')
|
|
35
|
+
puts
|
|
36
|
+
puts 'Check synthesis status with:'
|
|
37
|
+
puts " aictl agent inspect #{agent_name}"
|
|
38
|
+
synthesis_result = { success: true, timeout: true }
|
|
39
|
+
break
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
sleep interval
|
|
43
|
+
elapsed += interval
|
|
44
|
+
end
|
|
45
|
+
synthesis_result
|
|
46
|
+
rescue K8s::Error::NotFound
|
|
47
|
+
# Agent not found yet, keep waiting
|
|
48
|
+
sleep interval
|
|
49
|
+
elapsed += interval
|
|
50
|
+
retry if elapsed < max_wait
|
|
51
|
+
|
|
52
|
+
Formatters::ProgressFormatter.error('Agent resource not found')
|
|
53
|
+
return { success: false }
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
Formatters::ProgressFormatter.warn("Could not watch synthesis: #{e.message}")
|
|
56
|
+
return { success: true } # Continue anyway
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Check synthesis status for a specific agent
|
|
61
|
+
#
|
|
62
|
+
# @param k8s [Kubernetes::Client] Kubernetes client
|
|
63
|
+
# @param agent_name [String] Agent name
|
|
64
|
+
# @param namespace [String] Namespace
|
|
65
|
+
# @param synthesis_data [Hash] Hash to populate with synthesis metadata
|
|
66
|
+
# @param start_time [Time] Synthesis start time
|
|
67
|
+
# @return [Hash, nil] Synthesis status or nil if not complete
|
|
68
|
+
def check_synthesis_status(k8s, agent_name, namespace, synthesis_data, start_time)
|
|
69
|
+
agent = k8s.get_resource('LanguageAgent', agent_name, namespace)
|
|
70
|
+
conditions = agent.dig('status', 'conditions') || []
|
|
71
|
+
synthesis_status = agent.dig('status', 'synthesis')
|
|
72
|
+
|
|
73
|
+
# Capture synthesis metadata
|
|
74
|
+
if synthesis_status
|
|
75
|
+
synthesis_data[:model] = synthesis_status['model']
|
|
76
|
+
synthesis_data[:token_count] = synthesis_status['tokenCount']
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Check for synthesis completion
|
|
80
|
+
synthesized = conditions.find { |c| c['type'] == 'Synthesized' }
|
|
81
|
+
return nil unless synthesized
|
|
82
|
+
|
|
83
|
+
if synthesized['status'] == 'True'
|
|
84
|
+
duration = Time.now - start_time
|
|
85
|
+
{ success: true, duration: duration, **synthesis_data }
|
|
86
|
+
elsif synthesized['status'] == 'False'
|
|
87
|
+
Formatters::ProgressFormatter.error("Synthesis failed: #{synthesized['message']}")
|
|
88
|
+
{ success: false }
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|