language-operator 0.1.31 → 0.1.35

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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -8
  3. data/CHANGELOG.md +14 -0
  4. data/CI_STATUS.md +56 -0
  5. data/Gemfile.lock +2 -2
  6. data/Makefile +22 -6
  7. data/lib/language_operator/agent/base.rb +10 -6
  8. data/lib/language_operator/agent/executor.rb +19 -97
  9. data/lib/language_operator/agent/safety/ast_validator.rb +62 -43
  10. data/lib/language_operator/agent/safety/safe_executor.rb +27 -2
  11. data/lib/language_operator/agent/scheduler.rb +60 -0
  12. data/lib/language_operator/agent/task_executor.rb +548 -0
  13. data/lib/language_operator/agent.rb +90 -27
  14. data/lib/language_operator/cli/base_command.rb +117 -0
  15. data/lib/language_operator/cli/commands/agent.rb +339 -407
  16. data/lib/language_operator/cli/commands/cluster.rb +274 -290
  17. data/lib/language_operator/cli/commands/install.rb +110 -119
  18. data/lib/language_operator/cli/commands/model.rb +284 -184
  19. data/lib/language_operator/cli/commands/persona.rb +218 -284
  20. data/lib/language_operator/cli/commands/quickstart.rb +4 -5
  21. data/lib/language_operator/cli/commands/status.rb +31 -35
  22. data/lib/language_operator/cli/commands/system.rb +221 -233
  23. data/lib/language_operator/cli/commands/tool.rb +356 -422
  24. data/lib/language_operator/cli/commands/use.rb +19 -22
  25. data/lib/language_operator/cli/helpers/resource_dependency_checker.rb +0 -18
  26. data/lib/language_operator/cli/wizards/quickstart_wizard.rb +0 -1
  27. data/lib/language_operator/client/config.rb +20 -21
  28. data/lib/language_operator/config.rb +115 -3
  29. data/lib/language_operator/constants.rb +54 -0
  30. data/lib/language_operator/dsl/agent_context.rb +7 -7
  31. data/lib/language_operator/dsl/agent_definition.rb +111 -26
  32. data/lib/language_operator/dsl/config.rb +30 -66
  33. data/lib/language_operator/dsl/main_definition.rb +114 -0
  34. data/lib/language_operator/dsl/schema.rb +84 -43
  35. data/lib/language_operator/dsl/task_definition.rb +315 -0
  36. data/lib/language_operator/dsl.rb +0 -1
  37. data/lib/language_operator/instrumentation/task_tracer.rb +285 -0
  38. data/lib/language_operator/logger.rb +4 -4
  39. data/lib/language_operator/synthesis_test_harness.rb +324 -0
  40. data/lib/language_operator/templates/examples/agent_synthesis.tmpl +26 -8
  41. data/lib/language_operator/templates/schema/CHANGELOG.md +26 -0
  42. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
  43. data/lib/language_operator/templates/schema/agent_dsl_schema.json +84 -42
  44. data/lib/language_operator/type_coercion.rb +250 -0
  45. data/lib/language_operator/ux/base.rb +81 -0
  46. data/lib/language_operator/ux/concerns/README.md +155 -0
  47. data/lib/language_operator/ux/concerns/headings.rb +90 -0
  48. data/lib/language_operator/ux/concerns/input_validation.rb +146 -0
  49. data/lib/language_operator/ux/concerns/provider_helpers.rb +167 -0
  50. data/lib/language_operator/ux/create_agent.rb +252 -0
  51. data/lib/language_operator/ux/create_model.rb +267 -0
  52. data/lib/language_operator/ux/quickstart.rb +594 -0
  53. data/lib/language_operator/version.rb +1 -1
  54. data/lib/language_operator.rb +2 -0
  55. data/requirements/ARCHITECTURE.md +1 -0
  56. data/requirements/SCRATCH.md +153 -0
  57. data/requirements/dsl.md +0 -0
  58. data/requirements/features +1 -0
  59. data/requirements/personas +1 -0
  60. data/requirements/proposals +1 -0
  61. data/requirements/tasks/iterate.md +14 -15
  62. data/requirements/tasks/optimize.md +13 -4
  63. data/synth/001/Makefile +90 -0
  64. data/synth/001/agent.rb +26 -0
  65. data/synth/001/agent.yaml +7 -0
  66. data/synth/001/output.log +44 -0
  67. data/synth/Makefile +39 -0
  68. data/synth/README.md +342 -0
  69. metadata +37 -10
  70. data/lib/language_operator/dsl/workflow_definition.rb +0 -259
  71. data/test_agent_dsl.rb +0 -108
@@ -0,0 +1,315 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../loggable'
4
+ require_relative '../type_coercion'
5
+
6
+ module LanguageOperator
7
+ module Dsl
8
+ # Task definition for organic functions (DSL v1)
9
+ #
10
+ # Represents an organic function with a stable contract (inputs/outputs) where
11
+ # the implementation can evolve from neural (instructions-based) to symbolic
12
+ # (explicit code) without breaking callers.
13
+ #
14
+ # @example Neural task (LLM-based)
15
+ # task :analyze_data,
16
+ # instructions: "Analyze the data for anomalies",
17
+ # inputs: { data: 'array' },
18
+ # outputs: { issues: 'array', summary: 'string' }
19
+ #
20
+ # @example Symbolic task (explicit code)
21
+ # task :calculate_total,
22
+ # inputs: { items: 'array' },
23
+ # outputs: { total: 'number' }
24
+ # do |inputs|
25
+ # { total: inputs[:items].sum { |i| i['amount'] } }
26
+ # end
27
+ #
28
+ # @example Hybrid task (both instructions and code)
29
+ # task :fetch_user,
30
+ # instructions: "Fetch user data from database",
31
+ # inputs: { user_id: 'integer' },
32
+ # outputs: { user: 'hash', preferences: 'hash' }
33
+ # do |inputs|
34
+ # execute_tool('database', 'get_user', id: inputs[:user_id])
35
+ # end
36
+ class TaskDefinition
37
+ include LanguageOperator::Loggable
38
+
39
+ attr_reader :name, :inputs_schema, :outputs_schema, :instructions_text, :execute_block
40
+
41
+ # Supported types for input/output validation
42
+ SUPPORTED_TYPES = %w[string integer number boolean array hash any].freeze
43
+
44
+ # Initialize a new task definition
45
+ #
46
+ # @param name [Symbol] Task name
47
+ def initialize(name)
48
+ @name = name
49
+ @inputs_schema = {}
50
+ @outputs_schema = {}
51
+ @instructions_text = nil
52
+ @execute_block = nil
53
+ end
54
+
55
+ # Define or retrieve the input contract
56
+ #
57
+ # @param schema [Hash, nil] Input schema (param_name => type_string)
58
+ # @return [Hash] Current input schema
59
+ # @example
60
+ # inputs { user_id: 'integer', filter: 'string' }
61
+ def inputs(schema = nil)
62
+ return @inputs_schema if schema.nil?
63
+
64
+ validate_schema!(schema, 'inputs')
65
+ @inputs_schema = schema
66
+ end
67
+
68
+ # Define or retrieve the output contract
69
+ #
70
+ # @param schema [Hash, nil] Output schema (field_name => type_string)
71
+ # @return [Hash] Current output schema
72
+ # @example
73
+ # outputs { user: 'hash', success: 'boolean' }
74
+ def outputs(schema = nil)
75
+ return @outputs_schema if schema.nil?
76
+
77
+ validate_schema!(schema, 'outputs')
78
+ @outputs_schema = schema
79
+ end
80
+
81
+ # Define or retrieve the instructions (neural implementation)
82
+ #
83
+ # @param text [String, nil] Natural language instructions
84
+ # @return [String, nil] Current instructions
85
+ # @example
86
+ # instructions "Fetch user data from the database"
87
+ def instructions(text = nil)
88
+ return @instructions_text if text.nil?
89
+
90
+ @instructions_text = text
91
+ end
92
+
93
+ # Define the symbolic implementation
94
+ #
95
+ # @yield [inputs] Block that receives validated inputs and returns outputs
96
+ # @yieldparam inputs [Hash] Validated and coerced input parameters
97
+ # @yieldreturn [Hash] Output values matching the outputs schema
98
+ # @example
99
+ # execute do |inputs|
100
+ # { total: inputs[:items].sum { |i| i['amount'] } }
101
+ # end
102
+ def execute(&block)
103
+ @execute_block = block if block
104
+ end
105
+
106
+ # Check if this is a neural task (instructions-based)
107
+ #
108
+ # @return [Boolean] True if instructions are defined
109
+ def neural?
110
+ !@instructions_text.nil?
111
+ end
112
+
113
+ # Check if this is a symbolic task (code-based)
114
+ #
115
+ # @return [Boolean] True if execute block is defined
116
+ def symbolic?
117
+ !@execute_block.nil?
118
+ end
119
+
120
+ # Execute the task with given inputs
121
+ #
122
+ # @param input_params [Hash] Input parameters
123
+ # @param context [Object, nil] Execution context (optional)
124
+ # @return [Hash] Validated output matching outputs schema
125
+ # @raise [ArgumentError] If inputs or outputs don't match schema
126
+ def call(input_params, context = nil)
127
+ # Validate and coerce inputs
128
+ validated_inputs = validate_inputs(input_params)
129
+
130
+ # Execute based on implementation type
131
+ result = if symbolic?
132
+ # Symbolic execution (explicit code)
133
+ logger.debug('Executing symbolic task', task: @name)
134
+ execute_symbolic(validated_inputs, context)
135
+ elsif neural?
136
+ # Neural execution (LLM-based)
137
+ logger.debug('Executing neural task', task: @name, instructions: @instructions_text)
138
+ execute_neural(validated_inputs, context)
139
+ else
140
+ raise "Task #{@name} has no implementation (neither neural nor symbolic)"
141
+ end
142
+
143
+ # Validate outputs
144
+ validate_outputs(result)
145
+ end
146
+
147
+ # Validate input parameters against schema
148
+ #
149
+ # @param params [Hash] Input parameters
150
+ # @return [Hash] Validated and coerced parameters
151
+ # @raise [ArgumentError] If validation fails
152
+ def validate_inputs(params)
153
+ params = params.transform_keys(&:to_sym)
154
+ validated = {}
155
+
156
+ @inputs_schema.each do |key, type|
157
+ key_sym = key.to_sym
158
+ value = params[key_sym]
159
+
160
+ raise ArgumentError, "Missing required input parameter: #{key}" if value.nil?
161
+
162
+ validated[key_sym] = coerce_value(value, type, "input parameter '#{key}'")
163
+ end
164
+
165
+ # Check for unexpected parameters
166
+ extra_keys = params.keys - @inputs_schema.keys.map(&:to_sym)
167
+ logger.warn('Unexpected input parameters', task: @name, extra: extra_keys) unless extra_keys.empty?
168
+
169
+ validated
170
+ end
171
+
172
+ # Validate output values against schema
173
+ #
174
+ # @param result [Hash] Output values
175
+ # @return [Hash] Validated and coerced outputs
176
+ # @raise [ArgumentError] If validation fails
177
+ def validate_outputs(result)
178
+ return result if @outputs_schema.empty? # No schema = no validation
179
+
180
+ result = result.transform_keys(&:to_sym)
181
+ validated = {}
182
+
183
+ @outputs_schema.each do |key, type|
184
+ key_sym = key.to_sym
185
+ value = result[key_sym]
186
+
187
+ raise ArgumentError, "Missing required output field: #{key}" if value.nil?
188
+
189
+ validated[key_sym] = coerce_value(value, type, "output field '#{key}'")
190
+ end
191
+
192
+ validated
193
+ end
194
+
195
+ # Export task as JSON schema
196
+ #
197
+ # @return [Hash] JSON Schema representation
198
+ def to_schema
199
+ {
200
+ 'name' => @name.to_s,
201
+ 'type' => implementation_type,
202
+ 'instructions' => @instructions_text,
203
+ 'inputs' => schema_to_json(@inputs_schema),
204
+ 'outputs' => schema_to_json(@outputs_schema)
205
+ }
206
+ end
207
+
208
+ private
209
+
210
+ def logger_component
211
+ "Task:#{@name}"
212
+ end
213
+
214
+ # Determine implementation type
215
+ #
216
+ # @return [String] 'neural', 'symbolic', or 'hybrid'
217
+ def implementation_type
218
+ if neural? && symbolic?
219
+ 'hybrid'
220
+ elsif neural?
221
+ 'neural'
222
+ elsif symbolic?
223
+ 'symbolic'
224
+ else
225
+ 'undefined'
226
+ end
227
+ end
228
+
229
+ # Execute symbolic implementation
230
+ #
231
+ # @param inputs [Hash] Validated inputs
232
+ # @param context [Object, nil] Execution context
233
+ # @return [Hash] Result from execute block
234
+ def execute_symbolic(inputs, context)
235
+ if @execute_block.arity == 1
236
+ @execute_block.call(inputs)
237
+ elsif @execute_block.arity == 2
238
+ @execute_block.call(inputs, context)
239
+ else
240
+ @execute_block.call
241
+ end
242
+ end
243
+
244
+ # Execute neural implementation (stub for now)
245
+ #
246
+ # @param inputs [Hash] Validated inputs
247
+ # @param context [Object, nil] Execution context
248
+ # @return [Hash] Result from LLM
249
+ # @note This is a placeholder - actual neural execution happens in agent runtime
250
+ def execute_neural(inputs, context)
251
+ raise NotImplementedError, 'Neural task execution requires agent runtime context. ' \
252
+ "Task #{@name} should be executed via execute_task() in agent main block."
253
+ end
254
+
255
+ # Validate a schema hash
256
+ #
257
+ # @param schema [Hash] Schema to validate
258
+ # @param name [String] Schema name (for error messages)
259
+ # @raise [ArgumentError] If schema is invalid
260
+ def validate_schema!(schema, name)
261
+ raise ArgumentError, "#{name} schema must be a Hash, got #{schema.class}" unless schema.is_a?(Hash)
262
+
263
+ schema.each do |key, type|
264
+ unless type.is_a?(String) && SUPPORTED_TYPES.include?(type)
265
+ raise ArgumentError, "#{name} schema type for '#{key}' must be one of #{SUPPORTED_TYPES.join(', ')}, " \
266
+ "got '#{type}'"
267
+ end
268
+ end
269
+ end
270
+
271
+ # Coerce a value to the specified type
272
+ #
273
+ # @param value [Object] Value to coerce
274
+ # @param type [String] Target type
275
+ # @param context [String] Context for error messages
276
+ # @return [Object] Coerced value
277
+ # @raise [ArgumentError] If coercion fails
278
+ def coerce_value(value, type, context)
279
+ TypeCoercion.coerce(value, type)
280
+ rescue ArgumentError => e
281
+ # Re-raise with context added
282
+ raise ArgumentError, "#{e.message} for #{context}"
283
+ end
284
+
285
+ # Convert schema hash to JSON Schema format
286
+ #
287
+ # @param schema [Hash] Type schema
288
+ # @return [Hash] JSON Schema
289
+ def schema_to_json(schema)
290
+ {
291
+ 'type' => 'object',
292
+ 'properties' => schema.transform_values { |type| { 'type' => map_type_to_json_schema(type) } },
293
+ 'required' => schema.keys.map(&:to_s)
294
+ }
295
+ end
296
+
297
+ # Map internal type to JSON Schema type
298
+ #
299
+ # @param type [String] Internal type name
300
+ # @return [String] JSON Schema type
301
+ def map_type_to_json_schema(type)
302
+ case type
303
+ when 'integer' then 'integer'
304
+ when 'number' then 'number'
305
+ when 'string' then 'string'
306
+ when 'boolean' then 'boolean'
307
+ when 'array' then 'array'
308
+ when 'hash' then 'object'
309
+ when 'any' then 'any'
310
+ else type
311
+ end
312
+ end
313
+ end
314
+ end
315
+ end
@@ -13,7 +13,6 @@ require_relative 'dsl/context'
13
13
  require_relative 'dsl/execution_context'
14
14
  require_relative 'dsl/agent_definition'
15
15
  require_relative 'dsl/agent_context'
16
- require_relative 'dsl/workflow_definition'
17
16
  require_relative 'dsl/schema'
18
17
  require_relative 'agent/safety/ast_validator'
19
18
  require_relative 'agent/safety/safe_executor'
@@ -0,0 +1,285 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'opentelemetry/sdk'
4
+ require 'json'
5
+
6
+ module LanguageOperator
7
+ module Instrumentation
8
+ # OpenTelemetry instrumentation for task execution
9
+ #
10
+ # Provides comprehensive tracing for DSL v1 task execution including:
11
+ # - Neural task execution (LLM calls with tool access)
12
+ # - Symbolic task execution (direct Ruby code)
13
+ # - Input/output validation
14
+ # - Retry attempts
15
+ # - Tool call tracking (when available)
16
+ #
17
+ # Follows OpenTelemetry Semantic Conventions for GenAI:
18
+ # https://opentelemetry.io/docs/specs/semconv/gen-ai/
19
+ #
20
+ # @example Enable full data capture
21
+ # ENV['CAPTURE_TASK_INPUTS'] = 'true'
22
+ # ENV['CAPTURE_TASK_OUTPUTS'] = 'true'
23
+ # ENV['CAPTURE_TOOL_ARGS'] = 'true'
24
+ #
25
+ # @example Performance overhead
26
+ # Instrumentation adds <5% overhead with default settings
27
+ # Overhead may increase to ~10% with full data capture enabled
28
+ #
29
+ module TaskTracer
30
+ # Maximum length for captured data before truncation
31
+ MAX_CAPTURED_LENGTH = 1000
32
+
33
+ private
34
+
35
+ # Check if data capture is enabled for a specific type
36
+ #
37
+ # @param type [Symbol] Type of data (:inputs, :outputs, :tool_args, :tool_results)
38
+ # @return [Boolean] Whether capture is enabled
39
+ def capture_enabled?(type)
40
+ case type
41
+ when :inputs
42
+ ENV['CAPTURE_TASK_INPUTS'] == 'true'
43
+ when :outputs
44
+ ENV['CAPTURE_TASK_OUTPUTS'] == 'true'
45
+ when :tool_args
46
+ ENV['CAPTURE_TOOL_ARGS'] == 'true'
47
+ when :tool_results
48
+ ENV['CAPTURE_TOOL_RESULTS'] == 'true'
49
+ else
50
+ false
51
+ end
52
+ end
53
+
54
+ # Sanitize data for span attributes
55
+ #
56
+ # By default, only captures metadata (sizes, counts).
57
+ # Full data capture requires explicit opt-in via environment variables.
58
+ #
59
+ # @param data [Object] Data to sanitize
60
+ # @param type [Symbol] Type of data for capture control
61
+ # @param max_length [Integer] Maximum length before truncation
62
+ # @return [String, nil] Sanitized string or nil if capture disabled
63
+ def sanitize_data(data, type, max_length: MAX_CAPTURED_LENGTH)
64
+ return nil unless capture_enabled?(type)
65
+
66
+ str = case data
67
+ when String then data
68
+ when Hash then JSON.generate(data)
69
+ else data.to_s
70
+ end
71
+
72
+ # Truncate if too long
73
+ if str.length > max_length
74
+ "#{str[0...max_length]}... (truncated #{str.length - max_length} chars)"
75
+ else
76
+ str
77
+ end
78
+ rescue StandardError => e
79
+ logger&.warn('Failed to sanitize data for tracing', error: e.message)
80
+ nil
81
+ end
82
+
83
+ # Build attributes for neural task span following GenAI semantic conventions
84
+ #
85
+ # @param task [TaskDefinition] The task definition
86
+ # @param prompt [String] The generated prompt
87
+ # @param validated_inputs [Hash] Validated input parameters
88
+ # @return [Hash] Span attributes
89
+ def neural_task_attributes(_task, prompt, validated_inputs)
90
+ attributes = {
91
+ 'gen_ai.operation.name' => 'chat',
92
+ 'gen_ai.system' => determine_genai_system,
93
+ 'gen_ai.prompt.size' => prompt.bytesize
94
+ }
95
+
96
+ # Add model if available
97
+ if @agent.respond_to?(:config) && @agent.config
98
+ model = @agent.config.dig('llm', 'model') || @agent.config['model']
99
+ attributes['gen_ai.request.model'] = model if model
100
+ end
101
+
102
+ # Add sanitized prompt if capture enabled
103
+ if (sanitized_prompt = sanitize_data(prompt, :inputs))
104
+ attributes['gen_ai.prompt'] = sanitized_prompt
105
+ end
106
+
107
+ # Add input metadata
108
+ attributes['task.input.keys'] = validated_inputs.keys.map(&:to_s).join(',')
109
+ attributes['task.input.count'] = validated_inputs.size
110
+
111
+ attributes
112
+ end
113
+
114
+ # Determine GenAI system identifier from agent configuration
115
+ #
116
+ # @return [String] GenAI system identifier
117
+ def determine_genai_system
118
+ return 'ruby_llm' unless @agent.respond_to?(:config)
119
+
120
+ provider = @agent.config.dig('llm', 'provider') ||
121
+ @agent.config['provider'] ||
122
+ 'ruby_llm'
123
+
124
+ provider.to_s
125
+ end
126
+
127
+ # Build attributes for symbolic task span
128
+ #
129
+ # @param task [TaskDefinition] The task definition
130
+ # @return [Hash] Span attributes
131
+ def symbolic_task_attributes(task)
132
+ {
133
+ 'task.execution.type' => 'symbolic',
134
+ 'task.execution.has_block' => task.execute_block ? 'true' : 'false'
135
+ }
136
+ end
137
+
138
+ # Record token usage from LLM response on span
139
+ #
140
+ # @param response [Object] LLM response object
141
+ # @param span [OpenTelemetry::Trace::Span] The span to update
142
+ def record_token_usage(response, span)
143
+ return unless response
144
+
145
+ span.set_attribute('gen_ai.usage.input_tokens', response.input_tokens.to_i) if response.respond_to?(:input_tokens) && response.input_tokens
146
+
147
+ span.set_attribute('gen_ai.usage.output_tokens', response.output_tokens.to_i) if response.respond_to?(:output_tokens) && response.output_tokens
148
+
149
+ # Try to get model from response if available
150
+ span.set_attribute('gen_ai.response.model', response.model.to_s) if response.respond_to?(:model) && response.model
151
+
152
+ # Try to get response ID if available
153
+ span.set_attribute('gen_ai.response.id', response.id.to_s) if response.respond_to?(:id) && response.id
154
+
155
+ # Try to get finish reason if available
156
+ span.set_attribute('gen_ai.response.finish_reasons', response.stop_reason.to_s) if response.respond_to?(:stop_reason) && response.stop_reason
157
+ rescue StandardError => e
158
+ logger&.warn('Failed to record token usage', error: e.message)
159
+ end
160
+
161
+ # Record tool calls from LLM response
162
+ #
163
+ # Attempts to extract tool call information from the response object
164
+ # and create child spans for each tool invocation.
165
+ #
166
+ # @param response [Object] LLM response object
167
+ # @param parent_span [OpenTelemetry::Trace::Span] Parent span
168
+ def record_tool_calls(response, parent_span)
169
+ return unless response.respond_to?(:tool_calls)
170
+ return unless response.tool_calls&.any?
171
+
172
+ response.tool_calls.each do |tool_call|
173
+ record_single_tool_call(tool_call, parent_span)
174
+ end
175
+ rescue StandardError => e
176
+ logger&.warn('Failed to record tool calls', error: e.message)
177
+ end
178
+
179
+ # Record a single tool call as a span
180
+ #
181
+ # @param tool_call [Object] Tool call object
182
+ # @param parent_span [OpenTelemetry::Trace::Span] Parent span
183
+ def record_single_tool_call(tool_call, _parent_span)
184
+ tool_name = extract_tool_name(tool_call)
185
+
186
+ tracer.in_span("execute_tool #{tool_name}", attributes: build_tool_call_attributes(tool_call)) do |tool_span|
187
+ # Tool execution already completed by ruby_llm
188
+ # Just record the metadata
189
+ record_tool_result(tool_call.result, tool_span) if tool_call.respond_to?(:result) && tool_call.result
190
+ end
191
+ rescue StandardError => e
192
+ logger&.warn('Failed to record tool call span', error: e.message, tool: tool_name)
193
+ end
194
+
195
+ # Extract tool name from tool call object
196
+ #
197
+ # @param tool_call [Object] Tool call object
198
+ # @return [String] Tool name
199
+ def extract_tool_name(tool_call)
200
+ if tool_call.respond_to?(:name)
201
+ tool_call.name.to_s
202
+ elsif tool_call.respond_to?(:function) && tool_call.function.respond_to?(:name)
203
+ tool_call.function.name.to_s
204
+ else
205
+ 'unknown'
206
+ end
207
+ end
208
+
209
+ # Build attributes for tool call span
210
+ #
211
+ # @param tool_call [Object] Tool call object
212
+ # @return [Hash] Span attributes
213
+ def build_tool_call_attributes(tool_call)
214
+ attributes = {
215
+ 'gen_ai.operation.name' => 'execute_tool',
216
+ 'gen_ai.tool.name' => extract_tool_name(tool_call)
217
+ }
218
+
219
+ # Add tool call ID if available
220
+ attributes['gen_ai.tool.call.id'] = tool_call.id.to_s if tool_call.respond_to?(:id) && tool_call.id
221
+
222
+ # Add arguments if available and capture enabled
223
+ if tool_call.respond_to?(:arguments) && tool_call.arguments
224
+ args_str = tool_call.arguments.is_a?(String) ? tool_call.arguments : JSON.generate(tool_call.arguments)
225
+ attributes['gen_ai.tool.call.arguments.size'] = args_str.bytesize
226
+
227
+ if (sanitized_args = sanitize_data(tool_call.arguments, :tool_args))
228
+ attributes['gen_ai.tool.call.arguments'] = sanitized_args
229
+ end
230
+ elsif tool_call.respond_to?(:function) && tool_call.function.respond_to?(:arguments)
231
+ args = tool_call.function.arguments
232
+ args_str = args.is_a?(String) ? args : JSON.generate(args)
233
+ attributes['gen_ai.tool.call.arguments.size'] = args_str.bytesize
234
+
235
+ if (sanitized_args = sanitize_data(args, :tool_args))
236
+ attributes['gen_ai.tool.call.arguments'] = sanitized_args
237
+ end
238
+ end
239
+
240
+ attributes
241
+ end
242
+
243
+ # Record tool call result on span
244
+ #
245
+ # @param result [Object] Tool call result
246
+ # @param span [OpenTelemetry::Trace::Span] The span to update
247
+ def record_tool_result(result, span)
248
+ result_str = result.is_a?(String) ? result : JSON.generate(result)
249
+ span.set_attribute('gen_ai.tool.call.result.size', result_str.bytesize)
250
+
251
+ if (sanitized_result = sanitize_data(result, :tool_results))
252
+ span.set_attribute('gen_ai.tool.call.result', sanitized_result)
253
+ end
254
+ rescue StandardError => e
255
+ logger&.warn('Failed to record tool result', error: e.message)
256
+ end
257
+
258
+ # Record response parsing metadata
259
+ #
260
+ # @param response_text [String] Raw response text
261
+ # @param span [OpenTelemetry::Trace::Span] The span to update
262
+ def record_parse_metadata(response_text, span)
263
+ span.set_attribute('gen_ai.completion.size', response_text.bytesize)
264
+
265
+ # Add sanitized completion if capture enabled
266
+ if (sanitized_completion = sanitize_data(response_text, :outputs))
267
+ span.set_attribute('gen_ai.completion', sanitized_completion)
268
+ end
269
+ rescue StandardError => e
270
+ logger&.warn('Failed to record parse metadata', error: e.message)
271
+ end
272
+
273
+ # Record output validation metadata
274
+ #
275
+ # @param outputs [Hash] Task outputs
276
+ # @param span [OpenTelemetry::Trace::Span] The span to update
277
+ def record_output_metadata(outputs, span)
278
+ span.set_attribute('task.output.keys', outputs.keys.map(&:to_s).join(','))
279
+ span.set_attribute('task.output.count', outputs.size)
280
+ rescue StandardError => e
281
+ logger&.warn('Failed to record output metadata', error: e.message)
282
+ end
283
+ end
284
+ end
285
+ end
@@ -23,10 +23,10 @@ module LanguageOperator
23
23
  }.freeze
24
24
 
25
25
  LEVEL_EMOJI = {
26
- 'DEBUG' => '', # 🔍
27
- 'INFO' => '', # ℹ️
28
- 'WARN' => '', # ⚠️
29
- 'ERROR' => '' #
26
+ 'DEBUG' => "\e[1;90m·\e[0m", # Bold gray middot
27
+ 'INFO' => "\e[1;36m·\e[0m", # Bold cyan middot
28
+ 'WARN' => "\e[1;33m·\e[0m", # Bold yellow middot
29
+ 'ERROR' => "\e[1;31m·\e[0m" # Bold red middot
30
30
  }.freeze
31
31
 
32
32
  attr_reader :logger, :format, :show_timing