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 +4 -4
- data/Gemfile.lock +1 -1
- data/lib/language_operator/agent/executor.rb +11 -0
- data/lib/language_operator/agent/task_executor.rb +17 -1
- data/lib/language_operator/cli/commands/agent.rb +3 -1
- data/lib/language_operator/client/mcp_connector.rb +24 -0
- data/lib/language_operator/instrumentation/task_tracer.rb +64 -2
- data/lib/language_operator/kubernetes/resource_builder.rb +3 -1
- data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
- data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
- data/lib/language_operator/tool_loader.rb +5 -3
- data/lib/language_operator/version.rb +1 -1
- data/synth/003/Makefile +3 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: afc74193530a3f556debc6b31b0a43769da67eaf9c956d0a7b4f17477792bf94
|
|
4
|
+
data.tar.gz: ffc401b931b0b3bd427c92a36847986442b738f2508acd1206547aa4d14c92eb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5079fbb3621d2e3d0f00a9381bc7b6fa09c3e28ae029d3e6cde4b7e0ac42643c7881953dd66549f0379414c0a74053dfed301e832812d82902d0de553f791158
|
|
7
|
+
data.tar.gz: f24aea2d8cb473e7e9633fe713e663f637391c258695eacdd091b94dc12b206606408162f1d3cf3ae60484aa8097177bd4a858d392b132e07f6aaafadf63f35a
|
data/Gemfile.lock
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
|
|
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',
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
data/synth/003/Makefile
CHANGED