language-operator 0.1.61 → 0.1.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.claude/commands/persona.md +9 -0
- data/.claude/commands/task.md +46 -1
- data/.rubocop.yml +13 -0
- data/.rubocop_custom/use_ux_helper.rb +44 -0
- data/CHANGELOG.md +8 -0
- data/Gemfile.lock +12 -1
- data/Makefile +26 -7
- data/Makefile.common +50 -0
- data/bin/aictl +8 -1
- data/components/agent/Gemfile +1 -1
- data/components/agent/bin/langop-agent +7 -0
- data/docs/README.md +58 -0
- data/docs/{dsl/best-practices.md → best-practices.md} +4 -4
- data/docs/cli-reference.md +274 -0
- data/docs/{dsl/constraints.md → constraints.md} +5 -5
- data/docs/how-agents-work.md +156 -0
- data/docs/installation.md +218 -0
- data/docs/quickstart.md +299 -0
- data/docs/understanding-generated-code.md +265 -0
- data/docs/using-tools.md +457 -0
- data/docs/webhooks.md +509 -0
- data/examples/ux_helpers_demo.rb +296 -0
- data/lib/language_operator/agent/base.rb +11 -1
- data/lib/language_operator/agent/executor.rb +23 -6
- data/lib/language_operator/agent/safety/safe_executor.rb +41 -39
- data/lib/language_operator/agent/task_executor.rb +346 -63
- data/lib/language_operator/agent/web_server.rb +110 -14
- data/lib/language_operator/agent/webhook_authenticator.rb +39 -5
- data/lib/language_operator/agent.rb +88 -2
- data/lib/language_operator/cli/base_command.rb +17 -11
- data/lib/language_operator/cli/command_loader.rb +72 -0
- data/lib/language_operator/cli/commands/agent/base.rb +837 -0
- data/lib/language_operator/cli/commands/agent/code_operations.rb +102 -0
- data/lib/language_operator/cli/commands/agent/helpers/cluster_llm_client.rb +116 -0
- data/lib/language_operator/cli/commands/agent/helpers/code_parser.rb +115 -0
- data/lib/language_operator/cli/commands/agent/helpers/synthesis_watcher.rb +96 -0
- data/lib/language_operator/cli/commands/agent/learning.rb +289 -0
- data/lib/language_operator/cli/commands/agent/lifecycle.rb +102 -0
- data/lib/language_operator/cli/commands/agent/logs.rb +125 -0
- data/lib/language_operator/cli/commands/agent/workspace.rb +327 -0
- data/lib/language_operator/cli/commands/cluster.rb +129 -84
- data/lib/language_operator/cli/commands/install.rb +1 -1
- data/lib/language_operator/cli/commands/model/base.rb +215 -0
- data/lib/language_operator/cli/commands/model/test.rb +165 -0
- data/lib/language_operator/cli/commands/persona.rb +16 -34
- data/lib/language_operator/cli/commands/quickstart.rb +3 -2
- data/lib/language_operator/cli/commands/status.rb +40 -67
- data/lib/language_operator/cli/commands/system/base.rb +44 -0
- data/lib/language_operator/cli/commands/system/exec.rb +147 -0
- data/lib/language_operator/cli/commands/system/helpers/llm_synthesis.rb +183 -0
- data/lib/language_operator/cli/commands/system/helpers/pod_manager.rb +212 -0
- data/lib/language_operator/cli/commands/system/helpers/template_loader.rb +57 -0
- data/lib/language_operator/cli/commands/system/helpers/template_validator.rb +174 -0
- data/lib/language_operator/cli/commands/system/schema.rb +92 -0
- data/lib/language_operator/cli/commands/system/synthesis_template.rb +151 -0
- data/lib/language_operator/cli/commands/system/synthesize.rb +224 -0
- data/lib/language_operator/cli/commands/system/validate_template.rb +130 -0
- data/lib/language_operator/cli/commands/tool/base.rb +271 -0
- data/lib/language_operator/cli/commands/tool/install.rb +255 -0
- data/lib/language_operator/cli/commands/tool/search.rb +69 -0
- data/lib/language_operator/cli/commands/tool/test.rb +115 -0
- data/lib/language_operator/cli/commands/use.rb +29 -6
- data/lib/language_operator/cli/errors/handler.rb +20 -17
- data/lib/language_operator/cli/errors/suggestions.rb +3 -5
- data/lib/language_operator/cli/errors/thor_errors.rb +55 -0
- data/lib/language_operator/cli/formatters/code_formatter.rb +4 -11
- data/lib/language_operator/cli/formatters/log_formatter.rb +8 -15
- data/lib/language_operator/cli/formatters/progress_formatter.rb +6 -8
- data/lib/language_operator/cli/formatters/status_formatter.rb +26 -7
- data/lib/language_operator/cli/formatters/table_formatter.rb +47 -36
- data/lib/language_operator/cli/formatters/value_formatter.rb +75 -0
- data/lib/language_operator/cli/helpers/cluster_context.rb +5 -3
- data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +2 -1
- data/lib/language_operator/cli/helpers/label_utils.rb +97 -0
- data/lib/language_operator/{ux/concerns/provider_helpers.rb → cli/helpers/provider_helper.rb} +10 -29
- data/lib/language_operator/cli/helpers/schedule_builder.rb +21 -1
- data/lib/language_operator/cli/helpers/user_prompts.rb +19 -11
- data/lib/language_operator/cli/helpers/ux_helper.rb +538 -0
- data/lib/language_operator/{ux/concerns/input_validation.rb → cli/helpers/validation_helper.rb} +13 -66
- data/lib/language_operator/cli/main.rb +50 -40
- data/lib/language_operator/cli/templates/tools/generic.yaml +3 -0
- data/lib/language_operator/cli/wizards/agent_wizard.rb +12 -20
- data/lib/language_operator/cli/wizards/model_wizard.rb +271 -0
- data/lib/language_operator/cli/wizards/quickstart_wizard.rb +8 -34
- data/lib/language_operator/client/base.rb +28 -0
- data/lib/language_operator/client/config.rb +4 -1
- data/lib/language_operator/client/mcp_connector.rb +1 -1
- data/lib/language_operator/config/cluster_config.rb +3 -2
- data/lib/language_operator/config.rb +38 -11
- data/lib/language_operator/constants/kubernetes_labels.rb +80 -0
- data/lib/language_operator/constants.rb +13 -0
- data/lib/language_operator/dsl/http.rb +127 -10
- data/lib/language_operator/dsl.rb +153 -6
- data/lib/language_operator/errors.rb +50 -0
- data/lib/language_operator/kubernetes/client.rb +11 -6
- data/lib/language_operator/kubernetes/resource_builder.rb +58 -84
- data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
- data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
- data/lib/language_operator/type_coercion.rb +118 -34
- data/lib/language_operator/utils/secure_path.rb +74 -0
- data/lib/language_operator/utils.rb +7 -0
- data/lib/language_operator/validators.rb +54 -2
- data/lib/language_operator/version.rb +1 -1
- data/synth/001/Makefile +10 -2
- data/synth/001/agent.rb +16 -15
- data/synth/001/output.log +27 -10
- data/synth/002/Makefile +10 -2
- data/synth/003/Makefile +1 -1
- data/synth/003/README.md +205 -133
- data/synth/003/agent.optimized.rb +66 -0
- data/synth/003/agent.synthesized.rb +41 -0
- metadata +111 -35
- data/docs/dsl/agent-reference.md +0 -604
- data/docs/dsl/mcp-integration.md +0 -1177
- data/docs/dsl/webhooks.md +0 -932
- data/docs/dsl/workflows.md +0 -744
- data/lib/language_operator/cli/commands/agent.rb +0 -1712
- data/lib/language_operator/cli/commands/model.rb +0 -366
- data/lib/language_operator/cli/commands/system.rb +0 -1259
- data/lib/language_operator/cli/commands/tool.rb +0 -654
- data/lib/language_operator/cli/formatters/optimization_formatter.rb +0 -226
- data/lib/language_operator/cli/helpers/pastel_helper.rb +0 -24
- data/lib/language_operator/learning/adapters/base_adapter.rb +0 -149
- data/lib/language_operator/learning/adapters/jaeger_adapter.rb +0 -221
- data/lib/language_operator/learning/adapters/signoz_adapter.rb +0 -435
- data/lib/language_operator/learning/adapters/tempo_adapter.rb +0 -239
- data/lib/language_operator/learning/optimizer.rb +0 -319
- data/lib/language_operator/learning/pattern_detector.rb +0 -260
- data/lib/language_operator/learning/task_synthesizer.rb +0 -288
- data/lib/language_operator/learning/trace_analyzer.rb +0 -285
- data/lib/language_operator/templates/task_synthesis.tmpl +0 -98
- data/lib/language_operator/ux/base.rb +0 -81
- data/lib/language_operator/ux/concerns/README.md +0 -155
- data/lib/language_operator/ux/concerns/headings.rb +0 -90
- data/lib/language_operator/ux/create_agent.rb +0 -255
- data/lib/language_operator/ux/create_model.rb +0 -267
- data/lib/language_operator/ux/quickstart.rb +0 -594
- data/synth/003/agent.rb +0 -41
- data/synth/003/output.log +0 -68
- /data/docs/{architecture/agent-runtime.md → agent-internals.md} +0 -0
- /data/docs/{dsl/chat-endpoints.md → chat-endpoints.md} +0 -0
- /data/docs/{dsl/SCHEMA_VERSION.md → schema-versioning.md} +0 -0
|
@@ -1,288 +0,0 @@
|
|
|
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
|
|
@@ -1,285 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'logger'
|
|
4
|
-
require_relative 'adapters/base_adapter'
|
|
5
|
-
|
|
6
|
-
module LanguageOperator
|
|
7
|
-
module Learning
|
|
8
|
-
# Analyzes OpenTelemetry traces to detect patterns in task execution
|
|
9
|
-
#
|
|
10
|
-
# The TraceAnalyzer queries OTLP backends (SigNoz, Jaeger, Tempo) to retrieve
|
|
11
|
-
# execution traces for neural tasks, then analyzes them to determine if they
|
|
12
|
-
# exhibit consistent patterns that can be codified into symbolic implementations.
|
|
13
|
-
#
|
|
14
|
-
# Auto-detects available backends in order: SigNoz → Jaeger → Tempo
|
|
15
|
-
# Falls back gracefully if no backend is available (learning disabled).
|
|
16
|
-
#
|
|
17
|
-
# @example Basic usage
|
|
18
|
-
# analyzer = TraceAnalyzer.new(
|
|
19
|
-
# endpoint: ENV['OTEL_QUERY_ENDPOINT'],
|
|
20
|
-
# api_key: ENV['OTEL_QUERY_API_KEY']
|
|
21
|
-
# )
|
|
22
|
-
#
|
|
23
|
-
# analysis = analyzer.analyze_patterns(task_name: 'fetch_user_data')
|
|
24
|
-
# if analysis && analysis[:consistency] >= 0.85
|
|
25
|
-
# puts "Task is ready for learning!"
|
|
26
|
-
# puts "Tool sequence: #{analysis[:common_pattern]}"
|
|
27
|
-
# end
|
|
28
|
-
#
|
|
29
|
-
# @example Explicit backend selection
|
|
30
|
-
# ENV['OTEL_QUERY_BACKEND'] = 'signoz'
|
|
31
|
-
# analyzer = TraceAnalyzer.new(endpoint: 'https://signoz.example.com')
|
|
32
|
-
class TraceAnalyzer
|
|
33
|
-
# Minimum pattern consistency required for learning (configurable)
|
|
34
|
-
DEFAULT_CONSISTENCY_THRESHOLD = 0.85
|
|
35
|
-
|
|
36
|
-
# Default time range for queries (24 hours)
|
|
37
|
-
DEFAULT_TIME_RANGE = 24 * 60 * 60
|
|
38
|
-
|
|
39
|
-
# Initialize trace analyzer with backend connection
|
|
40
|
-
#
|
|
41
|
-
# @param endpoint [String, nil] OTLP backend endpoint (auto-detected from ENV if nil)
|
|
42
|
-
# @param api_key [String, nil] API key for authentication (if required)
|
|
43
|
-
# @param backend [String, nil] Explicit backend type ('signoz', 'jaeger', 'tempo')
|
|
44
|
-
# @param logger [Logger, nil] Logger instance (creates default if nil)
|
|
45
|
-
def initialize(endpoint: nil, api_key: nil, backend: nil, logger: nil)
|
|
46
|
-
@endpoint = endpoint || ENV.fetch('OTEL_QUERY_ENDPOINT', nil)
|
|
47
|
-
@api_key = api_key || ENV.fetch('OTEL_QUERY_API_KEY', nil)
|
|
48
|
-
@backend_type = backend || ENV.fetch('OTEL_QUERY_BACKEND', nil)
|
|
49
|
-
@logger = logger || ::Logger.new($stdout, level: ::Logger::WARN)
|
|
50
|
-
@adapter = detect_backend_adapter
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Check if learning is available (backend connected)
|
|
54
|
-
#
|
|
55
|
-
# @return [Boolean] True if a backend adapter is available
|
|
56
|
-
def available?
|
|
57
|
-
!@adapter.nil?
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# Query task execution traces from backend
|
|
61
|
-
#
|
|
62
|
-
# @param task_name [String] Name of task to query
|
|
63
|
-
# @param agent_name [String, nil] Optional agent name to filter by
|
|
64
|
-
# @param limit [Integer] Maximum number of traces to return
|
|
65
|
-
# @param time_range [Integer, Range<Time>] Time range in seconds or explicit range
|
|
66
|
-
# @return [Array<Hash>] Task execution data
|
|
67
|
-
def query_task_traces(task_name:, agent_name: nil, limit: 100, time_range: DEFAULT_TIME_RANGE)
|
|
68
|
-
unless available?
|
|
69
|
-
@logger.warn('No OTLP backend available, learning disabled')
|
|
70
|
-
return []
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
range = normalize_time_range(time_range)
|
|
74
|
-
|
|
75
|
-
filter = { task_name: task_name }
|
|
76
|
-
filter[:agent_name] = agent_name if agent_name
|
|
77
|
-
|
|
78
|
-
spans = @adapter.query_spans(
|
|
79
|
-
filter: filter,
|
|
80
|
-
time_range: range,
|
|
81
|
-
limit: limit
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
@adapter.extract_task_data(spans)
|
|
85
|
-
rescue StandardError => e
|
|
86
|
-
@logger.error("Failed to query task traces: #{e.message}")
|
|
87
|
-
@logger.debug(e.backtrace.join("\n"))
|
|
88
|
-
[]
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
# Analyze task execution patterns for consistency
|
|
92
|
-
#
|
|
93
|
-
# Determines if a neural task exhibits consistent behavior that can be
|
|
94
|
-
# learned and converted to a symbolic implementation.
|
|
95
|
-
#
|
|
96
|
-
# @param task_name [String] Name of task to analyze
|
|
97
|
-
# @param agent_name [String, nil] Optional agent name to filter by
|
|
98
|
-
# @param min_executions [Integer] Minimum executions required for analysis
|
|
99
|
-
# @param consistency_threshold [Float] Required consistency (0.0-1.0)
|
|
100
|
-
# @param time_range [Integer, Range<Time>, nil] Time range for query (seconds or explicit range)
|
|
101
|
-
# @return [Hash, nil] Analysis results or nil if insufficient data
|
|
102
|
-
def analyze_patterns(task_name:, agent_name: nil, min_executions: 10, consistency_threshold: DEFAULT_CONSISTENCY_THRESHOLD,
|
|
103
|
-
time_range: nil)
|
|
104
|
-
executions = query_task_traces(task_name: task_name, agent_name: agent_name, limit: 1000, time_range: time_range || DEFAULT_TIME_RANGE)
|
|
105
|
-
|
|
106
|
-
if executions.empty?
|
|
107
|
-
@logger.info("No executions found for task '#{task_name}'")
|
|
108
|
-
return nil
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
if executions.size < min_executions
|
|
112
|
-
@logger.info("Insufficient executions for task '#{task_name}': #{executions.size}/#{min_executions}")
|
|
113
|
-
return {
|
|
114
|
-
task_name: task_name,
|
|
115
|
-
execution_count: executions.size,
|
|
116
|
-
required_count: min_executions,
|
|
117
|
-
ready_for_learning: false,
|
|
118
|
-
reason: "Need #{min_executions - executions.size} more executions"
|
|
119
|
-
}
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
consistency_data = calculate_consistency(executions)
|
|
123
|
-
|
|
124
|
-
# Task is ready for learning only if:
|
|
125
|
-
# 1. Consistency meets threshold
|
|
126
|
-
# 2. There's an actual tool pattern to learn (not empty/pure LLM)
|
|
127
|
-
has_pattern = !consistency_data[:common_pattern].nil? && !consistency_data[:common_pattern].empty?
|
|
128
|
-
ready = consistency_data[:score] >= consistency_threshold && has_pattern
|
|
129
|
-
|
|
130
|
-
{
|
|
131
|
-
task_name: task_name,
|
|
132
|
-
execution_count: executions.size,
|
|
133
|
-
consistency_score: consistency_data[:score],
|
|
134
|
-
consistency_threshold: consistency_threshold,
|
|
135
|
-
ready_for_learning: ready,
|
|
136
|
-
reason: has_pattern ? nil : 'No tool calls to learn (pure LLM task)',
|
|
137
|
-
common_pattern: consistency_data[:common_pattern],
|
|
138
|
-
input_signatures: consistency_data[:input_signatures],
|
|
139
|
-
analysis_timestamp: Time.now.iso8601
|
|
140
|
-
}
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# Calculate pattern consistency across executions
|
|
144
|
-
#
|
|
145
|
-
# Groups executions by input signature and analyzes tool call sequences
|
|
146
|
-
# to determine how often the same pattern is used for the same inputs.
|
|
147
|
-
#
|
|
148
|
-
# @param executions [Array<Hash>] Task execution data
|
|
149
|
-
# @return [Hash] Consistency analysis with score and common pattern
|
|
150
|
-
def calculate_consistency(executions)
|
|
151
|
-
# Group by input signature
|
|
152
|
-
by_inputs = executions.group_by { |ex| normalize_inputs(ex[:inputs]) }
|
|
153
|
-
|
|
154
|
-
# For each input signature, find the most common tool call pattern
|
|
155
|
-
signature_patterns = by_inputs.map do |input_sig, execs|
|
|
156
|
-
patterns = execs.map { |ex| normalize_tool_calls(ex[:tool_calls]) }
|
|
157
|
-
pattern_counts = patterns.tally
|
|
158
|
-
most_common = pattern_counts.max_by { |_, count| count }
|
|
159
|
-
|
|
160
|
-
{
|
|
161
|
-
input_signature: input_sig,
|
|
162
|
-
total_executions: execs.size,
|
|
163
|
-
most_common_pattern: most_common[0],
|
|
164
|
-
pattern_count: most_common[1],
|
|
165
|
-
consistency: most_common[1].to_f / execs.size
|
|
166
|
-
}
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
# Overall consistency is weighted average across input signatures
|
|
170
|
-
total_execs = executions.size
|
|
171
|
-
weighted_consistency = signature_patterns.sum do |sig_data|
|
|
172
|
-
weight = sig_data[:total_executions].to_f / total_execs
|
|
173
|
-
weight * sig_data[:consistency]
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
# Find the globally most common pattern
|
|
177
|
-
all_patterns = signature_patterns.map { |s| s[:most_common_pattern] }
|
|
178
|
-
common_pattern = all_patterns.max_by { |p| all_patterns.count(p) }
|
|
179
|
-
|
|
180
|
-
{
|
|
181
|
-
score: weighted_consistency.round(3),
|
|
182
|
-
common_pattern: common_pattern,
|
|
183
|
-
input_signatures: signature_patterns.size
|
|
184
|
-
}
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
private
|
|
188
|
-
|
|
189
|
-
# Detect and initialize the appropriate backend adapter
|
|
190
|
-
#
|
|
191
|
-
# Auto-detection order: SigNoz → Jaeger → Tempo
|
|
192
|
-
# Falls back to nil if no backend is available
|
|
193
|
-
#
|
|
194
|
-
# @return [BaseAdapter, nil] Initialized adapter or nil
|
|
195
|
-
def detect_backend_adapter
|
|
196
|
-
return nil unless @endpoint
|
|
197
|
-
|
|
198
|
-
# Explicit backend selection
|
|
199
|
-
if @backend_type
|
|
200
|
-
adapter = create_adapter(@backend_type)
|
|
201
|
-
return adapter if adapter
|
|
202
|
-
|
|
203
|
-
@logger.warn("Requested backend '#{@backend_type}' not available, trying auto-detection")
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
# Auto-detect with fallback chain
|
|
207
|
-
%w[signoz jaeger tempo].each do |backend|
|
|
208
|
-
adapter = create_adapter(backend)
|
|
209
|
-
if adapter
|
|
210
|
-
@logger.info("Detected OTLP backend: #{backend} at #{@endpoint}")
|
|
211
|
-
return adapter
|
|
212
|
-
end
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
@logger.warn("No OTLP backend available at #{@endpoint}, learning disabled")
|
|
216
|
-
nil
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
# Create adapter instance for specified backend
|
|
220
|
-
#
|
|
221
|
-
# @param backend_type [String] Backend type
|
|
222
|
-
# @return [BaseAdapter, nil] Adapter instance or nil if unavailable
|
|
223
|
-
def create_adapter(backend_type)
|
|
224
|
-
require_relative "adapters/#{backend_type}_adapter"
|
|
225
|
-
|
|
226
|
-
adapter_class = case backend_type.downcase
|
|
227
|
-
when 'signoz'
|
|
228
|
-
Adapters::SignozAdapter
|
|
229
|
-
when 'jaeger'
|
|
230
|
-
Adapters::JaegerAdapter
|
|
231
|
-
when 'tempo'
|
|
232
|
-
Adapters::TempoAdapter
|
|
233
|
-
else
|
|
234
|
-
@logger.error("Unknown backend type: #{backend_type}")
|
|
235
|
-
return nil
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
return nil unless adapter_class.available?(@endpoint, @api_key)
|
|
239
|
-
|
|
240
|
-
adapter_class.new(@endpoint, @api_key, logger: @logger)
|
|
241
|
-
rescue LoadError => e
|
|
242
|
-
@logger.debug("Adapter #{backend_type} not available: #{e.message}")
|
|
243
|
-
nil
|
|
244
|
-
rescue StandardError => e
|
|
245
|
-
@logger.error("Failed to create #{backend_type} adapter: #{e.message}")
|
|
246
|
-
nil
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
# Normalize time range to Range<Time>
|
|
250
|
-
#
|
|
251
|
-
# @param time_range [Integer, Range<Time>] Time range
|
|
252
|
-
# @return [Range<Time>] Normalized time range
|
|
253
|
-
def normalize_time_range(time_range)
|
|
254
|
-
case time_range
|
|
255
|
-
when Range
|
|
256
|
-
time_range
|
|
257
|
-
when Integer
|
|
258
|
-
(Time.now - time_range)..Time.now
|
|
259
|
-
else
|
|
260
|
-
(Time.now - DEFAULT_TIME_RANGE)..Time.now
|
|
261
|
-
end
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
# Normalize inputs for comparison
|
|
265
|
-
#
|
|
266
|
-
# @param inputs [Hash] Task inputs
|
|
267
|
-
# @return [String] Normalized input signature
|
|
268
|
-
def normalize_inputs(inputs)
|
|
269
|
-
return '' unless inputs.is_a?(Hash)
|
|
270
|
-
|
|
271
|
-
inputs.sort.to_h.to_s
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
# Normalize tool calls for pattern matching
|
|
275
|
-
#
|
|
276
|
-
# @param tool_calls [Array<Hash>] Tool call sequence
|
|
277
|
-
# @return [String] Normalized pattern signature
|
|
278
|
-
def normalize_tool_calls(tool_calls)
|
|
279
|
-
return '' unless tool_calls.is_a?(Array)
|
|
280
|
-
|
|
281
|
-
tool_calls.map { |tc| tc[:tool_name] }.join(' → ')
|
|
282
|
-
end
|
|
283
|
-
end
|
|
284
|
-
end
|
|
285
|
-
end
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
You are analyzing whether an agentic task can be converted to symbolic Ruby code.
|
|
2
|
-
|
|
3
|
-
## What "Symbolic" Means
|
|
4
|
-
|
|
5
|
-
A task is **symbolic** (can be optimized) if it follows a predictable algorithm:
|
|
6
|
-
- Reading files, calling APIs, transforming data = SYMBOLIC (even though outputs vary based on data)
|
|
7
|
-
- The same code logic applies regardless of the actual data values
|
|
8
|
-
- Conditional branches based on data (if file exists, if value > threshold) are fine
|
|
9
|
-
|
|
10
|
-
A task is **neural** (cannot be optimized) only if it requires:
|
|
11
|
-
- Creative text generation (writing stories, poems, marketing copy)
|
|
12
|
-
- Subjective reasoning or judgment calls
|
|
13
|
-
- Understanding nuanced human intent that varies per request
|
|
14
|
-
|
|
15
|
-
**Key insight:** File I/O, API calls, and data transformation are deterministic CODE even if their outputs depend on external state. "Read a file and count lines" is symbolic - the algorithm is fixed.
|
|
16
|
-
|
|
17
|
-
## Task Definition
|
|
18
|
-
|
|
19
|
-
**Name:** {{.TaskName}}
|
|
20
|
-
**Instructions:** {{.Instructions}}
|
|
21
|
-
|
|
22
|
-
**Inputs:**
|
|
23
|
-
{{.Inputs}}
|
|
24
|
-
|
|
25
|
-
**Outputs:**
|
|
26
|
-
{{.Outputs}}
|
|
27
|
-
|
|
28
|
-
## Current Task Code
|
|
29
|
-
|
|
30
|
-
```ruby
|
|
31
|
-
{{.TaskCode}}
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
## Execution Traces ({{.TraceCount}} samples)
|
|
35
|
-
|
|
36
|
-
{{.Traces}}
|
|
37
|
-
|
|
38
|
-
## Pattern Analysis
|
|
39
|
-
|
|
40
|
-
- **Most Common Pattern:** {{.CommonPattern}}
|
|
41
|
-
- **Pattern Consistency:** {{.ConsistencyScore}}%
|
|
42
|
-
- **Unique Patterns Observed:** {{.UniquePatternCount}}
|
|
43
|
-
|
|
44
|
-
## Available Tools
|
|
45
|
-
|
|
46
|
-
{{.ToolsList}}
|
|
47
|
-
|
|
48
|
-
---
|
|
49
|
-
|
|
50
|
-
## Your Task
|
|
51
|
-
|
|
52
|
-
Analyze whether this neural task can be converted to symbolic Ruby code.
|
|
53
|
-
|
|
54
|
-
Questions to ask:
|
|
55
|
-
1. Is there a clear algorithm implied by the instructions?
|
|
56
|
-
2. Do the tool call patterns show a logical sequence?
|
|
57
|
-
3. Can conditional logic handle the variations seen in traces?
|
|
58
|
-
|
|
59
|
-
Tasks that ARE symbolic (optimize these):
|
|
60
|
-
- "Read file X and return its contents" → read_file, return content
|
|
61
|
-
- "Check if file exists, create if not" → get_file_info, conditional write_file
|
|
62
|
-
- "Fetch data from API and transform it" → api_call, data transformation
|
|
63
|
-
|
|
64
|
-
Tasks that are NOT symbolic (don't optimize):
|
|
65
|
-
- "Write a creative story continuation"
|
|
66
|
-
- "Decide what the user probably meant"
|
|
67
|
-
- "Generate marketing copy for this product"
|
|
68
|
-
|
|
69
|
-
## Output Format
|
|
70
|
-
|
|
71
|
-
Respond with valid JSON:
|
|
72
|
-
|
|
73
|
-
```json
|
|
74
|
-
{
|
|
75
|
-
"is_deterministic": true/false,
|
|
76
|
-
"confidence": 0.0-1.0,
|
|
77
|
-
"explanation": "Brief explanation",
|
|
78
|
-
"code": "Ruby code if symbolic, null otherwise"
|
|
79
|
-
}
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
**Code Requirements (if symbolic):**
|
|
83
|
-
- Use the DSL task format with a do block:
|
|
84
|
-
```ruby
|
|
85
|
-
task :task_name,
|
|
86
|
-
instructions: "Keep the original instructions for documentation",
|
|
87
|
-
inputs: { ... },
|
|
88
|
-
outputs: { ... } do |inputs|
|
|
89
|
-
# Helper methods available: execute_tool, execute_task, execute_llm
|
|
90
|
-
result = execute_tool(:tool_name, { arg: value })
|
|
91
|
-
{ output_key: result }
|
|
92
|
-
end
|
|
93
|
-
```
|
|
94
|
-
- Use `execute_tool(:tool_name, { arg: value })` for MCP tool calls
|
|
95
|
-
- Use `execute_task(:task_name, inputs: { ... })` to call other tasks
|
|
96
|
-
- Access inputs via the `inputs` hash parameter
|
|
97
|
-
- Return a hash matching the output schema
|
|
98
|
-
- Do NOT use system(), eval(), or other unsafe methods
|