language-operator 0.1.46 → 0.1.47

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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/task.md +10 -0
  3. data/Gemfile.lock +1 -1
  4. data/components/agent/.rubocop.yml +1 -0
  5. data/components/agent/Dockerfile +43 -0
  6. data/components/agent/Dockerfile.dev +38 -0
  7. data/components/agent/Gemfile +15 -0
  8. data/components/agent/Makefile +67 -0
  9. data/components/agent/bin/langop-agent +140 -0
  10. data/components/agent/config/config.yaml +47 -0
  11. data/components/base/Dockerfile +34 -0
  12. data/components/base/Makefile +42 -0
  13. data/components/base/entrypoint.sh +12 -0
  14. data/components/base/gem-credentials +2 -0
  15. data/components/tool/.gitignore +10 -0
  16. data/components/tool/.rubocop.yml +19 -0
  17. data/components/tool/.yardopts +7 -0
  18. data/components/tool/Dockerfile +44 -0
  19. data/components/tool/Dockerfile.dev +39 -0
  20. data/components/tool/Gemfile +18 -0
  21. data/components/tool/Makefile +77 -0
  22. data/components/tool/README.md +145 -0
  23. data/components/tool/config.ru +4 -0
  24. data/components/tool/examples/calculator.rb +63 -0
  25. data/components/tool/examples/example_tool.rb +190 -0
  26. data/components/tool/lib/langop/dsl.rb +20 -0
  27. data/components/tool/server.rb +7 -0
  28. data/lib/language_operator/agent/task_executor.rb +39 -7
  29. data/lib/language_operator/cli/commands/agent.rb +0 -3
  30. data/lib/language_operator/cli/commands/system.rb +1 -0
  31. data/lib/language_operator/cli/formatters/log_formatter.rb +19 -67
  32. data/lib/language_operator/cli/formatters/log_style.rb +151 -0
  33. data/lib/language_operator/cli/formatters/progress_formatter.rb +10 -6
  34. data/lib/language_operator/logger.rb +3 -8
  35. data/lib/language_operator/templates/agent_synthesis.tmpl +35 -28
  36. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
  37. data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
  38. data/lib/language_operator/version.rb +1 -1
  39. data/synth/001/README.md +72 -0
  40. data/synth/001/output.log +13 -13
  41. data/synth/002/Makefile +12 -0
  42. data/synth/002/README.md +287 -0
  43. data/synth/002/agent.rb +23 -0
  44. data/synth/002/agent.txt +1 -0
  45. data/synth/002/output.log +22 -0
  46. metadata +33 -3
  47. data/synth/Makefile +0 -39
  48. data/synth/README.md +0 -342
@@ -0,0 +1,77 @@
1
+ # Image configuration
2
+ IMAGE_NAME := ghcr.io/language-operator/tool
3
+ IMAGE_TAG := latest
4
+ IMAGE_FULL := $(IMAGE_NAME):$(IMAGE_TAG)
5
+
6
+ # Include shared targets
7
+ include ../../Makefile.common
8
+
9
+ # Default target
10
+ .PHONY: help
11
+ help:
12
+ @echo "Available targets:"
13
+ @echo " build - Build the Docker image"
14
+ @echo " build-dev - Build the Docker image with local gem"
15
+ @echo " push-dev - Build and push dev image to all k8s nodes"
16
+ @echo " scan - Scan the Docker image with Trivy"
17
+ @echo " shell - Run the image and exec into it with an interactive shell"
18
+ @echo " env - Display sorted list of environment variables in the image"
19
+ @echo " run - Run the MCP server on port 8080"
20
+ @echo " run-dev - Run with custom tools directory"
21
+ @echo " test - Run the server and test the endpoints"
22
+ @echo " lint - Run RuboCop linter"
23
+ @echo " lint-fix - Run RuboCop with auto-fix"
24
+ @echo " doc - Generate API documentation with YARD"
25
+ @echo " doc-serve - Serve documentation on http://localhost:8808"
26
+ @echo " doc-clean - Remove generated documentation"
27
+
28
+ # Build development image with local gem
29
+ .PHONY: build-dev
30
+ build-dev:
31
+ @echo "Building local gem..."
32
+ cd ../../../language-operator-gem && gem build language-operator.gemspec
33
+ @echo "Copying gem to build context..."
34
+ cp ../../../language-operator-gem/*.gem .
35
+ @echo "Building Docker image..."
36
+ docker build -f Dockerfile.dev -t $(IMAGE_NAME):dev -t $(IMAGE_NAME):$(IMAGE_TAG)-dev .
37
+ @echo "Cleaning up gem file..."
38
+ rm language-operator-*.gem
39
+
40
+ # Push development image to all k8s cluster nodes (containerd/k3s)
41
+ .PHONY: push-dev
42
+ push-dev: build-dev
43
+ @echo "Pushing development image to all k8s nodes..."
44
+ @for node in $$(kubectl get nodes -o jsonpath='{.items[*].metadata.name}'); do \
45
+ echo "==> Pushing to node: $$node"; \
46
+ docker save $(IMAGE_NAME):dev | ssh $$node 'sudo k3s ctr images import -' || echo "Failed to push to $$node"; \
47
+ done
48
+ @echo "✓ Development image pushed to all nodes!"
49
+
50
+ # Display sorted list of environment variables in the image
51
+ .PHONY: env
52
+ env:
53
+ docker run --rm $(IMAGE_FULL) /bin/sh -c 'env | sort'
54
+
55
+ # Run the MCP server
56
+ .PHONY: run
57
+ run:
58
+ docker run --rm -p 8080:80 --name mcp-server $(IMAGE_FULL)
59
+
60
+ # Run with custom tools directory
61
+ .PHONY: run-dev
62
+ run-dev:
63
+ docker run --rm -p 8080:80 -v $(PWD)/examples:/mcp --name mcp-server $(IMAGE_FULL)
64
+
65
+ # Override test to test server endpoints
66
+ .PHONY: test
67
+ test:
68
+ @echo "Testing health endpoint..."
69
+ @curl -s http://localhost:8080/health | jq .
70
+ @echo ""
71
+ @echo "Listing tools..."
72
+ @curl -s http://localhost:8080/tools | jq .
73
+ @echo ""
74
+ @echo "Calling calculator tool..."
75
+ @curl -s -X POST http://localhost:8080/tools/call \
76
+ -H "Content-Type: application/json" \
77
+ -d '{"name":"calculator","arguments":{"operation":"add","a":5,"b":3}}' | jq .
@@ -0,0 +1,145 @@
1
+ # based/server
2
+
3
+ An extendable tool server based on [the official MCP Ruby SDK](https://github.com/modelcontextprotocol/ruby-sdk).
4
+
5
+ ## Quick Start
6
+
7
+ Run the server with example tools:
8
+
9
+ ```bash
10
+ docker run -p 8080:80 based/server:latest
11
+ ```
12
+
13
+ Test the server:
14
+
15
+ ```bash
16
+ curl http://localhost:8080/health
17
+ curl http://localhost:8080/tools
18
+ ```
19
+
20
+ ## Creating Your Own MCP Server
21
+
22
+ ### 1. Create Tool Definitions
23
+
24
+ Create Ruby files using the MCP DSL to define your tools:
25
+
26
+ ```ruby
27
+ # tools/hello.rb
28
+ tool "greet" do
29
+ description "Greets a person by name"
30
+
31
+ parameter "name" do
32
+ type :string
33
+ required true
34
+ description "The name of the person to greet"
35
+ end
36
+
37
+ parameter "greeting" do
38
+ type :string
39
+ required false
40
+ description "Custom greeting (default: Hello)"
41
+ default "Hello"
42
+ end
43
+
44
+ execute do |params|
45
+ greeting = params["greeting"] || "Hello"
46
+ "#{greeting}, #{params['name']}!"
47
+ end
48
+ end
49
+ ```
50
+
51
+ ### 2. Mount Your Tools Directory
52
+
53
+ Run the container with your tools directory mounted at `/mcp/tools`:
54
+
55
+ ```bash
56
+ docker run -p 8080:80 -v $(pwd)/tools:/mcp/tools based/server:latest
57
+ ```
58
+
59
+ ### 3. Use Your Tools
60
+
61
+ Call your tools via the MCP protocol:
62
+
63
+ ```bash
64
+ # List available tools
65
+ curl -X POST http://localhost:8080/tools/list
66
+
67
+ # Call a tool
68
+ curl -X POST http://localhost:8080/tools/call \
69
+ -H "Content-Type: application/json" \
70
+ -d '{"name":"greet","arguments":{"name":"World"}}'
71
+ ```
72
+
73
+ ## DSL Reference
74
+
75
+ ### Defining a Tool
76
+
77
+ ```ruby
78
+ tool "tool_name" do
79
+ description "What this tool does"
80
+
81
+ parameter "param_name" do
82
+ type :string # :string, :number, :boolean, :array, :object
83
+ required true # or false
84
+ description "Parameter description"
85
+ enum ["option1", "option2"] # optional: restrict to specific values
86
+ default "default_value" # optional: default value
87
+ end
88
+
89
+ execute do |params|
90
+ # Your tool logic here
91
+ # Access parameters via params["param_name"]
92
+ # Return a string result
93
+ "Result: #{params['param_name']}"
94
+ end
95
+ end
96
+ ```
97
+
98
+ ### Parameter Types
99
+
100
+ - `:string` - Text values
101
+ - `:number` - Numeric values (integers or floats)
102
+ - `:boolean` - true/false
103
+ - `:array` - Lists of values
104
+ - `:object` - Complex objects
105
+
106
+ ### Multiple Tools Per File
107
+
108
+ You can define multiple tools in a single file:
109
+
110
+ ```ruby
111
+ tool "add" do
112
+ # ... tool definition
113
+ end
114
+
115
+ tool "subtract" do
116
+ # ... tool definition
117
+ end
118
+ ```
119
+
120
+ ## Example Tools
121
+
122
+ See [examples/calculator.rb](examples/calculator.rb) for complete examples including:
123
+ - Calculator with arithmetic operations
124
+ - Echo tool for simple string operations
125
+
126
+ ## Configuration
127
+
128
+ | Environment Variable | Default | Description |
129
+ | -- | -- | -- |
130
+ | PORT | 80 | Port to run HTTP server on |
131
+ | RACK_ENV | production | Rack environment |
132
+
133
+ ## API Endpoints
134
+
135
+ ### MCP Protocol Endpoints
136
+
137
+ - `POST /initialize` - Initialize MCP session
138
+ - `POST /tools/list` - List available tools
139
+ - `POST /tools/call` - Execute a tool
140
+
141
+ ### Debug Endpoints
142
+
143
+ - `GET /health` - Health check
144
+ - `GET /tools` - List loaded tools (simple format)
145
+ - `POST /reload` - Reload tools from `/mcp` directory
@@ -0,0 +1,4 @@
1
+ require_relative 'server'
2
+
3
+ # Completely bypass Rack::Protection by not using Sinatra's run! method
4
+ run Langop::Server
@@ -0,0 +1,63 @@
1
+ # Example tool: Calculator
2
+ # This file demonstrates the MCP DSL for defining tools
3
+
4
+ tool 'calculator' do
5
+ description 'Performs basic arithmetic operations on two numbers'
6
+
7
+ parameter 'operation' do
8
+ type :string
9
+ required true
10
+ description 'The arithmetic operation to perform'
11
+ enum %w[add subtract multiply divide]
12
+ end
13
+
14
+ parameter 'a' do
15
+ type :number
16
+ required true
17
+ description 'The first number'
18
+ end
19
+
20
+ parameter 'b' do
21
+ type :number
22
+ required true
23
+ description 'The second number'
24
+ end
25
+
26
+ execute do |params|
27
+ a = params['a']
28
+ b = params['b']
29
+
30
+ result = case params['operation']
31
+ when 'add'
32
+ a + b
33
+ when 'subtract'
34
+ a - b
35
+ when 'multiply'
36
+ a * b
37
+ when 'divide'
38
+ if b.zero?
39
+ 'Error: Division by zero'
40
+ else
41
+ a / b.to_f
42
+ end
43
+ else
44
+ 'Unknown operation'
45
+ end
46
+
47
+ "Result: #{result}"
48
+ end
49
+ end
50
+
51
+ tool 'echo' do
52
+ description 'Returns the input message'
53
+
54
+ parameter 'message' do
55
+ type :string
56
+ required true
57
+ description 'The message to echo back'
58
+ end
59
+
60
+ execute do |params|
61
+ params['message']
62
+ end
63
+ end
@@ -0,0 +1,190 @@
1
+ # Example tool demonstrating all the new helper features
2
+
3
+ # Example 1: Using built-in parameter validators
4
+ tool 'send_message' do
5
+ description 'Send a message to an email or phone number'
6
+
7
+ parameter 'recipient' do
8
+ type :string
9
+ required true
10
+ description 'Email address or phone number'
11
+ # You can use built-in validators
12
+ # email_format or phone_format
13
+ end
14
+
15
+ parameter 'message' do
16
+ type :string
17
+ required true
18
+ description 'The message to send'
19
+ end
20
+
21
+ execute do |params|
22
+ recipient = params['recipient']
23
+ message = params['message']
24
+
25
+ # Use validation helpers
26
+ if recipient.include?('@')
27
+ error = validate_email(recipient)
28
+ return error if error
29
+
30
+ "Email sent to #{recipient}: #{message}"
31
+ else
32
+ error = validate_phone(recipient)
33
+ return error if error
34
+
35
+ "SMS sent to #{recipient}: #{message}"
36
+ end
37
+ end
38
+ end
39
+
40
+ # Example 2: Using Config helper for environment variables
41
+ tool 'check_smtp_config' do
42
+ description 'Check SMTP configuration with fallback keys'
43
+
44
+ execute do |_params|
45
+ # Get config with multiple fallback keys
46
+ host = Config.get('SMTP_HOST', 'MAIL_HOST', default: 'localhost')
47
+ port = Config.get_int('SMTP_PORT', 'MAIL_PORT', default: 587)
48
+ use_tls = Config.get_bool('SMTP_TLS', 'MAIL_TLS', default: true)
49
+
50
+ # Check required configs
51
+ missing = Config.check_required('SMTP_USER', 'SMTP_PASSWORD')
52
+ return error("Missing configuration: #{missing.join(', ')}") unless missing.empty?
53
+
54
+ success <<~CONFIG
55
+ SMTP Configuration:
56
+ Host: #{host}
57
+ Port: #{port}
58
+ TLS: #{use_tls}
59
+ User: #{Config.get('SMTP_USER')}
60
+ CONFIG
61
+ end
62
+ end
63
+
64
+ # Example 3: Using HTTP helper
65
+ tool 'fetch_json' do
66
+ description 'Fetch JSON data from a URL'
67
+
68
+ parameter 'url' do
69
+ type :string
70
+ required true
71
+ description 'URL to fetch'
72
+ url_format # Built-in validator
73
+ end
74
+
75
+ execute do |params|
76
+ url = params['url']
77
+
78
+ # Use HTTP helper instead of curl
79
+ response = HTTP.get(url, headers: { 'Accept' => 'application/json' })
80
+
81
+ if response[:success]
82
+ if response[:json]
83
+ "Fetched JSON with #{response[:json].keys.length} keys"
84
+ else
85
+ "Response received but not JSON: #{truncate(response[:body])}"
86
+ end
87
+ else
88
+ error("Failed to fetch URL: #{response[:error]}")
89
+ end
90
+ end
91
+ end
92
+
93
+ # Example 4: Using Shell helper for safe command execution
94
+ tool 'safe_grep' do
95
+ description 'Safely search for a pattern in a file'
96
+
97
+ parameter 'pattern' do
98
+ type :string
99
+ required true
100
+ description 'Search pattern'
101
+ end
102
+
103
+ parameter 'file' do
104
+ type :string
105
+ required true
106
+ description 'File to search in'
107
+ end
108
+
109
+ execute do |params|
110
+ # This is safe from injection attacks!
111
+ result = Shell.run('grep', params['pattern'], params['file'])
112
+
113
+ if result[:success]
114
+ "Found matches:\n#{result[:output]}"
115
+ else
116
+ 'No matches found'
117
+ end
118
+ end
119
+ end
120
+
121
+ # Example 5: Using custom validation
122
+ tool 'restricted_command' do
123
+ description 'Run a command from an allowed list'
124
+
125
+ parameter 'command' do
126
+ type :string
127
+ required true
128
+ description 'Command to run (must be in allowed list)'
129
+ validate lambda { |cmd|
130
+ allowed = %w[ls pwd whoami date]
131
+ allowed.include?(cmd) || "Command '#{cmd}' not allowed. Allowed: #{allowed.join(', ')}"
132
+ }
133
+ end
134
+
135
+ execute do |params|
136
+ result = Shell.run(params['command'])
137
+ result[:output]
138
+ end
139
+ end
140
+
141
+ # Example 6: Using multiple helpers together
142
+ tool 'web_health_check' do
143
+ description 'Check if a web service is healthy'
144
+
145
+ parameter 'url' do
146
+ type :string
147
+ required true
148
+ description 'Service URL to check'
149
+ url_format
150
+ end
151
+
152
+ parameter 'expected_status' do
153
+ type :number
154
+ required false
155
+ description 'Expected HTTP status code'
156
+ default 200
157
+ end
158
+
159
+ execute do |params|
160
+ url = params['url']
161
+ expected = params['expected_status'] || 200
162
+
163
+ # Use HTTP helper
164
+ response = HTTP.head(url)
165
+
166
+ if response[:error]
167
+ error("Health check failed: #{response[:error]}")
168
+ elsif response[:status] == expected
169
+ success("✓ Service healthy (HTTP #{response[:status]})")
170
+ else
171
+ error("Service returned HTTP #{response[:status]}, expected #{expected}")
172
+ end
173
+ end
174
+ end
175
+
176
+ # Example 7: Using env_required helper
177
+ tool 'database_info' do
178
+ description 'Show database connection info'
179
+
180
+ execute do |_params|
181
+ # Check required env vars
182
+ error = env_required('DATABASE_URL', 'DB_HOST')
183
+ return error if error
184
+
185
+ # Safe to use now
186
+ db_url = env_get('DATABASE_URL', 'DB_URL')
187
+
188
+ success("Database configured: #{db_url}")
189
+ end
190
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'language_operator'
4
+
5
+ # Convenience - export LanguageOperator classes at top level for tool definitions
6
+ #
7
+ # This allows tool files to use simplified syntax:
8
+ # tool "example" do
9
+ # ...
10
+ # end
11
+ #
12
+ # Instead of:
13
+ # LanguageOperator::Dsl.define do
14
+ # tool "example" do
15
+ # ...
16
+ # end
17
+ # end
18
+
19
+ # Alias ToolLoader at top level for convenience
20
+ ToolLoader = LanguageOperator::ToolLoader
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'language_operator/tool_loader'
4
+
5
+ # Start the MCP server using LanguageOperator SDK
6
+ # This will load all tools from /mcp and start the server on PORT (default 80)
7
+ LanguageOperator::ToolLoader.start
@@ -76,7 +76,9 @@ module LanguageOperator
76
76
  @config = default_config.merge(config)
77
77
  logger.debug('TaskExecutor initialized',
78
78
  task_count: @tasks.size,
79
- timeout: @config[:timeout],
79
+ timeout_symbolic: @config[:timeout_symbolic],
80
+ timeout_neural: @config[:timeout_neural],
81
+ timeout_hybrid: @config[:timeout_hybrid],
80
82
  max_retries: @config[:max_retries])
81
83
  end
82
84
 
@@ -95,13 +97,11 @@ module LanguageOperator
95
97
  # @raise [TaskExecutionError] If task execution fails after retries
96
98
  def execute_task(task_name, inputs: {}, timeout: nil, max_retries: nil)
97
99
  execution_start = Time.now
98
- timeout ||= @config[:timeout]
99
100
  max_retries ||= @config[:max_retries]
100
101
 
101
102
  with_span('task_executor.execute_task', attributes: {
102
103
  'task.name' => task_name.to_s,
103
104
  'task.inputs' => inputs.keys.map(&:to_s).join(','),
104
- 'task.timeout' => timeout,
105
105
  'task.max_retries' => max_retries
106
106
  }) do
107
107
  # Find task definition
@@ -109,12 +109,19 @@ module LanguageOperator
109
109
  raise ArgumentError, "Task not found: #{task_name}. Available tasks: #{@tasks.keys.join(', ')}" unless task
110
110
 
111
111
  task_type = determine_task_type(task)
112
+
113
+ # Determine timeout based on task type if not explicitly provided
114
+ timeout ||= task_timeout_for_type(task)
115
+
112
116
  logger.info('Executing task',
113
117
  task: task_name,
114
118
  type: task_type,
115
119
  timeout: timeout,
116
120
  max_retries: max_retries)
117
121
 
122
+ # Add timeout to span attributes after it's determined
123
+ OpenTelemetry::Trace.current_span&.set_attribute('task.timeout', timeout)
124
+
118
125
  # Execute with retry logic
119
126
  execute_with_retry(task, task_name, inputs, timeout, max_retries, execution_start)
120
127
  end
@@ -335,10 +342,12 @@ module LanguageOperator
335
342
  # @return [Hash] Default configuration
336
343
  def default_config
337
344
  {
338
- timeout: 30.0, # Default timeout in seconds
339
- max_retries: 3, # Default max retry attempts
340
- retry_delay_base: 1.0, # Base delay for exponential backoff
341
- retry_delay_max: 10.0 # Maximum delay between retries
345
+ timeout_symbolic: 30.0, # Default timeout for symbolic tasks (seconds)
346
+ timeout_neural: 120.0, # Default timeout for neural tasks (seconds)
347
+ timeout_hybrid: 120.0, # Default timeout for hybrid tasks (seconds)
348
+ max_retries: 3, # Default max retry attempts
349
+ retry_delay_base: 1.0, # Base delay for exponential backoff
350
+ retry_delay_max: 10.0 # Maximum delay between retries
342
351
  }
343
352
  end
344
353
 
@@ -358,6 +367,29 @@ module LanguageOperator
358
367
  end
359
368
  end
360
369
 
370
+ # Determine appropriate timeout for a task based on its type
371
+ #
372
+ # Neural tasks typically require longer timeouts due to LLM API calls,
373
+ # while symbolic tasks (pure Ruby code) can use shorter timeouts.
374
+ #
375
+ # @param task [TaskDefinition] The task definition
376
+ # @return [Float] Timeout in seconds
377
+ def task_timeout_for_type(task)
378
+ if task.neural? && task.symbolic?
379
+ # Hybrid tasks use neural timeout (they may call LLM)
380
+ @config[:timeout_hybrid]
381
+ elsif task.neural?
382
+ # Neural tasks need longer timeout for LLM calls
383
+ @config[:timeout_neural]
384
+ elsif task.symbolic?
385
+ # Symbolic tasks use shorter timeout
386
+ @config[:timeout_symbolic]
387
+ else
388
+ # Default to symbolic timeout for undefined tasks
389
+ @config[:timeout_symbolic]
390
+ end
391
+ end
392
+
361
393
  # Execute task with retry logic and timeout
362
394
  #
363
395
  # @param task [TaskDefinition] The task definition
@@ -76,9 +76,6 @@ module LanguageOperator
76
76
 
77
77
  ctx = Helpers::ClusterContext.from_options(options.merge(cluster: cluster))
78
78
 
79
- Formatters::ProgressFormatter.info("Creating agent in cluster '#{ctx.name}'")
80
- puts
81
-
82
79
  # Generate agent name from description if not provided
83
80
  agent_name = options[:name] || generate_agent_name(description)
84
81
 
@@ -1105,6 +1105,7 @@ module LanguageOperator
1105
1105
  {
1106
1106
  'name' => 'agent',
1107
1107
  'image' => image,
1108
+ 'imagePullPolicy' => 'Always',
1108
1109
  'env' => env_vars,
1109
1110
  'volumeMounts' => [
1110
1111
  {