language-operator 0.1.57 → 0.1.59
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/Gemfile.lock +1 -1
- data/lib/language_operator/agent/base.rb +19 -0
- data/lib/language_operator/agent/executor.rb +11 -0
- data/lib/language_operator/agent/task_executor.rb +77 -22
- data/lib/language_operator/agent/telemetry.rb +22 -11
- data/lib/language_operator/agent.rb +3 -0
- data/lib/language_operator/cli/base_command.rb +7 -1
- data/lib/language_operator/cli/commands/agent.rb +578 -1
- data/lib/language_operator/cli/formatters/optimization_formatter.rb +226 -0
- data/lib/language_operator/cli/formatters/progress_formatter.rb +1 -1
- data/lib/language_operator/client/base.rb +72 -2
- data/lib/language_operator/client/mcp_connector.rb +28 -6
- data/lib/language_operator/instrumentation/task_tracer.rb +64 -2
- data/lib/language_operator/kubernetes/resource_builder.rb +3 -1
- data/lib/language_operator/learning/adapters/base_adapter.rb +147 -0
- data/lib/language_operator/learning/adapters/jaeger_adapter.rb +218 -0
- data/lib/language_operator/learning/adapters/signoz_adapter.rb +432 -0
- data/lib/language_operator/learning/adapters/tempo_adapter.rb +236 -0
- data/lib/language_operator/learning/optimizer.rb +318 -0
- data/lib/language_operator/learning/pattern_detector.rb +260 -0
- data/lib/language_operator/learning/task_synthesizer.rb +261 -0
- data/lib/language_operator/learning/trace_analyzer.rb +280 -0
- 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/templates/task_synthesis.tmpl +97 -0
- data/lib/language_operator/tool_loader.rb +5 -3
- data/lib/language_operator/ux/concerns/provider_helpers.rb +2 -2
- data/lib/language_operator/version.rb +1 -1
- data/synth/003/Makefile +10 -0
- data/synth/003/output.log +68 -0
- data/synth/README.md +1 -3
- 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,261 @@
|
|
|
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::WARN)
|
|
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.debug("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
|
+
tool_sequence = trace[:tool_calls]&.map { |tc| tc[:tool_name] }&.join(' → ') || '(no tools)'
|
|
168
|
+
duration = trace[:duration_ms]&.round(1) || 'unknown'
|
|
169
|
+
inputs_summary = trace[:inputs]&.keys&.join(', ') || 'none'
|
|
170
|
+
|
|
171
|
+
<<~TRACE
|
|
172
|
+
### Execution #{idx + 1}
|
|
173
|
+
- **Tool Sequence:** #{tool_sequence}
|
|
174
|
+
- **Duration:** #{duration}ms
|
|
175
|
+
- **Inputs:** #{inputs_summary}
|
|
176
|
+
TRACE
|
|
177
|
+
end.join("\n")
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Format schema hash as text
|
|
181
|
+
#
|
|
182
|
+
# @param schema [Hash] Input/output schema
|
|
183
|
+
# @return [String] Formatted text
|
|
184
|
+
def format_schema(schema)
|
|
185
|
+
return '(none)' if schema.nil? || schema.empty?
|
|
186
|
+
|
|
187
|
+
schema.map { |k, v| "- #{k}: #{v}" }.join("\n")
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Format task definition as Ruby code
|
|
191
|
+
#
|
|
192
|
+
# @param task_def [Dsl::TaskDefinition] Task definition
|
|
193
|
+
# @return [String] Ruby code representation
|
|
194
|
+
def format_task_code(task_def)
|
|
195
|
+
inputs_str = (task_def.inputs || {}).map { |k, v| "#{k}: '#{v}'" }.join(', ')
|
|
196
|
+
outputs_str = (task_def.outputs || {}).map { |k, v| "#{k}: '#{v}'" }.join(', ')
|
|
197
|
+
|
|
198
|
+
<<~RUBY
|
|
199
|
+
task :#{task_def.name},
|
|
200
|
+
instructions: "#{task_def.instructions}",
|
|
201
|
+
inputs: { #{inputs_str} },
|
|
202
|
+
outputs: { #{outputs_str} }
|
|
203
|
+
RUBY
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Call LLM with prompt
|
|
207
|
+
#
|
|
208
|
+
# @param prompt [String] Synthesis prompt
|
|
209
|
+
# @return [String] LLM response
|
|
210
|
+
def call_llm(prompt)
|
|
211
|
+
@llm_client.chat(prompt)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Parse JSON response from LLM
|
|
215
|
+
#
|
|
216
|
+
# @param response [String] LLM response text
|
|
217
|
+
# @return [Hash] Parsed result
|
|
218
|
+
def parse_response(response)
|
|
219
|
+
# Extract JSON from response (may be wrapped in markdown code block)
|
|
220
|
+
json_match = response.match(/```json\s*(.*?)\s*```/m) ||
|
|
221
|
+
response.match(/\{.*\}/m)
|
|
222
|
+
|
|
223
|
+
json_str = json_match ? json_match[1] || json_match[0] : response
|
|
224
|
+
|
|
225
|
+
parsed = JSON.parse(json_str, symbolize_names: true)
|
|
226
|
+
|
|
227
|
+
{
|
|
228
|
+
is_deterministic: parsed[:is_deterministic] == true,
|
|
229
|
+
confidence: parsed[:confidence].to_f,
|
|
230
|
+
explanation: parsed[:explanation] || 'No explanation provided',
|
|
231
|
+
code: parsed[:code]
|
|
232
|
+
}
|
|
233
|
+
rescue JSON::ParserError => e
|
|
234
|
+
@logger.warn("Failed to parse LLM response as JSON: #{e.message}")
|
|
235
|
+
{
|
|
236
|
+
is_deterministic: false,
|
|
237
|
+
confidence: 0.0,
|
|
238
|
+
explanation: "Failed to parse synthesis response: #{e.message}",
|
|
239
|
+
code: nil
|
|
240
|
+
}
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Validate generated code
|
|
244
|
+
#
|
|
245
|
+
# @param code [String] Ruby code to validate
|
|
246
|
+
# @return [Hash] Validation result with :valid and :errors
|
|
247
|
+
def validate_code(code)
|
|
248
|
+
errors = @validator.validate(code)
|
|
249
|
+
{
|
|
250
|
+
valid: errors.empty?,
|
|
251
|
+
errors: errors
|
|
252
|
+
}
|
|
253
|
+
rescue StandardError => e
|
|
254
|
+
{
|
|
255
|
+
valid: false,
|
|
256
|
+
errors: ["Validation error: #{e.message}"]
|
|
257
|
+
}
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|