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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/lib/language_operator/agent/base.rb +19 -0
  4. data/lib/language_operator/agent/executor.rb +11 -0
  5. data/lib/language_operator/agent/task_executor.rb +77 -22
  6. data/lib/language_operator/agent/telemetry.rb +22 -11
  7. data/lib/language_operator/agent.rb +3 -0
  8. data/lib/language_operator/cli/base_command.rb +7 -1
  9. data/lib/language_operator/cli/commands/agent.rb +578 -1
  10. data/lib/language_operator/cli/formatters/optimization_formatter.rb +226 -0
  11. data/lib/language_operator/cli/formatters/progress_formatter.rb +1 -1
  12. data/lib/language_operator/client/base.rb +72 -2
  13. data/lib/language_operator/client/mcp_connector.rb +28 -6
  14. data/lib/language_operator/instrumentation/task_tracer.rb +64 -2
  15. data/lib/language_operator/kubernetes/resource_builder.rb +3 -1
  16. data/lib/language_operator/learning/adapters/base_adapter.rb +147 -0
  17. data/lib/language_operator/learning/adapters/jaeger_adapter.rb +218 -0
  18. data/lib/language_operator/learning/adapters/signoz_adapter.rb +432 -0
  19. data/lib/language_operator/learning/adapters/tempo_adapter.rb +236 -0
  20. data/lib/language_operator/learning/optimizer.rb +318 -0
  21. data/lib/language_operator/learning/pattern_detector.rb +260 -0
  22. data/lib/language_operator/learning/task_synthesizer.rb +261 -0
  23. data/lib/language_operator/learning/trace_analyzer.rb +280 -0
  24. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
  25. data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
  26. data/lib/language_operator/templates/task_synthesis.tmpl +97 -0
  27. data/lib/language_operator/tool_loader.rb +5 -3
  28. data/lib/language_operator/ux/concerns/provider_helpers.rb +2 -2
  29. data/lib/language_operator/version.rb +1 -1
  30. data/synth/003/Makefile +10 -0
  31. data/synth/003/output.log +68 -0
  32. data/synth/README.md +1 -3
  33. metadata +12 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 531bb19bab7e2aaac4dbc7e430147c722e42cad7802a662a13e7aab0c29fdf3a
4
- data.tar.gz: a7f7ede687479319814e519c788a57906357e76bef99b45616282c1828b7959f
3
+ metadata.gz: 478995080aedadd2299a94fb609d3180634abd352440b985f4eece929275bdf8
4
+ data.tar.gz: f0b890c5825447e6ead0ec172d9467afd59138f92350457e733cf269125e6757
5
5
  SHA512:
6
- metadata.gz: a9782736dde3ec6caace204cd585de5a887c43086236325062f911e2d4271754b9a3d957c03b9ad2a8b795fcfadb6d6d5ad9f76016635182bf9ec45d7cbc1560
7
- data.tar.gz: 3b1817c3d6c8ef8eff5cba64c47e7bf85edac523376a78170f16a702d82fc1a7ac0db21f7df547e99da94ad27adcd78774693faf980dceca75a957c44fd1900b
6
+ metadata.gz: 55a51a910de5d8580b741690ca56a2ba489cf5424892b6a9db2b31756936b8c8c11b1fe7d85b9c042cd8c56f04ab6f117af027dc28bc6cd47c6e571cbf53d7e6
7
+ data.tar.gz: fcf2dcad6af25f904c806d0a7b0216a7c6aa4d6d3803000096c33c350cb56050387dcb783394534226d1cc52def72628d57157dc54af60e694c229607c796147
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- language-operator (0.1.57)
4
+ language-operator (0.1.59)
5
5
  faraday (~> 2.0)
6
6
  k8s-ruby (~> 0.17)
7
7
  mcp (~> 0.4)
@@ -63,6 +63,9 @@ module LanguageOperator
63
63
  else
64
64
  raise "Unknown agent mode: #{normalized_mode}"
65
65
  end
66
+ ensure
67
+ # Flush telemetry for short-lived processes (scheduled mode)
68
+ flush_telemetry if normalized_mode == 'scheduled'
66
69
  end
67
70
  end
68
71
 
@@ -112,6 +115,22 @@ module LanguageOperator
112
115
  @web_server = WebServer.new(self)
113
116
  @web_server.start
114
117
  end
118
+
119
+ # Flush OpenTelemetry spans to ensure they're exported before process exits
120
+ #
121
+ # Critical for short-lived processes (CronJobs) that exit quickly.
122
+ # BatchSpanProcessor buffers spans and exports periodically, so without
123
+ # explicit flushing, spans may be lost when the process terminates.
124
+ #
125
+ # @return [void]
126
+ def flush_telemetry
127
+ return unless ENV.fetch('OTEL_EXPORTER_OTLP_ENDPOINT', nil)
128
+
129
+ OpenTelemetry.tracer_provider.force_flush
130
+ logger.info('OpenTelemetry spans flushed to OTLP endpoint')
131
+ rescue StandardError => e
132
+ logger.warn("Failed to flush telemetry: #{e.message}")
133
+ end
115
134
  end
116
135
  end
117
136
  end
@@ -107,6 +107,17 @@ module LanguageOperator
107
107
  )
108
108
  end
109
109
 
110
+ # Capture thinking blocks before stripping (for observability)
111
+ thinking_blocks = result_text.scan(%r{\[THINK\](.*?)\[/THINK\]}m).flatten
112
+ if thinking_blocks.any?
113
+ logger.info('LLM thinking captured',
114
+ event: 'llm_thinking',
115
+ iteration: @iteration_count,
116
+ thinking_steps: thinking_blocks.length,
117
+ thinking: thinking_blocks,
118
+ thinking_preview: thinking_blocks.first&.[](0..500))
119
+ end
120
+
110
121
  # Log the actual LLM response content (strip [THINK] blocks)
111
122
  cleaned_response = result_text.gsub(%r{\[THINK\].*?\[/THINK\]}m, '').strip
112
123
  response_preview = cleaned_response.length > 500 ? "#{cleaned_response[0..500]}..." : cleaned_response
@@ -117,7 +117,8 @@ module LanguageOperator
117
117
  task: task_name,
118
118
  type: task_type,
119
119
  timeout: timeout,
120
- max_retries: max_retries)
120
+ max_retries: max_retries,
121
+ inputs: summarize_values(inputs))
121
122
 
122
123
  # Add timeout to span attributes after it's determined
123
124
  OpenTelemetry::Trace.current_span&.set_attribute('task.timeout', timeout)
@@ -164,10 +165,15 @@ module LanguageOperator
164
165
  logger.debug('Calling LLM with prompt', task: task.name, prompt_preview: prompt[0..200])
165
166
  response = @agent.send_message(prompt)
166
167
 
168
+ # Check for tool calls and log details
169
+ has_tool_calls = response.respond_to?(:tool_calls) && response.tool_calls&.any?
170
+ tool_call_count = has_tool_calls ? response.tool_calls.length : 0
171
+
167
172
  logger.info('LLM response received, extracting content',
168
173
  task: task.name,
169
174
  response_class: response.class.name,
170
- has_tool_calls: response.respond_to?(:tool_calls) && response.tool_calls&.any?)
175
+ has_tool_calls: has_tool_calls,
176
+ tool_call_count: tool_call_count)
171
177
 
172
178
  response_text = response.is_a?(String) ? response : response.content
173
179
 
@@ -209,21 +215,38 @@ module LanguageOperator
209
215
 
210
216
  # Helper method for symbolic tasks to execute tools
211
217
  #
212
- # This is a simplified interface - symbolic tasks should primarily use
213
- # execute_llm to leverage tools through the LLM interface, or call tools
214
- # directly through the MCP client if needed.
218
+ # Executes an MCP tool directly through the agent's MCP clients.
215
219
  #
216
- # @param tool_name [String] Name of the tool
217
- # @param action [String] Tool action/method
220
+ # @param tool_name [Symbol, String] Name of the tool to execute
218
221
  # @param params [Hash] Tool parameters
219
- # @return [Object] Tool response
220
- # @note For DSL v1, tools are accessed via LLM tool calling, not direct invocation
221
- def execute_tool(tool_name, action, params = {})
222
- # Build prompt to use the tool via LLM
223
- prompt = "Use the #{tool_name} tool to perform #{action} with parameters: #{params.inspect}"
224
- execute_llm(prompt)
225
- # Parse response - for now just return the text
226
- # TODO: More sophisticated tool result extraction
222
+ # @return [Object] Tool response (parsed from tool result)
223
+ def execute_tool(tool_name, params = {})
224
+ tool_name_str = tool_name.to_s
225
+
226
+ logger.info('Tool call initiated by symbolic task',
227
+ tool: tool_name_str,
228
+ params: summarize_values(params))
229
+
230
+ # Find the tool across all MCP clients
231
+ tool = @agent.tools.find { |t| t.name == tool_name_str }
232
+ raise ArgumentError, "Tool '#{tool_name_str}' not found" unless tool
233
+
234
+ # Execute the tool (it's a Proc/lambda wrapped by RubyLLM)
235
+ result = tool.call(**params)
236
+
237
+ logger.debug('Tool call completed',
238
+ tool: tool_name_str,
239
+ result_preview: result.is_a?(String) ? result[0..200] : result.class.name)
240
+
241
+ # Try to parse JSON response if it looks like JSON
242
+ if result.is_a?(String) && (result.strip.start_with?('{') || result.strip.start_with?('['))
243
+ JSON.parse(result, symbolize_names: true)
244
+ else
245
+ result
246
+ end
247
+ rescue JSON::ParserError
248
+ # Not JSON, return as-is
249
+ result
227
250
  end
228
251
 
229
252
  # Helper method for symbolic tasks to call LLM directly
@@ -286,6 +309,25 @@ module LanguageOperator
286
309
  'Agent::TaskExecutor'
287
310
  end
288
311
 
312
+ # Summarize hash values for logging (truncate long strings)
313
+ #
314
+ # @param hash [Hash] Hash to summarize
315
+ # @return [Hash] Summarized hash with truncated values
316
+ def summarize_values(hash)
317
+ return {} unless hash.is_a?(Hash)
318
+
319
+ hash.transform_values do |v|
320
+ case v
321
+ when String
322
+ v.length > 100 ? "#{v[0..97]}... (#{v.length} chars)" : v
323
+ when Array
324
+ v.length > 5 ? "#{v.first(3).inspect}... (#{v.length} items)" : v.inspect
325
+ else
326
+ v.inspect
327
+ end
328
+ end
329
+ end
330
+
289
331
  # Build prompt for neural task execution
290
332
  #
291
333
  # @param task [TaskDefinition] The task definition
@@ -311,10 +353,11 @@ module LanguageOperator
311
353
  prompt += "\n"
312
354
 
313
355
  prompt += "## Response Format\n"
314
- prompt += "Return ONLY valid JSON matching the output schema above.\n"
315
- prompt += "Do NOT include any explanations, thinking, or text before or after the JSON.\n"
316
- prompt += "Do NOT use [THINK] tags or any other markup.\n"
356
+ prompt += "You may include your reasoning in [THINK]...[/THINK] tags if helpful.\n"
317
357
  prompt += "Use available tools as needed to complete the task.\n"
358
+ prompt += "After using tools (if needed), return your final answer as valid JSON matching the output schema above.\n"
359
+ prompt += "Your final JSON response should come after any tool calls and thinking.\n"
360
+ prompt += "Do not include explanations outside of [THINK] tags - only the JSON output.\n"
318
361
 
319
362
  prompt
320
363
  end
@@ -326,6 +369,17 @@ module LanguageOperator
326
369
  # @return [Hash] Parsed outputs
327
370
  # @raise [RuntimeError] If parsing fails
328
371
  def parse_neural_response(response_text, task)
372
+ # Capture thinking blocks before stripping (for observability)
373
+ thinking_blocks = response_text.scan(%r{\[THINK\](.*?)\[/THINK\]}m).flatten
374
+ if thinking_blocks.any?
375
+ logger.info('LLM thinking captured',
376
+ event: 'llm_thinking',
377
+ task: task.name,
378
+ thinking_steps: thinking_blocks.length,
379
+ thinking: thinking_blocks,
380
+ thinking_preview: thinking_blocks.first&.[](0..500))
381
+ end
382
+
329
383
  # Strip thinking tags that some models add (e.g., [THINK]...[/THINK])
330
384
  cleaned_text = response_text.gsub(%r{\[THINK\].*?\[/THINK\]}m, '').strip
331
385
 
@@ -487,10 +541,11 @@ module LanguageOperator
487
541
  end
488
542
 
489
543
  execution_time = Time.now - attempt_start
490
- logger.debug('Task execution completed',
491
- task: task_name,
492
- attempt: attempt + 1,
493
- execution_time: execution_time.round(3))
544
+ logger.info('Task completed',
545
+ task: task_name,
546
+ attempt: attempt + 1,
547
+ execution_time: execution_time.round(3),
548
+ outputs: summarize_values(result))
494
549
 
495
550
  result
496
551
  rescue Timeout::Error => e
@@ -26,29 +26,40 @@ module LanguageOperator
26
26
  #
27
27
  # @return [void]
28
28
  def configure
29
- endpoint = ENV.fetch('OTEL_EXPORTER_OTLP_ENDPOINT', nil)
30
- return unless endpoint
29
+ return unless ENV.fetch('OTEL_EXPORTER_OTLP_ENDPOINT', nil)
30
+
31
+ # Configure custom error handler for detailed logging
32
+ OpenTelemetry.error_handler = lambda do |exception: nil, message: nil|
33
+ if exception
34
+ warn "OpenTelemetry error: #{message} - #{exception.class}: #{exception.message}"
35
+ warn exception.backtrace.first(5).join("\n") if exception.backtrace
36
+ else
37
+ warn "OpenTelemetry error: #{message}"
38
+ end
39
+ end
31
40
 
41
+ # Initialize OpenTelemetry SDK with OTLP exporter
42
+ # Uses environment variables set by the operator:
43
+ # - OTEL_EXPORTER_OTLP_ENDPOINT: http://host:port
44
+ # - OTEL_SERVICE_NAME: service name
32
45
  OpenTelemetry::SDK.configure do |c|
33
- c.service_name = 'language-operator-agent'
34
- c.service_version = LanguageOperator::VERSION
46
+ c.service_name = ENV.fetch('OTEL_SERVICE_NAME', 'language-operator-agent')
35
47
 
36
- # Configure resource attributes
37
- c.resource = OpenTelemetry::SDK::Resources::Resource.create(
38
- build_resource_attributes
39
- )
48
+ # Add resource attributes
49
+ c.resource = OpenTelemetry::SDK::Resources::Resource.create(build_resource_attributes)
40
50
 
41
- # Configure OTLP exporter
51
+ # Use OTLP HTTP exporter (reads endpoint from OTEL_EXPORTER_OTLP_ENDPOINT env var)
42
52
  c.add_span_processor(
43
53
  OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
44
54
  OpenTelemetry::Exporter::OTLP::Exporter.new(
45
- endpoint: endpoint
55
+ endpoint: "#{ENV.fetch('OTEL_EXPORTER_OTLP_ENDPOINT')}/v1/traces",
56
+ headers: {}
46
57
  )
47
58
  )
48
59
  )
49
60
  end
50
61
 
51
- # Restore trace context from TRACEPARENT if present
62
+ # Restore trace context from TRACEPARENT if present for distributed tracing
52
63
  restore_trace_context if ENV['TRACEPARENT']
53
64
  rescue StandardError => e
54
65
  warn "Failed to configure OpenTelemetry: #{e.message}"
@@ -185,6 +185,9 @@ module LanguageOperator
185
185
 
186
186
  logger.info('Scheduled execution completed - exiting',
187
187
  agent_name: agent_def.name)
188
+
189
+ # Flush telemetry for short-lived processes
190
+ agent.send(:flush_telemetry)
188
191
  when 'reactive', 'http', 'webhook'
189
192
  # Start web server with webhooks, MCP tools, and chat endpoint
190
193
  web_server = LanguageOperator::Agent::WebServer.new(agent)
@@ -20,7 +20,13 @@ module LanguageOperator
20
20
  yield
21
21
  rescue StandardError => e
22
22
  Formatters::ProgressFormatter.error("Failed to #{operation}: #{e.message}")
23
- raise if ENV['DEBUG']
23
+
24
+ # Show backtrace for debugging
25
+ if ENV['DEBUG']
26
+ puts "\nBacktrace:"
27
+ puts e.backtrace.join("\n")
28
+ raise
29
+ end
24
30
 
25
31
  exit 1
26
32
  end