language-operator 0.1.31 → 0.1.35
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/.rubocop.yml +7 -8
- data/CHANGELOG.md +14 -0
- data/CI_STATUS.md +56 -0
- data/Gemfile.lock +2 -2
- data/Makefile +22 -6
- data/lib/language_operator/agent/base.rb +10 -6
- data/lib/language_operator/agent/executor.rb +19 -97
- data/lib/language_operator/agent/safety/ast_validator.rb +62 -43
- data/lib/language_operator/agent/safety/safe_executor.rb +27 -2
- data/lib/language_operator/agent/scheduler.rb +60 -0
- data/lib/language_operator/agent/task_executor.rb +548 -0
- data/lib/language_operator/agent.rb +90 -27
- data/lib/language_operator/cli/base_command.rb +117 -0
- data/lib/language_operator/cli/commands/agent.rb +339 -407
- data/lib/language_operator/cli/commands/cluster.rb +274 -290
- data/lib/language_operator/cli/commands/install.rb +110 -119
- data/lib/language_operator/cli/commands/model.rb +284 -184
- data/lib/language_operator/cli/commands/persona.rb +218 -284
- data/lib/language_operator/cli/commands/quickstart.rb +4 -5
- data/lib/language_operator/cli/commands/status.rb +31 -35
- data/lib/language_operator/cli/commands/system.rb +221 -233
- data/lib/language_operator/cli/commands/tool.rb +356 -422
- data/lib/language_operator/cli/commands/use.rb +19 -22
- data/lib/language_operator/cli/helpers/resource_dependency_checker.rb +0 -18
- data/lib/language_operator/cli/wizards/quickstart_wizard.rb +0 -1
- data/lib/language_operator/client/config.rb +20 -21
- data/lib/language_operator/config.rb +115 -3
- data/lib/language_operator/constants.rb +54 -0
- data/lib/language_operator/dsl/agent_context.rb +7 -7
- data/lib/language_operator/dsl/agent_definition.rb +111 -26
- data/lib/language_operator/dsl/config.rb +30 -66
- data/lib/language_operator/dsl/main_definition.rb +114 -0
- data/lib/language_operator/dsl/schema.rb +84 -43
- data/lib/language_operator/dsl/task_definition.rb +315 -0
- data/lib/language_operator/dsl.rb +0 -1
- data/lib/language_operator/instrumentation/task_tracer.rb +285 -0
- data/lib/language_operator/logger.rb +4 -4
- data/lib/language_operator/synthesis_test_harness.rb +324 -0
- data/lib/language_operator/templates/examples/agent_synthesis.tmpl +26 -8
- data/lib/language_operator/templates/schema/CHANGELOG.md +26 -0
- data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
- data/lib/language_operator/templates/schema/agent_dsl_schema.json +84 -42
- data/lib/language_operator/type_coercion.rb +250 -0
- data/lib/language_operator/ux/base.rb +81 -0
- data/lib/language_operator/ux/concerns/README.md +155 -0
- data/lib/language_operator/ux/concerns/headings.rb +90 -0
- data/lib/language_operator/ux/concerns/input_validation.rb +146 -0
- data/lib/language_operator/ux/concerns/provider_helpers.rb +167 -0
- data/lib/language_operator/ux/create_agent.rb +252 -0
- data/lib/language_operator/ux/create_model.rb +267 -0
- data/lib/language_operator/ux/quickstart.rb +594 -0
- data/lib/language_operator/version.rb +1 -1
- data/lib/language_operator.rb +2 -0
- data/requirements/ARCHITECTURE.md +1 -0
- data/requirements/SCRATCH.md +153 -0
- data/requirements/dsl.md +0 -0
- data/requirements/features +1 -0
- data/requirements/personas +1 -0
- data/requirements/proposals +1 -0
- data/requirements/tasks/iterate.md +14 -15
- data/requirements/tasks/optimize.md +13 -4
- data/synth/001/Makefile +90 -0
- data/synth/001/agent.rb +26 -0
- data/synth/001/agent.yaml +7 -0
- data/synth/001/output.log +44 -0
- data/synth/Makefile +39 -0
- data/synth/README.md +342 -0
- metadata +37 -10
- data/lib/language_operator/dsl/workflow_definition.rb +0 -259
- data/test_agent_dsl.rb +0 -108
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative '../base_command'
|
|
4
4
|
require 'pastel'
|
|
5
5
|
require_relative '../formatters/progress_formatter'
|
|
6
6
|
require_relative '../../config/cluster_config'
|
|
@@ -9,37 +9,34 @@ module LanguageOperator
|
|
|
9
9
|
module CLI
|
|
10
10
|
module Commands
|
|
11
11
|
# Switch cluster context command
|
|
12
|
-
class Use <
|
|
12
|
+
class Use < BaseCommand
|
|
13
13
|
desc 'use CLUSTER', 'Switch to a different cluster context'
|
|
14
14
|
def self.exit_on_failure?
|
|
15
15
|
true
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def switch(cluster_name)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
handle_command_error('switch cluster') do
|
|
20
|
+
unless Config::ClusterConfig.cluster_exists?(cluster_name)
|
|
21
|
+
Formatters::ProgressFormatter.error("Cluster '#{cluster_name}' not found")
|
|
22
|
+
puts "\nAvailable clusters:"
|
|
23
|
+
Config::ClusterConfig.list_clusters.each do |cluster|
|
|
24
|
+
puts " - #{cluster[:name]}"
|
|
25
|
+
end
|
|
26
|
+
exit 1
|
|
24
27
|
end
|
|
25
|
-
exit 1
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
Config::ClusterConfig.set_current_cluster(cluster_name)
|
|
29
|
-
cluster = Config::ClusterConfig.get_cluster(cluster_name)
|
|
30
28
|
|
|
31
|
-
|
|
29
|
+
Config::ClusterConfig.set_current_cluster(cluster_name)
|
|
30
|
+
cluster = Config::ClusterConfig.get_cluster(cluster_name)
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
puts "\nCluster Details"
|
|
35
|
-
puts '----------------'
|
|
36
|
-
puts "Name: #{pastel.bold.white(cluster[:name])}"
|
|
37
|
-
puts "Namespace: #{pastel.bold.white(cluster[:namespace])}"
|
|
38
|
-
rescue StandardError => e
|
|
39
|
-
Formatters::ProgressFormatter.error("Failed to switch cluster: #{e.message}")
|
|
40
|
-
raise if ENV['DEBUG']
|
|
32
|
+
Formatters::ProgressFormatter.success("Switched to cluster '#{cluster_name}'")
|
|
41
33
|
|
|
42
|
-
|
|
34
|
+
pastel = Pastel.new
|
|
35
|
+
puts "\nCluster Details"
|
|
36
|
+
puts '----------------'
|
|
37
|
+
puts "Name: #{pastel.bold.white(cluster[:name])}"
|
|
38
|
+
puts "Namespace: #{pastel.bold.white(cluster[:namespace])}"
|
|
39
|
+
end
|
|
43
40
|
end
|
|
44
41
|
end
|
|
45
42
|
end
|
|
@@ -50,24 +50,6 @@ module LanguageOperator
|
|
|
50
50
|
def self.tool_usage_count(agents, tool_name)
|
|
51
51
|
agents_using_tool(agents, tool_name).size
|
|
52
52
|
end
|
|
53
|
-
|
|
54
|
-
# Count how many agents use a specific model
|
|
55
|
-
#
|
|
56
|
-
# @param agents [Array<Hash>] Array of agent resources
|
|
57
|
-
# @param model_name [String] Name of the model
|
|
58
|
-
# @return [Integer] Count of agents using this model
|
|
59
|
-
def self.model_usage_count(agents, model_name)
|
|
60
|
-
agents_using_model(agents, model_name).size
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Count how many agents use a specific persona
|
|
64
|
-
#
|
|
65
|
-
# @param agents [Array<Hash>] Array of agent resources
|
|
66
|
-
# @param persona_name [String] Name of the persona
|
|
67
|
-
# @return [Integer] Count of agents using this persona
|
|
68
|
-
def self.persona_usage_count(agents, persona_name)
|
|
69
|
-
agents_using_persona(agents, persona_name).size
|
|
70
|
-
end
|
|
71
53
|
end
|
|
72
54
|
end
|
|
73
55
|
end
|
|
@@ -413,7 +413,6 @@ module LanguageOperator
|
|
|
413
413
|
end
|
|
414
414
|
|
|
415
415
|
def create_model_resource(cluster_info, name, provider, model, api_key = nil, endpoint = nil)
|
|
416
|
-
# rubocop:disable Metrics/BlockLength
|
|
417
416
|
Formatters::ProgressFormatter.with_spinner("Creating model '#{name}'") do
|
|
418
417
|
k8s = Kubernetes::Client.new(
|
|
419
418
|
kubeconfig: cluster_info[:kubeconfig],
|
|
@@ -34,12 +34,13 @@ module LanguageOperator
|
|
|
34
34
|
{
|
|
35
35
|
'llm' => {
|
|
36
36
|
'provider' => detect_provider_from_env,
|
|
37
|
-
'model' =>
|
|
37
|
+
'model' => LanguageOperator::Config.get('LLM_MODEL', default: default_model_from_env),
|
|
38
38
|
'endpoint' => parse_model_endpoint_from_env,
|
|
39
|
-
'api_key' =>
|
|
39
|
+
'api_key' => LanguageOperator::Config.get('OPENAI_API_KEY', 'ANTHROPIC_API_KEY',
|
|
40
|
+
default: 'dummy-key-for-local-proxy')
|
|
40
41
|
},
|
|
41
42
|
'mcp_servers' => parse_mcp_servers_from_env,
|
|
42
|
-
'debug' =>
|
|
43
|
+
'debug' => LanguageOperator::Config.get_bool('DEBUG', default: false)
|
|
43
44
|
}
|
|
44
45
|
end
|
|
45
46
|
|
|
@@ -49,15 +50,12 @@ module LanguageOperator
|
|
|
49
50
|
#
|
|
50
51
|
# @return [String, nil] Model endpoint URL
|
|
51
52
|
def self.parse_model_endpoint_from_env
|
|
52
|
-
# Support MODEL_ENDPOINTS (operator sets this)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
# Fallback to legacy OPENAI_ENDPOINT
|
|
59
|
-
ENV.fetch('OPENAI_ENDPOINT', nil)
|
|
60
|
-
end
|
|
53
|
+
# Support MODEL_ENDPOINTS (operator sets this) - take first from comma-separated list
|
|
54
|
+
endpoints = LanguageOperator::Config.get_array('MODEL_ENDPOINTS')
|
|
55
|
+
return endpoints.first unless endpoints.empty?
|
|
56
|
+
|
|
57
|
+
# Fallback to legacy OPENAI_ENDPOINT
|
|
58
|
+
LanguageOperator::Config.get('OPENAI_ENDPOINT')
|
|
61
59
|
end
|
|
62
60
|
|
|
63
61
|
# Load configuration with automatic fallback to environment variables
|
|
@@ -79,11 +77,11 @@ module LanguageOperator
|
|
|
79
77
|
# @return [String] Provider name (openai_compatible, openai, or anthropic)
|
|
80
78
|
# @raise [RuntimeError] If no API key or endpoint is found
|
|
81
79
|
def self.detect_provider_from_env
|
|
82
|
-
if
|
|
80
|
+
if LanguageOperator::Config.set?('OPENAI_ENDPOINT', 'MODEL_ENDPOINTS')
|
|
83
81
|
'openai_compatible'
|
|
84
|
-
elsif
|
|
82
|
+
elsif LanguageOperator::Config.set?('OPENAI_API_KEY')
|
|
85
83
|
'openai'
|
|
86
|
-
elsif
|
|
84
|
+
elsif LanguageOperator::Config.set?('ANTHROPIC_API_KEY')
|
|
87
85
|
'anthropic'
|
|
88
86
|
else
|
|
89
87
|
raise 'No API key or endpoint found. Set OPENAI_ENDPOINT or MODEL_ENDPOINTS for local LLM, ' \
|
|
@@ -109,21 +107,22 @@ module LanguageOperator
|
|
|
109
107
|
# @return [Array<Hash>] Array of MCP server configurations
|
|
110
108
|
def self.parse_mcp_servers_from_env
|
|
111
109
|
# Support both MCP_SERVERS (comma-separated) and legacy MCP_URL
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
servers = LanguageOperator::Config.get_array('MCP_SERVERS')
|
|
111
|
+
|
|
112
|
+
if !servers.empty?
|
|
114
113
|
# Parse comma-separated URLs
|
|
115
|
-
|
|
114
|
+
servers.map.with_index do |url, index|
|
|
116
115
|
{
|
|
117
116
|
'name' => "default-tools-#{index}",
|
|
118
|
-
'url' => url
|
|
117
|
+
'url' => url,
|
|
119
118
|
'transport' => 'streamable',
|
|
120
119
|
'enabled' => true
|
|
121
120
|
}
|
|
122
121
|
end
|
|
123
|
-
elsif
|
|
122
|
+
elsif (url = LanguageOperator::Config.get('MCP_URL'))
|
|
124
123
|
[{
|
|
125
124
|
'name' => 'default-tools',
|
|
126
|
-
'url' =>
|
|
125
|
+
'url' => url,
|
|
127
126
|
'transport' => 'streamable',
|
|
128
127
|
'enabled' => true
|
|
129
128
|
}]
|
|
@@ -8,8 +8,9 @@ module LanguageOperator
|
|
|
8
8
|
# - Key-to-env-var mappings
|
|
9
9
|
# - Prefixes (e.g., SMTP_HOST from prefix: 'SMTP')
|
|
10
10
|
# - Default values
|
|
11
|
-
# - Type conversion (string, integer, boolean, float)
|
|
11
|
+
# - Type conversion (string, integer, boolean, float, array)
|
|
12
12
|
# - Required config validation
|
|
13
|
+
# - Multiple fallback keys
|
|
13
14
|
#
|
|
14
15
|
# @example Load SMTP configuration
|
|
15
16
|
# config = LanguageOperator::Config.load(
|
|
@@ -75,7 +76,8 @@ module LanguageOperator
|
|
|
75
76
|
# Convert a string value to the specified type
|
|
76
77
|
#
|
|
77
78
|
# @param value [String, nil] Raw string value from environment
|
|
78
|
-
# @param type [Symbol] Target type (:string, :integer, :boolean, :float)
|
|
79
|
+
# @param type [Symbol] Target type (:string, :integer, :boolean, :float, :array)
|
|
80
|
+
# @param separator [String] Separator for array type (default: ',')
|
|
79
81
|
# @return [Object] Converted value
|
|
80
82
|
#
|
|
81
83
|
# @example String conversion
|
|
@@ -92,7 +94,10 @@ module LanguageOperator
|
|
|
92
94
|
#
|
|
93
95
|
# @example Float conversion
|
|
94
96
|
# Config.convert_type('3.14', :float) # => 3.14
|
|
95
|
-
|
|
97
|
+
#
|
|
98
|
+
# @example Array conversion
|
|
99
|
+
# Config.convert_type('a,b,c', :array) # => ["a", "b", "c"]
|
|
100
|
+
def self.convert_type(value, type, separator: ',')
|
|
96
101
|
return nil if value.nil?
|
|
97
102
|
|
|
98
103
|
case type
|
|
@@ -104,6 +109,10 @@ module LanguageOperator
|
|
|
104
109
|
value.to_f
|
|
105
110
|
when :boolean
|
|
106
111
|
%w[true 1 yes on].include?(value.to_s.downcase)
|
|
112
|
+
when :array
|
|
113
|
+
return [] if value.to_s.empty?
|
|
114
|
+
|
|
115
|
+
value.to_s.split(separator).map(&:strip).reject(&:empty?)
|
|
107
116
|
else
|
|
108
117
|
value
|
|
109
118
|
end
|
|
@@ -134,5 +143,108 @@ module LanguageOperator
|
|
|
134
143
|
validate_required!(config, required) unless required.empty?
|
|
135
144
|
config
|
|
136
145
|
end
|
|
146
|
+
|
|
147
|
+
# Get environment variable with multiple fallback keys
|
|
148
|
+
#
|
|
149
|
+
# @param keys [Array<String>] Environment variable names to try
|
|
150
|
+
# @param default [Object, nil] Default value if none found
|
|
151
|
+
# @return [String, nil] The first non-nil value or default
|
|
152
|
+
#
|
|
153
|
+
# @example
|
|
154
|
+
# Config.get('SMTP_HOST', 'MAIL_HOST', default: 'localhost')
|
|
155
|
+
def self.get(*keys, default: nil)
|
|
156
|
+
keys.each do |key|
|
|
157
|
+
value = ENV.fetch(key.to_s, nil)
|
|
158
|
+
return value if value
|
|
159
|
+
end
|
|
160
|
+
default
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Get required environment variable with fallback keys
|
|
164
|
+
#
|
|
165
|
+
# @param keys [Array<String>] Environment variable names to try
|
|
166
|
+
# @return [String] The first non-nil value
|
|
167
|
+
# @raise [ArgumentError] If none of the keys are set
|
|
168
|
+
#
|
|
169
|
+
# @example
|
|
170
|
+
# Config.require('DATABASE_URL', 'DB_URL')
|
|
171
|
+
def self.require(*keys)
|
|
172
|
+
value = get(*keys)
|
|
173
|
+
raise ArgumentError, "Missing required configuration: #{keys.join(' or ')}" unless value
|
|
174
|
+
|
|
175
|
+
value
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Get environment variable as integer
|
|
179
|
+
#
|
|
180
|
+
# @param keys [Array<String>] Environment variable names to try
|
|
181
|
+
# @param default [Integer, nil] Default value if none found
|
|
182
|
+
# @return [Integer, nil] The value converted to integer, or default
|
|
183
|
+
#
|
|
184
|
+
# @example
|
|
185
|
+
# Config.get_int('MAX_WORKERS', default: 4)
|
|
186
|
+
def self.get_int(*keys, default: nil)
|
|
187
|
+
value = get(*keys)
|
|
188
|
+
return default if value.nil?
|
|
189
|
+
|
|
190
|
+
value.to_i
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Get environment variable as boolean
|
|
194
|
+
#
|
|
195
|
+
# Treats 'true', '1', 'yes', 'on' as true (case insensitive).
|
|
196
|
+
#
|
|
197
|
+
# @param keys [Array<String>] Environment variable names to try
|
|
198
|
+
# @param default [Boolean] Default value if none found
|
|
199
|
+
# @return [Boolean] The value as boolean
|
|
200
|
+
#
|
|
201
|
+
# @example
|
|
202
|
+
# Config.get_bool('USE_TLS', 'ENABLE_TLS', default: true)
|
|
203
|
+
def self.get_bool(*keys, default: false)
|
|
204
|
+
value = get(*keys)
|
|
205
|
+
return default if value.nil?
|
|
206
|
+
|
|
207
|
+
%w[true 1 yes on].include?(value.to_s.downcase)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Get environment variable as array (split by separator)
|
|
211
|
+
#
|
|
212
|
+
# @param keys [Array<String>] Environment variable names to try
|
|
213
|
+
# @param default [Array] Default value if none found
|
|
214
|
+
# @param separator [String] Character to split on (default: ',')
|
|
215
|
+
# @return [Array<String>] The value split into array
|
|
216
|
+
#
|
|
217
|
+
# @example
|
|
218
|
+
# Config.get_array('ALLOWED_HOSTS', separator: ',')
|
|
219
|
+
def self.get_array(*keys, default: [], separator: ',')
|
|
220
|
+
value = get(*keys)
|
|
221
|
+
return default if value.nil? || value.empty?
|
|
222
|
+
|
|
223
|
+
value.split(separator).map(&:strip).reject(&:empty?)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Check if environment variable is set (even if empty string)
|
|
227
|
+
#
|
|
228
|
+
# @param keys [Array<String>] Environment variable names to check
|
|
229
|
+
# @return [Boolean] True if any key is set
|
|
230
|
+
#
|
|
231
|
+
# @example
|
|
232
|
+
# Config.set?('DEBUG', 'VERBOSE')
|
|
233
|
+
def self.set?(*keys)
|
|
234
|
+
keys.any? { |key| ENV.key?(key.to_s) }
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Get all environment variables matching a prefix
|
|
238
|
+
#
|
|
239
|
+
# @param prefix [String] Prefix to match
|
|
240
|
+
# @return [Hash<String, String>] Hash with prefix removed from keys
|
|
241
|
+
#
|
|
242
|
+
# @example
|
|
243
|
+
# Config.with_prefix('DATABASE_')
|
|
244
|
+
# # Returns { 'URL' => '...', 'POOL_SIZE' => '5' } for DATABASE_URL and DATABASE_POOL_SIZE
|
|
245
|
+
def self.with_prefix(prefix)
|
|
246
|
+
ENV.select { |key, _| key.start_with?(prefix) }
|
|
247
|
+
.transform_keys { |key| key.sub(prefix, '') }
|
|
248
|
+
end
|
|
137
249
|
end
|
|
138
250
|
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LanguageOperator
|
|
4
|
+
# Shared constants used across the Language Operator gem
|
|
5
|
+
module Constants
|
|
6
|
+
# Agent execution modes with their aliases
|
|
7
|
+
# Primary modes are the canonical forms used in CRD schemas
|
|
8
|
+
# Aliases are accepted for backwards compatibility and convenience
|
|
9
|
+
EXECUTION_MODES = {
|
|
10
|
+
autonomous: %w[autonomous interactive].freeze,
|
|
11
|
+
scheduled: %w[scheduled event-driven].freeze,
|
|
12
|
+
reactive: %w[reactive http webhook].freeze
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
# Primary (canonical) execution modes for schema definitions
|
|
16
|
+
PRIMARY_MODES = %w[autonomous scheduled reactive].freeze
|
|
17
|
+
|
|
18
|
+
# All valid mode strings (primary + aliases)
|
|
19
|
+
ALL_MODE_ALIASES = EXECUTION_MODES.values.flatten.freeze
|
|
20
|
+
|
|
21
|
+
# Normalizes a mode string to its primary canonical form
|
|
22
|
+
#
|
|
23
|
+
# @param mode_string [String] The mode string to normalize
|
|
24
|
+
# @return [String] The canonical primary mode
|
|
25
|
+
# @raise [ArgumentError] if the mode string is not recognized
|
|
26
|
+
#
|
|
27
|
+
# @example
|
|
28
|
+
# Constants.normalize_mode('interactive') # => 'autonomous'
|
|
29
|
+
# Constants.normalize_mode('webhook') # => 'reactive'
|
|
30
|
+
# Constants.normalize_mode('scheduled') # => 'scheduled'
|
|
31
|
+
def self.normalize_mode(mode_string)
|
|
32
|
+
return nil if mode_string.nil?
|
|
33
|
+
|
|
34
|
+
mode_string = mode_string.to_s.downcase.strip
|
|
35
|
+
|
|
36
|
+
EXECUTION_MODES.each do |primary, aliases|
|
|
37
|
+
return primary.to_s if aliases.include?(mode_string)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
raise ArgumentError, "Unknown execution mode: #{mode_string}. " \
|
|
41
|
+
"Valid modes: #{ALL_MODE_ALIASES.join(', ')}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Validates that a mode string is recognized
|
|
45
|
+
#
|
|
46
|
+
# @param mode_string [String] The mode string to validate
|
|
47
|
+
# @return [Boolean] true if valid, false otherwise
|
|
48
|
+
def self.valid_mode?(mode_string)
|
|
49
|
+
return false if mode_string.nil?
|
|
50
|
+
|
|
51
|
+
ALL_MODE_ALIASES.include?(mode_string.to_s.downcase.strip)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -13,14 +13,14 @@ module LanguageOperator
|
|
|
13
13
|
#
|
|
14
14
|
# schedule "0 12 * * *"
|
|
15
15
|
#
|
|
16
|
-
#
|
|
17
|
-
# "
|
|
18
|
-
#
|
|
19
|
-
#
|
|
16
|
+
# task :search,
|
|
17
|
+
# instructions: "search for recent news",
|
|
18
|
+
# inputs: {},
|
|
19
|
+
# outputs: { results: 'array' }
|
|
20
20
|
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
21
|
+
# main do |inputs|
|
|
22
|
+
# results = execute_task(:search)
|
|
23
|
+
# results
|
|
24
24
|
# end
|
|
25
25
|
# end
|
|
26
26
|
class AgentContext
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '
|
|
3
|
+
require_relative 'main_definition'
|
|
4
|
+
require_relative 'task_definition'
|
|
4
5
|
require_relative 'webhook_definition'
|
|
5
6
|
require_relative 'mcp_server_definition'
|
|
6
7
|
require_relative 'chat_endpoint_definition'
|
|
@@ -11,7 +12,7 @@ module LanguageOperator
|
|
|
11
12
|
module Dsl
|
|
12
13
|
# Agent definition for autonomous agents
|
|
13
14
|
#
|
|
14
|
-
# Defines an agent with objectives,
|
|
15
|
+
# Defines an agent with objectives, tasks, main execution block, schedule, and constraints.
|
|
15
16
|
# Used within the DSL to create agents that can be executed standalone
|
|
16
17
|
# or deployed to Kubernetes.
|
|
17
18
|
#
|
|
@@ -21,14 +22,14 @@ module LanguageOperator
|
|
|
21
22
|
#
|
|
22
23
|
# schedule "0 12 * * *"
|
|
23
24
|
#
|
|
24
|
-
#
|
|
25
|
-
# "
|
|
26
|
-
#
|
|
27
|
-
#
|
|
25
|
+
# task :search,
|
|
26
|
+
# instructions: "search for latest news",
|
|
27
|
+
# inputs: {},
|
|
28
|
+
# outputs: { results: 'array' }
|
|
28
29
|
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
30
|
+
# main do |inputs|
|
|
31
|
+
# results = execute_task(:search)
|
|
32
|
+
# results
|
|
32
33
|
# end
|
|
33
34
|
# end
|
|
34
35
|
#
|
|
@@ -47,7 +48,7 @@ module LanguageOperator
|
|
|
47
48
|
class AgentDefinition
|
|
48
49
|
include LanguageOperator::Loggable
|
|
49
50
|
|
|
50
|
-
attr_reader :name, :description, :persona, :schedule, :objectives, :
|
|
51
|
+
attr_reader :name, :description, :persona, :schedule, :objectives, :main, :tasks,
|
|
51
52
|
:constraints, :output_config, :execution_mode, :webhooks, :mcp_server, :chat_endpoint
|
|
52
53
|
|
|
53
54
|
def initialize(name)
|
|
@@ -56,7 +57,8 @@ module LanguageOperator
|
|
|
56
57
|
@persona = nil
|
|
57
58
|
@schedule = nil
|
|
58
59
|
@objectives = []
|
|
59
|
-
@
|
|
60
|
+
@main = nil
|
|
61
|
+
@tasks = {}
|
|
60
62
|
@constraints = {}
|
|
61
63
|
@output_config = {}
|
|
62
64
|
@execution_mode = :autonomous
|
|
@@ -118,16 +120,95 @@ module LanguageOperator
|
|
|
118
120
|
@objectives << text
|
|
119
121
|
end
|
|
120
122
|
|
|
121
|
-
# Define
|
|
123
|
+
# Define main execution block (DSL v1)
|
|
122
124
|
#
|
|
123
|
-
#
|
|
124
|
-
#
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
# The main block is the imperative entry point for agent execution.
|
|
126
|
+
# It receives agent inputs and returns agent outputs. Use execute_task()
|
|
127
|
+
# to call organic functions (tasks) defined with the task directive.
|
|
128
|
+
#
|
|
129
|
+
# @yield Main execution block
|
|
130
|
+
# @return [MainDefinition] Current main definition
|
|
131
|
+
# @example
|
|
132
|
+
# main do |inputs|
|
|
133
|
+
# result = execute_task(:fetch_data, inputs: inputs)
|
|
134
|
+
# execute_task(:process_data, inputs: result)
|
|
135
|
+
# end
|
|
136
|
+
def main(&block)
|
|
137
|
+
return @main if block.nil?
|
|
138
|
+
|
|
139
|
+
@main = MainDefinition.new
|
|
140
|
+
@main.execute(&block) if block
|
|
141
|
+
@main
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Define a task (organic function) - DSL v1
|
|
145
|
+
#
|
|
146
|
+
# Tasks are the core primitive of DSL v1, representing organic functions with
|
|
147
|
+
# stable input/output contracts. Tasks can be neural (instructions-based),
|
|
148
|
+
# symbolic (code-based), or hybrid (both).
|
|
149
|
+
#
|
|
150
|
+
# @param name [Symbol] Task name
|
|
151
|
+
# @param options [Hash] Task configuration
|
|
152
|
+
# @option options [Hash] :inputs Input schema (param => type)
|
|
153
|
+
# @option options [Hash] :outputs Output schema (field => type)
|
|
154
|
+
# @option options [String] :instructions Natural language instructions (neural)
|
|
155
|
+
# @yield [inputs] Symbolic implementation block (optional)
|
|
156
|
+
# @yieldparam inputs [Hash] Validated input parameters
|
|
157
|
+
# @yieldreturn [Hash] Output matching outputs schema
|
|
158
|
+
# @return [TaskDefinition] The task definition
|
|
159
|
+
#
|
|
160
|
+
# @example Neural task
|
|
161
|
+
# task :analyze_data,
|
|
162
|
+
# instructions: "Analyze the data for anomalies",
|
|
163
|
+
# inputs: { data: 'array' },
|
|
164
|
+
# outputs: { issues: 'array', summary: 'string' }
|
|
165
|
+
#
|
|
166
|
+
# @example Symbolic task
|
|
167
|
+
# task :calculate_total,
|
|
168
|
+
# inputs: { items: 'array' },
|
|
169
|
+
# outputs: { total: 'number' }
|
|
170
|
+
# do |inputs|
|
|
171
|
+
# { total: inputs[:items].sum { |i| i['amount'] } }
|
|
172
|
+
# end
|
|
173
|
+
#
|
|
174
|
+
# @example Hybrid task
|
|
175
|
+
# task :fetch_user,
|
|
176
|
+
# instructions: "Fetch user from database",
|
|
177
|
+
# inputs: { user_id: 'integer' },
|
|
178
|
+
# outputs: { user: 'hash' }
|
|
179
|
+
# do |inputs|
|
|
180
|
+
# execute_tool('database', 'get_user', id: inputs[:user_id])
|
|
181
|
+
# end
|
|
182
|
+
def task(name, **options, &block)
|
|
183
|
+
# Create task definition
|
|
184
|
+
task_def = TaskDefinition.new(name)
|
|
185
|
+
|
|
186
|
+
# Configure from options (keyword arguments)
|
|
187
|
+
task_def.inputs(options[:inputs]) if options[:inputs]
|
|
188
|
+
task_def.outputs(options[:outputs]) if options[:outputs]
|
|
189
|
+
task_def.instructions(options[:instructions]) if options[:instructions]
|
|
190
|
+
|
|
191
|
+
# Symbolic implementation (if block provided)
|
|
192
|
+
task_def.execute(&block) if block
|
|
193
|
+
|
|
194
|
+
# Store in tasks collection
|
|
195
|
+
@tasks[name] = task_def
|
|
196
|
+
|
|
197
|
+
task_type = if task_def.neural? && task_def.symbolic?
|
|
198
|
+
'hybrid'
|
|
199
|
+
elsif task_def.neural?
|
|
200
|
+
'neural'
|
|
201
|
+
else
|
|
202
|
+
'symbolic'
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
logger.debug('Task defined',
|
|
206
|
+
name: name,
|
|
207
|
+
type: task_type,
|
|
208
|
+
inputs: options[:inputs]&.keys || [],
|
|
209
|
+
outputs: options[:outputs]&.keys || [])
|
|
127
210
|
|
|
128
|
-
|
|
129
|
-
@workflow.instance_eval(&block) if block
|
|
130
|
-
@workflow
|
|
211
|
+
task_def
|
|
131
212
|
end
|
|
132
213
|
|
|
133
214
|
# Define constraints (max_iterations, timeout, etc.)
|
|
@@ -213,7 +294,7 @@ module LanguageOperator
|
|
|
213
294
|
name: @name,
|
|
214
295
|
mode: @execution_mode,
|
|
215
296
|
objectives_count: @objectives.size,
|
|
216
|
-
|
|
297
|
+
has_main: !@main.nil?)
|
|
217
298
|
|
|
218
299
|
case @execution_mode
|
|
219
300
|
when :scheduled
|
|
@@ -317,7 +398,7 @@ module LanguageOperator
|
|
|
317
398
|
def execute_objectives
|
|
318
399
|
logger.info('Executing objectives',
|
|
319
400
|
total: @objectives.size,
|
|
320
|
-
|
|
401
|
+
has_main: !@main.nil?)
|
|
321
402
|
|
|
322
403
|
@objectives.each_with_index do |objective, index|
|
|
323
404
|
logger.info('Executing objective',
|
|
@@ -325,13 +406,13 @@ module LanguageOperator
|
|
|
325
406
|
total: @objectives.size,
|
|
326
407
|
objective: objective[0..100])
|
|
327
408
|
|
|
328
|
-
# If
|
|
329
|
-
if @
|
|
330
|
-
logger.timed('Objective
|
|
331
|
-
@
|
|
409
|
+
# If main defined, execute it; otherwise just log
|
|
410
|
+
if @main
|
|
411
|
+
logger.timed('Objective main execution') do
|
|
412
|
+
@main.call({ objective: objective })
|
|
332
413
|
end
|
|
333
414
|
else
|
|
334
|
-
logger.warn('No
|
|
415
|
+
logger.warn('No main block defined, skipping execution')
|
|
335
416
|
end
|
|
336
417
|
end
|
|
337
418
|
|
|
@@ -353,6 +434,10 @@ module LanguageOperator
|
|
|
353
434
|
@constraints[:timeout] = value
|
|
354
435
|
end
|
|
355
436
|
|
|
437
|
+
def max_retries(value)
|
|
438
|
+
@constraints[:max_retries] = value
|
|
439
|
+
end
|
|
440
|
+
|
|
356
441
|
def memory(value)
|
|
357
442
|
@constraints[:memory] = value
|
|
358
443
|
end
|