language-operator 0.0.1 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +125 -0
- data/CHANGELOG.md +53 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +284 -0
- data/LICENSE +229 -21
- data/Makefile +77 -0
- data/README.md +3 -11
- data/Rakefile +34 -0
- data/bin/aictl +7 -0
- data/completions/_aictl +232 -0
- data/completions/aictl.bash +121 -0
- data/completions/aictl.fish +114 -0
- data/docs/architecture/agent-runtime.md +585 -0
- data/docs/dsl/agent-reference.md +591 -0
- data/docs/dsl/best-practices.md +1078 -0
- data/docs/dsl/chat-endpoints.md +895 -0
- data/docs/dsl/constraints.md +671 -0
- data/docs/dsl/mcp-integration.md +1177 -0
- data/docs/dsl/webhooks.md +932 -0
- data/docs/dsl/workflows.md +744 -0
- data/examples/README.md +569 -0
- data/examples/agent_example.rb +86 -0
- data/examples/chat_endpoint_agent.rb +118 -0
- data/examples/github_webhook_agent.rb +171 -0
- data/examples/mcp_agent.rb +158 -0
- data/examples/oauth_callback_agent.rb +296 -0
- data/examples/stripe_webhook_agent.rb +219 -0
- data/examples/webhook_agent.rb +80 -0
- data/lib/language_operator/agent/base.rb +110 -0
- data/lib/language_operator/agent/executor.rb +440 -0
- data/lib/language_operator/agent/instrumentation.rb +54 -0
- data/lib/language_operator/agent/metrics_tracker.rb +183 -0
- data/lib/language_operator/agent/safety/ast_validator.rb +272 -0
- data/lib/language_operator/agent/safety/audit_logger.rb +104 -0
- data/lib/language_operator/agent/safety/budget_tracker.rb +175 -0
- data/lib/language_operator/agent/safety/content_filter.rb +93 -0
- data/lib/language_operator/agent/safety/manager.rb +207 -0
- data/lib/language_operator/agent/safety/rate_limiter.rb +150 -0
- data/lib/language_operator/agent/safety/safe_executor.rb +115 -0
- data/lib/language_operator/agent/scheduler.rb +183 -0
- data/lib/language_operator/agent/telemetry.rb +116 -0
- data/lib/language_operator/agent/web_server.rb +610 -0
- data/lib/language_operator/agent/webhook_authenticator.rb +226 -0
- data/lib/language_operator/agent.rb +149 -0
- data/lib/language_operator/cli/commands/agent.rb +1252 -0
- data/lib/language_operator/cli/commands/cluster.rb +335 -0
- data/lib/language_operator/cli/commands/install.rb +404 -0
- data/lib/language_operator/cli/commands/model.rb +266 -0
- data/lib/language_operator/cli/commands/persona.rb +396 -0
- data/lib/language_operator/cli/commands/quickstart.rb +22 -0
- data/lib/language_operator/cli/commands/status.rb +156 -0
- data/lib/language_operator/cli/commands/tool.rb +537 -0
- data/lib/language_operator/cli/commands/use.rb +47 -0
- data/lib/language_operator/cli/errors/handler.rb +180 -0
- data/lib/language_operator/cli/errors/suggestions.rb +176 -0
- data/lib/language_operator/cli/formatters/code_formatter.rb +81 -0
- data/lib/language_operator/cli/formatters/log_formatter.rb +290 -0
- data/lib/language_operator/cli/formatters/progress_formatter.rb +53 -0
- data/lib/language_operator/cli/formatters/table_formatter.rb +179 -0
- data/lib/language_operator/cli/formatters/value_formatter.rb +113 -0
- data/lib/language_operator/cli/helpers/cluster_context.rb +62 -0
- data/lib/language_operator/cli/helpers/cluster_validator.rb +101 -0
- data/lib/language_operator/cli/helpers/editor_helper.rb +58 -0
- data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +167 -0
- data/lib/language_operator/cli/helpers/resource_dependency_checker.rb +74 -0
- data/lib/language_operator/cli/helpers/schedule_builder.rb +108 -0
- data/lib/language_operator/cli/helpers/user_prompts.rb +69 -0
- data/lib/language_operator/cli/main.rb +232 -0
- data/lib/language_operator/cli/templates/tools/generic.yaml +66 -0
- data/lib/language_operator/cli/wizards/agent_wizard.rb +246 -0
- data/lib/language_operator/cli/wizards/quickstart_wizard.rb +588 -0
- data/lib/language_operator/client/base.rb +214 -0
- data/lib/language_operator/client/config.rb +136 -0
- data/lib/language_operator/client/cost_calculator.rb +37 -0
- data/lib/language_operator/client/mcp_connector.rb +123 -0
- data/lib/language_operator/client.rb +19 -0
- data/lib/language_operator/config/cluster_config.rb +101 -0
- data/lib/language_operator/config/tool_patterns.yaml +57 -0
- data/lib/language_operator/config/tool_registry.rb +96 -0
- data/lib/language_operator/config.rb +138 -0
- data/lib/language_operator/dsl/adapter.rb +124 -0
- data/lib/language_operator/dsl/agent_context.rb +90 -0
- data/lib/language_operator/dsl/agent_definition.rb +427 -0
- data/lib/language_operator/dsl/chat_endpoint_definition.rb +115 -0
- data/lib/language_operator/dsl/config.rb +119 -0
- data/lib/language_operator/dsl/context.rb +50 -0
- data/lib/language_operator/dsl/execution_context.rb +47 -0
- data/lib/language_operator/dsl/helpers.rb +109 -0
- data/lib/language_operator/dsl/http.rb +184 -0
- data/lib/language_operator/dsl/mcp_server_definition.rb +73 -0
- data/lib/language_operator/dsl/parameter_definition.rb +124 -0
- data/lib/language_operator/dsl/registry.rb +36 -0
- data/lib/language_operator/dsl/shell.rb +125 -0
- data/lib/language_operator/dsl/tool_definition.rb +112 -0
- data/lib/language_operator/dsl/webhook_authentication.rb +114 -0
- data/lib/language_operator/dsl/webhook_definition.rb +106 -0
- data/lib/language_operator/dsl/workflow_definition.rb +259 -0
- data/lib/language_operator/dsl.rb +160 -0
- data/lib/language_operator/errors.rb +60 -0
- data/lib/language_operator/kubernetes/client.rb +279 -0
- data/lib/language_operator/kubernetes/resource_builder.rb +194 -0
- data/lib/language_operator/loggable.rb +47 -0
- data/lib/language_operator/logger.rb +141 -0
- data/lib/language_operator/retry.rb +123 -0
- data/lib/language_operator/retryable.rb +132 -0
- data/lib/language_operator/tool_loader.rb +242 -0
- data/lib/language_operator/validators.rb +170 -0
- data/lib/language_operator/version.rb +1 -1
- data/lib/language_operator.rb +65 -3
- data/requirements/tasks/challenge.md +9 -0
- data/requirements/tasks/iterate.md +36 -0
- data/requirements/tasks/optimize.md +21 -0
- data/requirements/tasks/tag.md +5 -0
- data/test_agent_dsl.rb +108 -0
- metadata +503 -20
|
@@ -0,0 +1,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
|