language-operator 0.1.58 → 0.1.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/components/agent/Gemfile +1 -1
  4. data/lib/language_operator/agent/base.rb +22 -0
  5. data/lib/language_operator/agent/task_executor.rb +80 -23
  6. data/lib/language_operator/agent/telemetry.rb +22 -11
  7. data/lib/language_operator/agent.rb +3 -0
  8. data/lib/language_operator/cli/base_command.rb +7 -1
  9. data/lib/language_operator/cli/commands/agent.rb +575 -0
  10. data/lib/language_operator/cli/formatters/optimization_formatter.rb +226 -0
  11. data/lib/language_operator/cli/formatters/progress_formatter.rb +1 -1
  12. data/lib/language_operator/client/base.rb +74 -2
  13. data/lib/language_operator/client/mcp_connector.rb +4 -6
  14. data/lib/language_operator/dsl/task_definition.rb +7 -6
  15. data/lib/language_operator/learning/adapters/base_adapter.rb +149 -0
  16. data/lib/language_operator/learning/adapters/jaeger_adapter.rb +221 -0
  17. data/lib/language_operator/learning/adapters/signoz_adapter.rb +435 -0
  18. data/lib/language_operator/learning/adapters/tempo_adapter.rb +239 -0
  19. data/lib/language_operator/learning/optimizer.rb +319 -0
  20. data/lib/language_operator/learning/pattern_detector.rb +260 -0
  21. data/lib/language_operator/learning/task_synthesizer.rb +288 -0
  22. data/lib/language_operator/learning/trace_analyzer.rb +285 -0
  23. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
  24. data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
  25. data/lib/language_operator/templates/task_synthesis.tmpl +98 -0
  26. data/lib/language_operator/ux/concerns/provider_helpers.rb +2 -2
  27. data/lib/language_operator/version.rb +1 -1
  28. data/synth/003/Makefile +10 -3
  29. data/synth/003/output.log +68 -0
  30. data/synth/README.md +1 -3
  31. metadata +12 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: afc74193530a3f556debc6b31b0a43769da67eaf9c956d0a7b4f17477792bf94
4
- data.tar.gz: ffc401b931b0b3bd427c92a36847986442b738f2508acd1206547aa4d14c92eb
3
+ metadata.gz: 614dadae712dd0b6c1b72314497c73cdbcba8aa247a63cf3d01dbd789b0becaf
4
+ data.tar.gz: 72ad29bec20d5fdc6da1f235cd054617567c2516522e1a45bbd52f09c15bdb65
5
5
  SHA512:
6
- metadata.gz: 5079fbb3621d2e3d0f00a9381bc7b6fa09c3e28ae029d3e6cde4b7e0ac42643c7881953dd66549f0379414c0a74053dfed301e832812d82902d0de553f791158
7
- data.tar.gz: f24aea2d8cb473e7e9633fe713e663f637391c258695eacdd091b94dc12b206606408162f1d3cf3ae60484aa8097177bd4a858d392b132e07f6aaafadf63f35a
6
+ metadata.gz: cd062a5b0420dd602f411dd30b1e6fd7aa3dd7bce77ddbf7e94af95eb6991b609f57aa797c59f519150e518e89ba0839b43ca5be9a216a576b26ad9bb3b05ba0
7
+ data.tar.gz: cfdaa40cc2ced23bd59343b6c995499e9c718c1782f8c8c50766d1180399adc2b40c6fafd21865bb0c9130f1e9c46713639faa7985afa739f28bc47d4e66f993
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- language-operator (0.1.58)
4
+ language-operator (0.1.61)
5
5
  faraday (~> 2.0)
6
6
  k8s-ruby (~> 0.17)
7
7
  mcp (~> 0.4)
@@ -2,7 +2,7 @@
2
2
 
3
3
  source 'https://rubygems.org'
4
4
 
5
- gem 'language-operator', '~> 0.1.46'
5
+ gem 'language-operator', '~> 0.1.59'
6
6
 
7
7
  # Agent-specific dependencies for autonomous execution
8
8
  gem 'concurrent-ruby', '~> 1.3'
@@ -29,6 +29,9 @@ module LanguageOperator
29
29
  def initialize(config)
30
30
  super
31
31
 
32
+ # Log version
33
+ logger.info "Language Operator v#{LanguageOperator::VERSION}"
34
+
32
35
  # Initialize OpenTelemetry
33
36
  LanguageOperator::Agent::Telemetry.configure
34
37
  otel_enabled = !ENV.fetch('OTEL_EXPORTER_OTLP_ENDPOINT', nil).nil?
@@ -63,6 +66,9 @@ module LanguageOperator
63
66
  else
64
67
  raise "Unknown agent mode: #{normalized_mode}"
65
68
  end
69
+ ensure
70
+ # Flush telemetry for short-lived processes (scheduled mode)
71
+ flush_telemetry if normalized_mode == 'scheduled'
66
72
  end
67
73
  end
68
74
 
@@ -112,6 +118,22 @@ module LanguageOperator
112
118
  @web_server = WebServer.new(self)
113
119
  @web_server.start
114
120
  end
121
+
122
+ # Flush OpenTelemetry spans to ensure they're exported before process exits
123
+ #
124
+ # Critical for short-lived processes (CronJobs) that exit quickly.
125
+ # BatchSpanProcessor buffers spans and exports periodically, so without
126
+ # explicit flushing, spans may be lost when the process terminates.
127
+ #
128
+ # @return [void]
129
+ def flush_telemetry
130
+ return unless ENV.fetch('OTEL_EXPORTER_OTLP_ENDPOINT', nil)
131
+
132
+ OpenTelemetry.tracer_provider.force_flush
133
+ logger.info('OpenTelemetry spans flushed to OTLP endpoint')
134
+ rescue StandardError => e
135
+ logger.warn("Failed to flush telemetry: #{e.message}")
136
+ end
115
137
  end
116
138
  end
117
139
  end
@@ -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)
@@ -214,21 +215,47 @@ module LanguageOperator
214
215
 
215
216
  # Helper method for symbolic tasks to execute tools
216
217
  #
217
- # This is a simplified interface - symbolic tasks should primarily use
218
- # execute_llm to leverage tools through the LLM interface, or call tools
219
- # directly through the MCP client if needed.
218
+ # Executes an MCP tool directly through the agent's MCP clients.
220
219
  #
221
- # @param tool_name [String] Name of the tool
222
- # @param action [String] Tool action/method
220
+ # @param tool_name [Symbol, String] Name of the tool to execute
223
221
  # @param params [Hash] Tool parameters
224
- # @return [Object] Tool response
225
- # @note For DSL v1, tools are accessed via LLM tool calling, not direct invocation
226
- def execute_tool(tool_name, action, params = {})
227
- # Build prompt to use the tool via LLM
228
- prompt = "Use the #{tool_name} tool to perform #{action} with parameters: #{params.inspect}"
229
- execute_llm(prompt)
230
- # Parse response - for now just return the text
231
- # 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
+ # Extract text from MCP Content objects
238
+ text_result = if result.is_a?(RubyLLM::MCP::Content)
239
+ result.text
240
+ elsif result.respond_to?(:map) && result.first.is_a?(RubyLLM::MCP::Content)
241
+ result.map(&:text).join
242
+ else
243
+ result
244
+ end
245
+
246
+ logger.debug('Tool call completed',
247
+ tool: tool_name_str,
248
+ result_preview: text_result.is_a?(String) ? text_result[0..200] : text_result.class.name)
249
+
250
+ # Try to parse JSON response if it looks like JSON
251
+ if text_result.is_a?(String) && (text_result.strip.start_with?('{') || text_result.strip.start_with?('['))
252
+ JSON.parse(text_result, symbolize_names: true)
253
+ else
254
+ text_result
255
+ end
256
+ rescue JSON::ParserError
257
+ # Not JSON, return as-is
258
+ text_result
232
259
  end
233
260
 
234
261
  # Helper method for symbolic tasks to call LLM directly
@@ -291,6 +318,25 @@ module LanguageOperator
291
318
  'Agent::TaskExecutor'
292
319
  end
293
320
 
321
+ # Summarize hash values for logging (truncate long strings)
322
+ #
323
+ # @param hash [Hash] Hash to summarize
324
+ # @return [Hash] Summarized hash with truncated values
325
+ def summarize_values(hash)
326
+ return {} unless hash.is_a?(Hash)
327
+
328
+ hash.transform_values do |v|
329
+ case v
330
+ when String
331
+ v.length > 100 ? "#{v[0..97]}... (#{v.length} chars)" : v
332
+ when Array
333
+ v.length > 5 ? "#{v.first(3).inspect}... (#{v.length} items)" : v.inspect
334
+ else
335
+ v.inspect
336
+ end
337
+ end
338
+ end
339
+
294
340
  # Build prompt for neural task execution
295
341
  #
296
342
  # @param task [TaskDefinition] The task definition
@@ -316,10 +362,11 @@ module LanguageOperator
316
362
  prompt += "\n"
317
363
 
318
364
  prompt += "## Response Format\n"
319
- prompt += "Return ONLY valid JSON matching the output schema above.\n"
320
- prompt += "Do NOT include any explanations, thinking, or text before or after the JSON.\n"
321
- prompt += "Do NOT use [THINK] tags or any other markup.\n"
365
+ prompt += "You may include your reasoning in [THINK]...[/THINK] tags if helpful.\n"
322
366
  prompt += "Use available tools as needed to complete the task.\n"
367
+ prompt += "After using tools (if needed), return your final answer as valid JSON matching the output schema above.\n"
368
+ prompt += "Your final JSON response should come after any tool calls and thinking.\n"
369
+ prompt += "Do not include explanations outside of [THINK] tags - only the JSON output.\n"
323
370
 
324
371
  prompt
325
372
  end
@@ -342,8 +389,15 @@ module LanguageOperator
342
389
  thinking_preview: thinking_blocks.first&.[](0..500))
343
390
  end
344
391
 
345
- # Strip thinking tags that some models add (e.g., [THINK]...[/THINK])
346
- cleaned_text = response_text.gsub(%r{\[THINK\].*?\[/THINK\]}m, '').strip
392
+ # Strip thinking tags that some models add (e.g., [THINK]...[/THINK] or unclosed [THINK]...)
393
+ # First try to strip matched pairs, then strip any remaining unclosed [THINK] content
394
+ logger.debug('Parsing neural response', task: task.name, response_length: response_text.length, response_start: response_text[0..100])
395
+
396
+ cleaned_text = response_text.gsub(%r{\[THINK\].*?\[/THINK\]}m, '')
397
+ .gsub(/\[THINK\].*?(?=\{|$)/m, '')
398
+ .strip
399
+
400
+ logger.debug('After stripping THINK tags', cleaned_length: cleaned_text.length, cleaned_start: cleaned_text[0..100])
347
401
 
348
402
  # Try to extract JSON from response
349
403
  # Look for JSON code blocks first
@@ -356,6 +410,8 @@ module LanguageOperator
356
410
  json_object_match ? json_object_match[0] : cleaned_text
357
411
  end
358
412
 
413
+ logger.debug('Extracted JSON text', json_length: json_text.length, json_start: json_text[0..100])
414
+
359
415
  # Parse JSON
360
416
  parsed = JSON.parse(json_text)
361
417
 
@@ -503,10 +559,11 @@ module LanguageOperator
503
559
  end
504
560
 
505
561
  execution_time = Time.now - attempt_start
506
- logger.debug('Task execution completed',
507
- task: task_name,
508
- attempt: attempt + 1,
509
- execution_time: execution_time.round(3))
562
+ logger.info('Task completed',
563
+ task: task_name,
564
+ attempt: attempt + 1,
565
+ execution_time: execution_time.round(3),
566
+ outputs: summarize_values(result))
510
567
 
511
568
  result
512
569
  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