language-operator 0.0.1 â 0.1.31
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 +88 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +284 -0
- data/LICENSE +229 -21
- data/Makefile +82 -0
- data/README.md +3 -11
- data/Rakefile +63 -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/SCHEMA_VERSION.md +250 -0
- data/docs/dsl/agent-reference.md +604 -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/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 +127 -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 +1205 -0
- data/lib/language_operator/cli/commands/cluster.rb +371 -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 +393 -0
- data/lib/language_operator/cli/commands/quickstart.rb +22 -0
- data/lib/language_operator/cli/commands/status.rb +143 -0
- data/lib/language_operator/cli/commands/system.rb +772 -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 +77 -0
- data/lib/language_operator/cli/formatters/log_formatter.rb +288 -0
- data/lib/language_operator/cli/formatters/progress_formatter.rb +49 -0
- data/lib/language_operator/cli/formatters/status_formatter.rb +37 -0
- data/lib/language_operator/cli/formatters/table_formatter.rb +163 -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/pastel_helper.rb +24 -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 +236 -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/schema.rb +1102 -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 +161 -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/templates/README.md +23 -0
- data/lib/language_operator/templates/examples/agent_synthesis.tmpl +115 -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 +93 -0
- data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +306 -0
- data/lib/language_operator/templates/schema/agent_dsl_schema.json +452 -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 +507 -20
|
@@ -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
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LanguageOperator
|
|
4
|
+
# Mixin module to provide automatic logger initialization for classes
|
|
5
|
+
# that need logging capabilities.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# class MyClass
|
|
9
|
+
# include LanguageOperator::Loggable
|
|
10
|
+
#
|
|
11
|
+
# def process
|
|
12
|
+
# logger.info("Processing started")
|
|
13
|
+
# # ...
|
|
14
|
+
# end
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# The logger component name is automatically derived from the class name.
|
|
18
|
+
# You can override the component name by defining a `logger_component` method.
|
|
19
|
+
#
|
|
20
|
+
# @example Custom component name
|
|
21
|
+
# class MyClass
|
|
22
|
+
# include LanguageOperator::Loggable
|
|
23
|
+
#
|
|
24
|
+
# def logger_component
|
|
25
|
+
# 'CustomName'
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
module Loggable
|
|
29
|
+
# Returns a logger instance for this class.
|
|
30
|
+
# Lazily initializes the logger on first access.
|
|
31
|
+
#
|
|
32
|
+
# @return [LanguageOperator::Logger] Logger instance
|
|
33
|
+
def logger
|
|
34
|
+
@logger ||= LanguageOperator::Logger.new(component: logger_component)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
# Returns the component name to use for the logger.
|
|
40
|
+
# Defaults to the class name, but can be overridden.
|
|
41
|
+
#
|
|
42
|
+
# @return [String] Component name for the logger
|
|
43
|
+
def logger_component
|
|
44
|
+
self.class.name
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'logger'
|
|
4
|
+
|
|
5
|
+
module LanguageOperator
|
|
6
|
+
# Structured logger with configurable output formats and levels
|
|
7
|
+
#
|
|
8
|
+
# Supports multiple output formats:
|
|
9
|
+
# - :pretty (default): Human-readable with emojis and colors
|
|
10
|
+
# - :text: Plain text with timestamps
|
|
11
|
+
# - :json: Structured JSON output
|
|
12
|
+
#
|
|
13
|
+
# Environment variables:
|
|
14
|
+
# - LOG_LEVEL: DEBUG, INFO, WARN, ERROR (default: INFO)
|
|
15
|
+
# - LOG_FORMAT: pretty, text, json (default: pretty)
|
|
16
|
+
# - LOG_TIMING: true/false - Include operation timing (default: true)
|
|
17
|
+
class Logger
|
|
18
|
+
LEVELS = {
|
|
19
|
+
'DEBUG' => ::Logger::DEBUG,
|
|
20
|
+
'INFO' => ::Logger::INFO,
|
|
21
|
+
'WARN' => ::Logger::WARN,
|
|
22
|
+
'ERROR' => ::Logger::ERROR
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
LEVEL_EMOJI = {
|
|
26
|
+
'DEBUG' => '', # đ
|
|
27
|
+
'INFO' => '', # âšī¸
|
|
28
|
+
'WARN' => '', # â ī¸
|
|
29
|
+
'ERROR' => '' # â
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
attr_reader :logger, :format, :show_timing
|
|
33
|
+
|
|
34
|
+
def initialize(component: 'Langop', format: nil, level: nil)
|
|
35
|
+
@component = component
|
|
36
|
+
@format = format || ENV.fetch('LOG_FORMAT', 'pretty').to_sym
|
|
37
|
+
@show_timing = ENV.fetch('LOG_TIMING', 'true') == 'true'
|
|
38
|
+
|
|
39
|
+
log_level_name = level || ENV.fetch('LOG_LEVEL', 'INFO')
|
|
40
|
+
log_level = LEVELS[log_level_name.upcase] || ::Logger::INFO
|
|
41
|
+
|
|
42
|
+
@logger = ::Logger.new($stdout)
|
|
43
|
+
@logger.level = log_level
|
|
44
|
+
@logger.formatter = method(:format_message)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def debug(message, **metadata)
|
|
48
|
+
log(:debug, message, **metadata)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def info(message, **metadata)
|
|
52
|
+
log(:info, message, **metadata)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def warn(message, **metadata)
|
|
56
|
+
log(:warn, message, **metadata)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def error(message, **metadata)
|
|
60
|
+
log(:error, message, **metadata)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Log with timing information
|
|
64
|
+
def timed(message, **metadata)
|
|
65
|
+
start_time = Time.now
|
|
66
|
+
result = yield if block_given?
|
|
67
|
+
duration = Time.now - start_time
|
|
68
|
+
|
|
69
|
+
info(message, **metadata, duration_s: duration.round(3))
|
|
70
|
+
result
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def log(severity, message, **metadata)
|
|
76
|
+
@logger.send(severity) do
|
|
77
|
+
case @format
|
|
78
|
+
when :json
|
|
79
|
+
format_json(severity, message, **metadata)
|
|
80
|
+
when :text
|
|
81
|
+
format_text(severity, message, **metadata)
|
|
82
|
+
else
|
|
83
|
+
format_pretty(severity, message, **metadata)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def format_message(_severity, _timestamp, _progname, msg)
|
|
89
|
+
"#{msg}\n" # Already formatted by log method, add newline
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def format_json(severity, message, **metadata)
|
|
93
|
+
require 'json'
|
|
94
|
+
JSON.generate({
|
|
95
|
+
timestamp: Time.now.iso8601,
|
|
96
|
+
level: severity.to_s.upcase,
|
|
97
|
+
component: @component,
|
|
98
|
+
message: message,
|
|
99
|
+
**metadata
|
|
100
|
+
})
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def format_text(severity, message, **metadata)
|
|
104
|
+
parts = [
|
|
105
|
+
Time.now.strftime('%Y-%m-%d %H:%M:%S'),
|
|
106
|
+
severity.to_s.upcase.ljust(5),
|
|
107
|
+
"[#{@component}]",
|
|
108
|
+
message
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
metadata_str = format_metadata(**metadata)
|
|
112
|
+
parts << metadata_str unless metadata_str.empty?
|
|
113
|
+
|
|
114
|
+
parts.join(' ')
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def format_pretty(severity, message, **metadata)
|
|
118
|
+
emoji = LEVEL_EMOJI[severity.to_s.upcase] || 'âĸ'
|
|
119
|
+
parts = [emoji, message]
|
|
120
|
+
|
|
121
|
+
metadata_str = format_metadata(**metadata)
|
|
122
|
+
parts << "(#{metadata_str})" unless metadata_str.empty?
|
|
123
|
+
|
|
124
|
+
parts.join(' ')
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def format_metadata(**metadata)
|
|
128
|
+
return '' if metadata.empty?
|
|
129
|
+
|
|
130
|
+
metadata.map do |key, value|
|
|
131
|
+
if key == :duration_s && @show_timing
|
|
132
|
+
"#{value}s"
|
|
133
|
+
elsif value.is_a?(String) && value.length > 100
|
|
134
|
+
"#{key}=#{value[0..97]}..."
|
|
135
|
+
else
|
|
136
|
+
"#{key}=#{value}"
|
|
137
|
+
end
|
|
138
|
+
end.join(', ')
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|