language-operator 0.1.58 → 0.1.61

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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/components/agent/Gemfile +1 -1
  4. data/lib/language_operator/agent/base.rb +22 -0
  5. data/lib/language_operator/agent/task_executor.rb +80 -23
  6. data/lib/language_operator/agent/telemetry.rb +22 -11
  7. data/lib/language_operator/agent.rb +3 -0
  8. data/lib/language_operator/cli/base_command.rb +7 -1
  9. data/lib/language_operator/cli/commands/agent.rb +575 -0
  10. data/lib/language_operator/cli/formatters/optimization_formatter.rb +226 -0
  11. data/lib/language_operator/cli/formatters/progress_formatter.rb +1 -1
  12. data/lib/language_operator/client/base.rb +74 -2
  13. data/lib/language_operator/client/mcp_connector.rb +4 -6
  14. data/lib/language_operator/dsl/task_definition.rb +7 -6
  15. data/lib/language_operator/learning/adapters/base_adapter.rb +149 -0
  16. data/lib/language_operator/learning/adapters/jaeger_adapter.rb +221 -0
  17. data/lib/language_operator/learning/adapters/signoz_adapter.rb +435 -0
  18. data/lib/language_operator/learning/adapters/tempo_adapter.rb +239 -0
  19. data/lib/language_operator/learning/optimizer.rb +319 -0
  20. data/lib/language_operator/learning/pattern_detector.rb +260 -0
  21. data/lib/language_operator/learning/task_synthesizer.rb +288 -0
  22. data/lib/language_operator/learning/trace_analyzer.rb +285 -0
  23. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
  24. data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
  25. data/lib/language_operator/templates/task_synthesis.tmpl +98 -0
  26. data/lib/language_operator/ux/concerns/provider_helpers.rb +2 -2
  27. data/lib/language_operator/version.rb +1 -1
  28. data/synth/003/Makefile +10 -3
  29. data/synth/003/output.log +68 -0
  30. data/synth/README.md +1 -3
  31. metadata +12 -1
@@ -0,0 +1,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module LanguageOperator
6
+ module Learning
7
+ # Detects patterns in task execution traces and generates symbolic code
8
+ #
9
+ # The PatternDetector analyzes execution patterns from TraceAnalyzer and
10
+ # converts deterministic neural task behavior into symbolic Ruby DSL v1 code.
11
+ # This enables the learning system to automatically optimize neural tasks
12
+ # into faster, cheaper symbolic implementations.
13
+ #
14
+ # @example Basic usage
15
+ # analyzer = TraceAnalyzer.new(endpoint: ENV['OTEL_QUERY_ENDPOINT'])
16
+ # validator = Agent::Safety::ASTValidator.new
17
+ # detector = PatternDetector.new(trace_analyzer: analyzer, validator: validator)
18
+ #
19
+ # analysis = analyzer.analyze_patterns(task_name: 'fetch_user_data')
20
+ # result = detector.detect_pattern(analysis_result: analysis)
21
+ #
22
+ # if result[:success]
23
+ # puts "Generated code:"
24
+ # puts result[:generated_code]
25
+ # end
26
+ class PatternDetector
27
+ # Default minimum consistency threshold for learning
28
+ DEFAULT_CONSISTENCY_THRESHOLD = 0.85
29
+
30
+ # Default minimum executions required before learning
31
+ DEFAULT_MIN_EXECUTIONS = 10
32
+
33
+ # Minimum pattern confidence for code generation
34
+ MIN_PATTERN_CONFIDENCE = 0.75
35
+
36
+ # Initialize pattern detector
37
+ #
38
+ # @param trace_analyzer [TraceAnalyzer] Analyzer for querying execution traces
39
+ # @param validator [Agent::Safety::ASTValidator] Validator for generated code
40
+ # @param logger [Logger, nil] Logger instance (creates default if nil)
41
+ def initialize(trace_analyzer:, validator:, logger: nil)
42
+ @trace_analyzer = trace_analyzer
43
+ @validator = validator
44
+ @logger = logger || ::Logger.new($stdout, level: ::Logger::WARN)
45
+ end
46
+
47
+ # Detect patterns and generate symbolic code
48
+ #
49
+ # Main entry point for pattern detection. Takes analysis results from
50
+ # TraceAnalyzer and generates validated symbolic code if the pattern
51
+ # meets consistency and execution count thresholds.
52
+ #
53
+ # @param analysis_result [Hash] Result from TraceAnalyzer#analyze_patterns
54
+ # @return [Hash] Detection result with generated code and metadata
55
+ def detect_pattern(analysis_result:)
56
+ # Validate that we can generate code from this analysis
57
+ return early_rejection_result(analysis_result) unless can_generate_code?(analysis_result)
58
+
59
+ # Generate symbolic code from the pattern
60
+ code = generate_symbolic_code(
61
+ pattern: analysis_result[:common_pattern],
62
+ task_name: analysis_result[:task_name]
63
+ )
64
+
65
+ # Validate the generated code
66
+ validation_result = validate_generated_code(code: code)
67
+
68
+ # Build result
69
+ {
70
+ success: validation_result[:valid],
71
+ task_name: analysis_result[:task_name],
72
+ generated_code: code,
73
+ validation_violations: validation_result[:violations],
74
+ consistency_score: analysis_result[:consistency_score],
75
+ execution_count: analysis_result[:execution_count],
76
+ pattern: analysis_result[:common_pattern],
77
+ ready_to_deploy: validation_result[:valid] && analysis_result[:consistency_score] >= 0.90,
78
+ generated_at: Time.now.iso8601
79
+ }
80
+ end
81
+
82
+ # Generate symbolic Ruby code from tool call pattern
83
+ #
84
+ # Converts a deterministic tool call sequence into a valid Ruby DSL v1
85
+ # task definition with chained execute_task calls.
86
+ #
87
+ # @param pattern [String] Tool sequence like "db_fetch → cache_get → api"
88
+ # @param task_name [String] Name of the task being learned
89
+ # @return [String] Complete Ruby DSL v1 agent definition
90
+ def generate_symbolic_code(pattern:, task_name:)
91
+ sequence = extract_tool_sequence(pattern)
92
+
93
+ # Generate the task code body with chained execute_task calls
94
+ task_body = generate_task_code(sequence: sequence)
95
+
96
+ # Wrap in complete agent definition
97
+ generate_agent_wrapper(
98
+ task_name: task_name,
99
+ task_body: task_body
100
+ )
101
+ end
102
+
103
+ # Validate generated code with ASTValidator
104
+ #
105
+ # @param code [String] Ruby code to validate
106
+ # @return [Hash] Validation result with violations
107
+ def validate_generated_code(code:)
108
+ violations = @validator.validate(code)
109
+
110
+ {
111
+ valid: violations.empty?,
112
+ violations: violations,
113
+ safe_methods_used: true
114
+ }
115
+ rescue StandardError => e
116
+ @logger.error("Failed to validate generated code: #{e.message}")
117
+ {
118
+ valid: false,
119
+ violations: [{ type: :validation_error, message: e.message }],
120
+ safe_methods_used: false
121
+ }
122
+ end
123
+
124
+ private
125
+
126
+ # Check if pattern analysis meets criteria for code generation
127
+ #
128
+ # @param analysis_result [Hash] Analysis result from TraceAnalyzer
129
+ # @return [Boolean] True if code can be generated
130
+ def can_generate_code?(analysis_result)
131
+ return false unless analysis_result.is_a?(Hash)
132
+
133
+ # Check if ready for learning flag is set
134
+ return false unless analysis_result[:ready_for_learning]
135
+
136
+ # Check execution count
137
+ return false if analysis_result[:execution_count].to_i < DEFAULT_MIN_EXECUTIONS
138
+
139
+ # Check consistency score
140
+ return false if analysis_result[:consistency_score].to_f < DEFAULT_CONSISTENCY_THRESHOLD
141
+
142
+ # Check pattern exists
143
+ return false if analysis_result[:common_pattern].nil? || analysis_result[:common_pattern].empty?
144
+
145
+ true
146
+ end
147
+
148
+ # Build early rejection result when criteria not met
149
+ #
150
+ # @param analysis_result [Hash, nil] Analysis result if available
151
+ # @return [Hash] Rejection result with reason
152
+ def early_rejection_result(analysis_result)
153
+ if analysis_result.nil? || !analysis_result.is_a?(Hash)
154
+ return {
155
+ success: false,
156
+ reason: 'Invalid analysis result'
157
+ }
158
+ end
159
+
160
+ reasons = []
161
+ if analysis_result[:execution_count].to_i < DEFAULT_MIN_EXECUTIONS
162
+ reasons << "Insufficient executions (#{analysis_result[:execution_count]}/#{DEFAULT_MIN_EXECUTIONS})"
163
+ end
164
+ if analysis_result[:consistency_score].to_f < DEFAULT_CONSISTENCY_THRESHOLD
165
+ reasons << "Low consistency (#{analysis_result[:consistency_score]}/#{DEFAULT_CONSISTENCY_THRESHOLD})"
166
+ end
167
+ reasons << 'No common pattern found' if analysis_result[:common_pattern].nil? || analysis_result[:common_pattern].empty?
168
+
169
+ {
170
+ success: false,
171
+ task_name: analysis_result[:task_name],
172
+ execution_count: analysis_result[:execution_count],
173
+ consistency_score: analysis_result[:consistency_score],
174
+ ready_for_learning: false,
175
+ reason: reasons.join('; ')
176
+ }
177
+ end
178
+
179
+ # Extract tool sequence from pattern string
180
+ #
181
+ # @param pattern [String] Pattern like "db_fetch → cache_get → api"
182
+ # @return [Array<Symbol>] Sequence like [:db_fetch, :cache_get, :api]
183
+ def extract_tool_sequence(pattern)
184
+ pattern.split('→').map(&:strip).map(&:to_sym)
185
+ end
186
+
187
+ # Generate task code body with chained execute_task calls
188
+ #
189
+ # Creates Ruby code that executes tools in sequence, passing outputs
190
+ # from each tool to the next one as inputs.
191
+ #
192
+ # @param sequence [Array<Symbol>] Tool sequence
193
+ # @return [String] Ruby code for task body
194
+ def generate_task_code(sequence:)
195
+ return ' { result: {} }' if sequence.empty?
196
+
197
+ lines = []
198
+
199
+ # First call: use original inputs
200
+ first_tool = sequence[0]
201
+ lines << "step1_result = execute_task(:#{first_tool}, inputs: inputs)"
202
+
203
+ # Middle calls: chain outputs from previous step
204
+ if sequence.size > 1
205
+ sequence[1..-2].each_with_index do |tool, index|
206
+ step_num = index + 2
207
+ prev_step = "step#{step_num - 1}_result"
208
+ lines << "step#{step_num}_result = execute_task(:#{tool}, inputs: #{prev_step})"
209
+ end
210
+
211
+ # Final call
212
+ final_tool = sequence[-1]
213
+ last_step = "step#{sequence.size - 1}_result"
214
+ lines << "final_result = execute_task(:#{final_tool}, inputs: #{last_step})"
215
+ else
216
+ lines << 'final_result = step1_result'
217
+ end
218
+
219
+ # Return statement
220
+ lines << '{ result: final_result }'
221
+
222
+ # Indent and join
223
+ lines.map { |line| " #{line}" }.join("\n")
224
+ end
225
+
226
+ # Generate complete agent wrapper with task definition
227
+ #
228
+ # @param task_name [String] Name of the task
229
+ # @param task_body [String] Generated task code body
230
+ # @return [String] Complete Ruby DSL v1 agent definition
231
+ def generate_agent_wrapper(task_name:, task_body:)
232
+ # Convert task name to kebab-case for agent name
233
+ agent_name = task_name.to_s.gsub('_', '-')
234
+
235
+ <<~RUBY
236
+ # frozen_string_literal: true
237
+
238
+ require 'language_operator'
239
+
240
+ LanguageOperator::Dsl.define_agents do
241
+ agent "#{agent_name}-symbolic" do
242
+ description "Symbolic implementation of #{task_name} (learned from execution patterns)"
243
+
244
+ task :core_pattern,
245
+ inputs: { data: 'hash' },
246
+ outputs: { result: 'hash' }
247
+ do |inputs|
248
+ #{task_body}
249
+ end
250
+
251
+ main do |inputs|
252
+ execute_task(:core_pattern, inputs: inputs)
253
+ end
254
+ end
255
+ end
256
+ RUBY
257
+ end
258
+ end
259
+ end
260
+ end
@@ -0,0 +1,288 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module LanguageOperator
6
+ module Learning
7
+ # Synthesizes deterministic code for neural tasks using LLM analysis
8
+ #
9
+ # TaskSynthesizer uses an LLM to analyze task definitions and execution traces,
10
+ # then generates optimized Ruby code if the task can be made deterministic.
11
+ # This approach can handle inconsistent traces better than pure pattern matching
12
+ # because the LLM can understand intent and unify variations.
13
+ #
14
+ # @example Basic usage
15
+ # synthesizer = TaskSynthesizer.new(
16
+ # llm_client: my_llm_client,
17
+ # validator: ASTValidator.new
18
+ # )
19
+ #
20
+ # result = synthesizer.synthesize(
21
+ # task_definition: task_def,
22
+ # traces: execution_traces,
23
+ # available_tools: tool_list
24
+ # )
25
+ #
26
+ # if result[:is_deterministic]
27
+ # puts result[:code]
28
+ # end
29
+ class TaskSynthesizer
30
+ # Template file name
31
+ TEMPLATE_FILE = 'task_synthesis.tmpl'
32
+
33
+ # Initialize synthesizer
34
+ #
35
+ # @param llm_client [Object] Client for LLM API calls (must respond to #chat)
36
+ # @param validator [Agent::Safety::ASTValidator] Code validator
37
+ # @param logger [Logger, nil] Logger instance
38
+ def initialize(llm_client:, validator:, logger: nil)
39
+ @llm_client = llm_client
40
+ @validator = validator
41
+ @logger = logger || ::Logger.new($stdout, level: ::Logger::INFO)
42
+ end
43
+
44
+ # Synthesize deterministic code for a task
45
+ #
46
+ # @param task_definition [Dsl::TaskDefinition] Task to optimize
47
+ # @param traces [Array<Hash>] Execution traces from TraceAnalyzer
48
+ # @param available_tools [Array<String>] List of available tool names
49
+ # @param consistency_score [Float] Current consistency score
50
+ # @param common_pattern [String, nil] Most common tool pattern
51
+ # @return [Hash] Synthesis result with :is_deterministic, :code, :explanation, :confidence
52
+ def synthesize(task_definition:, traces:, available_tools: [], consistency_score: 0.0, common_pattern: nil)
53
+ # Build prompt from template
54
+ prompt = build_prompt(
55
+ task_definition: task_definition,
56
+ traces: traces,
57
+ available_tools: available_tools,
58
+ consistency_score: consistency_score,
59
+ common_pattern: common_pattern
60
+ )
61
+
62
+ @logger.info("Task synthesis prompt:\n#{prompt}")
63
+
64
+ # Call LLM
65
+ response = call_llm(prompt)
66
+
67
+ # Parse JSON response
68
+ result = parse_response(response)
69
+
70
+ # Validate generated code if present
71
+ if result[:is_deterministic] && result[:code]
72
+ validation = validate_code(result[:code])
73
+ unless validation[:valid]
74
+ @logger.warn("Generated code failed validation: #{validation[:errors].join(', ')}")
75
+ result[:validation_errors] = validation[:errors]
76
+ result[:is_deterministic] = false
77
+ result[:explanation] = "Generated code failed safety validation: #{validation[:errors].first}"
78
+ end
79
+ end
80
+
81
+ result
82
+ rescue StandardError => e
83
+ @logger.error("Task synthesis failed: #{e.message}")
84
+ @logger.error(e.backtrace&.first(10)&.join("\n"))
85
+ {
86
+ is_deterministic: false,
87
+ confidence: 0.0,
88
+ explanation: "Synthesis error: #{e.message}",
89
+ code: nil
90
+ }
91
+ end
92
+
93
+ private
94
+
95
+ # Build synthesis prompt from template
96
+ #
97
+ # @return [String] Rendered prompt
98
+ def build_prompt(task_definition:, traces:, available_tools:, consistency_score:, common_pattern:)
99
+ template = load_template
100
+
101
+ # Pre-render traces since our template engine doesn't support loops
102
+ traces_text = format_traces(traces)
103
+ inputs_text = format_schema(task_definition.inputs)
104
+ outputs_text = format_schema(task_definition.outputs)
105
+ tools_text = available_tools.map { |t| "- #{t}" }.join("\n")
106
+
107
+ # Count unique patterns
108
+ unique_patterns = traces.map { |t| (t[:tool_calls] || []).map { |tc| tc[:tool_name] }.join(' → ') }.uniq.size
109
+
110
+ # Build data hash for template substitution
111
+ data = {
112
+ 'TaskName' => task_definition.name.to_s,
113
+ 'Instructions' => task_definition.instructions || '(none)',
114
+ 'Inputs' => inputs_text,
115
+ 'Outputs' => outputs_text,
116
+ 'TaskCode' => format_task_code(task_definition),
117
+ 'TraceCount' => traces.size.to_s,
118
+ 'Traces' => traces_text,
119
+ 'CommonPattern' => common_pattern || '(none detected)',
120
+ 'ConsistencyScore' => (consistency_score * 100).round(1).to_s,
121
+ 'UniquePatternCount' => unique_patterns.to_s,
122
+ 'ToolsList' => tools_text.empty? ? '(none available)' : tools_text
123
+ }
124
+
125
+ render_template(template, data)
126
+ end
127
+
128
+ # Load template file
129
+ #
130
+ # @return [String] Template content
131
+ def load_template
132
+ template_path = File.join(__dir__, '..', 'templates', TEMPLATE_FILE)
133
+ File.read(template_path)
134
+ end
135
+
136
+ # Render Go-style template with data
137
+ #
138
+ # @param template [String] Template content
139
+ # @param data [Hash] Data to substitute
140
+ # @return [String] Rendered content
141
+ def render_template(template, data)
142
+ result = template.dup
143
+
144
+ # Remove range blocks (we pre-render these)
145
+ result.gsub!(/{{range.*?}}.*?{{end}}/m, '')
146
+
147
+ # Remove if blocks for empty values
148
+ result.gsub!(/{{if not \.Inputs}}.*?{{end}}/m, '')
149
+ result.gsub!(/{{if \.InputSummary}}.*?{{end}}/m, '')
150
+
151
+ # Replace simple variables {{.Variable}}
152
+ data.each do |key, value|
153
+ result.gsub!("{{.#{key}}}", value.to_s)
154
+ end
155
+
156
+ result
157
+ end
158
+
159
+ # Format traces for template
160
+ #
161
+ # @param traces [Array<Hash>] Execution traces
162
+ # @return [String] Formatted traces text
163
+ def format_traces(traces)
164
+ return '(no traces available)' if traces.empty?
165
+
166
+ traces.first(10).each_with_index.map do |trace, idx|
167
+ format_single_trace(trace, idx)
168
+ end.join("\n")
169
+ end
170
+
171
+ # Format a single trace execution
172
+ #
173
+ # @param trace [Hash] Single trace data
174
+ # @param idx [Integer] Trace index
175
+ # @return [String] Formatted trace
176
+ def format_single_trace(trace, idx)
177
+ tool_sequence = trace[:tool_calls]&.map { |tc| tc[:tool_name] }&.join(' → ') || '(no tools)'
178
+ duration = trace[:duration_ms]&.round(1) || 'unknown'
179
+ inputs_summary = trace[:inputs]&.keys&.join(', ') || 'none'
180
+ tool_details = format_tool_calls(trace[:tool_calls])
181
+
182
+ <<~TRACE
183
+ ### Execution #{idx + 1}
184
+ - **Tool Sequence:** #{tool_sequence}
185
+ - **Duration:** #{duration}ms
186
+ - **Inputs:** #{inputs_summary}
187
+ - **Tool Calls:**
188
+ #{tool_details}
189
+ TRACE
190
+ end
191
+
192
+ # Format tool call details
193
+ #
194
+ # @param tool_calls [Array<Hash>, nil] Tool call data
195
+ # @return [String] Formatted tool calls
196
+ def format_tool_calls(tool_calls)
197
+ return ' (no tool calls)' unless tool_calls&.any?
198
+
199
+ tool_calls.map do |tc|
200
+ details = " - #{tc[:tool_name]}"
201
+ details += "\n Args: #{tc[:arguments]}" if tc[:arguments]
202
+ details += "\n Result: #{tc[:result]}" if tc[:result]
203
+ details
204
+ end.join("\n")
205
+ end
206
+
207
+ # Format schema hash as text
208
+ #
209
+ # @param schema [Hash] Input/output schema
210
+ # @return [String] Formatted text
211
+ def format_schema(schema)
212
+ return '(none)' if schema.nil? || schema.empty?
213
+
214
+ schema.map { |k, v| "- #{k}: #{v}" }.join("\n")
215
+ end
216
+
217
+ # Format task definition as Ruby code
218
+ #
219
+ # @param task_def [Dsl::TaskDefinition] Task definition
220
+ # @return [String] Ruby code representation
221
+ def format_task_code(task_def)
222
+ inputs_str = (task_def.inputs || {}).map { |k, v| "#{k}: '#{v}'" }.join(', ')
223
+ outputs_str = (task_def.outputs || {}).map { |k, v| "#{k}: '#{v}'" }.join(', ')
224
+
225
+ <<~RUBY
226
+ task :#{task_def.name},
227
+ instructions: "#{task_def.instructions}",
228
+ inputs: { #{inputs_str} },
229
+ outputs: { #{outputs_str} }
230
+ RUBY
231
+ end
232
+
233
+ # Call LLM with prompt
234
+ #
235
+ # @param prompt [String] Synthesis prompt
236
+ # @return [String] LLM response
237
+ def call_llm(prompt)
238
+ @llm_client.chat(prompt)
239
+ end
240
+
241
+ # Parse JSON response from LLM
242
+ #
243
+ # @param response [String] LLM response text
244
+ # @return [Hash] Parsed result
245
+ def parse_response(response)
246
+ # Extract JSON from response (may be wrapped in markdown code block)
247
+ json_match = response.match(/```json\s*(.*?)\s*```/m) ||
248
+ response.match(/\{.*\}/m)
249
+
250
+ json_str = json_match ? json_match[1] || json_match[0] : response
251
+
252
+ parsed = JSON.parse(json_str, symbolize_names: true)
253
+
254
+ {
255
+ is_deterministic: parsed[:is_deterministic] == true,
256
+ confidence: parsed[:confidence].to_f,
257
+ explanation: parsed[:explanation] || 'No explanation provided',
258
+ code: parsed[:code]
259
+ }
260
+ rescue JSON::ParserError => e
261
+ @logger.warn("Failed to parse LLM response as JSON: #{e.message}")
262
+ {
263
+ is_deterministic: false,
264
+ confidence: 0.0,
265
+ explanation: "Failed to parse synthesis response: #{e.message}",
266
+ code: nil
267
+ }
268
+ end
269
+
270
+ # Validate generated code
271
+ #
272
+ # @param code [String] Ruby code to validate
273
+ # @return [Hash] Validation result with :valid and :errors
274
+ def validate_code(code)
275
+ errors = @validator.validate(code)
276
+ {
277
+ valid: errors.empty?,
278
+ errors: errors
279
+ }
280
+ rescue StandardError => e
281
+ {
282
+ valid: false,
283
+ errors: ["Validation error: #{e.message}"]
284
+ }
285
+ end
286
+ end
287
+ end
288
+ end