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
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'pastel'
|
|
4
|
-
|
|
5
|
-
module LanguageOperator
|
|
6
|
-
module CLI
|
|
7
|
-
module Formatters
|
|
8
|
-
# Formats optimization analysis and proposals for CLI display
|
|
9
|
-
class OptimizationFormatter
|
|
10
|
-
def initialize
|
|
11
|
-
@pastel = Pastel.new
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
# Format analysis results showing optimization opportunities
|
|
15
|
-
#
|
|
16
|
-
# @param agent_name [String] Name of the agent
|
|
17
|
-
# @param opportunities [Array<Hash>] Optimization opportunities
|
|
18
|
-
# @return [String] Formatted output
|
|
19
|
-
def format_analysis(agent_name:, opportunities:)
|
|
20
|
-
output = []
|
|
21
|
-
output << ''
|
|
22
|
-
output << @pastel.bold("Analyzing agent '#{agent_name}'...")
|
|
23
|
-
output << @pastel.dim('─' * 70)
|
|
24
|
-
output << ''
|
|
25
|
-
|
|
26
|
-
if opportunities.empty?
|
|
27
|
-
output << @pastel.yellow('No optimization opportunities found.')
|
|
28
|
-
output << ''
|
|
29
|
-
output << 'Possible reasons:'
|
|
30
|
-
output << ' • All tasks are already symbolic'
|
|
31
|
-
output << " • Neural tasks haven't executed enough times (need 10+)"
|
|
32
|
-
output << ' • Execution patterns are too inconsistent (<85%)'
|
|
33
|
-
output << ''
|
|
34
|
-
return output.join("\n")
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Group by status
|
|
38
|
-
ready = opportunities.select { |opp| opp[:ready_for_learning] }
|
|
39
|
-
not_ready = opportunities.reject { |opp| opp[:ready_for_learning] }
|
|
40
|
-
|
|
41
|
-
output << @pastel.bold("Found #{opportunities.size} neural task(s)\n")
|
|
42
|
-
|
|
43
|
-
# Show ready tasks
|
|
44
|
-
if ready.any?
|
|
45
|
-
output << @pastel.green.bold("✓ Ready for Optimization (#{ready.size})")
|
|
46
|
-
output << ''
|
|
47
|
-
ready.each do |opp|
|
|
48
|
-
output << format_opportunity(opp, ready: true)
|
|
49
|
-
output << ''
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Show not ready tasks
|
|
54
|
-
if not_ready.any?
|
|
55
|
-
output << @pastel.yellow.bold("⚠ Not Ready (#{not_ready.size})")
|
|
56
|
-
output << ''
|
|
57
|
-
not_ready.each do |opp|
|
|
58
|
-
output << format_opportunity(opp, ready: false)
|
|
59
|
-
output << ''
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Summary
|
|
64
|
-
output << @pastel.dim('─' * 70)
|
|
65
|
-
output << if ready.any?
|
|
66
|
-
@pastel.green.bold("#{ready.size}/#{opportunities.size} tasks eligible for optimization")
|
|
67
|
-
else
|
|
68
|
-
@pastel.yellow("0/#{opportunities.size} tasks ready - check requirements above")
|
|
69
|
-
end
|
|
70
|
-
output << ''
|
|
71
|
-
|
|
72
|
-
output.join("\n")
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# Format a single optimization opportunity
|
|
76
|
-
#
|
|
77
|
-
# @param opp [Hash] Opportunity data
|
|
78
|
-
# @param ready [Boolean] Whether task is ready for optimization
|
|
79
|
-
# @return [String] Formatted output
|
|
80
|
-
def format_opportunity(opp, ready:)
|
|
81
|
-
output = []
|
|
82
|
-
|
|
83
|
-
# Task name
|
|
84
|
-
status_icon = ready ? @pastel.green('✓') : @pastel.yellow('⚠')
|
|
85
|
-
output << " #{status_icon} #{@pastel.bold(opp[:task_name])}"
|
|
86
|
-
|
|
87
|
-
# Metrics
|
|
88
|
-
exec_label = @pastel.dim(' Executions:')
|
|
89
|
-
exec_value = format_count_status(opp[:execution_count], 10, ready)
|
|
90
|
-
output << "#{exec_label} #{exec_value}"
|
|
91
|
-
|
|
92
|
-
cons_label = @pastel.dim(' Consistency:')
|
|
93
|
-
cons_value = format_percentage_status(opp[:consistency_score], 0.85, ready)
|
|
94
|
-
output << "#{cons_label} #{cons_value}"
|
|
95
|
-
|
|
96
|
-
# Pattern or reason
|
|
97
|
-
if ready && opp[:common_pattern]
|
|
98
|
-
output << "#{@pastel.dim(' Pattern:')} #{opp[:common_pattern]}"
|
|
99
|
-
elsif opp[:reason]
|
|
100
|
-
output << "#{@pastel.dim(' Reason:')} #{@pastel.yellow(opp[:reason])}"
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
output.join("\n")
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Format optimization proposal with diff and metrics
|
|
107
|
-
#
|
|
108
|
-
# @param proposal [Hash] Proposal data
|
|
109
|
-
# @return [String] Formatted output
|
|
110
|
-
def format_proposal(proposal:)
|
|
111
|
-
output = []
|
|
112
|
-
output << ''
|
|
113
|
-
output << @pastel.bold("Optimization Proposal: #{proposal[:task_name]}")
|
|
114
|
-
output << @pastel.dim('=' * 70)
|
|
115
|
-
output << ''
|
|
116
|
-
|
|
117
|
-
# Current code
|
|
118
|
-
output << @pastel.yellow.bold('Current (Neural):')
|
|
119
|
-
output << @pastel.dim('─' * 70)
|
|
120
|
-
proposal[:current_code].each_line do |line|
|
|
121
|
-
output << @pastel.yellow(" #{line.rstrip}")
|
|
122
|
-
end
|
|
123
|
-
output << ''
|
|
124
|
-
|
|
125
|
-
# Proposed code
|
|
126
|
-
output << @pastel.green.bold('Proposed (Symbolic):')
|
|
127
|
-
output << @pastel.dim('─' * 70)
|
|
128
|
-
proposal[:proposed_code].each_line do |line|
|
|
129
|
-
output << @pastel.green(" #{line.rstrip}")
|
|
130
|
-
end
|
|
131
|
-
output << ''
|
|
132
|
-
|
|
133
|
-
# Performance impact
|
|
134
|
-
output << @pastel.bold('Performance Impact:')
|
|
135
|
-
output << @pastel.dim('─' * 70)
|
|
136
|
-
impact = proposal[:performance_impact]
|
|
137
|
-
output << format_impact_line('Execution Time:', impact[:current_avg_time], impact[:optimized_avg_time], 's', impact[:time_reduction_pct])
|
|
138
|
-
output << format_impact_line('Cost Per Call:', impact[:current_avg_cost], impact[:optimized_avg_cost], '$', impact[:cost_reduction_pct])
|
|
139
|
-
output << ''
|
|
140
|
-
output << " #{@pastel.dim('Projected Monthly Savings:')} #{@pastel.green.bold("$#{impact[:projected_monthly_savings]}")}"
|
|
141
|
-
output << ''
|
|
142
|
-
|
|
143
|
-
# Metadata
|
|
144
|
-
output << @pastel.bold('Analysis:')
|
|
145
|
-
output << @pastel.dim('─' * 70)
|
|
146
|
-
output << " #{@pastel.dim('Executions Observed:')} #{proposal[:execution_count]}"
|
|
147
|
-
output << " #{@pastel.dim('Pattern Consistency:')} #{format_percentage(proposal[:consistency_score])}"
|
|
148
|
-
output << " #{@pastel.dim('Tool Sequence:')} #{proposal[:pattern]}"
|
|
149
|
-
output << " #{@pastel.dim('Validation:')} #{proposal[:validation_violations].empty? ? @pastel.green('✓ Passed') : @pastel.red('✗ Failed')}"
|
|
150
|
-
output << ''
|
|
151
|
-
|
|
152
|
-
output.join("\n")
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
# Format success message after applying optimization
|
|
156
|
-
#
|
|
157
|
-
# @param result [Hash] Application result
|
|
158
|
-
# @return [String] Formatted output
|
|
159
|
-
def format_success(result:)
|
|
160
|
-
output = []
|
|
161
|
-
output << ''
|
|
162
|
-
|
|
163
|
-
if result[:success]
|
|
164
|
-
output << @pastel.green.bold('✓ Optimization applied successfully!')
|
|
165
|
-
output << ''
|
|
166
|
-
output << " Task '#{result[:task_name]}' has been optimized to symbolic execution."
|
|
167
|
-
output << ''
|
|
168
|
-
output << @pastel.dim('Next steps:')
|
|
169
|
-
output << " • Monitor performance: aictl agent logs #{result[:task_name]}"
|
|
170
|
-
output << " • View changes: aictl agent code #{result[:task_name]}"
|
|
171
|
-
else
|
|
172
|
-
output << @pastel.red.bold('✗ Optimization failed!')
|
|
173
|
-
output << ''
|
|
174
|
-
output << " Task: #{result[:task_name]}"
|
|
175
|
-
output << " Error: #{result[:error]}"
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
output << ''
|
|
179
|
-
output.join("\n")
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
private
|
|
183
|
-
|
|
184
|
-
# Format a count with status indicator
|
|
185
|
-
def format_count_status(count, threshold, ready)
|
|
186
|
-
if count >= threshold
|
|
187
|
-
@pastel.green("#{count} (≥#{threshold})")
|
|
188
|
-
else
|
|
189
|
-
ready ? @pastel.yellow("#{count}/#{threshold}") : @pastel.red("#{count}/#{threshold}")
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
# Format a percentage with status indicator
|
|
194
|
-
def format_percentage_status(score, threshold, ready)
|
|
195
|
-
return @pastel.red('N/A') if score.nil?
|
|
196
|
-
|
|
197
|
-
pct = (score * 100).round(1)
|
|
198
|
-
threshold_pct = (threshold * 100).round(1)
|
|
199
|
-
|
|
200
|
-
if score >= threshold
|
|
201
|
-
@pastel.green("#{pct}% (≥#{threshold_pct}%)")
|
|
202
|
-
else
|
|
203
|
-
ready ? @pastel.yellow("#{pct}%/#{threshold_pct}%") : @pastel.red("#{pct}%/#{threshold_pct}%")
|
|
204
|
-
end
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
# Format percentage value
|
|
208
|
-
def format_percentage(score)
|
|
209
|
-
return @pastel.red('N/A') if score.nil?
|
|
210
|
-
|
|
211
|
-
pct = (score * 100).round(1)
|
|
212
|
-
@pastel.green("#{pct}%")
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
# Format performance impact line
|
|
216
|
-
def format_impact_line(label, current, optimized, unit, reduction_pct)
|
|
217
|
-
current_str = unit == '$' ? format('$%.4f', current) : "#{current}#{unit}"
|
|
218
|
-
optimized_str = unit == '$' ? format('$%.4f', optimized) : "#{optimized}#{unit}"
|
|
219
|
-
|
|
220
|
-
" #{@pastel.dim(label)} #{current_str} → #{@pastel.green(optimized_str)} " \
|
|
221
|
-
"#{@pastel.green("(#{reduction_pct}% faster)")}"
|
|
222
|
-
end
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
end
|
|
226
|
-
end
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'pastel'
|
|
4
|
-
|
|
5
|
-
module LanguageOperator
|
|
6
|
-
module CLI
|
|
7
|
-
module Helpers
|
|
8
|
-
# Shared module providing Pastel color functionality
|
|
9
|
-
# to CLI commands, formatters, and helpers.
|
|
10
|
-
#
|
|
11
|
-
# Usage:
|
|
12
|
-
# include PastelHelper
|
|
13
|
-
# puts pastel.green("Success!")
|
|
14
|
-
module PastelHelper
|
|
15
|
-
# Returns a memoized Pastel instance for colorizing terminal output
|
|
16
|
-
#
|
|
17
|
-
# @return [Pastel] Pastel instance
|
|
18
|
-
def pastel
|
|
19
|
-
@pastel ||= Pastel.new
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module LanguageOperator
|
|
4
|
-
module Learning
|
|
5
|
-
module Adapters
|
|
6
|
-
# Abstract base class for OTLP backend query adapters
|
|
7
|
-
#
|
|
8
|
-
# Defines the interface that all backend adapters must implement.
|
|
9
|
-
# Adapters translate generic query requests into backend-specific
|
|
10
|
-
# API calls and normalize responses.
|
|
11
|
-
#
|
|
12
|
-
# @example Implementing a custom adapter
|
|
13
|
-
# class CustomAdapter < BaseAdapter
|
|
14
|
-
# def self.available?(endpoint, api_key = nil)
|
|
15
|
-
# # Check if backend is reachable
|
|
16
|
-
# true
|
|
17
|
-
# end
|
|
18
|
-
#
|
|
19
|
-
# def query_spans(filter:, time_range:, limit:)
|
|
20
|
-
# # Query backend and return normalized spans
|
|
21
|
-
# []
|
|
22
|
-
# end
|
|
23
|
-
# end
|
|
24
|
-
class BaseAdapter
|
|
25
|
-
# Initialize adapter with connection details
|
|
26
|
-
#
|
|
27
|
-
# @param endpoint [String] Backend endpoint URL
|
|
28
|
-
# @param api_key [String, nil] API key for authentication (if required)
|
|
29
|
-
# @param options [Hash] Additional adapter-specific options
|
|
30
|
-
def initialize(endpoint, api_key = nil, **options)
|
|
31
|
-
@endpoint = endpoint
|
|
32
|
-
@api_key = api_key
|
|
33
|
-
@logger = options[:logger] || ::Logger.new($stdout, level: ::Logger::WARN)
|
|
34
|
-
@options = options
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Check if this backend is available at the given endpoint
|
|
38
|
-
#
|
|
39
|
-
# @param endpoint [String] Backend endpoint URL
|
|
40
|
-
# @param api_key [String, nil] API key for authentication (optional)
|
|
41
|
-
# @return [Boolean] True if backend is reachable and compatible
|
|
42
|
-
def self.available?(endpoint, api_key = nil)
|
|
43
|
-
raise NotImplementedError, "#{self}.available? must be implemented"
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Query spans from the backend
|
|
47
|
-
#
|
|
48
|
-
# @param filter [Hash] Filter criteria
|
|
49
|
-
# @option filter [String] :task_name Task name to filter by
|
|
50
|
-
# @option filter [Hash] :attributes Additional span attributes to match
|
|
51
|
-
# @param time_range [Range<Time>] Time range for query
|
|
52
|
-
# @param limit [Integer] Maximum number of spans to return
|
|
53
|
-
# @return [Array<Hash>] Array of normalized span hashes
|
|
54
|
-
def query_spans(filter:, time_range:, limit:)
|
|
55
|
-
raise NotImplementedError, "#{self.class}#query_spans must be implemented"
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Extract task execution data from spans
|
|
59
|
-
#
|
|
60
|
-
# Groups spans by trace and extracts task-specific metadata:
|
|
61
|
-
# - inputs: Task input parameters
|
|
62
|
-
# - outputs: Task output values
|
|
63
|
-
# - tool_calls: Sequence of tools invoked
|
|
64
|
-
# - duration: Execution duration
|
|
65
|
-
#
|
|
66
|
-
# @param spans [Array<Hash>] Raw spans from backend
|
|
67
|
-
# @return [Array<Hash>] Task execution data grouped by trace
|
|
68
|
-
def extract_task_data(spans)
|
|
69
|
-
spans.group_by { |span| span[:trace_id] }.map do |trace_id, trace_spans|
|
|
70
|
-
task_span = trace_spans.find { |s| s[:name]&.include?('task_executor') }
|
|
71
|
-
next unless task_span
|
|
72
|
-
|
|
73
|
-
{
|
|
74
|
-
trace_id: trace_id,
|
|
75
|
-
task_name: task_span.dig(:attributes, 'task.name'),
|
|
76
|
-
inputs: extract_inputs(task_span),
|
|
77
|
-
outputs: extract_outputs(task_span),
|
|
78
|
-
tool_calls: extract_tool_calls(trace_spans),
|
|
79
|
-
duration_ms: task_span[:duration_ms],
|
|
80
|
-
timestamp: task_span[:timestamp]
|
|
81
|
-
}
|
|
82
|
-
end.compact
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
protected
|
|
86
|
-
|
|
87
|
-
attr_reader :endpoint, :api_key, :options
|
|
88
|
-
|
|
89
|
-
# Extract inputs from task span attributes
|
|
90
|
-
#
|
|
91
|
-
# @param span [Hash] Task execution span
|
|
92
|
-
# @return [Hash] Input parameters
|
|
93
|
-
def extract_inputs(span)
|
|
94
|
-
attrs = span[:attributes] || {}
|
|
95
|
-
input_keys = attrs['task.input.keys']&.split(',') || []
|
|
96
|
-
|
|
97
|
-
input_keys.each_with_object({}) do |key, inputs|
|
|
98
|
-
value_attr = "task.input.#{key}"
|
|
99
|
-
inputs[key.to_sym] = attrs[value_attr] if attrs[value_attr]
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
# Extract outputs from task span attributes
|
|
104
|
-
#
|
|
105
|
-
# @param span [Hash] Task execution span
|
|
106
|
-
# @return [Hash] Output values
|
|
107
|
-
def extract_outputs(span)
|
|
108
|
-
attrs = span[:attributes] || {}
|
|
109
|
-
output_keys = attrs['task.output.keys']&.split(',') || []
|
|
110
|
-
|
|
111
|
-
output_keys.each_with_object({}) do |key, outputs|
|
|
112
|
-
value_attr = "task.output.#{key}"
|
|
113
|
-
outputs[key.to_sym] = attrs[value_attr] if attrs[value_attr]
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# Extract tool call sequence from trace spans
|
|
118
|
-
#
|
|
119
|
-
# @param trace_spans [Array<Hash>] All spans in trace
|
|
120
|
-
# @return [Array<Hash>] Ordered tool call sequence
|
|
121
|
-
def extract_tool_calls(trace_spans)
|
|
122
|
-
trace_spans
|
|
123
|
-
.select { |s| s.dig(:attributes, 'gen_ai.operation.name') == 'execute_tool' }
|
|
124
|
-
.sort_by { |s| s[:timestamp] }
|
|
125
|
-
.map do |tool_span|
|
|
126
|
-
{
|
|
127
|
-
tool_name: tool_span.dig(:attributes, 'gen_ai.tool.name'),
|
|
128
|
-
arguments: tool_span.dig(:attributes, 'gen_ai.tool.call.arguments'),
|
|
129
|
-
arguments_size: tool_span.dig(:attributes, 'gen_ai.tool.call.arguments.size'),
|
|
130
|
-
result: tool_span.dig(:attributes, 'gen_ai.tool.call.result'),
|
|
131
|
-
result_size: tool_span.dig(:attributes, 'gen_ai.tool.call.result.size')
|
|
132
|
-
}
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
# Parse time range into backend-specific format
|
|
137
|
-
#
|
|
138
|
-
# @param time_range [Range<Time>] Time range
|
|
139
|
-
# @return [Hash] Start and end times
|
|
140
|
-
def parse_time_range(time_range)
|
|
141
|
-
{
|
|
142
|
-
start: time_range.begin || (Time.now - (24 * 60 * 60)),
|
|
143
|
-
end: time_range.end || Time.now
|
|
144
|
-
}
|
|
145
|
-
end
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
end
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'net/http'
|
|
4
|
-
require 'json'
|
|
5
|
-
require 'uri'
|
|
6
|
-
require_relative 'base_adapter'
|
|
7
|
-
|
|
8
|
-
module LanguageOperator
|
|
9
|
-
module Learning
|
|
10
|
-
module Adapters
|
|
11
|
-
# Jaeger backend adapter for trace queries
|
|
12
|
-
#
|
|
13
|
-
# Queries Jaeger's trace storage via gRPC QueryService API (port 16685).
|
|
14
|
-
# Falls back to HTTP API (port 16686) if gRPC is unavailable, though
|
|
15
|
-
# HTTP API is undocumented and not recommended for production use.
|
|
16
|
-
#
|
|
17
|
-
# Note: This implementation uses HTTP fallback initially. Full gRPC support
|
|
18
|
-
# requires the 'grpc' gem and generated protobuf stubs.
|
|
19
|
-
#
|
|
20
|
-
# @example Basic usage
|
|
21
|
-
# adapter = JaegerAdapter.new('http://jaeger-query:16686')
|
|
22
|
-
#
|
|
23
|
-
# spans = adapter.query_spans(
|
|
24
|
-
# filter: { task_name: 'fetch_data' },
|
|
25
|
-
# time_range: (Time.now - 3600)..Time.now,
|
|
26
|
-
# limit: 100
|
|
27
|
-
# )
|
|
28
|
-
class JaegerAdapter < BaseAdapter
|
|
29
|
-
# Jaeger HTTP API search endpoint
|
|
30
|
-
SEARCH_PATH = '/api/traces'
|
|
31
|
-
|
|
32
|
-
# Jaeger gRPC port (for future gRPC implementation)
|
|
33
|
-
GRPC_PORT = 16_685
|
|
34
|
-
|
|
35
|
-
# Check if Jaeger is available at endpoint
|
|
36
|
-
#
|
|
37
|
-
# @param endpoint [String] Jaeger endpoint URL
|
|
38
|
-
# @param _api_key [String, nil] API key (unused, Jaeger typically doesn't require auth)
|
|
39
|
-
# @return [Boolean] True if Jaeger API is reachable
|
|
40
|
-
def self.available?(endpoint, _api_key = nil)
|
|
41
|
-
# Try HTTP query endpoint first
|
|
42
|
-
uri = URI.join(endpoint, SEARCH_PATH)
|
|
43
|
-
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', open_timeout: 2, read_timeout: 2) do |http|
|
|
44
|
-
request = Net::HTTP::Get.new("#{uri.path}?service=test&limit=1")
|
|
45
|
-
http.request(request)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
response.is_a?(Net::HTTPSuccess)
|
|
49
|
-
rescue StandardError
|
|
50
|
-
false
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Query spans from Jaeger
|
|
54
|
-
#
|
|
55
|
-
# Uses HTTP API for search. Note: Jaeger searches by trace attributes (tags),
|
|
56
|
-
# returning traces that contain at least one matching span.
|
|
57
|
-
#
|
|
58
|
-
# @param filter [Hash] Filter criteria
|
|
59
|
-
# @option filter [String] :task_name Task name to filter by
|
|
60
|
-
# @param time_range [Range<Time>] Time range for query
|
|
61
|
-
# @param limit [Integer] Maximum traces to return
|
|
62
|
-
# @return [Array<Hash>] Normalized span data
|
|
63
|
-
def query_spans(filter:, time_range:, limit:)
|
|
64
|
-
times = parse_time_range(time_range)
|
|
65
|
-
traces = search_traces(filter, times, limit)
|
|
66
|
-
extract_spans_from_traces(traces)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
private
|
|
70
|
-
|
|
71
|
-
# Search traces via Jaeger HTTP API
|
|
72
|
-
#
|
|
73
|
-
# @param filter [Hash] Filter criteria
|
|
74
|
-
# @param times [Hash] Start and end times
|
|
75
|
-
# @param limit [Integer] Result limit
|
|
76
|
-
# @return [Array<Hash>] Trace data
|
|
77
|
-
def search_traces(filter, times, limit)
|
|
78
|
-
uri = build_search_uri(filter, times, limit)
|
|
79
|
-
|
|
80
|
-
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', open_timeout: 5, read_timeout: 30) do |http|
|
|
81
|
-
request = Net::HTTP::Get.new(uri.request_uri)
|
|
82
|
-
request['Accept'] = 'application/json'
|
|
83
|
-
|
|
84
|
-
response = http.request(request)
|
|
85
|
-
|
|
86
|
-
raise "Jaeger query failed: #{response.code} #{response.message}" unless response.is_a?(Net::HTTPSuccess)
|
|
87
|
-
|
|
88
|
-
result = JSON.parse(response.body, symbolize_names: true)
|
|
89
|
-
result[:data] || []
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
# Build Jaeger search URI with query parameters
|
|
94
|
-
#
|
|
95
|
-
# @param filter [Hash] Filter criteria
|
|
96
|
-
# @param times [Hash] Start and end times
|
|
97
|
-
# @param limit [Integer] Result limit
|
|
98
|
-
# @return [URI] Complete URI with query params
|
|
99
|
-
def build_search_uri(filter, times, limit)
|
|
100
|
-
params = {
|
|
101
|
-
limit: limit,
|
|
102
|
-
start: (times[:start].to_f * 1_000_000).to_i, # Microseconds
|
|
103
|
-
end: (times[:end].to_f * 1_000_000).to_i
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
# Jaeger requires a service name for search
|
|
107
|
-
# We use a wildcard or extract from task name
|
|
108
|
-
params[:service] = extract_service_name(filter)
|
|
109
|
-
|
|
110
|
-
# Add tag filters
|
|
111
|
-
tags = {}
|
|
112
|
-
tags['task.name'] = filter[:task_name] if filter[:task_name]
|
|
113
|
-
tags['agent.name'] = filter[:agent_name] if filter[:agent_name]
|
|
114
|
-
params[:tags] = tags.to_json unless tags.empty?
|
|
115
|
-
|
|
116
|
-
uri = URI.join(@endpoint, SEARCH_PATH)
|
|
117
|
-
uri.query = URI.encode_www_form(params)
|
|
118
|
-
uri
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
# Extract service name from filter or use wildcard
|
|
122
|
-
#
|
|
123
|
-
# @param filter [Hash] Filter criteria
|
|
124
|
-
# @return [String] Service name
|
|
125
|
-
def extract_service_name(filter)
|
|
126
|
-
# Jaeger requires service name, but we don't always know it
|
|
127
|
-
# Use task name prefix or wildcard
|
|
128
|
-
if filter[:task_name]
|
|
129
|
-
# Assume service name from task (e.g., "user_service.fetch_user")
|
|
130
|
-
parts = filter[:task_name].split('.')
|
|
131
|
-
parts.size > 1 ? parts[0] : 'agent'
|
|
132
|
-
else
|
|
133
|
-
'agent' # Default service name
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
# Extract all spans from traces
|
|
138
|
-
#
|
|
139
|
-
# @param traces [Array<Hash>] Jaeger trace data
|
|
140
|
-
# @return [Array<Hash>] Normalized spans
|
|
141
|
-
def extract_spans_from_traces(traces)
|
|
142
|
-
spans = []
|
|
143
|
-
|
|
144
|
-
traces.each do |trace|
|
|
145
|
-
trace_id = trace[:traceID]
|
|
146
|
-
process_map = build_process_map(trace[:processes])
|
|
147
|
-
|
|
148
|
-
(trace[:spans] || []).each do |span_data|
|
|
149
|
-
spans << normalize_span(span_data, trace_id, process_map)
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
spans
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
# Build process map for resource attributes
|
|
157
|
-
#
|
|
158
|
-
# @param processes [Hash] Process definitions
|
|
159
|
-
# @return [Hash] Process ID to name mapping
|
|
160
|
-
def build_process_map(processes)
|
|
161
|
-
return {} unless processes.is_a?(Hash)
|
|
162
|
-
|
|
163
|
-
processes.transform_values do |process|
|
|
164
|
-
process[:serviceName] || 'unknown'
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
# Normalize Jaeger span to common format
|
|
169
|
-
#
|
|
170
|
-
# @param span_data [Hash] Raw Jaeger span
|
|
171
|
-
# @param trace_id [String] Trace ID
|
|
172
|
-
# @param process_map [Hash] Process mapping
|
|
173
|
-
# @return [Hash] Normalized span
|
|
174
|
-
def normalize_span(span_data, trace_id, process_map)
|
|
175
|
-
process_id = span_data[:processID] || 'p1'
|
|
176
|
-
service_name = process_map[process_id] || 'unknown'
|
|
177
|
-
|
|
178
|
-
{
|
|
179
|
-
span_id: span_data[:spanID],
|
|
180
|
-
trace_id: trace_id,
|
|
181
|
-
name: span_data[:operationName] || service_name,
|
|
182
|
-
timestamp: parse_timestamp(span_data[:startTime]),
|
|
183
|
-
duration_ms: (span_data[:duration] || 0) / 1000.0, # Microseconds to milliseconds
|
|
184
|
-
attributes: parse_tags(span_data[:tags])
|
|
185
|
-
}
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
# Parse Jaeger timestamp (microseconds) to Time
|
|
189
|
-
#
|
|
190
|
-
# @param timestamp [Integer] Timestamp in microseconds
|
|
191
|
-
# @return [Time] Parsed time
|
|
192
|
-
def parse_timestamp(timestamp)
|
|
193
|
-
return Time.now unless timestamp
|
|
194
|
-
|
|
195
|
-
Time.at(timestamp / 1_000_000.0)
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
# Parse Jaeger tags into flat attributes hash
|
|
199
|
-
#
|
|
200
|
-
# @param tags [Array<Hash>] Tag array
|
|
201
|
-
# @return [Hash] Flat attributes
|
|
202
|
-
def parse_tags(tags)
|
|
203
|
-
return {} unless tags.is_a?(Array)
|
|
204
|
-
|
|
205
|
-
tags.each_with_object({}) do |tag, attrs|
|
|
206
|
-
key = tag[:key].to_s
|
|
207
|
-
value = tag[:value]
|
|
208
|
-
|
|
209
|
-
# Jaeger tags have type-specific value fields
|
|
210
|
-
# Extract the actual value
|
|
211
|
-
attrs[key] = if value.is_a?(Hash)
|
|
212
|
-
value[:stringValue] || value[:intValue] || value[:floatValue] || value[:boolValue]
|
|
213
|
-
else
|
|
214
|
-
value
|
|
215
|
-
end
|
|
216
|
-
end
|
|
217
|
-
end
|
|
218
|
-
end
|
|
219
|
-
end
|
|
220
|
-
end
|
|
221
|
-
end
|