language-operator 0.1.30 → 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 +49 -0
- data/CI_STATUS.md +56 -0
- data/Gemfile.lock +2 -2
- data/Makefile +28 -7
- data/Rakefile +29 -0
- data/docs/dsl/SCHEMA_VERSION.md +250 -0
- data/docs/dsl/agent-reference.md +13 -0
- 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 +39 -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 +351 -466
- data/lib/language_operator/cli/commands/cluster.rb +276 -256
- 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 +220 -289
- data/lib/language_operator/cli/commands/quickstart.rb +4 -5
- data/lib/language_operator/cli/commands/status.rb +36 -53
- data/lib/language_operator/cli/commands/system.rb +760 -0
- 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/formatters/code_formatter.rb +3 -7
- data/lib/language_operator/cli/formatters/log_formatter.rb +3 -5
- data/lib/language_operator/cli/formatters/progress_formatter.rb +3 -7
- data/lib/language_operator/cli/formatters/status_formatter.rb +37 -0
- data/lib/language_operator/cli/formatters/table_formatter.rb +10 -26
- data/lib/language_operator/cli/helpers/pastel_helper.rb +24 -0
- data/lib/language_operator/cli/helpers/resource_dependency_checker.rb +0 -18
- data/lib/language_operator/cli/main.rb +4 -0
- 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 +1143 -0
- data/lib/language_operator/dsl/task_definition.rb +315 -0
- data/lib/language_operator/dsl.rb +1 -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/README.md +23 -0
- data/lib/language_operator/templates/examples/agent_synthesis.tmpl +133 -0
- data/lib/language_operator/templates/examples/persona_distillation.tmpl +19 -0
- data/lib/language_operator/templates/schema/.gitkeep +0 -0
- data/lib/language_operator/templates/schema/CHANGELOG.md +119 -0
- data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +306 -0
- data/lib/language_operator/templates/schema/agent_dsl_schema.json +494 -0
- 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 +49 -18
- data/examples/README.md +0 -569
- data/examples/agent_example.rb +0 -86
- data/examples/chat_endpoint_agent.rb +0 -118
- data/examples/github_webhook_agent.rb +0 -171
- data/examples/mcp_agent.rb +0 -158
- data/examples/oauth_callback_agent.rb +0 -296
- data/examples/stripe_webhook_agent.rb +0 -219
- data/examples/webhook_agent.rb +0 -80
- 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
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'rouge'
|
|
4
|
-
|
|
4
|
+
require_relative '../helpers/pastel_helper'
|
|
5
5
|
|
|
6
6
|
module LanguageOperator
|
|
7
7
|
module CLI
|
|
@@ -9,6 +9,8 @@ module LanguageOperator
|
|
|
9
9
|
# Formatter for displaying syntax-highlighted code in the terminal
|
|
10
10
|
class CodeFormatter
|
|
11
11
|
class << self
|
|
12
|
+
include Helpers::PastelHelper
|
|
13
|
+
|
|
12
14
|
# Display Ruby code with syntax highlighting
|
|
13
15
|
#
|
|
14
16
|
# @param code_content [String] The Ruby code to display
|
|
@@ -68,12 +70,6 @@ module LanguageOperator
|
|
|
68
70
|
puts highlighted
|
|
69
71
|
puts
|
|
70
72
|
end
|
|
71
|
-
|
|
72
|
-
private
|
|
73
|
-
|
|
74
|
-
def pastel
|
|
75
|
-
@pastel ||= Pastel.new
|
|
76
|
-
end
|
|
77
73
|
end
|
|
78
74
|
end
|
|
79
75
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative '../helpers/pastel_helper'
|
|
4
4
|
require 'json'
|
|
5
5
|
require 'time'
|
|
6
6
|
|
|
@@ -10,6 +10,8 @@ module LanguageOperator
|
|
|
10
10
|
# Formatter for displaying agent execution logs with color and icons
|
|
11
11
|
class LogFormatter
|
|
12
12
|
class << self
|
|
13
|
+
include Helpers::PastelHelper
|
|
14
|
+
|
|
13
15
|
# Format a single log line from kubectl output
|
|
14
16
|
#
|
|
15
17
|
# @param line [String] Raw log line from kubectl (with [pod/container] prefix)
|
|
@@ -33,10 +35,6 @@ module LanguageOperator
|
|
|
33
35
|
|
|
34
36
|
private
|
|
35
37
|
|
|
36
|
-
def pastel
|
|
37
|
-
@pastel ||= Pastel.new
|
|
38
|
-
end
|
|
39
|
-
|
|
40
38
|
# Parse the kubectl prefix from the log line
|
|
41
39
|
# Returns [prefix, content] or [nil, original_line]
|
|
42
40
|
def parse_kubectl_prefix(line)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'tty-spinner'
|
|
4
|
-
|
|
4
|
+
require_relative '../helpers/pastel_helper'
|
|
5
5
|
|
|
6
6
|
module LanguageOperator
|
|
7
7
|
module CLI
|
|
@@ -9,6 +9,8 @@ module LanguageOperator
|
|
|
9
9
|
# Beautiful progress output for CLI operations
|
|
10
10
|
class ProgressFormatter
|
|
11
11
|
class << self
|
|
12
|
+
include Helpers::PastelHelper
|
|
13
|
+
|
|
12
14
|
def with_spinner(message, success_msg: nil, &block)
|
|
13
15
|
spinner = TTY::Spinner.new("[:spinner] #{message}...", format: :dots, success_mark: pastel.green('✔'))
|
|
14
16
|
spinner.auto_spin
|
|
@@ -40,12 +42,6 @@ module LanguageOperator
|
|
|
40
42
|
def warn(message)
|
|
41
43
|
puts "[#{pastel.yellow('⚠')}] #{message}"
|
|
42
44
|
end
|
|
43
|
-
|
|
44
|
-
private
|
|
45
|
-
|
|
46
|
-
def pastel
|
|
47
|
-
@pastel ||= Pastel.new
|
|
48
|
-
end
|
|
49
45
|
end
|
|
50
46
|
end
|
|
51
47
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../helpers/pastel_helper'
|
|
4
|
+
|
|
5
|
+
module LanguageOperator
|
|
6
|
+
module CLI
|
|
7
|
+
module Formatters
|
|
8
|
+
# Unified formatter for status indicators across all commands
|
|
9
|
+
#
|
|
10
|
+
# Provides consistent colored status dots (●) for resource states
|
|
11
|
+
class StatusFormatter
|
|
12
|
+
extend Helpers::PastelHelper
|
|
13
|
+
|
|
14
|
+
# Format a status string with colored indicator
|
|
15
|
+
#
|
|
16
|
+
# @param status [String, Symbol] The status to format
|
|
17
|
+
# @return [String] Formatted status with colored dot
|
|
18
|
+
def self.format(status)
|
|
19
|
+
status_str = status.to_s
|
|
20
|
+
|
|
21
|
+
case status_str.downcase
|
|
22
|
+
when 'ready', 'running', 'active'
|
|
23
|
+
"#{pastel.green('●')} #{status_str}"
|
|
24
|
+
when 'pending', 'creating', 'synthesizing'
|
|
25
|
+
"#{pastel.yellow('●')} #{status_str}"
|
|
26
|
+
when 'failed', 'error'
|
|
27
|
+
"#{pastel.red('●')} #{status_str}"
|
|
28
|
+
when 'paused', 'stopped', 'suspended'
|
|
29
|
+
"#{pastel.dim('●')} #{status_str}"
|
|
30
|
+
else
|
|
31
|
+
"#{pastel.dim('●')} #{status_str}"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'tty-table'
|
|
4
|
-
|
|
4
|
+
require_relative '../helpers/pastel_helper'
|
|
5
|
+
require_relative 'status_formatter'
|
|
5
6
|
|
|
6
7
|
module LanguageOperator
|
|
7
8
|
module CLI
|
|
@@ -9,6 +10,8 @@ module LanguageOperator
|
|
|
9
10
|
# Table output for CLI list commands
|
|
10
11
|
class TableFormatter
|
|
11
12
|
class << self
|
|
13
|
+
include Helpers::PastelHelper
|
|
14
|
+
|
|
12
15
|
def clusters(clusters)
|
|
13
16
|
return ProgressFormatter.info('No clusters found') if clusters.empty?
|
|
14
17
|
|
|
@@ -20,7 +23,7 @@ module LanguageOperator
|
|
|
20
23
|
cluster[:agents] || 0,
|
|
21
24
|
cluster[:tools] || 0,
|
|
22
25
|
cluster[:models] || 0,
|
|
23
|
-
|
|
26
|
+
StatusFormatter.format(cluster[:status] || 'Unknown')
|
|
24
27
|
]
|
|
25
28
|
end
|
|
26
29
|
|
|
@@ -36,7 +39,7 @@ module LanguageOperator
|
|
|
36
39
|
[
|
|
37
40
|
agent[:name],
|
|
38
41
|
agent[:mode],
|
|
39
|
-
|
|
42
|
+
StatusFormatter.format(agent[:status]),
|
|
40
43
|
agent[:next_run] || 'N/A',
|
|
41
44
|
agent[:executions] || 0
|
|
42
45
|
]
|
|
@@ -58,7 +61,7 @@ module LanguageOperator
|
|
|
58
61
|
cluster_name,
|
|
59
62
|
agent[:name],
|
|
60
63
|
agent[:mode],
|
|
61
|
-
|
|
64
|
+
StatusFormatter.format(agent[:status]),
|
|
62
65
|
agent[:next_run] || 'N/A',
|
|
63
66
|
agent[:executions] || 0
|
|
64
67
|
]
|
|
@@ -77,7 +80,7 @@ module LanguageOperator
|
|
|
77
80
|
[
|
|
78
81
|
tool[:name],
|
|
79
82
|
tool[:type],
|
|
80
|
-
|
|
83
|
+
StatusFormatter.format(tool[:status]),
|
|
81
84
|
tool[:agents_using] || 0
|
|
82
85
|
]
|
|
83
86
|
end
|
|
@@ -112,7 +115,7 @@ module LanguageOperator
|
|
|
112
115
|
model[:name],
|
|
113
116
|
model[:provider],
|
|
114
117
|
model[:model],
|
|
115
|
-
|
|
118
|
+
StatusFormatter.format(model[:status])
|
|
116
119
|
]
|
|
117
120
|
end
|
|
118
121
|
|
|
@@ -133,7 +136,7 @@ module LanguageOperator
|
|
|
133
136
|
cluster[:agents] || 0,
|
|
134
137
|
cluster[:tools] || 0,
|
|
135
138
|
cluster[:models] || 0,
|
|
136
|
-
|
|
139
|
+
StatusFormatter.format(cluster[:status] || 'Unknown')
|
|
137
140
|
]
|
|
138
141
|
end
|
|
139
142
|
|
|
@@ -148,30 +151,11 @@ module LanguageOperator
|
|
|
148
151
|
|
|
149
152
|
private
|
|
150
153
|
|
|
151
|
-
def status_indicator(status)
|
|
152
|
-
case status&.downcase
|
|
153
|
-
when 'ready', 'running', 'active'
|
|
154
|
-
"#{pastel.green('●')} #{status}"
|
|
155
|
-
when 'pending', 'creating', 'synthesizing'
|
|
156
|
-
"#{pastel.yellow('●')} #{status}"
|
|
157
|
-
when 'failed', 'error'
|
|
158
|
-
"#{pastel.red('●')} #{status}"
|
|
159
|
-
when 'paused', 'stopped'
|
|
160
|
-
"#{pastel.dim('●')} #{status}"
|
|
161
|
-
else
|
|
162
|
-
"#{pastel.dim('●')} #{status || 'Unknown'}"
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
|
|
166
154
|
def truncate(text, length)
|
|
167
155
|
return text if text.nil? || text.length <= length
|
|
168
156
|
|
|
169
157
|
"#{text[0...(length - 3)]}..."
|
|
170
158
|
end
|
|
171
|
-
|
|
172
|
-
def pastel
|
|
173
|
-
@pastel ||= Pastel.new
|
|
174
|
-
end
|
|
175
159
|
end
|
|
176
160
|
end
|
|
177
161
|
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pastel'
|
|
4
|
+
|
|
5
|
+
module LanguageOperator
|
|
6
|
+
module CLI
|
|
7
|
+
module Helpers
|
|
8
|
+
# Shared module providing Pastel color functionality
|
|
9
|
+
# to CLI commands, formatters, and helpers.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# include PastelHelper
|
|
13
|
+
# puts pastel.green("Success!")
|
|
14
|
+
module PastelHelper
|
|
15
|
+
# Returns a memoized Pastel instance for colorizing terminal output
|
|
16
|
+
#
|
|
17
|
+
# @return [Pastel] Pastel instance
|
|
18
|
+
def pastel
|
|
19
|
+
@pastel ||= Pastel.new
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
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
|
|
@@ -11,6 +11,7 @@ require_relative 'commands/tool'
|
|
|
11
11
|
require_relative 'commands/model'
|
|
12
12
|
require_relative 'commands/quickstart'
|
|
13
13
|
require_relative 'commands/install'
|
|
14
|
+
require_relative 'commands/system'
|
|
14
15
|
require_relative 'formatters/progress_formatter'
|
|
15
16
|
require_relative '../config/cluster_config'
|
|
16
17
|
require_relative '../kubernetes/client'
|
|
@@ -88,6 +89,9 @@ module LanguageOperator
|
|
|
88
89
|
desc 'model SUBCOMMAND ...ARGS', 'Manage language models'
|
|
89
90
|
subcommand 'model', Commands::Model
|
|
90
91
|
|
|
92
|
+
desc 'system SUBCOMMAND ...ARGS', 'System commands for schema and metadata'
|
|
93
|
+
subcommand 'system', Commands::System
|
|
94
|
+
|
|
91
95
|
desc 'quickstart', 'Interactive setup wizard for first-time users'
|
|
92
96
|
def quickstart
|
|
93
97
|
Commands::Quickstart.new.invoke(:start)
|
|
@@ -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
|