language-operator 0.1.61 → 0.1.62

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