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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac246ba1f01262701f8e193f697a2df7753f23f376c71a8cbfc24ab87a59e088
4
- data.tar.gz: b75afe1475935a9b9ccbe3cd717d46e5b180ba123818fc53ff954f228f5663c8
3
+ metadata.gz: 531bb19bab7e2aaac4dbc7e430147c722e42cad7802a662a13e7aab0c29fdf3a
4
+ data.tar.gz: a7f7ede687479319814e519c788a57906357e76bef99b45616282c1828b7959f
5
5
  SHA512:
6
- metadata.gz: d66f44b6d599faaf6c0d93368ff03a50f50372c0924ad8b85f387c842c0302fa5e31c37472dbe484236fc917957ed33a2fa8b80502df0605a1d91909a5613b80
7
- data.tar.gz: 727ac1463af4516edcc552582909f77351f913fbdad2ae260bcc62f1f0f058006cf56dbb71ccd4910ec0f63aeb41ec7c812ea2ab3ee8f413dab5e620443cd5a1
6
+ metadata.gz: a9782736dde3ec6caace204cd585de5a887c43086236325062f911e2d4271754b9a3d957c03b9ad2a8b795fcfadb6d6d5ad9f76016635182bf9ec45d7cbc1560
7
+ data.tar.gz: 3b1817c3d6c8ef8eff5cba64c47e7bf85edac523376a78170f16a702d82fc1a7ac0db21f7df547e99da94ad27adcd78774693faf980dceca75a957c44fd1900b
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- language-operator (0.1.56)
4
+ language-operator (0.1.57)
5
5
  faraday (~> 2.0)
6
6
  k8s-ruby (~> 0.17)
7
7
  mcp (~> 0.4)
@@ -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: 240.0, # Default timeout for neural tasks (seconds)
371
- timeout_hybrid: 240.0, # Default timeout for hybrid tasks (seconds)
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
- define_singleton_method(:name) { tool_def.name }
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
@@ -2,7 +2,7 @@
2
2
  :openapi: 3.0.3
3
3
  :info:
4
4
  :title: Language Operator Agent API
5
- :version: 0.1.56
5
+ :version: 0.1.57
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.56",
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LanguageOperator
4
- VERSION = '0.1.56'
4
+ VERSION = '0.1.57'
5
5
  end
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.56
4
+ version: 0.1.57
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Ryan