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,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