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,588 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tty-prompt'
4
+ require 'pastel'
5
+ require 'k8s-ruby'
6
+ require_relative '../formatters/progress_formatter'
7
+ require_relative '../../config/cluster_config'
8
+ require_relative '../../kubernetes/client'
9
+ require_relative '../../kubernetes/resource_builder'
10
+
11
+ module LanguageOperator
12
+ module CLI
13
+ module Wizards
14
+ # Interactive quickstart wizard for first-time setup
15
+ class QuickstartWizard
16
+ def initialize
17
+ @prompt = TTY::Prompt.new
18
+ @pastel = Pastel.new
19
+ end
20
+
21
+ def run
22
+ show_welcome
23
+
24
+ # Step 1: Cluster setup
25
+ cluster_info = setup_cluster
26
+ return unless cluster_info
27
+
28
+ # Step 2: Model configuration
29
+ model_info = configure_model(cluster_info)
30
+ return unless model_info
31
+
32
+ # Step 3: Example agent
33
+ agent_created = create_example_agent(cluster_info, model_info)
34
+
35
+ # Show next steps
36
+ show_next_steps(agent_created: agent_created)
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :prompt, :pastel
42
+
43
+ def show_welcome
44
+ puts
45
+ puts pastel.cyan('╭────────────────────────────────────────────────╮')
46
+ puts "#{pastel.cyan('│')} Welcome to Language Operator! 🎉 #{pastel.cyan('│')}"
47
+ puts "#{pastel.cyan('│')} Let's get you set up (takes ~5 minutes) #{pastel.cyan('│')}"
48
+ puts pastel.cyan('╰────────────────────────────────────────────────╯')
49
+ puts
50
+ puts 'This wizard will help you:'
51
+ puts ' 1. Connect to your Kubernetes cluster'
52
+ puts ' 2. Configure a language model'
53
+ puts ' 3. Create your first autonomous agent'
54
+ puts
55
+ puts pastel.dim('Press Enter to begin...')
56
+ $stdin.gets
57
+ end
58
+
59
+ def setup_cluster
60
+ puts
61
+ puts '─' * 50
62
+ puts pastel.cyan('Step 1/3: Connect to Kubernetes')
63
+ puts '─' * 50
64
+ puts
65
+
66
+ # Check if user has kubectl configured
67
+ has_kubectl = prompt.yes?('Do you have kubectl configured?')
68
+
69
+ unless has_kubectl
70
+ show_kubernetes_setup_guide
71
+ return nil
72
+ end
73
+
74
+ # Get available contexts
75
+ contexts = get_kubectl_contexts
76
+
77
+ if contexts.empty?
78
+ Formatters::ProgressFormatter.error('No kubectl contexts found')
79
+ puts
80
+ puts 'Configure kubectl first, then run quickstart again.'
81
+ return nil
82
+ end
83
+
84
+ puts
85
+ puts 'Great! I found these contexts in your kubeconfig:'
86
+ puts
87
+
88
+ # Let user select context
89
+ context = prompt.select('Which context should we use?', contexts)
90
+
91
+ # Generate cluster name
92
+ cluster_name = prompt.ask('Name for this cluster:', default: 'my-cluster') do |q|
93
+ q.required true
94
+ q.validate(/^[a-z0-9-]+$/, 'Name must be lowercase alphanumeric with hyphens')
95
+ end
96
+
97
+ # Create cluster configuration
98
+ create_cluster_config(cluster_name, context)
99
+ end
100
+
101
+ def get_kubectl_contexts
102
+ kubeconfig_path = ENV.fetch('KUBECONFIG', File.expand_path('~/.kube/config'))
103
+
104
+ return [] unless File.exist?(kubeconfig_path)
105
+
106
+ config = K8s::Config.load_file(kubeconfig_path)
107
+ config.contexts.map(&:name)
108
+ rescue StandardError => e
109
+ Formatters::ProgressFormatter.error("Failed to load kubeconfig: #{e.message}")
110
+ []
111
+ end
112
+
113
+ def create_cluster_config(name, context)
114
+ kubeconfig_path = ENV.fetch('KUBECONFIG', File.expand_path('~/.kube/config'))
115
+
116
+ Formatters::ProgressFormatter.with_spinner("Creating cluster '#{name}'") do
117
+ # Create Kubernetes client to verify connection
118
+ k8s = Kubernetes::Client.new(kubeconfig: kubeconfig_path, context: context)
119
+
120
+ # Get namespace from context or use default
121
+ namespace = k8s.current_namespace || 'default'
122
+
123
+ # Check if operator is installed
124
+ unless k8s.operator_installed?
125
+ puts
126
+ Formatters::ProgressFormatter.warn('Language Operator not found in cluster')
127
+ puts
128
+ puts 'The operator needs to be installed first.'
129
+ puts 'Install with:'
130
+ puts ' aictl install'
131
+ puts
132
+ exit 1
133
+ end
134
+
135
+ # Save cluster config
136
+ Config::ClusterConfig.add_cluster(name, namespace, kubeconfig_path, context)
137
+ Config::ClusterConfig.set_current_cluster(name)
138
+
139
+ { name: name, namespace: namespace, kubeconfig: kubeconfig_path, context: context, k8s: k8s }
140
+ end
141
+
142
+ {
143
+ name: name,
144
+ namespace: (kubeconfig_path && K8s::Config.load_file(kubeconfig_path).context(context).namespace) || 'default',
145
+ kubeconfig: kubeconfig_path,
146
+ context: context
147
+ }
148
+ rescue StandardError => e
149
+ puts
150
+ Formatters::ProgressFormatter.error("Failed to connect: #{e.message}")
151
+ nil
152
+ end
153
+
154
+ def show_kubernetes_setup_guide
155
+ puts
156
+ puts pastel.yellow('Kubernetes Setup Required')
157
+ puts
158
+ puts 'Language Operator needs a Kubernetes cluster to run.'
159
+ puts
160
+ puts 'Quick options:'
161
+ puts
162
+ puts ' 1. Docker Desktop (easiest for local development)'
163
+ puts ' • Enable Kubernetes in Docker Desktop settings'
164
+ puts ' • kubectl will be configured automatically'
165
+ puts
166
+ puts ' 2. Minikube (lightweight local cluster)'
167
+ puts ' • Install: brew install minikube'
168
+ puts ' • Start: minikube start'
169
+ puts
170
+ puts ' 3. Kind (Kubernetes in Docker)'
171
+ puts ' • Install: brew install kind'
172
+ puts ' • Create cluster: kind create cluster'
173
+ puts
174
+ puts 'After setting up kubectl, run quickstart again:'
175
+ puts ' aictl quickstart'
176
+ puts
177
+ end
178
+
179
+ def configure_model(cluster_info)
180
+ puts
181
+ puts '─' * 50
182
+ puts pastel.cyan('Step 2/3: Configure Language Model')
183
+ puts '─' * 50
184
+ puts
185
+ puts 'Agents need an LLM to understand instructions.'
186
+ puts
187
+
188
+ # Provider selection
189
+ provider = prompt.select('Which provider do you want to use?') do |menu|
190
+ menu.choice 'Anthropic (Claude)', :anthropic
191
+ menu.choice 'OpenAI (GPT-4)', :openai
192
+ menu.choice 'Local model (Ollama)', :ollama
193
+ menu.choice 'Other', :other
194
+ end
195
+
196
+ case provider
197
+ when :anthropic
198
+ setup_anthropic_model(cluster_info)
199
+ when :openai
200
+ setup_openai_model(cluster_info)
201
+ when :ollama
202
+ setup_ollama_model(cluster_info)
203
+ when :other
204
+ setup_custom_model(cluster_info)
205
+ end
206
+ end
207
+
208
+ def setup_anthropic_model(cluster_info)
209
+ puts
210
+ has_key = prompt.yes?('Do you have an Anthropic API key?')
211
+
212
+ unless has_key
213
+ puts
214
+ puts "Get an API key at: #{pastel.cyan('https://console.anthropic.com')}"
215
+ puts
216
+ puts pastel.dim('Press Enter when you have your key...')
217
+ $stdin.gets
218
+ end
219
+
220
+ puts
221
+ api_key = prompt.mask('Enter your Anthropic API key:')
222
+
223
+ # Test connection
224
+ test_result = Formatters::ProgressFormatter.with_spinner('Testing connection') do
225
+ test_anthropic_connection(api_key)
226
+ end
227
+
228
+ unless test_result[:success]
229
+ Formatters::ProgressFormatter.error("Connection failed: #{test_result[:error]}")
230
+ return nil
231
+ end
232
+
233
+ # Create model resource
234
+ model_name = 'claude'
235
+ model_id = 'claude-3-5-sonnet-20241022'
236
+
237
+ create_model_resource(cluster_info, model_name, 'anthropic', model_id, api_key)
238
+
239
+ { name: model_name, provider: 'anthropic', model: model_id }
240
+ end
241
+
242
+ def setup_openai_model(cluster_info)
243
+ puts
244
+ has_key = prompt.yes?('Do you have an OpenAI API key?')
245
+
246
+ unless has_key
247
+ puts
248
+ puts "Get an API key at: #{pastel.cyan('https://platform.openai.com/api-keys')}"
249
+ puts
250
+ puts pastel.dim('Press Enter when you have your key...')
251
+ $stdin.gets
252
+ end
253
+
254
+ puts
255
+ api_key = prompt.mask('Enter your OpenAI API key:')
256
+
257
+ # Test connection
258
+ test_result = Formatters::ProgressFormatter.with_spinner('Testing connection') do
259
+ test_openai_connection(api_key)
260
+ end
261
+
262
+ unless test_result[:success]
263
+ Formatters::ProgressFormatter.error("Connection failed: #{test_result[:error]}")
264
+ return nil
265
+ end
266
+
267
+ # Create model resource
268
+ model_name = 'gpt4'
269
+ model_id = 'gpt-4-turbo'
270
+
271
+ create_model_resource(cluster_info, model_name, 'openai', model_id, api_key)
272
+
273
+ { name: model_name, provider: 'openai', model: model_id }
274
+ end
275
+
276
+ def setup_ollama_model(cluster_info)
277
+ puts
278
+ puts 'Ollama runs LLMs locally on your machine.'
279
+ puts
280
+
281
+ endpoint = prompt.ask('Ollama endpoint:', default: 'http://localhost:11434')
282
+ model_id = prompt.ask('Model name:', default: 'llama3')
283
+
284
+ # Test connection
285
+ test_result = Formatters::ProgressFormatter.with_spinner('Testing connection') do
286
+ test_ollama_connection(endpoint, model_id)
287
+ end
288
+
289
+ unless test_result[:success]
290
+ Formatters::ProgressFormatter.error("Connection failed: #{test_result[:error]}")
291
+ puts
292
+ puts 'Make sure Ollama is running and the model is pulled:'
293
+ puts " ollama pull #{model_id}"
294
+ return nil
295
+ end
296
+
297
+ # Create model resource
298
+ model_name = 'local'
299
+
300
+ create_model_resource(cluster_info, model_name, 'openai-compatible', model_id, nil, endpoint)
301
+
302
+ { name: model_name, provider: 'ollama', model: model_id }
303
+ end
304
+
305
+ def setup_custom_model(cluster_info)
306
+ puts
307
+ puts 'Configure a custom OpenAI-compatible endpoint.'
308
+ puts
309
+
310
+ endpoint = prompt.ask('API endpoint URL:') do |q|
311
+ q.required true
312
+ q.validate(%r{^https?://})
313
+ q.messages[:valid?] = 'Must be a valid HTTP(S) URL'
314
+ end
315
+
316
+ requires_auth = prompt.yes?('Does this endpoint require authentication?')
317
+
318
+ api_key = nil
319
+ api_key = prompt.mask('Enter API key:') if requires_auth
320
+
321
+ # Try to fetch available models from the endpoint
322
+ puts
323
+ available_models = fetch_available_models(endpoint, api_key)
324
+
325
+ model_id = if available_models && !available_models.empty?
326
+ prompt.select('Select a model:', available_models, per_page: 10)
327
+ else
328
+ prompt.ask('Model identifier:') do |q|
329
+ q.required true
330
+ end
331
+ end
332
+
333
+ puts
334
+ Formatters::ProgressFormatter.info('Skipping connection test for custom endpoint')
335
+
336
+ # Create model resource
337
+ model_name = 'custom'
338
+
339
+ create_model_resource(cluster_info, model_name, 'openai-compatible', model_id, api_key, endpoint)
340
+
341
+ { name: model_name, provider: 'custom', model: model_id }
342
+ end
343
+
344
+ def test_anthropic_connection(api_key)
345
+ require 'ruby_llm'
346
+
347
+ client = RubyLLM.new(provider: :anthropic, api_key: api_key)
348
+ client.chat([{ role: 'user', content: 'Test' }], model: 'claude-3-5-sonnet-20241022', max_tokens: 10)
349
+
350
+ { success: true }
351
+ rescue StandardError => e
352
+ { success: false, error: e.message }
353
+ end
354
+
355
+ def test_openai_connection(api_key)
356
+ require 'ruby_llm'
357
+
358
+ client = RubyLLM.new(provider: :openai, api_key: api_key)
359
+ client.chat([{ role: 'user', content: 'Test' }], model: 'gpt-4-turbo', max_tokens: 10)
360
+
361
+ { success: true }
362
+ rescue StandardError => e
363
+ { success: false, error: e.message }
364
+ end
365
+
366
+ def test_ollama_connection(endpoint, model)
367
+ require 'ruby_llm'
368
+
369
+ client = RubyLLM.new(provider: :openai_compatible, url: endpoint)
370
+ client.chat([{ role: 'user', content: 'Test' }], model: model, max_tokens: 10)
371
+
372
+ { success: true }
373
+ rescue StandardError => e
374
+ { success: false, error: e.message }
375
+ end
376
+
377
+ def fetch_available_models(endpoint, api_key = nil)
378
+ require 'net/http'
379
+ require 'json'
380
+ require 'uri'
381
+
382
+ models_url = URI.join(endpoint, '/v1/models').to_s
383
+
384
+ models = nil
385
+ count = 0
386
+
387
+ Formatters::ProgressFormatter.with_spinner('Fetching available models') do
388
+ uri = URI(models_url)
389
+ request = Net::HTTP::Get.new(uri)
390
+ request['Authorization'] = "Bearer #{api_key}" if api_key
391
+ request['Content-Type'] = 'application/json'
392
+
393
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
394
+ http.request(request)
395
+ end
396
+
397
+ if response.is_a?(Net::HTTPSuccess)
398
+ data = JSON.parse(response.body)
399
+ # Extract model IDs from the response
400
+ models = data['data']&.map { |m| m['id'] } || []
401
+ count = models.size
402
+ else
403
+ Formatters::ProgressFormatter.warn("Could not fetch models (HTTP #{response.code})")
404
+ end
405
+ end
406
+
407
+ # Show count after spinner completes
408
+ puts pastel.dim("Found #{count} models") if count.positive?
409
+ models
410
+ rescue StandardError => e
411
+ Formatters::ProgressFormatter.warn("Could not fetch models: #{e.message}")
412
+ nil
413
+ end
414
+
415
+ def create_model_resource(cluster_info, name, provider, model, api_key = nil, endpoint = nil)
416
+ # rubocop:disable Metrics/BlockLength
417
+ Formatters::ProgressFormatter.with_spinner("Creating model '#{name}'") do
418
+ k8s = Kubernetes::Client.new(
419
+ kubeconfig: cluster_info[:kubeconfig],
420
+ context: cluster_info[:context]
421
+ )
422
+
423
+ # Create API key secret if provided
424
+ if api_key
425
+ secret_name = "#{name}-api-key"
426
+ secret = {
427
+ 'apiVersion' => 'v1',
428
+ 'kind' => 'Secret',
429
+ 'metadata' => {
430
+ 'name' => secret_name,
431
+ 'namespace' => cluster_info[:namespace]
432
+ },
433
+ 'type' => 'Opaque',
434
+ 'stringData' => {
435
+ 'api-key' => api_key
436
+ }
437
+ }
438
+ k8s.apply_resource(secret)
439
+ end
440
+
441
+ # Create LanguageModel resource
442
+ resource = Kubernetes::ResourceBuilder.language_model(
443
+ name,
444
+ provider: provider,
445
+ model: model,
446
+ endpoint: endpoint,
447
+ cluster: cluster_info[:namespace]
448
+ )
449
+
450
+ # Add API key reference if we created a secret
451
+ if api_key
452
+ resource['spec']['apiKeySecret'] = {
453
+ 'name' => "#{name}-api-key",
454
+ 'key' => 'api-key'
455
+ }
456
+ end
457
+
458
+ k8s.apply_resource(resource)
459
+ end
460
+ # rubocop:enable Metrics/BlockLength
461
+ end
462
+
463
+ # rubocop:disable Naming/PredicateMethod
464
+ def create_example_agent(cluster_info, model_info)
465
+ puts
466
+ puts '─' * 50
467
+ puts pastel.cyan('Step 3/3: Create Your First Agent')
468
+ puts '─' * 50
469
+ puts
470
+
471
+ # Ask if user wants to create an example agent
472
+ create_agent = prompt.yes?('Would you like to create a simple agent to see how things work?')
473
+
474
+ unless create_agent
475
+ puts
476
+ puts pastel.dim('Skipping example agent creation.')
477
+ puts
478
+ return false
479
+ end
480
+
481
+ puts
482
+ puts "I'll create an agent that tells you fun facts about Ruby."
483
+ puts
484
+
485
+ agent_name = 'ruby-facts'
486
+ description = 'Tell me an interesting fun fact about the Ruby programming language'
487
+
488
+ k8s = Kubernetes::Client.new(
489
+ kubeconfig: cluster_info[:kubeconfig],
490
+ context: cluster_info[:context]
491
+ )
492
+
493
+ # Create agent
494
+ Formatters::ProgressFormatter.with_spinner("Creating agent '#{agent_name}'") do
495
+ resource = Kubernetes::ResourceBuilder.language_agent(
496
+ agent_name,
497
+ instructions: description,
498
+ cluster: cluster_info[:namespace],
499
+ persona: nil,
500
+ tools: [],
501
+ models: [model_info[:name]]
502
+ )
503
+
504
+ k8s.apply_resource(resource)
505
+ end
506
+
507
+ # Wait for synthesis
508
+ wait_for_synthesis(k8s, agent_name, cluster_info[:namespace])
509
+
510
+ true
511
+ end
512
+ # rubocop:enable Naming/PredicateMethod
513
+
514
+ def wait_for_synthesis(k8s, agent_name, namespace)
515
+ max_wait = 300 # 5 minutes for quickstart
516
+ interval = 2
517
+ elapsed = 0
518
+
519
+ Formatters::ProgressFormatter.with_spinner('Synthesizing code') do
520
+ loop do
521
+ agent = k8s.get_resource('LanguageAgent', agent_name, namespace)
522
+ conditions = agent.dig('status', 'conditions') || []
523
+ synthesized = conditions.find { |c| c['type'] == 'Synthesized' }
524
+
525
+ if synthesized
526
+ if synthesized['status'] == 'True'
527
+ break # Success
528
+ elsif synthesized['status'] == 'False'
529
+ raise StandardError, "Synthesis failed: #{synthesized['message']}"
530
+ end
531
+ end
532
+
533
+ if elapsed >= max_wait
534
+ Formatters::ProgressFormatter.warn('Synthesis is taking longer than expected, continuing in background')
535
+ break
536
+ end
537
+
538
+ sleep interval
539
+ elapsed += interval
540
+ end
541
+ end
542
+
543
+ puts
544
+ rescue K8s::Error::NotFound
545
+ # Agent not found yet, retry
546
+ retry if elapsed < max_wait
547
+ raise
548
+ end
549
+
550
+ def show_next_steps(agent_created: false)
551
+ puts
552
+ puts pastel.cyan("What's Next?")
553
+ puts
554
+
555
+ if agent_created
556
+ puts '1. Check your agent status:'
557
+ puts " #{pastel.dim('aictl agent inspect ruby-facts')}"
558
+ puts
559
+ puts '2. View the agent output:'
560
+ puts " #{pastel.dim('aictl agent logs ruby-facts')}"
561
+ puts
562
+ puts '3. Create another agent:'
563
+ puts " #{pastel.dim('aictl agent create "your task here"')}"
564
+ puts
565
+ puts '4. View all your agents:'
566
+ puts " #{pastel.dim('aictl agent list')}"
567
+ else
568
+ puts '1. Create your own agent:'
569
+ puts " #{pastel.dim('aictl agent create "your task here"')}"
570
+ puts
571
+ puts '2. Use the interactive wizard:'
572
+ puts " #{pastel.dim('aictl agent create --wizard')}"
573
+ puts
574
+ puts '3. View your agents:'
575
+ puts " #{pastel.dim('aictl agent list')}"
576
+ puts
577
+ puts '4. Check agent status:'
578
+ puts " #{pastel.dim('aictl agent inspect <agent-name>')}"
579
+ end
580
+
581
+ puts
582
+ puts pastel.green('Welcome to autonomous automation! 🚀')
583
+ puts
584
+ end
585
+ end
586
+ end
587
+ end
588
+ end