language-operator 0.1.57 → 0.1.58

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 531bb19bab7e2aaac4dbc7e430147c722e42cad7802a662a13e7aab0c29fdf3a
4
- data.tar.gz: a7f7ede687479319814e519c788a57906357e76bef99b45616282c1828b7959f
3
+ metadata.gz: afc74193530a3f556debc6b31b0a43769da67eaf9c956d0a7b4f17477792bf94
4
+ data.tar.gz: ffc401b931b0b3bd427c92a36847986442b738f2508acd1206547aa4d14c92eb
5
5
  SHA512:
6
- metadata.gz: a9782736dde3ec6caace204cd585de5a887c43086236325062f911e2d4271754b9a3d957c03b9ad2a8b795fcfadb6d6d5ad9f76016635182bf9ec45d7cbc1560
7
- data.tar.gz: 3b1817c3d6c8ef8eff5cba64c47e7bf85edac523376a78170f16a702d82fc1a7ac0db21f7df547e99da94ad27adcd78774693faf980dceca75a957c44fd1900b
6
+ metadata.gz: 5079fbb3621d2e3d0f00a9381bc7b6fa09c3e28ae029d3e6cde4b7e0ac42643c7881953dd66549f0379414c0a74053dfed301e832812d82902d0de553f791158
7
+ data.tar.gz: f24aea2d8cb473e7e9633fe713e663f637391c258695eacdd091b94dc12b206606408162f1d3cf3ae60484aa8097177bd4a858d392b132e07f6aaafadf63f35a
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.58)
5
5
  faraday (~> 2.0)
6
6
  k8s-ruby (~> 0.17)
7
7
  mcp (~> 0.4)
@@ -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
@@ -164,10 +164,15 @@ module LanguageOperator
164
164
  logger.debug('Calling LLM with prompt', task: task.name, prompt_preview: prompt[0..200])
165
165
  response = @agent.send_message(prompt)
166
166
 
167
+ # Check for tool calls and log details
168
+ has_tool_calls = response.respond_to?(:tool_calls) && response.tool_calls&.any?
169
+ tool_call_count = has_tool_calls ? response.tool_calls.length : 0
170
+
167
171
  logger.info('LLM response received, extracting content',
168
172
  task: task.name,
169
173
  response_class: response.class.name,
170
- has_tool_calls: response.respond_to?(:tool_calls) && response.tool_calls&.any?)
174
+ has_tool_calls: has_tool_calls,
175
+ tool_call_count: tool_call_count)
171
176
 
172
177
  response_text = response.is_a?(String) ? response : response.content
173
178
 
@@ -326,6 +331,17 @@ module LanguageOperator
326
331
  # @return [Hash] Parsed outputs
327
332
  # @raise [RuntimeError] If parsing fails
328
333
  def parse_neural_response(response_text, task)
334
+ # Capture thinking blocks before stripping (for observability)
335
+ thinking_blocks = response_text.scan(%r{\[THINK\](.*?)\[/THINK\]}m).flatten
336
+ if thinking_blocks.any?
337
+ logger.info('LLM thinking captured',
338
+ event: 'llm_thinking',
339
+ task: task.name,
340
+ thinking_steps: thinking_blocks.length,
341
+ thinking: thinking_blocks,
342
+ thinking_preview: thinking_blocks.first&.[](0..500))
343
+ end
344
+
329
345
  # Strip thinking tags that some models add (e.g., [THINK]...[/THINK])
330
346
  cleaned_text = response_text.gsub(%r{\[THINK\].*?\[/THINK\]}m, '').strip
331
347
 
@@ -44,6 +44,7 @@ module LanguageOperator
44
44
  option :persona, type: :string, desc: 'Persona to use for the agent'
45
45
  option :tools, type: :array, desc: 'Tools to make available to the agent'
46
46
  option :models, type: :array, desc: 'Models to make available to the agent'
47
+ option :workspace, type: :boolean, default: true, desc: 'Enable workspace for state persistence'
47
48
  option :dry_run, type: :boolean, default: false, desc: 'Preview what would be created without applying'
48
49
  option :wizard, type: :boolean, default: false, desc: 'Use interactive wizard mode'
49
50
  def create(description = nil)
@@ -98,7 +99,8 @@ module LanguageOperator
98
99
  cluster: ctx.namespace,
99
100
  persona: options[:persona],
100
101
  tools: options[:tools] || [],
101
- models: models
102
+ models: models,
103
+ workspace: options[:workspace]
102
104
  )
103
105
 
104
106
  # Dry-run mode: preview without applying
@@ -61,6 +61,9 @@ module LanguageOperator
61
61
 
62
62
  @chat.with_tools(*all_tools) unless all_tools.empty?
63
63
 
64
+ # Set up callbacks to log tool invocations
65
+ setup_tool_callbacks
66
+
64
67
  logger.info('Chat session initialized', with_tools: !all_tools.empty?)
65
68
  end
66
69
 
@@ -125,6 +128,27 @@ module LanguageOperator
125
128
 
126
129
  chat_params
127
130
  end
131
+
132
+ # Set up callbacks to log tool calls and results
133
+ #
134
+ # @return [void]
135
+ def setup_tool_callbacks
136
+ @chat.on_tool_call do |tool_call|
137
+ logger.info('Tool call initiated by LLM',
138
+ event: 'tool_call_initiated',
139
+ tool_name: tool_call.name,
140
+ tool_id: tool_call.id,
141
+ arguments: tool_call.arguments,
142
+ arguments_json: tool_call.arguments.to_json)
143
+ end
144
+
145
+ @chat.on_tool_result do |result|
146
+ logger.info('Tool call result received',
147
+ event: 'tool_result_received',
148
+ result: result,
149
+ result_preview: result.to_s[0..500])
150
+ end
151
+ end
128
152
  end
129
153
  end
130
154
  end
@@ -26,6 +26,7 @@ module LanguageOperator
26
26
  # Instrumentation adds <5% overhead with default settings
27
27
  # Overhead may increase to ~10% with full data capture enabled
28
28
  #
29
+ # rubocop:disable Metrics/ModuleLength
29
30
  module TaskTracer
30
31
  # Maximum length for captured data before truncation
31
32
  MAX_CAPTURED_LENGTH = 1000
@@ -169,6 +170,11 @@ module LanguageOperator
169
170
  return unless response.respond_to?(:tool_calls)
170
171
  return unless response.tool_calls&.any?
171
172
 
173
+ logger&.info('Tool calls detected in LLM response',
174
+ event: 'tool_calls_detected',
175
+ tool_call_count: response.tool_calls.length,
176
+ tool_names: response.tool_calls.map { |tc| extract_tool_name(tc) })
177
+
172
178
  response.tool_calls.each do |tool_call|
173
179
  record_single_tool_call(tool_call, parent_span)
174
180
  end
@@ -182,11 +188,26 @@ module LanguageOperator
182
188
  # @param parent_span [OpenTelemetry::Trace::Span] Parent span
183
189
  def record_single_tool_call(tool_call, _parent_span)
184
190
  tool_name = extract_tool_name(tool_call)
191
+ tool_id = tool_call.respond_to?(:id) ? tool_call.id : nil
192
+
193
+ # Extract and log tool arguments
194
+ arguments = extract_tool_arguments(tool_call)
185
195
 
196
+ logger&.info('Tool invoked by LLM',
197
+ event: 'tool_call_invoked',
198
+ tool_name: tool_name,
199
+ tool_id: tool_id,
200
+ arguments: arguments,
201
+ arguments_json: (arguments.is_a?(Hash) ? JSON.generate(arguments) : arguments.to_s))
202
+
203
+ start_time = Time.now
186
204
  tracer.in_span("execute_tool #{tool_name}", attributes: build_tool_call_attributes(tool_call)) do |tool_span|
187
205
  # Tool execution already completed by ruby_llm
188
206
  # Just record the metadata
189
- record_tool_result(tool_call.result, tool_span) if tool_call.respond_to?(:result) && tool_call.result
207
+ if tool_call.respond_to?(:result) && tool_call.result
208
+ duration_ms = ((Time.now - start_time) * 1000).round(2)
209
+ record_tool_result(tool_call.result, tool_span, tool_name, tool_id, duration_ms)
210
+ end
190
211
  end
191
212
  rescue StandardError => e
192
213
  logger&.warn('Failed to record tool call span', error: e.message, tool: tool_name)
@@ -206,6 +227,34 @@ module LanguageOperator
206
227
  end
207
228
  end
208
229
 
230
+ # Extract tool arguments from tool call object
231
+ #
232
+ # @param tool_call [Object] Tool call object
233
+ # @return [Hash, String] Tool arguments
234
+ def extract_tool_arguments(tool_call)
235
+ if tool_call.respond_to?(:arguments)
236
+ args = tool_call.arguments
237
+ parse_json_args(args)
238
+ elsif tool_call.respond_to?(:function) && tool_call.function.respond_to?(:arguments)
239
+ args = tool_call.function.arguments
240
+ parse_json_args(args)
241
+ else
242
+ {}
243
+ end
244
+ end
245
+
246
+ # Parse JSON arguments safely
247
+ #
248
+ # @param args [String, Object] Arguments to parse
249
+ # @return [Hash, String] Parsed arguments or original
250
+ def parse_json_args(args)
251
+ return args unless args.is_a?(String)
252
+
253
+ JSON.parse(args)
254
+ rescue JSON::ParserError
255
+ args
256
+ end
257
+
209
258
  # Build attributes for tool call span
210
259
  #
211
260
  # @param tool_call [Object] Tool call object
@@ -244,13 +293,25 @@ module LanguageOperator
244
293
  #
245
294
  # @param result [Object] Tool call result
246
295
  # @param span [OpenTelemetry::Trace::Span] The span to update
247
- def record_tool_result(result, span)
296
+ # @param tool_name [String] Tool name (for logging)
297
+ # @param tool_id [String] Tool call ID (for logging)
298
+ # @param duration_ms [Float] Execution duration in milliseconds (for logging)
299
+ def record_tool_result(result, span, tool_name = nil, tool_id = nil, duration_ms = nil)
248
300
  result_str = result.is_a?(String) ? result : JSON.generate(result)
249
301
  span.set_attribute('gen_ai.tool.call.result.size', result_str.bytesize)
250
302
 
251
303
  if (sanitized_result = sanitize_data(result, :tool_results))
252
304
  span.set_attribute('gen_ai.tool.call.result', sanitized_result)
253
305
  end
306
+
307
+ # Log tool execution completion
308
+ logger&.info('Tool execution completed',
309
+ event: 'tool_call_completed',
310
+ tool_name: tool_name,
311
+ tool_id: tool_id,
312
+ result_size: result_str.bytesize,
313
+ result: sanitize_data(result, :tool_results),
314
+ duration_ms: duration_ms)
254
315
  rescue StandardError => e
255
316
  logger&.warn('Failed to record tool result', error: e.message)
256
317
  end
@@ -281,5 +342,6 @@ module LanguageOperator
281
342
  logger&.warn('Failed to record output metadata', error: e.message)
282
343
  end
283
344
  end
345
+ # rubocop:enable Metrics/ModuleLength
284
346
  end
285
347
  end
@@ -25,7 +25,7 @@ module LanguageOperator
25
25
 
26
26
  # Build a LanguageAgent resource
27
27
  def language_agent(name, instructions:, cluster: nil, schedule: nil, persona: nil, tools: [], models: [],
28
- mode: nil, labels: {})
28
+ mode: nil, workspace: true, labels: {})
29
29
  # Determine mode: reactive, scheduled, or autonomous
30
30
  spec_mode = mode || (schedule ? 'scheduled' : 'autonomous')
31
31
 
@@ -41,6 +41,8 @@ module LanguageOperator
41
41
  spec['toolRefs'] = tools.map { |t| { 'name' => t } } unless tools.empty?
42
42
  # Convert model names to modelRef objects
43
43
  spec['modelRefs'] = models.map { |m| { 'name' => m } } unless models.empty?
44
+ # Enable workspace by default for state persistence
45
+ spec['workspace'] = { 'enabled' => workspace } if workspace
44
46
 
45
47
  {
46
48
  'apiVersion' => 'langop.io/v1alpha1',
@@ -2,7 +2,7 @@
2
2
  :openapi: 3.0.3
3
3
  :info:
4
4
  :title: Language Operator Agent API
5
- :version: 0.1.57
5
+ :version: 0.1.58
6
6
  :description: HTTP API endpoints exposed by Language Operator reactive agents
7
7
  :contact:
8
8
  :name: Language Operator
@@ -3,7 +3,7 @@
3
3
  "$id": "https://github.com/language-operator/language-operator-gem/schema/agent-dsl.json",
4
4
  "title": "Language Operator Agent DSL",
5
5
  "description": "Schema for defining autonomous AI agents using the Language Operator DSL",
6
- "version": "0.1.57",
6
+ "version": "0.1.58",
7
7
  "type": "object",
8
8
  "properties": {
9
9
  "name": {
@@ -173,7 +173,7 @@ module LanguageOperator
173
173
  #
174
174
  # @param tool_def [LanguageOperator::Dsl::ToolDefinition] Tool definition from DSL
175
175
  # @return [Class] MCP::Tool subclass
176
- # rubocop:disable Metrics/MethodLength
176
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
177
177
  def self.create_mcp_tool(tool_def)
178
178
  # Capture tool name and tracer for use in the dynamic class
179
179
  tool_name = tool_def.name
@@ -213,7 +213,9 @@ module LanguageOperator
213
213
  'tool.type' => 'custom'
214
214
  }) do |span|
215
215
  # Execute the tool's block
216
- result = @execute_block.call(params)
216
+ # Convert symbol keys to string keys for consistency with DSL expectations
217
+ string_params = params.transform_keys(&:to_s)
218
+ result = @execute_block.call(string_params)
217
219
 
218
220
  # Set success attribute
219
221
  span.set_attribute('tool.result', 'success')
@@ -235,6 +237,6 @@ module LanguageOperator
235
237
  end
236
238
  end
237
239
  end
238
- # rubocop:enable Metrics/MethodLength
240
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
239
241
  end
240
242
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LanguageOperator
4
- VERSION = '0.1.57'
4
+ VERSION = '0.1.58'
5
5
  end
data/synth/003/Makefile CHANGED
@@ -7,6 +7,9 @@ TOOLS := workspace
7
7
  create:
8
8
  cat agent.txt | $(AICTL) create --name $(AGENT) --tools "$(TOOLS)"
9
9
 
10
+ run:
11
+ kubectl create job --from=cronjob/$(AGENT) $(AGENT)-manual
12
+
10
13
  code:
11
14
  $(AICTL) code $(AGENT)
12
15
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: language-operator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.57
4
+ version: 0.1.58
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Ryan