language-operator 0.1.59 → 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 (144) 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 +14 -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 +369 -68
  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 +31 -1
  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/task_definition.rb +7 -6
  95. data/lib/language_operator/dsl.rb +153 -6
  96. data/lib/language_operator/errors.rb +50 -0
  97. data/lib/language_operator/kubernetes/client.rb +11 -6
  98. data/lib/language_operator/kubernetes/resource_builder.rb +58 -84
  99. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
  100. data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
  101. data/lib/language_operator/type_coercion.rb +118 -34
  102. data/lib/language_operator/utils/secure_path.rb +74 -0
  103. data/lib/language_operator/utils.rb +7 -0
  104. data/lib/language_operator/validators.rb +54 -2
  105. data/lib/language_operator/version.rb +1 -1
  106. data/synth/001/Makefile +10 -2
  107. data/synth/001/agent.rb +16 -15
  108. data/synth/001/output.log +27 -10
  109. data/synth/002/Makefile +10 -2
  110. data/synth/003/Makefile +3 -3
  111. data/synth/003/README.md +205 -133
  112. data/synth/003/agent.optimized.rb +66 -0
  113. data/synth/003/agent.synthesized.rb +41 -0
  114. metadata +111 -35
  115. data/docs/dsl/agent-reference.md +0 -604
  116. data/docs/dsl/mcp-integration.md +0 -1177
  117. data/docs/dsl/webhooks.md +0 -932
  118. data/docs/dsl/workflows.md +0 -744
  119. data/lib/language_operator/cli/commands/agent.rb +0 -1712
  120. data/lib/language_operator/cli/commands/model.rb +0 -366
  121. data/lib/language_operator/cli/commands/system.rb +0 -1259
  122. data/lib/language_operator/cli/commands/tool.rb +0 -654
  123. data/lib/language_operator/cli/formatters/optimization_formatter.rb +0 -226
  124. data/lib/language_operator/cli/helpers/pastel_helper.rb +0 -24
  125. data/lib/language_operator/learning/adapters/base_adapter.rb +0 -147
  126. data/lib/language_operator/learning/adapters/jaeger_adapter.rb +0 -218
  127. data/lib/language_operator/learning/adapters/signoz_adapter.rb +0 -432
  128. data/lib/language_operator/learning/adapters/tempo_adapter.rb +0 -236
  129. data/lib/language_operator/learning/optimizer.rb +0 -318
  130. data/lib/language_operator/learning/pattern_detector.rb +0 -260
  131. data/lib/language_operator/learning/task_synthesizer.rb +0 -261
  132. data/lib/language_operator/learning/trace_analyzer.rb +0 -280
  133. data/lib/language_operator/templates/task_synthesis.tmpl +0 -97
  134. data/lib/language_operator/ux/base.rb +0 -81
  135. data/lib/language_operator/ux/concerns/README.md +0 -155
  136. data/lib/language_operator/ux/concerns/headings.rb +0 -90
  137. data/lib/language_operator/ux/create_agent.rb +0 -255
  138. data/lib/language_operator/ux/create_model.rb +0 -267
  139. data/lib/language_operator/ux/quickstart.rb +0 -594
  140. data/synth/003/agent.rb +0 -41
  141. data/synth/003/output.log +0 -68
  142. /data/docs/{architecture/agent-runtime.md → agent-internals.md} +0 -0
  143. /data/docs/{dsl/chat-endpoints.md → chat-endpoints.md} +0 -0
  144. /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,147 +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_size: tool_span.dig(:attributes, 'gen_ai.tool.call.arguments.size'),
129
- result_size: tool_span.dig(:attributes, 'gen_ai.tool.call.result.size')
130
- }
131
- end
132
- end
133
-
134
- # Parse time range into backend-specific format
135
- #
136
- # @param time_range [Range<Time>] Time range
137
- # @return [Hash] Start and end times
138
- def parse_time_range(time_range)
139
- {
140
- start: time_range.begin || (Time.now - (24 * 60 * 60)),
141
- end: time_range.end || Time.now
142
- }
143
- end
144
- end
145
- end
146
- end
147
- end
@@ -1,218 +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
- params[:tags] = { 'task.name' => filter[:task_name] }.to_json if filter[:task_name]
112
-
113
- uri = URI.join(@endpoint, SEARCH_PATH)
114
- uri.query = URI.encode_www_form(params)
115
- uri
116
- end
117
-
118
- # Extract service name from filter or use wildcard
119
- #
120
- # @param filter [Hash] Filter criteria
121
- # @return [String] Service name
122
- def extract_service_name(filter)
123
- # Jaeger requires service name, but we don't always know it
124
- # Use task name prefix or wildcard
125
- if filter[:task_name]
126
- # Assume service name from task (e.g., "user_service.fetch_user")
127
- parts = filter[:task_name].split('.')
128
- parts.size > 1 ? parts[0] : 'agent'
129
- else
130
- 'agent' # Default service name
131
- end
132
- end
133
-
134
- # Extract all spans from traces
135
- #
136
- # @param traces [Array<Hash>] Jaeger trace data
137
- # @return [Array<Hash>] Normalized spans
138
- def extract_spans_from_traces(traces)
139
- spans = []
140
-
141
- traces.each do |trace|
142
- trace_id = trace[:traceID]
143
- process_map = build_process_map(trace[:processes])
144
-
145
- (trace[:spans] || []).each do |span_data|
146
- spans << normalize_span(span_data, trace_id, process_map)
147
- end
148
- end
149
-
150
- spans
151
- end
152
-
153
- # Build process map for resource attributes
154
- #
155
- # @param processes [Hash] Process definitions
156
- # @return [Hash] Process ID to name mapping
157
- def build_process_map(processes)
158
- return {} unless processes.is_a?(Hash)
159
-
160
- processes.transform_values do |process|
161
- process[:serviceName] || 'unknown'
162
- end
163
- end
164
-
165
- # Normalize Jaeger span to common format
166
- #
167
- # @param span_data [Hash] Raw Jaeger span
168
- # @param trace_id [String] Trace ID
169
- # @param process_map [Hash] Process mapping
170
- # @return [Hash] Normalized span
171
- def normalize_span(span_data, trace_id, process_map)
172
- process_id = span_data[:processID] || 'p1'
173
- service_name = process_map[process_id] || 'unknown'
174
-
175
- {
176
- span_id: span_data[:spanID],
177
- trace_id: trace_id,
178
- name: span_data[:operationName] || service_name,
179
- timestamp: parse_timestamp(span_data[:startTime]),
180
- duration_ms: (span_data[:duration] || 0) / 1000.0, # Microseconds to milliseconds
181
- attributes: parse_tags(span_data[:tags])
182
- }
183
- end
184
-
185
- # Parse Jaeger timestamp (microseconds) to Time
186
- #
187
- # @param timestamp [Integer] Timestamp in microseconds
188
- # @return [Time] Parsed time
189
- def parse_timestamp(timestamp)
190
- return Time.now unless timestamp
191
-
192
- Time.at(timestamp / 1_000_000.0)
193
- end
194
-
195
- # Parse Jaeger tags into flat attributes hash
196
- #
197
- # @param tags [Array<Hash>] Tag array
198
- # @return [Hash] Flat attributes
199
- def parse_tags(tags)
200
- return {} unless tags.is_a?(Array)
201
-
202
- tags.each_with_object({}) do |tag, attrs|
203
- key = tag[:key].to_s
204
- value = tag[:value]
205
-
206
- # Jaeger tags have type-specific value fields
207
- # Extract the actual value
208
- attrs[key] = if value.is_a?(Hash)
209
- value[:stringValue] || value[:intValue] || value[:floatValue] || value[:boolValue]
210
- else
211
- value
212
- end
213
- end
214
- end
215
- end
216
- end
217
- end
218
- end