language-operator 0.1.56 → 0.1.57
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/task_executor.rb +8 -4
- data/lib/language_operator/cli/commands/tool.rb +183 -0
- data/lib/language_operator/cli/templates/tools/generic.yaml +3 -3
- data/lib/language_operator/client/mcp_connector.rb +7 -0
- data/lib/language_operator/dsl/adapter.rb +2 -2
- 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 +3 -0
- data/lib/language_operator/version.rb +1 -1
- 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: 531bb19bab7e2aaac4dbc7e430147c722e42cad7802a662a13e7aab0c29fdf3a
|
|
4
|
+
data.tar.gz: a7f7ede687479319814e519c788a57906357e76bef99b45616282c1828b7959f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a9782736dde3ec6caace204cd585de5a887c43086236325062f911e2d4271754b9a3d957c03b9ad2a8b795fcfadb6d6d5ad9f76016635182bf9ec45d7cbc1560
|
|
7
|
+
data.tar.gz: 3b1817c3d6c8ef8eff5cba64c47e7bf85edac523376a78170f16a702d82fc1a7ac0db21f7df547e99da94ad27adcd78774693faf980dceca75a957c44fd1900b
|
data/Gemfile.lock
CHANGED
|
@@ -155,15 +155,19 @@ module LanguageOperator
|
|
|
155
155
|
|
|
156
156
|
logger.info('Sending prompt to LLM',
|
|
157
157
|
task: task.name,
|
|
158
|
-
prompt_length: prompt.length
|
|
158
|
+
prompt_length: prompt.length,
|
|
159
|
+
available_tools: @agent.respond_to?(:tools) ? @agent.tools.map(&:name) : 'N/A')
|
|
159
160
|
|
|
160
161
|
# Execute LLM call within traced span
|
|
161
162
|
outputs = tracer.in_span('gen_ai.chat', attributes: neural_task_attributes(task, prompt, validated_inputs)) do |span|
|
|
162
163
|
# Call LLM with full tool access
|
|
164
|
+
logger.debug('Calling LLM with prompt', task: task.name, prompt_preview: prompt[0..200])
|
|
163
165
|
response = @agent.send_message(prompt)
|
|
164
166
|
|
|
165
167
|
logger.info('LLM response received, extracting content',
|
|
166
|
-
task: task.name
|
|
168
|
+
task: task.name,
|
|
169
|
+
response_class: response.class.name,
|
|
170
|
+
has_tool_calls: response.respond_to?(:tool_calls) && response.tool_calls&.any?)
|
|
167
171
|
|
|
168
172
|
response_text = response.is_a?(String) ? response : response.content
|
|
169
173
|
|
|
@@ -367,8 +371,8 @@ module LanguageOperator
|
|
|
367
371
|
def default_config
|
|
368
372
|
{
|
|
369
373
|
timeout_symbolic: 30.0, # Default timeout for symbolic tasks (seconds)
|
|
370
|
-
timeout_neural:
|
|
371
|
-
timeout_hybrid:
|
|
374
|
+
timeout_neural: 360.0, # Default timeout for neural tasks (seconds)
|
|
375
|
+
timeout_hybrid: 360.0, # Default timeout for hybrid tasks (seconds)
|
|
372
376
|
max_retries: 3, # Default max retry attempts
|
|
373
377
|
retry_delay_base: 1.0, # Base delay for exponential backoff
|
|
374
378
|
retry_delay_max: 10.0 # Maximum delay between retries
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'yaml'
|
|
4
4
|
require 'erb'
|
|
5
5
|
require 'net/http'
|
|
6
|
+
require 'json'
|
|
6
7
|
require_relative '../base_command'
|
|
7
8
|
require_relative '../formatters/progress_formatter'
|
|
8
9
|
require_relative '../formatters/table_formatter'
|
|
@@ -66,6 +67,140 @@ module LanguageOperator
|
|
|
66
67
|
end
|
|
67
68
|
end
|
|
68
69
|
|
|
70
|
+
desc 'inspect NAME', 'Show detailed tool information'
|
|
71
|
+
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
72
|
+
def inspect(name)
|
|
73
|
+
handle_command_error('inspect tool') do
|
|
74
|
+
tool = get_resource_or_exit('LanguageTool', name)
|
|
75
|
+
|
|
76
|
+
puts "Tool: #{name}"
|
|
77
|
+
puts " Cluster: #{ctx.name}"
|
|
78
|
+
puts " Namespace: #{ctx.namespace}"
|
|
79
|
+
puts
|
|
80
|
+
|
|
81
|
+
# Status
|
|
82
|
+
status = tool.dig('status', 'phase') || 'Unknown'
|
|
83
|
+
puts "Status: #{status}"
|
|
84
|
+
puts
|
|
85
|
+
|
|
86
|
+
# Spec details
|
|
87
|
+
puts 'Configuration:'
|
|
88
|
+
puts " Type: #{tool.dig('spec', 'type') || 'mcp'}"
|
|
89
|
+
puts " Image: #{tool.dig('spec', 'image')}"
|
|
90
|
+
puts " Deployment Mode: #{tool.dig('spec', 'deploymentMode') || 'sidecar'}"
|
|
91
|
+
puts " Port: #{tool.dig('spec', 'port') || 8080}"
|
|
92
|
+
puts " Replicas: #{tool.dig('spec', 'replicas') || 1}"
|
|
93
|
+
puts
|
|
94
|
+
|
|
95
|
+
# Resources
|
|
96
|
+
resources = tool.dig('spec', 'resources')
|
|
97
|
+
if resources
|
|
98
|
+
puts 'Resources:'
|
|
99
|
+
if resources['requests']
|
|
100
|
+
puts ' Requests:'
|
|
101
|
+
puts " CPU: #{resources['requests']['cpu']}"
|
|
102
|
+
puts " Memory: #{resources['requests']['memory']}"
|
|
103
|
+
end
|
|
104
|
+
if resources['limits']
|
|
105
|
+
puts ' Limits:'
|
|
106
|
+
puts " CPU: #{resources['limits']['cpu']}"
|
|
107
|
+
puts " Memory: #{resources['limits']['memory']}"
|
|
108
|
+
end
|
|
109
|
+
puts
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# RBAC
|
|
113
|
+
rbac = tool.dig('spec', 'rbac')
|
|
114
|
+
if rbac && rbac['clusterRole']
|
|
115
|
+
rules = rbac.dig('clusterRole', 'rules') || []
|
|
116
|
+
puts "RBAC Permissions (#{rules.length} rules):"
|
|
117
|
+
rules.each_with_index do |rule, idx|
|
|
118
|
+
puts " Rule #{idx + 1}:"
|
|
119
|
+
puts " API Groups: #{rule['apiGroups'].join(', ')}"
|
|
120
|
+
puts " Resources: #{rule['resources'].join(', ')}"
|
|
121
|
+
puts " Verbs: #{rule['verbs'].join(', ')}"
|
|
122
|
+
end
|
|
123
|
+
puts
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Egress rules
|
|
127
|
+
egress = tool.dig('spec', 'egress') || []
|
|
128
|
+
if egress.any?
|
|
129
|
+
puts "Network Egress (#{egress.length} rules):"
|
|
130
|
+
egress.each_with_index do |rule, idx|
|
|
131
|
+
puts " Rule #{idx + 1}: #{rule['description']}"
|
|
132
|
+
puts " DNS: #{rule['dns'].join(', ')}" if rule['dns']
|
|
133
|
+
puts " CIDR: #{rule['cidr']}" if rule['cidr']
|
|
134
|
+
if rule['ports']
|
|
135
|
+
ports_str = rule['ports'].map { |p| "#{p['port']}/#{p['protocol']}" }.join(', ')
|
|
136
|
+
puts " Ports: #{ports_str}"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
puts
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Try to fetch MCP capabilities
|
|
143
|
+
capabilities = fetch_mcp_capabilities(name, tool, ctx.namespace)
|
|
144
|
+
if capabilities && capabilities['tools'] && capabilities['tools'].any?
|
|
145
|
+
puts "MCP Tools (#{capabilities['tools'].length}):"
|
|
146
|
+
capabilities['tools'].each_with_index do |mcp_tool, idx|
|
|
147
|
+
tool_name = mcp_tool['name']
|
|
148
|
+
|
|
149
|
+
# Generate a meaningful name if empty
|
|
150
|
+
if tool_name.nil? || tool_name.empty?
|
|
151
|
+
# Try to derive from description (first few words)
|
|
152
|
+
if mcp_tool['description']
|
|
153
|
+
# Take first 3-4 words and convert to snake_case
|
|
154
|
+
words = mcp_tool['description'].split(/\s+/).first(4)
|
|
155
|
+
derived_name = words.join('_').downcase.gsub(/[^a-z0-9_]/, '')
|
|
156
|
+
tool_name = "#{name}_#{derived_name}".gsub(/__+/, '_').sub(/_$/, '')
|
|
157
|
+
else
|
|
158
|
+
tool_name = "#{name}_tool_#{idx + 1}"
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
puts " #{tool_name}"
|
|
163
|
+
puts " Description: #{mcp_tool['description']}" if mcp_tool['description']
|
|
164
|
+
next unless mcp_tool['inputSchema'] && mcp_tool['inputSchema']['properties']
|
|
165
|
+
|
|
166
|
+
params = mcp_tool['inputSchema']['properties'].keys
|
|
167
|
+
required = mcp_tool['inputSchema']['required'] || []
|
|
168
|
+
param_list = params.map { |p| required.include?(p) ? "#{p}*" : p }
|
|
169
|
+
puts " Parameters: #{param_list.join(', ')}"
|
|
170
|
+
end
|
|
171
|
+
puts ' (* = required)'
|
|
172
|
+
puts
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Get agents using this tool
|
|
176
|
+
agents = ctx.client.list_resources('LanguageAgent', namespace: ctx.namespace)
|
|
177
|
+
agents_using = agents.select do |agent|
|
|
178
|
+
tools = agent.dig('spec', 'tools') || []
|
|
179
|
+
tools.include?(name)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
if agents_using.any?
|
|
183
|
+
puts "Agents using this tool (#{agents_using.count}):"
|
|
184
|
+
agents_using.each do |agent|
|
|
185
|
+
puts " - #{agent.dig('metadata', 'name')}"
|
|
186
|
+
end
|
|
187
|
+
else
|
|
188
|
+
puts 'No agents using this tool'
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
puts
|
|
192
|
+
puts 'Labels:'
|
|
193
|
+
labels = tool.dig('metadata', 'labels') || {}
|
|
194
|
+
if labels.empty?
|
|
195
|
+
puts ' (none)'
|
|
196
|
+
else
|
|
197
|
+
labels.each do |key, value|
|
|
198
|
+
puts " #{key}: #{value}"
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
69
204
|
desc 'delete NAME', 'Delete a tool'
|
|
70
205
|
option :cluster, type: :string, desc: 'Override current cluster context'
|
|
71
206
|
option :force, type: :boolean, default: false, desc: 'Skip confirmation'
|
|
@@ -465,6 +600,54 @@ module LanguageOperator
|
|
|
465
600
|
end
|
|
466
601
|
end
|
|
467
602
|
end
|
|
603
|
+
|
|
604
|
+
private
|
|
605
|
+
|
|
606
|
+
# Fetch MCP capabilities from a running tool server
|
|
607
|
+
#
|
|
608
|
+
# @param name [String] Tool name
|
|
609
|
+
# @param tool [Hash] Tool resource
|
|
610
|
+
# @param namespace [String] Kubernetes namespace
|
|
611
|
+
# @return [Hash, nil] MCP capabilities or nil if unavailable
|
|
612
|
+
def fetch_mcp_capabilities(name, tool, namespace)
|
|
613
|
+
return nil unless tool.dig('status', 'phase') == 'Running'
|
|
614
|
+
|
|
615
|
+
# Get the service endpoint
|
|
616
|
+
port = tool.dig('spec', 'port') || 80
|
|
617
|
+
|
|
618
|
+
# Try to query the MCP server using kubectl port-forward
|
|
619
|
+
# This is a fallback approach since we can't directly connect from CLI
|
|
620
|
+
begin
|
|
621
|
+
# Try to find a pod for this tool
|
|
622
|
+
label_selector = "app.kubernetes.io/name=#{name}"
|
|
623
|
+
pods = ctx.client.list_resources('Pod', namespace: namespace, label_selector: label_selector)
|
|
624
|
+
|
|
625
|
+
return nil if pods.empty?
|
|
626
|
+
|
|
627
|
+
pod_name = pods.first.dig('metadata', 'name')
|
|
628
|
+
|
|
629
|
+
# Query the MCP server using JSON-RPC protocol
|
|
630
|
+
# MCP uses the tools/list method to list available tools
|
|
631
|
+
json_rpc_request = {
|
|
632
|
+
jsonrpc: '2.0',
|
|
633
|
+
id: 1,
|
|
634
|
+
method: 'tools/list',
|
|
635
|
+
params: {}
|
|
636
|
+
}.to_json
|
|
637
|
+
|
|
638
|
+
result = `kubectl exec -n #{namespace} #{pod_name} -- curl -s -X POST \
|
|
639
|
+
http://localhost:#{port}/mcp/tools/list -H "Content-Type: application/json" \
|
|
640
|
+
-d '#{json_rpc_request}' 2>/dev/null`
|
|
641
|
+
|
|
642
|
+
return nil if result.empty?
|
|
643
|
+
|
|
644
|
+
response = JSON.parse(result)
|
|
645
|
+
response['result']
|
|
646
|
+
rescue StandardError
|
|
647
|
+
# Silently fail - capabilities are optional information
|
|
648
|
+
nil
|
|
649
|
+
end
|
|
650
|
+
end
|
|
468
651
|
end
|
|
469
652
|
end
|
|
470
653
|
end
|
|
@@ -51,15 +51,15 @@ spec:
|
|
|
51
51
|
<% rbac['clusterRole']['rules'].each do |rule| -%>
|
|
52
52
|
- apiGroups:
|
|
53
53
|
<% rule['apiGroups'].each do |group| -%>
|
|
54
|
-
- <%= group %>
|
|
54
|
+
- "<%= group %>"
|
|
55
55
|
<% end -%>
|
|
56
56
|
resources:
|
|
57
57
|
<% rule['resources'].each do |resource| -%>
|
|
58
|
-
- <%= resource %>
|
|
58
|
+
- "<%= resource %>"
|
|
59
59
|
<% end -%>
|
|
60
60
|
verbs:
|
|
61
61
|
<% rule['verbs'].each do |verb| -%>
|
|
62
|
-
- <%= verb %>
|
|
62
|
+
- "<%= verb %>"
|
|
63
63
|
<% end -%>
|
|
64
64
|
<% end -%>
|
|
65
65
|
<% end -%>
|
|
@@ -27,6 +27,13 @@ module LanguageOperator
|
|
|
27
27
|
tool_count = client.tools.length
|
|
28
28
|
all_tools.concat(client.tools)
|
|
29
29
|
|
|
30
|
+
# Debug: inspect tool objects
|
|
31
|
+
if @debug
|
|
32
|
+
logger.debug('MCP tool objects inspection',
|
|
33
|
+
server: server_config['name'],
|
|
34
|
+
tools_inspect: client.tools.map { |t| { class: t.class.name, name: t.name, methods: t.methods.grep(/name/) } })
|
|
35
|
+
end
|
|
36
|
+
|
|
30
37
|
logger.info('MCP server connected',
|
|
31
38
|
server: server_config['name'],
|
|
32
39
|
tool_count: tool_count,
|
|
@@ -20,8 +20,8 @@ module LanguageOperator
|
|
|
20
20
|
|
|
21
21
|
# Create a dynamic class that extends MCP::Tool
|
|
22
22
|
Class.new(MCP::Tool) do
|
|
23
|
-
# Set the tool name
|
|
24
|
-
|
|
23
|
+
# Set the tool name using the MCP SDK's method
|
|
24
|
+
tool_name tool_def.name
|
|
25
25
|
|
|
26
26
|
# Set the description
|
|
27
27
|
description tool_def.description
|
|
@@ -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.57",
|
|
7
7
|
"type": "object",
|
|
8
8
|
"properties": {
|
|
9
9
|
"name": {
|
|
@@ -181,6 +181,9 @@ module LanguageOperator
|
|
|
181
181
|
|
|
182
182
|
# Create a dynamic MCP::Tool class
|
|
183
183
|
Class.new(MCP::Tool) do
|
|
184
|
+
# Set tool name (required for MCP protocol)
|
|
185
|
+
tool_name tool_def.name
|
|
186
|
+
|
|
184
187
|
description tool_def.description || "Tool: #{tool_def.name}"
|
|
185
188
|
|
|
186
189
|
# Build input schema from parameters
|