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