language-operator 0.1.59 → 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.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/components/agent/Gemfile +1 -1
- data/lib/language_operator/agent/base.rb +3 -0
- data/lib/language_operator/agent/task_executor.rb +25 -7
- data/lib/language_operator/client/base.rb +3 -1
- data/lib/language_operator/dsl/task_definition.rb +7 -6
- data/lib/language_operator/learning/adapters/base_adapter.rb +2 -0
- data/lib/language_operator/learning/adapters/jaeger_adapter.rb +4 -1
- data/lib/language_operator/learning/adapters/signoz_adapter.rb +3 -0
- data/lib/language_operator/learning/adapters/tempo_adapter.rb +3 -0
- data/lib/language_operator/learning/optimizer.rb +3 -2
- data/lib/language_operator/learning/task_synthesizer.rb +39 -12
- data/lib/language_operator/learning/trace_analyzer.rb +9 -4
- 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/templates/task_synthesis.tmpl +4 -3
- data/lib/language_operator/version.rb +1 -1
- data/synth/003/Makefile +2 -2
- 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: 614dadae712dd0b6c1b72314497c73cdbcba8aa247a63cf3d01dbd789b0becaf
|
|
4
|
+
data.tar.gz: 72ad29bec20d5fdc6da1f235cd054617567c2516522e1a45bbd52f09c15bdb65
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cd062a5b0420dd602f411dd30b1e6fd7aa3dd7bce77ddbf7e94af95eb6991b609f57aa797c59f519150e518e89ba0839b43ca5be9a216a576b26ad9bb3b05ba0
|
|
7
|
+
data.tar.gz: cfdaa40cc2ced23bd59343b6c995499e9c718c1782f8c8c50766d1180399adc2b40c6fafd21865bb0c9130f1e9c46713639faa7985afa739f28bc47d4e66f993
|
data/Gemfile.lock
CHANGED
data/components/agent/Gemfile
CHANGED
|
@@ -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?
|
|
@@ -234,19 +234,28 @@ module LanguageOperator
|
|
|
234
234
|
# Execute the tool (it's a Proc/lambda wrapped by RubyLLM)
|
|
235
235
|
result = tool.call(**params)
|
|
236
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
|
+
|
|
237
246
|
logger.debug('Tool call completed',
|
|
238
247
|
tool: tool_name_str,
|
|
239
|
-
result_preview:
|
|
248
|
+
result_preview: text_result.is_a?(String) ? text_result[0..200] : text_result.class.name)
|
|
240
249
|
|
|
241
250
|
# Try to parse JSON response if it looks like JSON
|
|
242
|
-
if
|
|
243
|
-
JSON.parse(
|
|
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)
|
|
244
253
|
else
|
|
245
|
-
|
|
254
|
+
text_result
|
|
246
255
|
end
|
|
247
256
|
rescue JSON::ParserError
|
|
248
257
|
# Not JSON, return as-is
|
|
249
|
-
|
|
258
|
+
text_result
|
|
250
259
|
end
|
|
251
260
|
|
|
252
261
|
# Helper method for symbolic tasks to call LLM directly
|
|
@@ -380,8 +389,15 @@ module LanguageOperator
|
|
|
380
389
|
thinking_preview: thinking_blocks.first&.[](0..500))
|
|
381
390
|
end
|
|
382
391
|
|
|
383
|
-
# Strip thinking tags that some models add (e.g., [THINK]...[/THINK])
|
|
384
|
-
|
|
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])
|
|
385
401
|
|
|
386
402
|
# Try to extract JSON from response
|
|
387
403
|
# Look for JSON code blocks first
|
|
@@ -394,6 +410,8 @@ module LanguageOperator
|
|
|
394
410
|
json_object_match ? json_object_match[0] : cleaned_text
|
|
395
411
|
end
|
|
396
412
|
|
|
413
|
+
logger.debug('Extracted JSON text', json_length: json_text.length, json_start: json_text[0..100])
|
|
414
|
+
|
|
397
415
|
# Parse JSON
|
|
398
416
|
parsed = JSON.parse(json_text)
|
|
399
417
|
|
|
@@ -215,14 +215,16 @@ module LanguageOperator
|
|
|
215
215
|
tracer.in_span("execute_tool.#{tool_name}", attributes: {
|
|
216
216
|
'gen_ai.operation.name' => 'execute_tool',
|
|
217
217
|
'gen_ai.tool.name' => tool_name,
|
|
218
|
+
'gen_ai.tool.call.arguments' => arguments.to_json[0..1000],
|
|
218
219
|
'gen_ai.tool.call.arguments.size' => arguments.to_json.bytesize
|
|
219
220
|
}) do |span|
|
|
220
221
|
# Execute the original tool
|
|
221
222
|
result = original_tool.call(arguments)
|
|
222
223
|
|
|
223
|
-
# Record the result
|
|
224
|
+
# Record the result (truncated for telemetry)
|
|
224
225
|
result_str = result.is_a?(String) ? result : result.to_json
|
|
225
226
|
span.set_attribute('gen_ai.tool.call.result.size', result_str.bytesize)
|
|
227
|
+
span.set_attribute('gen_ai.tool.call.result', result_str[0..2000])
|
|
226
228
|
|
|
227
229
|
result
|
|
228
230
|
rescue StandardError => e
|
|
@@ -229,15 +229,16 @@ module LanguageOperator
|
|
|
229
229
|
# Execute symbolic implementation
|
|
230
230
|
#
|
|
231
231
|
# @param inputs [Hash] Validated inputs
|
|
232
|
-
# @param context [Object, nil] Execution context
|
|
232
|
+
# @param context [Object, nil] Execution context (TaskExecutor)
|
|
233
233
|
# @return [Hash] Result from execute block
|
|
234
234
|
def execute_symbolic(inputs, context)
|
|
235
|
-
if
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
235
|
+
if context
|
|
236
|
+
# Execute block in context's scope to make helper methods available
|
|
237
|
+
# (execute_tool, execute_task, execute_llm, etc.)
|
|
238
|
+
context.instance_exec(inputs, &@execute_block)
|
|
239
239
|
else
|
|
240
|
-
|
|
240
|
+
# Fallback for standalone execution without context
|
|
241
|
+
@execute_block.call(inputs)
|
|
241
242
|
end
|
|
242
243
|
end
|
|
243
244
|
|
|
@@ -125,7 +125,9 @@ module LanguageOperator
|
|
|
125
125
|
.map do |tool_span|
|
|
126
126
|
{
|
|
127
127
|
tool_name: tool_span.dig(:attributes, 'gen_ai.tool.name'),
|
|
128
|
+
arguments: tool_span.dig(:attributes, 'gen_ai.tool.call.arguments'),
|
|
128
129
|
arguments_size: tool_span.dig(:attributes, 'gen_ai.tool.call.arguments.size'),
|
|
130
|
+
result: tool_span.dig(:attributes, 'gen_ai.tool.call.result'),
|
|
129
131
|
result_size: tool_span.dig(:attributes, 'gen_ai.tool.call.result.size')
|
|
130
132
|
}
|
|
131
133
|
end
|
|
@@ -108,7 +108,10 @@ module LanguageOperator
|
|
|
108
108
|
params[:service] = extract_service_name(filter)
|
|
109
109
|
|
|
110
110
|
# Add tag filters
|
|
111
|
-
|
|
111
|
+
tags = {}
|
|
112
|
+
tags['task.name'] = filter[:task_name] if filter[:task_name]
|
|
113
|
+
tags['agent.name'] = filter[:agent_name] if filter[:agent_name]
|
|
114
|
+
params[:tags] = tags.to_json unless tags.empty?
|
|
112
115
|
|
|
113
116
|
uri = URI.join(@endpoint, SEARCH_PATH)
|
|
114
117
|
uri.query = URI.encode_www_form(params)
|
|
@@ -216,6 +216,9 @@ module LanguageOperator
|
|
|
216
216
|
# Filter by task name (attribute name should NOT be quoted)
|
|
217
217
|
expressions << "task.name = '#{filter[:task_name]}'" if filter[:task_name]
|
|
218
218
|
|
|
219
|
+
# Filter by agent name
|
|
220
|
+
expressions << "agent.name = '#{filter[:agent_name]}'" if filter[:agent_name]
|
|
221
|
+
|
|
219
222
|
# Additional attribute filters
|
|
220
223
|
if filter[:attributes].is_a?(Hash)
|
|
221
224
|
filter[:attributes].each do |key, value|
|
|
@@ -76,6 +76,9 @@ module LanguageOperator
|
|
|
76
76
|
# Filter by task name
|
|
77
77
|
conditions << "span.\"task.name\" = \"#{escape_traceql_value(filter[:task_name])}\"" if filter[:task_name]
|
|
78
78
|
|
|
79
|
+
# Filter by agent name
|
|
80
|
+
conditions << "span.\"agent.name\" = \"#{escape_traceql_value(filter[:agent_name])}\"" if filter[:agent_name]
|
|
81
|
+
|
|
79
82
|
# Additional attribute filters
|
|
80
83
|
if filter[:attributes].is_a?(Hash)
|
|
81
84
|
filter[:attributes].each do |key, value|
|
|
@@ -74,6 +74,7 @@ module LanguageOperator
|
|
|
74
74
|
neural_tasks.each do |task|
|
|
75
75
|
analysis = @trace_analyzer.analyze_patterns(
|
|
76
76
|
task_name: task[:name],
|
|
77
|
+
agent_name: @agent_name,
|
|
77
78
|
min_executions: min_executions,
|
|
78
79
|
consistency_threshold: min_consistency,
|
|
79
80
|
time_range: time_range
|
|
@@ -108,10 +109,10 @@ module LanguageOperator
|
|
|
108
109
|
task_def = find_task_definition(task_name)
|
|
109
110
|
raise ArgumentError, "Task '#{task_name}' not found" unless task_def
|
|
110
111
|
|
|
111
|
-
analysis = @trace_analyzer.analyze_patterns(task_name: task_name)
|
|
112
|
+
analysis = @trace_analyzer.analyze_patterns(task_name: task_name, agent_name: @agent_name)
|
|
112
113
|
raise ArgumentError, "No execution data found for task '#{task_name}'" unless analysis
|
|
113
114
|
|
|
114
|
-
traces = @trace_analyzer.query_task_traces(task_name: task_name, limit: 20)
|
|
115
|
+
traces = @trace_analyzer.query_task_traces(task_name: task_name, agent_name: @agent_name, limit: 20)
|
|
115
116
|
detection_result = @pattern_detector.detect_pattern(analysis_result: analysis) unless use_synthesis
|
|
116
117
|
|
|
117
118
|
return propose_via_synthesis(task_name, task_def, analysis, traces) if should_use_synthesis?(use_synthesis, detection_result)
|
|
@@ -38,7 +38,7 @@ module LanguageOperator
|
|
|
38
38
|
def initialize(llm_client:, validator:, logger: nil)
|
|
39
39
|
@llm_client = llm_client
|
|
40
40
|
@validator = validator
|
|
41
|
-
@logger = logger || ::Logger.new($stdout, level: ::Logger::
|
|
41
|
+
@logger = logger || ::Logger.new($stdout, level: ::Logger::INFO)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
# Synthesize deterministic code for a task
|
|
@@ -59,7 +59,7 @@ module LanguageOperator
|
|
|
59
59
|
common_pattern: common_pattern
|
|
60
60
|
)
|
|
61
61
|
|
|
62
|
-
@logger.
|
|
62
|
+
@logger.info("Task synthesis prompt:\n#{prompt}")
|
|
63
63
|
|
|
64
64
|
# Call LLM
|
|
65
65
|
response = call_llm(prompt)
|
|
@@ -164,16 +164,43 @@ module LanguageOperator
|
|
|
164
164
|
return '(no traces available)' if traces.empty?
|
|
165
165
|
|
|
166
166
|
traces.first(10).each_with_index.map do |trace, idx|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
167
|
+
format_single_trace(trace, idx)
|
|
168
|
+
end.join("\n")
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Format a single trace execution
|
|
172
|
+
#
|
|
173
|
+
# @param trace [Hash] Single trace data
|
|
174
|
+
# @param idx [Integer] Trace index
|
|
175
|
+
# @return [String] Formatted trace
|
|
176
|
+
def format_single_trace(trace, idx)
|
|
177
|
+
tool_sequence = trace[:tool_calls]&.map { |tc| tc[:tool_name] }&.join(' → ') || '(no tools)'
|
|
178
|
+
duration = trace[:duration_ms]&.round(1) || 'unknown'
|
|
179
|
+
inputs_summary = trace[:inputs]&.keys&.join(', ') || 'none'
|
|
180
|
+
tool_details = format_tool_calls(trace[:tool_calls])
|
|
181
|
+
|
|
182
|
+
<<~TRACE
|
|
183
|
+
### Execution #{idx + 1}
|
|
184
|
+
- **Tool Sequence:** #{tool_sequence}
|
|
185
|
+
- **Duration:** #{duration}ms
|
|
186
|
+
- **Inputs:** #{inputs_summary}
|
|
187
|
+
- **Tool Calls:**
|
|
188
|
+
#{tool_details}
|
|
189
|
+
TRACE
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Format tool call details
|
|
193
|
+
#
|
|
194
|
+
# @param tool_calls [Array<Hash>, nil] Tool call data
|
|
195
|
+
# @return [String] Formatted tool calls
|
|
196
|
+
def format_tool_calls(tool_calls)
|
|
197
|
+
return ' (no tool calls)' unless tool_calls&.any?
|
|
198
|
+
|
|
199
|
+
tool_calls.map do |tc|
|
|
200
|
+
details = " - #{tc[:tool_name]}"
|
|
201
|
+
details += "\n Args: #{tc[:arguments]}" if tc[:arguments]
|
|
202
|
+
details += "\n Result: #{tc[:result]}" if tc[:result]
|
|
203
|
+
details
|
|
177
204
|
end.join("\n")
|
|
178
205
|
end
|
|
179
206
|
|
|
@@ -60,10 +60,11 @@ module LanguageOperator
|
|
|
60
60
|
# Query task execution traces from backend
|
|
61
61
|
#
|
|
62
62
|
# @param task_name [String] Name of task to query
|
|
63
|
+
# @param agent_name [String, nil] Optional agent name to filter by
|
|
63
64
|
# @param limit [Integer] Maximum number of traces to return
|
|
64
65
|
# @param time_range [Integer, Range<Time>] Time range in seconds or explicit range
|
|
65
66
|
# @return [Array<Hash>] Task execution data
|
|
66
|
-
def query_task_traces(task_name:, limit: 100, time_range: DEFAULT_TIME_RANGE)
|
|
67
|
+
def query_task_traces(task_name:, agent_name: nil, limit: 100, time_range: DEFAULT_TIME_RANGE)
|
|
67
68
|
unless available?
|
|
68
69
|
@logger.warn('No OTLP backend available, learning disabled')
|
|
69
70
|
return []
|
|
@@ -71,8 +72,11 @@ module LanguageOperator
|
|
|
71
72
|
|
|
72
73
|
range = normalize_time_range(time_range)
|
|
73
74
|
|
|
75
|
+
filter = { task_name: task_name }
|
|
76
|
+
filter[:agent_name] = agent_name if agent_name
|
|
77
|
+
|
|
74
78
|
spans = @adapter.query_spans(
|
|
75
|
-
filter:
|
|
79
|
+
filter: filter,
|
|
76
80
|
time_range: range,
|
|
77
81
|
limit: limit
|
|
78
82
|
)
|
|
@@ -90,13 +94,14 @@ module LanguageOperator
|
|
|
90
94
|
# learned and converted to a symbolic implementation.
|
|
91
95
|
#
|
|
92
96
|
# @param task_name [String] Name of task to analyze
|
|
97
|
+
# @param agent_name [String, nil] Optional agent name to filter by
|
|
93
98
|
# @param min_executions [Integer] Minimum executions required for analysis
|
|
94
99
|
# @param consistency_threshold [Float] Required consistency (0.0-1.0)
|
|
95
100
|
# @param time_range [Integer, Range<Time>, nil] Time range for query (seconds or explicit range)
|
|
96
101
|
# @return [Hash, nil] Analysis results or nil if insufficient data
|
|
97
|
-
def analyze_patterns(task_name:, min_executions: 10, consistency_threshold: DEFAULT_CONSISTENCY_THRESHOLD,
|
|
102
|
+
def analyze_patterns(task_name:, agent_name: nil, min_executions: 10, consistency_threshold: DEFAULT_CONSISTENCY_THRESHOLD,
|
|
98
103
|
time_range: nil)
|
|
99
|
-
executions = query_task_traces(task_name: task_name, limit: 1000, time_range: time_range || DEFAULT_TIME_RANGE)
|
|
104
|
+
executions = query_task_traces(task_name: task_name, agent_name: agent_name, limit: 1000, time_range: time_range || DEFAULT_TIME_RANGE)
|
|
100
105
|
|
|
101
106
|
if executions.empty?
|
|
102
107
|
@logger.info("No executions found for task '#{task_name}'")
|
|
@@ -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.61",
|
|
7
7
|
"type": "object",
|
|
8
8
|
"properties": {
|
|
9
9
|
"name": {
|
|
@@ -86,12 +86,13 @@ Respond with valid JSON:
|
|
|
86
86
|
instructions: "Keep the original instructions for documentation",
|
|
87
87
|
inputs: { ... },
|
|
88
88
|
outputs: { ... } do |inputs|
|
|
89
|
-
#
|
|
90
|
-
execute_tool(:tool_name, { arg: value })
|
|
89
|
+
# Helper methods available: execute_tool, execute_task, execute_llm
|
|
90
|
+
result = execute_tool(:tool_name, { arg: value })
|
|
91
91
|
{ output_key: result }
|
|
92
92
|
end
|
|
93
93
|
```
|
|
94
|
-
- Use `execute_tool(:tool_name, { arg: value })` for tool calls
|
|
94
|
+
- Use `execute_tool(:tool_name, { arg: value })` for MCP tool calls
|
|
95
|
+
- Use `execute_task(:task_name, inputs: { ... })` to call other tasks
|
|
95
96
|
- Access inputs via the `inputs` hash parameter
|
|
96
97
|
- Return a hash matching the output schema
|
|
97
98
|
- Do NOT use system(), eval(), or other unsafe methods
|
data/synth/003/Makefile
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
.PHONY: create code logs clean
|
|
2
2
|
|
|
3
|
-
AGENT :=
|
|
3
|
+
AGENT := s003
|
|
4
4
|
AICTL := bundle exec ../../bin/aictl agent
|
|
5
5
|
TOOLS := workspace
|
|
6
6
|
|
|
@@ -28,4 +28,4 @@ clean:
|
|
|
28
28
|
|
|
29
29
|
save:
|
|
30
30
|
$(AICTL) code $(AGENT) --raw > agent.rb
|
|
31
|
-
$(AICTL) logs $(AGENT) > output.log
|
|
31
|
+
$(AICTL) logs $(AGENT) > output.log
|