language-operator 0.0.1 → 0.1.30
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 +125 -0
- data/CHANGELOG.md +53 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +284 -0
- data/LICENSE +229 -21
- data/Makefile +77 -0
- data/README.md +3 -11
- data/Rakefile +34 -0
- data/bin/aictl +7 -0
- data/completions/_aictl +232 -0
- data/completions/aictl.bash +121 -0
- data/completions/aictl.fish +114 -0
- data/docs/architecture/agent-runtime.md +585 -0
- data/docs/dsl/agent-reference.md +591 -0
- data/docs/dsl/best-practices.md +1078 -0
- data/docs/dsl/chat-endpoints.md +895 -0
- data/docs/dsl/constraints.md +671 -0
- data/docs/dsl/mcp-integration.md +1177 -0
- data/docs/dsl/webhooks.md +932 -0
- data/docs/dsl/workflows.md +744 -0
- data/examples/README.md +569 -0
- data/examples/agent_example.rb +86 -0
- data/examples/chat_endpoint_agent.rb +118 -0
- data/examples/github_webhook_agent.rb +171 -0
- data/examples/mcp_agent.rb +158 -0
- data/examples/oauth_callback_agent.rb +296 -0
- data/examples/stripe_webhook_agent.rb +219 -0
- data/examples/webhook_agent.rb +80 -0
- data/lib/language_operator/agent/base.rb +110 -0
- data/lib/language_operator/agent/executor.rb +440 -0
- data/lib/language_operator/agent/instrumentation.rb +54 -0
- data/lib/language_operator/agent/metrics_tracker.rb +183 -0
- data/lib/language_operator/agent/safety/ast_validator.rb +272 -0
- data/lib/language_operator/agent/safety/audit_logger.rb +104 -0
- data/lib/language_operator/agent/safety/budget_tracker.rb +175 -0
- data/lib/language_operator/agent/safety/content_filter.rb +93 -0
- data/lib/language_operator/agent/safety/manager.rb +207 -0
- data/lib/language_operator/agent/safety/rate_limiter.rb +150 -0
- data/lib/language_operator/agent/safety/safe_executor.rb +115 -0
- data/lib/language_operator/agent/scheduler.rb +183 -0
- data/lib/language_operator/agent/telemetry.rb +116 -0
- data/lib/language_operator/agent/web_server.rb +610 -0
- data/lib/language_operator/agent/webhook_authenticator.rb +226 -0
- data/lib/language_operator/agent.rb +149 -0
- data/lib/language_operator/cli/commands/agent.rb +1252 -0
- data/lib/language_operator/cli/commands/cluster.rb +335 -0
- data/lib/language_operator/cli/commands/install.rb +404 -0
- data/lib/language_operator/cli/commands/model.rb +266 -0
- data/lib/language_operator/cli/commands/persona.rb +396 -0
- data/lib/language_operator/cli/commands/quickstart.rb +22 -0
- data/lib/language_operator/cli/commands/status.rb +156 -0
- data/lib/language_operator/cli/commands/tool.rb +537 -0
- data/lib/language_operator/cli/commands/use.rb +47 -0
- data/lib/language_operator/cli/errors/handler.rb +180 -0
- data/lib/language_operator/cli/errors/suggestions.rb +176 -0
- data/lib/language_operator/cli/formatters/code_formatter.rb +81 -0
- data/lib/language_operator/cli/formatters/log_formatter.rb +290 -0
- data/lib/language_operator/cli/formatters/progress_formatter.rb +53 -0
- data/lib/language_operator/cli/formatters/table_formatter.rb +179 -0
- data/lib/language_operator/cli/formatters/value_formatter.rb +113 -0
- data/lib/language_operator/cli/helpers/cluster_context.rb +62 -0
- data/lib/language_operator/cli/helpers/cluster_validator.rb +101 -0
- data/lib/language_operator/cli/helpers/editor_helper.rb +58 -0
- data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +167 -0
- data/lib/language_operator/cli/helpers/resource_dependency_checker.rb +74 -0
- data/lib/language_operator/cli/helpers/schedule_builder.rb +108 -0
- data/lib/language_operator/cli/helpers/user_prompts.rb +69 -0
- data/lib/language_operator/cli/main.rb +232 -0
- data/lib/language_operator/cli/templates/tools/generic.yaml +66 -0
- data/lib/language_operator/cli/wizards/agent_wizard.rb +246 -0
- data/lib/language_operator/cli/wizards/quickstart_wizard.rb +588 -0
- data/lib/language_operator/client/base.rb +214 -0
- data/lib/language_operator/client/config.rb +136 -0
- data/lib/language_operator/client/cost_calculator.rb +37 -0
- data/lib/language_operator/client/mcp_connector.rb +123 -0
- data/lib/language_operator/client.rb +19 -0
- data/lib/language_operator/config/cluster_config.rb +101 -0
- data/lib/language_operator/config/tool_patterns.yaml +57 -0
- data/lib/language_operator/config/tool_registry.rb +96 -0
- data/lib/language_operator/config.rb +138 -0
- data/lib/language_operator/dsl/adapter.rb +124 -0
- data/lib/language_operator/dsl/agent_context.rb +90 -0
- data/lib/language_operator/dsl/agent_definition.rb +427 -0
- data/lib/language_operator/dsl/chat_endpoint_definition.rb +115 -0
- data/lib/language_operator/dsl/config.rb +119 -0
- data/lib/language_operator/dsl/context.rb +50 -0
- data/lib/language_operator/dsl/execution_context.rb +47 -0
- data/lib/language_operator/dsl/helpers.rb +109 -0
- data/lib/language_operator/dsl/http.rb +184 -0
- data/lib/language_operator/dsl/mcp_server_definition.rb +73 -0
- data/lib/language_operator/dsl/parameter_definition.rb +124 -0
- data/lib/language_operator/dsl/registry.rb +36 -0
- data/lib/language_operator/dsl/shell.rb +125 -0
- data/lib/language_operator/dsl/tool_definition.rb +112 -0
- data/lib/language_operator/dsl/webhook_authentication.rb +114 -0
- data/lib/language_operator/dsl/webhook_definition.rb +106 -0
- data/lib/language_operator/dsl/workflow_definition.rb +259 -0
- data/lib/language_operator/dsl.rb +160 -0
- data/lib/language_operator/errors.rb +60 -0
- data/lib/language_operator/kubernetes/client.rb +279 -0
- data/lib/language_operator/kubernetes/resource_builder.rb +194 -0
- data/lib/language_operator/loggable.rb +47 -0
- data/lib/language_operator/logger.rb +141 -0
- data/lib/language_operator/retry.rb +123 -0
- data/lib/language_operator/retryable.rb +132 -0
- data/lib/language_operator/tool_loader.rb +242 -0
- data/lib/language_operator/validators.rb +170 -0
- data/lib/language_operator/version.rb +1 -1
- data/lib/language_operator.rb +65 -3
- data/requirements/tasks/challenge.md +9 -0
- data/requirements/tasks/iterate.md +36 -0
- data/requirements/tasks/optimize.md +21 -0
- data/requirements/tasks/tag.md +5 -0
- data/test_agent_dsl.rb +108 -0
- metadata +503 -20
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'version'
|
|
4
|
+
require_relative 'dsl/tool_definition'
|
|
5
|
+
require_relative 'dsl/parameter_definition'
|
|
6
|
+
require_relative 'dsl/registry'
|
|
7
|
+
require_relative 'dsl/adapter'
|
|
8
|
+
require_relative 'dsl/config'
|
|
9
|
+
require_relative 'dsl/helpers'
|
|
10
|
+
require_relative 'dsl/http'
|
|
11
|
+
require_relative 'dsl/shell'
|
|
12
|
+
require_relative 'dsl/context'
|
|
13
|
+
require_relative 'dsl/execution_context'
|
|
14
|
+
require_relative 'dsl/agent_definition'
|
|
15
|
+
require_relative 'dsl/agent_context'
|
|
16
|
+
require_relative 'dsl/workflow_definition'
|
|
17
|
+
require_relative 'agent/safety/ast_validator'
|
|
18
|
+
require_relative 'agent/safety/safe_executor'
|
|
19
|
+
|
|
20
|
+
module LanguageOperator
|
|
21
|
+
# DSL for defining MCP tools and autonomous agents
|
|
22
|
+
#
|
|
23
|
+
# Provides a clean, Ruby-like DSL for defining tools that can be served
|
|
24
|
+
# via the Model Context Protocol (MCP) and agents that can execute autonomously.
|
|
25
|
+
#
|
|
26
|
+
# @example Define a tool
|
|
27
|
+
# LanguageOperator::Dsl.define do
|
|
28
|
+
# tool "greet" do
|
|
29
|
+
# description "Greet a user by name"
|
|
30
|
+
#
|
|
31
|
+
# parameter :name do
|
|
32
|
+
# type :string
|
|
33
|
+
# required true
|
|
34
|
+
# description "Name to greet"
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
# execute do |params|
|
|
38
|
+
# "Hello, #{params['name']}!"
|
|
39
|
+
# end
|
|
40
|
+
# end
|
|
41
|
+
# end
|
|
42
|
+
#
|
|
43
|
+
# @example Access tools
|
|
44
|
+
# registry = LanguageOperator::Dsl.registry
|
|
45
|
+
# tool = registry.get("greet")
|
|
46
|
+
# result = tool.call({"name" => "Alice"})
|
|
47
|
+
module Dsl
|
|
48
|
+
class << self
|
|
49
|
+
# Global registry for tools
|
|
50
|
+
#
|
|
51
|
+
# @return [Registry] The global tool registry
|
|
52
|
+
def registry
|
|
53
|
+
@registry ||= Registry.new
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Global registry for agents
|
|
57
|
+
#
|
|
58
|
+
# @return [AgentRegistry] The global agent registry
|
|
59
|
+
def agent_registry
|
|
60
|
+
@agent_registry ||= AgentRegistry.new
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Define tools using the DSL
|
|
64
|
+
#
|
|
65
|
+
# @yield Block containing tool definitions
|
|
66
|
+
# @return [Registry] The global registry with defined tools
|
|
67
|
+
#
|
|
68
|
+
# @example
|
|
69
|
+
# LanguageOperator::Dsl.define do
|
|
70
|
+
# tool "example" do
|
|
71
|
+
# # ...
|
|
72
|
+
# end
|
|
73
|
+
# end
|
|
74
|
+
def define(&)
|
|
75
|
+
context = Context.new(registry)
|
|
76
|
+
context.instance_eval(&)
|
|
77
|
+
registry
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Define agents using the DSL
|
|
81
|
+
#
|
|
82
|
+
# @yield Block containing agent definitions
|
|
83
|
+
# @return [AgentRegistry] The global agent registry
|
|
84
|
+
#
|
|
85
|
+
# @example
|
|
86
|
+
# LanguageOperator::Dsl.define_agents do
|
|
87
|
+
# agent "news-summarizer" do
|
|
88
|
+
# # ...
|
|
89
|
+
# end
|
|
90
|
+
# end
|
|
91
|
+
def define_agents(&)
|
|
92
|
+
context = AgentContext.new(agent_registry)
|
|
93
|
+
context.instance_eval(&)
|
|
94
|
+
agent_registry
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Load tools from a file
|
|
98
|
+
#
|
|
99
|
+
# @param file_path [String] Path to the tool definition file
|
|
100
|
+
# @return [Registry] The global registry with loaded tools
|
|
101
|
+
#
|
|
102
|
+
# @example
|
|
103
|
+
# LanguageOperator::Dsl.load_file("mcp/tools.rb")
|
|
104
|
+
def load_file(file_path)
|
|
105
|
+
code = File.read(file_path)
|
|
106
|
+
context = Context.new(registry)
|
|
107
|
+
|
|
108
|
+
# Execute in sandbox with validation
|
|
109
|
+
executor = Agent::Safety::SafeExecutor.new(context)
|
|
110
|
+
executor.eval(code, file_path)
|
|
111
|
+
|
|
112
|
+
registry
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Load agents from a file
|
|
116
|
+
#
|
|
117
|
+
# @param file_path [String] Path to the agent definition file
|
|
118
|
+
# @return [AgentRegistry] The global agent registry
|
|
119
|
+
#
|
|
120
|
+
# @example
|
|
121
|
+
# LanguageOperator::Dsl.load_agent_file("agents/news-summarizer.rb")
|
|
122
|
+
def load_agent_file(file_path)
|
|
123
|
+
code = File.read(file_path)
|
|
124
|
+
context = AgentContext.new(agent_registry)
|
|
125
|
+
|
|
126
|
+
# Execute in sandbox with validation
|
|
127
|
+
executor = Agent::Safety::SafeExecutor.new(context)
|
|
128
|
+
executor.eval(code, file_path)
|
|
129
|
+
|
|
130
|
+
agent_registry
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Clear all defined tools
|
|
134
|
+
#
|
|
135
|
+
# @return [void]
|
|
136
|
+
def clear!
|
|
137
|
+
registry.clear
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Clear all defined agents
|
|
141
|
+
#
|
|
142
|
+
# @return [void]
|
|
143
|
+
def clear_agents!
|
|
144
|
+
agent_registry.clear
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Create an MCP server from the defined tools
|
|
148
|
+
#
|
|
149
|
+
# @param server_name [String] Name of the MCP server
|
|
150
|
+
# @param server_context [Hash] Additional context for the server
|
|
151
|
+
# @return [MCP::Server] The MCP server instance
|
|
152
|
+
#
|
|
153
|
+
# @example
|
|
154
|
+
# server = LanguageOperator::Dsl.create_server(server_name: "my-tools")
|
|
155
|
+
def create_server(server_name: 'langop-tools', server_context: {})
|
|
156
|
+
Adapter.create_mcp_server(registry, server_name: server_name, server_context: server_context)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LanguageOperator
|
|
4
|
+
# Standardized error formatting module for consistent error messages across tools
|
|
5
|
+
module Errors
|
|
6
|
+
# Resource not found error
|
|
7
|
+
# @param resource_type [String] Type of resource (e.g., "Pod", "LanguageAgent")
|
|
8
|
+
# @param identifier [String] Resource identifier (name, ID, etc.)
|
|
9
|
+
# @return [String] Formatted error message
|
|
10
|
+
def self.not_found(resource_type, identifier)
|
|
11
|
+
"Error: #{resource_type} not found - #{identifier}"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Access denied error
|
|
15
|
+
# @param context [String] Additional context (default: "check RBAC permissions")
|
|
16
|
+
# @return [String] Formatted error message
|
|
17
|
+
def self.access_denied(context = 'check RBAC permissions')
|
|
18
|
+
"Error: Access denied - #{context}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Invalid JSON parameter error
|
|
22
|
+
# @param param_name [String] Name of the parameter
|
|
23
|
+
# @return [String] Formatted error message
|
|
24
|
+
def self.invalid_json(param_name)
|
|
25
|
+
"Error: Invalid JSON in #{param_name} parameter"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Missing configuration error
|
|
29
|
+
# @param missing_vars [String, Array<String>] Missing variable(s)
|
|
30
|
+
# @return [String] Formatted error message
|
|
31
|
+
def self.missing_config(missing_vars)
|
|
32
|
+
vars = Array(missing_vars).join(', ')
|
|
33
|
+
"Error: Missing configuration: #{vars}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Invalid parameter value error
|
|
37
|
+
# @param param_name [String] Parameter name
|
|
38
|
+
# @param value [String] Invalid value
|
|
39
|
+
# @param expected [String] Expected format/value
|
|
40
|
+
# @return [String] Formatted error message
|
|
41
|
+
def self.invalid_parameter(param_name, value, expected)
|
|
42
|
+
"Error: Invalid #{param_name} '#{value}'. Expected: #{expected}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Generic operation failed error
|
|
46
|
+
# @param operation [String] Operation that failed
|
|
47
|
+
# @param reason [String] Reason for failure
|
|
48
|
+
# @return [String] Formatted error message
|
|
49
|
+
def self.operation_failed(operation, reason)
|
|
50
|
+
"Error: #{operation} failed - #{reason}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Empty/missing required field error
|
|
54
|
+
# @param field_name [String] Name of the field
|
|
55
|
+
# @return [String] Formatted error message
|
|
56
|
+
def self.empty_field(field_name)
|
|
57
|
+
"Error: #{field_name} cannot be empty"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'k8s-ruby'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
|
|
6
|
+
module LanguageOperator
|
|
7
|
+
module Kubernetes
|
|
8
|
+
# Kubernetes client wrapper for interacting with language-operator resources
|
|
9
|
+
class Client
|
|
10
|
+
attr_reader :client
|
|
11
|
+
|
|
12
|
+
# Get singleton K8s client instance with automatic config detection
|
|
13
|
+
# @return [LanguageOperator::Kubernetes::Client] Client instance
|
|
14
|
+
# @raise [RuntimeError] if client initialization fails
|
|
15
|
+
def self.instance
|
|
16
|
+
@instance ||= build_singleton
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Reset the singleton (useful for testing)
|
|
20
|
+
# @return [nil]
|
|
21
|
+
def self.reset!
|
|
22
|
+
@instance = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Check if running inside a Kubernetes cluster
|
|
26
|
+
# @return [Boolean] True if in-cluster, false otherwise
|
|
27
|
+
def self.in_cluster?
|
|
28
|
+
File.exist?('/var/run/secrets/kubernetes.io/serviceaccount/token')
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(kubeconfig: nil, context: nil, in_cluster: false)
|
|
32
|
+
@in_cluster = in_cluster
|
|
33
|
+
@kubeconfig = kubeconfig || ENV.fetch('KUBECONFIG', File.expand_path('~/.kube/config'))
|
|
34
|
+
@context = context
|
|
35
|
+
@client = build_client
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Get the current Kubernetes context name
|
|
39
|
+
def current_context
|
|
40
|
+
return nil if @in_cluster
|
|
41
|
+
|
|
42
|
+
config = K8s::Config.load_file(@kubeconfig)
|
|
43
|
+
@context || config.current_context
|
|
44
|
+
rescue Errno::ENOENT
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Get the current namespace from the context
|
|
49
|
+
def current_namespace
|
|
50
|
+
if @in_cluster
|
|
51
|
+
# In-cluster: read from service account namespace
|
|
52
|
+
File.read('/var/run/secrets/kubernetes.io/serviceaccount/namespace').strip
|
|
53
|
+
else
|
|
54
|
+
config = K8s::Config.load_file(@kubeconfig)
|
|
55
|
+
context_name = current_context
|
|
56
|
+
context_obj = config.context(context_name)
|
|
57
|
+
context_obj&.namespace
|
|
58
|
+
end
|
|
59
|
+
rescue Errno::ENOENT
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Create or update a Kubernetes resource
|
|
64
|
+
def apply_resource(resource)
|
|
65
|
+
namespace = resource.dig('metadata', 'namespace')
|
|
66
|
+
name = resource.dig('metadata', 'name')
|
|
67
|
+
kind = resource['kind']
|
|
68
|
+
api_version = resource['apiVersion']
|
|
69
|
+
|
|
70
|
+
begin
|
|
71
|
+
# Try to get existing resource
|
|
72
|
+
existing = get_resource(kind, name, namespace, api_version)
|
|
73
|
+
if existing
|
|
74
|
+
# Merge existing metadata (especially resourceVersion) with new resource
|
|
75
|
+
merged_resource = if resource.is_a?(Hash)
|
|
76
|
+
resource.dup
|
|
77
|
+
else
|
|
78
|
+
resource.to_h
|
|
79
|
+
end
|
|
80
|
+
merged_resource['metadata'] ||= {}
|
|
81
|
+
merged_resource['metadata']['resourceVersion'] = existing.metadata.resourceVersion
|
|
82
|
+
merged_resource['metadata']['uid'] = existing.metadata.uid if existing.metadata.uid
|
|
83
|
+
|
|
84
|
+
# Update existing resource
|
|
85
|
+
update_resource(kind, name, namespace, merged_resource, api_version)
|
|
86
|
+
else
|
|
87
|
+
# Create new resource
|
|
88
|
+
create_resource(resource)
|
|
89
|
+
end
|
|
90
|
+
rescue K8s::Error::NotFound
|
|
91
|
+
# Resource doesn't exist, create it
|
|
92
|
+
create_resource(resource)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Create a resource
|
|
97
|
+
def create_resource(resource)
|
|
98
|
+
resource_client = resource_client_for_resource(resource)
|
|
99
|
+
# Convert hash to K8s::Resource if needed
|
|
100
|
+
k8s_resource = resource.is_a?(K8s::Resource) ? resource : K8s::Resource.new(resource)
|
|
101
|
+
resource_client.create_resource(k8s_resource)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Update a resource
|
|
105
|
+
def update_resource(kind, _name, namespace, resource, api_version)
|
|
106
|
+
resource_client = resource_client_for(kind, namespace, api_version)
|
|
107
|
+
# Convert hash to K8s::Resource if needed
|
|
108
|
+
k8s_resource = resource.is_a?(K8s::Resource) ? resource : K8s::Resource.new(resource)
|
|
109
|
+
resource_client.update_resource(k8s_resource)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Get a resource
|
|
113
|
+
def get_resource(kind, name, namespace = nil, api_version = nil)
|
|
114
|
+
resource_client = resource_client_for(kind, namespace, api_version || default_api_version(kind))
|
|
115
|
+
resource_client.get(name)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# List resources
|
|
119
|
+
def list_resources(kind, namespace: nil, api_version: nil, label_selector: nil)
|
|
120
|
+
resource_client = resource_client_for(kind, namespace, api_version || default_api_version(kind))
|
|
121
|
+
opts = {}
|
|
122
|
+
opts[:labelSelector] = label_selector if label_selector
|
|
123
|
+
|
|
124
|
+
resource_client.list(**opts)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Delete a resource
|
|
128
|
+
def delete_resource(kind, name, namespace = nil, api_version = nil)
|
|
129
|
+
resource_client = resource_client_for(kind, namespace, api_version || default_api_version(kind))
|
|
130
|
+
resource_client.delete(name)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Check if namespace exists
|
|
134
|
+
def namespace_exists?(name)
|
|
135
|
+
@client.api('v1').resource('namespaces').get(name)
|
|
136
|
+
true
|
|
137
|
+
rescue K8s::Error::NotFound
|
|
138
|
+
false
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Create namespace
|
|
142
|
+
def create_namespace(name, labels: {})
|
|
143
|
+
resource = {
|
|
144
|
+
'apiVersion' => 'v1',
|
|
145
|
+
'kind' => 'Namespace',
|
|
146
|
+
'metadata' => {
|
|
147
|
+
'name' => name,
|
|
148
|
+
'labels' => labels
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
create_resource(resource)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Check if operator is installed
|
|
155
|
+
def operator_installed?
|
|
156
|
+
# Check if LanguageCluster CRD exists
|
|
157
|
+
@client.apis(prefetch_resources: true)
|
|
158
|
+
.find { |api| api.api_version == 'langop.io/v1alpha1' }
|
|
159
|
+
rescue StandardError
|
|
160
|
+
false
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Get operator version
|
|
164
|
+
def operator_version
|
|
165
|
+
deployment = @client.api('apps/v1')
|
|
166
|
+
.resource('deployments', namespace: 'kube-system')
|
|
167
|
+
.get('language-operator')
|
|
168
|
+
deployment.dig('metadata', 'labels', 'app.kubernetes.io/version') || 'unknown'
|
|
169
|
+
rescue K8s::Error::NotFound
|
|
170
|
+
nil
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
private
|
|
174
|
+
|
|
175
|
+
# Build singleton instance with automatic config detection
|
|
176
|
+
def self.build_singleton
|
|
177
|
+
if in_cluster?
|
|
178
|
+
new(in_cluster: true)
|
|
179
|
+
else
|
|
180
|
+
new
|
|
181
|
+
end
|
|
182
|
+
rescue StandardError => e
|
|
183
|
+
raise "Failed to initialize Kubernetes client: #{e.message}"
|
|
184
|
+
end
|
|
185
|
+
private_class_method :build_singleton
|
|
186
|
+
|
|
187
|
+
def build_client
|
|
188
|
+
if @in_cluster
|
|
189
|
+
K8s::Client.in_cluster_config
|
|
190
|
+
else
|
|
191
|
+
config = K8s::Config.load_file(@kubeconfig)
|
|
192
|
+
if @context
|
|
193
|
+
# Set the current-context to the specified context
|
|
194
|
+
config_hash = config.to_h
|
|
195
|
+
config_hash['current-context'] = @context
|
|
196
|
+
config = K8s::Config.new(**config_hash)
|
|
197
|
+
end
|
|
198
|
+
K8s::Client.config(config)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def resource_client_for_resource(resource)
|
|
203
|
+
kind = resource['kind']
|
|
204
|
+
namespace = resource.dig('metadata', 'namespace')
|
|
205
|
+
api_version = resource['apiVersion']
|
|
206
|
+
resource_client_for(kind, namespace, api_version)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def resource_client_for(kind, namespace, api_version)
|
|
210
|
+
api_client = api_for_version(api_version)
|
|
211
|
+
resource_name = kind_to_resource_name(kind)
|
|
212
|
+
if namespace
|
|
213
|
+
api_client.resource(resource_name, namespace: namespace)
|
|
214
|
+
else
|
|
215
|
+
api_client.resource(resource_name)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def api_for_version(api_version)
|
|
220
|
+
if api_version.include?('/')
|
|
221
|
+
group, version = api_version.split('/', 2)
|
|
222
|
+
@client.api("#{group}/#{version}")
|
|
223
|
+
else
|
|
224
|
+
@client.api(api_version)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def kind_to_resource_name(kind)
|
|
229
|
+
# Convert Kind (singular, capitalized) to resource name (plural, lowercase)
|
|
230
|
+
case kind.downcase
|
|
231
|
+
when 'languagecluster'
|
|
232
|
+
'languageclusters'
|
|
233
|
+
when 'languageagent'
|
|
234
|
+
'languageagents'
|
|
235
|
+
when 'languagetool'
|
|
236
|
+
'languagetools'
|
|
237
|
+
when 'languagemodel'
|
|
238
|
+
'languagemodels'
|
|
239
|
+
when 'languageclient'
|
|
240
|
+
'languageclients'
|
|
241
|
+
when 'languagepersona'
|
|
242
|
+
'languagepersonas'
|
|
243
|
+
when 'namespace'
|
|
244
|
+
'namespaces'
|
|
245
|
+
when 'configmap'
|
|
246
|
+
'configmaps'
|
|
247
|
+
when 'secret'
|
|
248
|
+
'secrets'
|
|
249
|
+
when 'service'
|
|
250
|
+
'services'
|
|
251
|
+
when 'deployment'
|
|
252
|
+
'deployments'
|
|
253
|
+
when 'statefulset'
|
|
254
|
+
'statefulsets'
|
|
255
|
+
when 'cronjob'
|
|
256
|
+
'cronjobs'
|
|
257
|
+
else
|
|
258
|
+
# Generic pluralization - add 's'
|
|
259
|
+
"#{kind.downcase}s"
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def default_api_version(kind)
|
|
264
|
+
case kind.downcase
|
|
265
|
+
when 'languagecluster', 'languageagent', 'languagetool', 'languagemodel', 'languageclient', 'languagepersona'
|
|
266
|
+
'langop.io/v1alpha1'
|
|
267
|
+
when 'namespace', 'configmap', 'secret', 'service'
|
|
268
|
+
'v1'
|
|
269
|
+
when 'deployment', 'statefulset'
|
|
270
|
+
'apps/v1'
|
|
271
|
+
when 'cronjob'
|
|
272
|
+
'batch/v1'
|
|
273
|
+
else
|
|
274
|
+
'v1'
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LanguageOperator
|
|
4
|
+
module Kubernetes
|
|
5
|
+
# Builds Kubernetes resource manifests for language-operator
|
|
6
|
+
class ResourceBuilder
|
|
7
|
+
class << self
|
|
8
|
+
# Build a LanguageCluster resource
|
|
9
|
+
def language_cluster(name, namespace: nil, labels: {})
|
|
10
|
+
{
|
|
11
|
+
'apiVersion' => 'langop.io/v1alpha1',
|
|
12
|
+
'kind' => 'LanguageCluster',
|
|
13
|
+
'metadata' => {
|
|
14
|
+
'name' => name,
|
|
15
|
+
'namespace' => namespace || 'default',
|
|
16
|
+
'labels' => default_labels.merge(labels)
|
|
17
|
+
},
|
|
18
|
+
'spec' => {
|
|
19
|
+
'namespace' => namespace || name,
|
|
20
|
+
'resourceQuota' => default_resource_quota,
|
|
21
|
+
'networkPolicy' => default_network_policy
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Build a LanguageAgent resource
|
|
27
|
+
def language_agent(name, instructions:, cluster: nil, schedule: nil, persona: nil, tools: [], models: [],
|
|
28
|
+
mode: nil, labels: {})
|
|
29
|
+
# Determine mode: reactive, scheduled, or autonomous
|
|
30
|
+
spec_mode = mode || (schedule ? 'scheduled' : 'autonomous')
|
|
31
|
+
|
|
32
|
+
spec = {
|
|
33
|
+
'instructions' => instructions,
|
|
34
|
+
'mode' => spec_mode,
|
|
35
|
+
'image' => 'ghcr.io/language-operator/agent:latest'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
spec['schedule'] = schedule if schedule
|
|
39
|
+
spec['persona'] = persona if persona
|
|
40
|
+
spec['tools'] = tools unless tools.empty?
|
|
41
|
+
# Convert model names to modelRef objects
|
|
42
|
+
spec['modelRefs'] = models.map { |m| { 'name' => m } } unless models.empty?
|
|
43
|
+
|
|
44
|
+
{
|
|
45
|
+
'apiVersion' => 'langop.io/v1alpha1',
|
|
46
|
+
'kind' => 'LanguageAgent',
|
|
47
|
+
'metadata' => {
|
|
48
|
+
'name' => name,
|
|
49
|
+
'namespace' => cluster || 'default',
|
|
50
|
+
'labels' => default_labels.merge(labels)
|
|
51
|
+
},
|
|
52
|
+
'spec' => spec
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Build a LanguageTool resource
|
|
57
|
+
def language_tool(name, type:, config: {}, cluster: nil, labels: {})
|
|
58
|
+
{
|
|
59
|
+
'apiVersion' => 'langop.io/v1alpha1',
|
|
60
|
+
'kind' => 'LanguageTool',
|
|
61
|
+
'metadata' => {
|
|
62
|
+
'name' => name,
|
|
63
|
+
'namespace' => cluster || 'default',
|
|
64
|
+
'labels' => default_labels.merge(labels)
|
|
65
|
+
},
|
|
66
|
+
'spec' => {
|
|
67
|
+
'type' => type,
|
|
68
|
+
'config' => config
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Build a LanguageModel resource
|
|
74
|
+
def language_model(name, provider:, model:, endpoint: nil, cluster: nil, labels: {})
|
|
75
|
+
spec = {
|
|
76
|
+
'provider' => provider,
|
|
77
|
+
'modelName' => model
|
|
78
|
+
}
|
|
79
|
+
spec['endpoint'] = endpoint if endpoint
|
|
80
|
+
|
|
81
|
+
{
|
|
82
|
+
'apiVersion' => 'langop.io/v1alpha1',
|
|
83
|
+
'kind' => 'LanguageModel',
|
|
84
|
+
'metadata' => {
|
|
85
|
+
'name' => name,
|
|
86
|
+
'namespace' => cluster || 'default',
|
|
87
|
+
'labels' => default_labels.merge(labels)
|
|
88
|
+
},
|
|
89
|
+
'spec' => spec
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Build a LanguagePersona resource
|
|
94
|
+
def language_persona(name, description:, tone:, system_prompt:, cluster: nil, labels: {})
|
|
95
|
+
{
|
|
96
|
+
'apiVersion' => 'langop.io/v1alpha1',
|
|
97
|
+
'kind' => 'LanguagePersona',
|
|
98
|
+
'metadata' => {
|
|
99
|
+
'name' => name,
|
|
100
|
+
'namespace' => cluster || 'default',
|
|
101
|
+
'labels' => default_labels.merge(labels)
|
|
102
|
+
},
|
|
103
|
+
'spec' => {
|
|
104
|
+
'displayName' => name.split('-').map(&:capitalize).join(' '),
|
|
105
|
+
'description' => description,
|
|
106
|
+
'tone' => tone,
|
|
107
|
+
'systemPrompt' => system_prompt
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Build a LanguagePersona resource with full spec control
|
|
113
|
+
def build_persona(name:, spec:, namespace: nil, labels: {})
|
|
114
|
+
{
|
|
115
|
+
'apiVersion' => 'langop.io/v1alpha1',
|
|
116
|
+
'kind' => 'LanguagePersona',
|
|
117
|
+
'metadata' => {
|
|
118
|
+
'name' => name,
|
|
119
|
+
'namespace' => namespace || 'default',
|
|
120
|
+
'labels' => default_labels.merge(labels)
|
|
121
|
+
},
|
|
122
|
+
'spec' => spec
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Build a Kubernetes Service resource for a reactive agent
|
|
127
|
+
#
|
|
128
|
+
# @param agent_name [String] Name of the agent
|
|
129
|
+
# @param namespace [String] Kubernetes namespace
|
|
130
|
+
# @param port [Integer] Service port (default: 8080)
|
|
131
|
+
# @param labels [Hash] Additional labels
|
|
132
|
+
# @return [Hash] Service manifest
|
|
133
|
+
def agent_service(agent_name, namespace: nil, port: 8080, labels: {})
|
|
134
|
+
{
|
|
135
|
+
'apiVersion' => 'v1',
|
|
136
|
+
'kind' => 'Service',
|
|
137
|
+
'metadata' => {
|
|
138
|
+
'name' => agent_name,
|
|
139
|
+
'namespace' => namespace || 'default',
|
|
140
|
+
'labels' => default_labels.merge(
|
|
141
|
+
'app.kubernetes.io/name' => agent_name,
|
|
142
|
+
'app.kubernetes.io/component' => 'agent'
|
|
143
|
+
).merge(labels)
|
|
144
|
+
},
|
|
145
|
+
'spec' => {
|
|
146
|
+
'type' => 'ClusterIP',
|
|
147
|
+
'selector' => {
|
|
148
|
+
'app.kubernetes.io/name' => agent_name,
|
|
149
|
+
'app.kubernetes.io/component' => 'agent'
|
|
150
|
+
},
|
|
151
|
+
'ports' => [
|
|
152
|
+
{
|
|
153
|
+
'name' => 'http',
|
|
154
|
+
'protocol' => 'TCP',
|
|
155
|
+
'port' => port,
|
|
156
|
+
'targetPort' => port
|
|
157
|
+
}
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
def default_labels
|
|
166
|
+
{
|
|
167
|
+
'app.kubernetes.io/managed-by' => 'aictl',
|
|
168
|
+
'app.kubernetes.io/part-of' => 'language-operator'
|
|
169
|
+
}
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def default_resource_quota
|
|
173
|
+
{
|
|
174
|
+
'hard' => {
|
|
175
|
+
'requests.cpu' => '4',
|
|
176
|
+
'requests.memory' => '8Gi',
|
|
177
|
+
'limits.cpu' => '8',
|
|
178
|
+
'limits.memory' => '16Gi'
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def default_network_policy
|
|
184
|
+
{
|
|
185
|
+
'egress' => {
|
|
186
|
+
'allowDNS' => ['8.8.8.8/32', '8.8.4.4/32'],
|
|
187
|
+
'allowHTTPS' => ['0.0.0.0/0']
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|