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.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +125 -0
  3. data/CHANGELOG.md +53 -0
  4. data/Gemfile +8 -0
  5. data/Gemfile.lock +284 -0
  6. data/LICENSE +229 -21
  7. data/Makefile +77 -0
  8. data/README.md +3 -11
  9. data/Rakefile +34 -0
  10. data/bin/aictl +7 -0
  11. data/completions/_aictl +232 -0
  12. data/completions/aictl.bash +121 -0
  13. data/completions/aictl.fish +114 -0
  14. data/docs/architecture/agent-runtime.md +585 -0
  15. data/docs/dsl/agent-reference.md +591 -0
  16. data/docs/dsl/best-practices.md +1078 -0
  17. data/docs/dsl/chat-endpoints.md +895 -0
  18. data/docs/dsl/constraints.md +671 -0
  19. data/docs/dsl/mcp-integration.md +1177 -0
  20. data/docs/dsl/webhooks.md +932 -0
  21. data/docs/dsl/workflows.md +744 -0
  22. data/examples/README.md +569 -0
  23. data/examples/agent_example.rb +86 -0
  24. data/examples/chat_endpoint_agent.rb +118 -0
  25. data/examples/github_webhook_agent.rb +171 -0
  26. data/examples/mcp_agent.rb +158 -0
  27. data/examples/oauth_callback_agent.rb +296 -0
  28. data/examples/stripe_webhook_agent.rb +219 -0
  29. data/examples/webhook_agent.rb +80 -0
  30. data/lib/language_operator/agent/base.rb +110 -0
  31. data/lib/language_operator/agent/executor.rb +440 -0
  32. data/lib/language_operator/agent/instrumentation.rb +54 -0
  33. data/lib/language_operator/agent/metrics_tracker.rb +183 -0
  34. data/lib/language_operator/agent/safety/ast_validator.rb +272 -0
  35. data/lib/language_operator/agent/safety/audit_logger.rb +104 -0
  36. data/lib/language_operator/agent/safety/budget_tracker.rb +175 -0
  37. data/lib/language_operator/agent/safety/content_filter.rb +93 -0
  38. data/lib/language_operator/agent/safety/manager.rb +207 -0
  39. data/lib/language_operator/agent/safety/rate_limiter.rb +150 -0
  40. data/lib/language_operator/agent/safety/safe_executor.rb +115 -0
  41. data/lib/language_operator/agent/scheduler.rb +183 -0
  42. data/lib/language_operator/agent/telemetry.rb +116 -0
  43. data/lib/language_operator/agent/web_server.rb +610 -0
  44. data/lib/language_operator/agent/webhook_authenticator.rb +226 -0
  45. data/lib/language_operator/agent.rb +149 -0
  46. data/lib/language_operator/cli/commands/agent.rb +1252 -0
  47. data/lib/language_operator/cli/commands/cluster.rb +335 -0
  48. data/lib/language_operator/cli/commands/install.rb +404 -0
  49. data/lib/language_operator/cli/commands/model.rb +266 -0
  50. data/lib/language_operator/cli/commands/persona.rb +396 -0
  51. data/lib/language_operator/cli/commands/quickstart.rb +22 -0
  52. data/lib/language_operator/cli/commands/status.rb +156 -0
  53. data/lib/language_operator/cli/commands/tool.rb +537 -0
  54. data/lib/language_operator/cli/commands/use.rb +47 -0
  55. data/lib/language_operator/cli/errors/handler.rb +180 -0
  56. data/lib/language_operator/cli/errors/suggestions.rb +176 -0
  57. data/lib/language_operator/cli/formatters/code_formatter.rb +81 -0
  58. data/lib/language_operator/cli/formatters/log_formatter.rb +290 -0
  59. data/lib/language_operator/cli/formatters/progress_formatter.rb +53 -0
  60. data/lib/language_operator/cli/formatters/table_formatter.rb +179 -0
  61. data/lib/language_operator/cli/formatters/value_formatter.rb +113 -0
  62. data/lib/language_operator/cli/helpers/cluster_context.rb +62 -0
  63. data/lib/language_operator/cli/helpers/cluster_validator.rb +101 -0
  64. data/lib/language_operator/cli/helpers/editor_helper.rb +58 -0
  65. data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +167 -0
  66. data/lib/language_operator/cli/helpers/resource_dependency_checker.rb +74 -0
  67. data/lib/language_operator/cli/helpers/schedule_builder.rb +108 -0
  68. data/lib/language_operator/cli/helpers/user_prompts.rb +69 -0
  69. data/lib/language_operator/cli/main.rb +232 -0
  70. data/lib/language_operator/cli/templates/tools/generic.yaml +66 -0
  71. data/lib/language_operator/cli/wizards/agent_wizard.rb +246 -0
  72. data/lib/language_operator/cli/wizards/quickstart_wizard.rb +588 -0
  73. data/lib/language_operator/client/base.rb +214 -0
  74. data/lib/language_operator/client/config.rb +136 -0
  75. data/lib/language_operator/client/cost_calculator.rb +37 -0
  76. data/lib/language_operator/client/mcp_connector.rb +123 -0
  77. data/lib/language_operator/client.rb +19 -0
  78. data/lib/language_operator/config/cluster_config.rb +101 -0
  79. data/lib/language_operator/config/tool_patterns.yaml +57 -0
  80. data/lib/language_operator/config/tool_registry.rb +96 -0
  81. data/lib/language_operator/config.rb +138 -0
  82. data/lib/language_operator/dsl/adapter.rb +124 -0
  83. data/lib/language_operator/dsl/agent_context.rb +90 -0
  84. data/lib/language_operator/dsl/agent_definition.rb +427 -0
  85. data/lib/language_operator/dsl/chat_endpoint_definition.rb +115 -0
  86. data/lib/language_operator/dsl/config.rb +119 -0
  87. data/lib/language_operator/dsl/context.rb +50 -0
  88. data/lib/language_operator/dsl/execution_context.rb +47 -0
  89. data/lib/language_operator/dsl/helpers.rb +109 -0
  90. data/lib/language_operator/dsl/http.rb +184 -0
  91. data/lib/language_operator/dsl/mcp_server_definition.rb +73 -0
  92. data/lib/language_operator/dsl/parameter_definition.rb +124 -0
  93. data/lib/language_operator/dsl/registry.rb +36 -0
  94. data/lib/language_operator/dsl/shell.rb +125 -0
  95. data/lib/language_operator/dsl/tool_definition.rb +112 -0
  96. data/lib/language_operator/dsl/webhook_authentication.rb +114 -0
  97. data/lib/language_operator/dsl/webhook_definition.rb +106 -0
  98. data/lib/language_operator/dsl/workflow_definition.rb +259 -0
  99. data/lib/language_operator/dsl.rb +160 -0
  100. data/lib/language_operator/errors.rb +60 -0
  101. data/lib/language_operator/kubernetes/client.rb +279 -0
  102. data/lib/language_operator/kubernetes/resource_builder.rb +194 -0
  103. data/lib/language_operator/loggable.rb +47 -0
  104. data/lib/language_operator/logger.rb +141 -0
  105. data/lib/language_operator/retry.rb +123 -0
  106. data/lib/language_operator/retryable.rb +132 -0
  107. data/lib/language_operator/tool_loader.rb +242 -0
  108. data/lib/language_operator/validators.rb +170 -0
  109. data/lib/language_operator/version.rb +1 -1
  110. data/lib/language_operator.rb +65 -3
  111. data/requirements/tasks/challenge.md +9 -0
  112. data/requirements/tasks/iterate.md +36 -0
  113. data/requirements/tasks/optimize.md +21 -0
  114. data/requirements/tasks/tag.md +5 -0
  115. data/test_agent_dsl.rb +108 -0
  116. 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