language-operator 0.1.31 → 0.1.35
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 +7 -8
- data/CHANGELOG.md +14 -0
- data/CI_STATUS.md +56 -0
- data/Gemfile.lock +2 -2
- data/Makefile +22 -6
- data/lib/language_operator/agent/base.rb +10 -6
- data/lib/language_operator/agent/executor.rb +19 -97
- data/lib/language_operator/agent/safety/ast_validator.rb +62 -43
- data/lib/language_operator/agent/safety/safe_executor.rb +27 -2
- data/lib/language_operator/agent/scheduler.rb +60 -0
- data/lib/language_operator/agent/task_executor.rb +548 -0
- data/lib/language_operator/agent.rb +90 -27
- data/lib/language_operator/cli/base_command.rb +117 -0
- data/lib/language_operator/cli/commands/agent.rb +339 -407
- data/lib/language_operator/cli/commands/cluster.rb +274 -290
- data/lib/language_operator/cli/commands/install.rb +110 -119
- data/lib/language_operator/cli/commands/model.rb +284 -184
- data/lib/language_operator/cli/commands/persona.rb +218 -284
- data/lib/language_operator/cli/commands/quickstart.rb +4 -5
- data/lib/language_operator/cli/commands/status.rb +31 -35
- data/lib/language_operator/cli/commands/system.rb +221 -233
- data/lib/language_operator/cli/commands/tool.rb +356 -422
- data/lib/language_operator/cli/commands/use.rb +19 -22
- data/lib/language_operator/cli/helpers/resource_dependency_checker.rb +0 -18
- data/lib/language_operator/cli/wizards/quickstart_wizard.rb +0 -1
- data/lib/language_operator/client/config.rb +20 -21
- data/lib/language_operator/config.rb +115 -3
- data/lib/language_operator/constants.rb +54 -0
- data/lib/language_operator/dsl/agent_context.rb +7 -7
- data/lib/language_operator/dsl/agent_definition.rb +111 -26
- data/lib/language_operator/dsl/config.rb +30 -66
- data/lib/language_operator/dsl/main_definition.rb +114 -0
- data/lib/language_operator/dsl/schema.rb +84 -43
- data/lib/language_operator/dsl/task_definition.rb +315 -0
- data/lib/language_operator/dsl.rb +0 -1
- data/lib/language_operator/instrumentation/task_tracer.rb +285 -0
- data/lib/language_operator/logger.rb +4 -4
- data/lib/language_operator/synthesis_test_harness.rb +324 -0
- data/lib/language_operator/templates/examples/agent_synthesis.tmpl +26 -8
- data/lib/language_operator/templates/schema/CHANGELOG.md +26 -0
- data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
- data/lib/language_operator/templates/schema/agent_dsl_schema.json +84 -42
- data/lib/language_operator/type_coercion.rb +250 -0
- data/lib/language_operator/ux/base.rb +81 -0
- data/lib/language_operator/ux/concerns/README.md +155 -0
- data/lib/language_operator/ux/concerns/headings.rb +90 -0
- data/lib/language_operator/ux/concerns/input_validation.rb +146 -0
- data/lib/language_operator/ux/concerns/provider_helpers.rb +167 -0
- data/lib/language_operator/ux/create_agent.rb +252 -0
- data/lib/language_operator/ux/create_model.rb +267 -0
- data/lib/language_operator/ux/quickstart.rb +594 -0
- data/lib/language_operator/version.rb +1 -1
- data/lib/language_operator.rb +2 -0
- data/requirements/ARCHITECTURE.md +1 -0
- data/requirements/SCRATCH.md +153 -0
- data/requirements/dsl.md +0 -0
- data/requirements/features +1 -0
- data/requirements/personas +1 -0
- data/requirements/proposals +1 -0
- data/requirements/tasks/iterate.md +14 -15
- data/requirements/tasks/optimize.md +13 -4
- data/synth/001/Makefile +90 -0
- data/synth/001/agent.rb +26 -0
- data/synth/001/agent.yaml +7 -0
- data/synth/001/output.log +44 -0
- data/synth/Makefile +39 -0
- data/synth/README.md +342 -0
- metadata +37 -10
- data/lib/language_operator/dsl/workflow_definition.rb +0 -259
- data/test_agent_dsl.rb +0 -108
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative 'concerns/headings'
|
|
5
|
+
require_relative 'concerns/provider_helpers'
|
|
6
|
+
require_relative 'concerns/input_validation'
|
|
7
|
+
require_relative '../cli/formatters/progress_formatter'
|
|
8
|
+
require_relative '../kubernetes/client'
|
|
9
|
+
require_relative '../kubernetes/resource_builder'
|
|
10
|
+
|
|
11
|
+
module LanguageOperator
|
|
12
|
+
module Ux
|
|
13
|
+
# Interactive flow for creating language models
|
|
14
|
+
#
|
|
15
|
+
# Guides users through provider selection, credential input,
|
|
16
|
+
# model selection, and resource creation.
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# Ux::CreateModel.execute(ctx)
|
|
20
|
+
#
|
|
21
|
+
# rubocop:disable Metrics/ClassLength, Metrics/MethodLength, Naming/PredicateMethod
|
|
22
|
+
class CreateModel < Base
|
|
23
|
+
include Concerns::Headings
|
|
24
|
+
include Concerns::ProviderHelpers
|
|
25
|
+
include Concerns::InputValidation
|
|
26
|
+
|
|
27
|
+
# Execute the model creation flow
|
|
28
|
+
#
|
|
29
|
+
# @return [Boolean] true if model was created successfully
|
|
30
|
+
def execute
|
|
31
|
+
title("Add a model to cluster '#{ctx.name}'")
|
|
32
|
+
|
|
33
|
+
# Step: Provider selection
|
|
34
|
+
subheading('[1/5] Provider')
|
|
35
|
+
provider_info = select_provider
|
|
36
|
+
return false unless provider_info
|
|
37
|
+
|
|
38
|
+
# Step: Get credentials
|
|
39
|
+
subheading('[2/5] Credentials')
|
|
40
|
+
credentials = get_credentials(provider_info)
|
|
41
|
+
return false unless credentials
|
|
42
|
+
|
|
43
|
+
# Step 3: Test connection
|
|
44
|
+
subheading('[3/5] Test Connection')
|
|
45
|
+
test_result = test_connection(provider_info, credentials)
|
|
46
|
+
return false unless test_result[:success]
|
|
47
|
+
|
|
48
|
+
# Step 4: Select model
|
|
49
|
+
subheading('[4/5] Model')
|
|
50
|
+
model_id = select_model(provider_info, credentials)
|
|
51
|
+
return false unless model_id
|
|
52
|
+
|
|
53
|
+
# Step 5: Get display name
|
|
54
|
+
subheading('[5/5] Display Name')
|
|
55
|
+
model_name = get_model_name(model_id)
|
|
56
|
+
return false unless model_name
|
|
57
|
+
|
|
58
|
+
# Step 6: Create resources
|
|
59
|
+
success = create_model_resource(model_name, provider_info, credentials, model_id)
|
|
60
|
+
return false unless success
|
|
61
|
+
|
|
62
|
+
# Step 7: Show success
|
|
63
|
+
show_success(model_name, model_id, provider_info)
|
|
64
|
+
|
|
65
|
+
true
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def select_provider
|
|
71
|
+
provider = prompt.select('Select a provider:') do |menu|
|
|
72
|
+
menu.choice 'Anthropic', :anthropic
|
|
73
|
+
menu.choice 'OpenAI', :openai
|
|
74
|
+
menu.choice 'Other (OpenAI-compatible)', :openai_compatible
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
case provider
|
|
78
|
+
when :anthropic
|
|
79
|
+
{ provider: :anthropic, provider_key: 'anthropic', display_name: 'Anthropic' }
|
|
80
|
+
when :openai
|
|
81
|
+
{ provider: :openai, provider_key: 'openai', display_name: 'OpenAI' }
|
|
82
|
+
when :openai_compatible
|
|
83
|
+
endpoint = ask_endpoint
|
|
84
|
+
return nil unless endpoint
|
|
85
|
+
|
|
86
|
+
{ provider: :openai_compatible, provider_key: 'openai-compatible',
|
|
87
|
+
display_name: 'OpenAI-Compatible', endpoint: endpoint }
|
|
88
|
+
end
|
|
89
|
+
rescue TTY::Reader::InputInterrupt
|
|
90
|
+
CLI::Formatters::ProgressFormatter.error('Cancelled')
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def ask_endpoint
|
|
95
|
+
url = ask_url('API endpoint URL (e.g., http://localhost:11434):')
|
|
96
|
+
return nil unless url
|
|
97
|
+
|
|
98
|
+
url
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def get_credentials(provider_info)
|
|
102
|
+
case provider_info[:provider]
|
|
103
|
+
when :anthropic
|
|
104
|
+
show_credential_help('Anthropic', 'https://console.anthropic.com')
|
|
105
|
+
api_key = ask_secret('Enter your Anthropic API key:')
|
|
106
|
+
return nil unless api_key
|
|
107
|
+
|
|
108
|
+
{ api_key: api_key }
|
|
109
|
+
when :openai
|
|
110
|
+
show_credential_help('OpenAI', 'https://platform.openai.com/api-keys')
|
|
111
|
+
api_key = ask_secret('Enter your OpenAI API key:')
|
|
112
|
+
return nil unless api_key
|
|
113
|
+
|
|
114
|
+
{ api_key: api_key }
|
|
115
|
+
when :openai_compatible
|
|
116
|
+
needs_auth = ask_yes_no('Does this endpoint require authentication?', default: false)
|
|
117
|
+
return nil if needs_auth.nil?
|
|
118
|
+
|
|
119
|
+
api_key = needs_auth ? ask_secret('Enter API key:') : nil
|
|
120
|
+
{ api_key: api_key, endpoint: provider_info[:endpoint] }
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def show_credential_help(_provider_name, url)
|
|
125
|
+
puts "If you need to, get your API key at #{pastel.cyan(url)}."
|
|
126
|
+
puts
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def test_connection(provider_info, credentials)
|
|
130
|
+
test_result = CLI::Formatters::ProgressFormatter.with_spinner('Testing connection') do
|
|
131
|
+
test_provider_connection(
|
|
132
|
+
provider_info[:provider],
|
|
133
|
+
api_key: credentials[:api_key],
|
|
134
|
+
endpoint: provider_info[:endpoint]
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
unless test_result[:success]
|
|
139
|
+
CLI::Formatters::ProgressFormatter.error("Connection failed: #{test_result[:error]}")
|
|
140
|
+
puts
|
|
141
|
+
retry_choice = ask_yes_no('Try again with different credentials?', default: false)
|
|
142
|
+
if retry_choice
|
|
143
|
+
new_credentials = get_credentials(provider_info)
|
|
144
|
+
return { success: false } unless new_credentials
|
|
145
|
+
|
|
146
|
+
return test_connection(provider_info, new_credentials)
|
|
147
|
+
end
|
|
148
|
+
return { success: false }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
CLI::Formatters::ProgressFormatter.success('Connection successful')
|
|
152
|
+
{ success: true }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def select_model(provider_info, credentials)
|
|
156
|
+
available_models = fetch_provider_models(
|
|
157
|
+
provider_info[:provider],
|
|
158
|
+
api_key: credentials[:api_key],
|
|
159
|
+
endpoint: provider_info[:endpoint]
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
if available_models.nil? || available_models.empty?
|
|
163
|
+
CLI::Formatters::ProgressFormatter.warn('Could not fetch available models')
|
|
164
|
+
puts
|
|
165
|
+
model_id = prompt.ask('Enter model identifier manually:') do |q|
|
|
166
|
+
q.required true
|
|
167
|
+
end
|
|
168
|
+
return model_id
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
ask_select('Select a model:', available_models, per_page: 10)
|
|
172
|
+
rescue TTY::Reader::InputInterrupt
|
|
173
|
+
CLI::Formatters::ProgressFormatter.error('Cancelled')
|
|
174
|
+
nil
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def get_model_name(model_id)
|
|
178
|
+
# Generate smart default from model_id
|
|
179
|
+
# Examples: "gpt-4-turbo" → "gpt-4-turbo", "claude-3-opus-20240229" → "claude-3-opus-20240229"
|
|
180
|
+
# "mistralai/magistral-small-2509" → "magistral-small-2509"
|
|
181
|
+
default_name = model_id.split('/').last.downcase.gsub(/[^0-9a-z]/i, '-').gsub(/-+/, '-')
|
|
182
|
+
default_name = default_name[0..62] if default_name.length > 63 # K8s limit is 63 chars
|
|
183
|
+
|
|
184
|
+
ask_k8s_name('Your name for this model:', default: default_name)
|
|
185
|
+
rescue TTY::Reader::InputInterrupt
|
|
186
|
+
CLI::Formatters::ProgressFormatter.error('Cancelled')
|
|
187
|
+
nil
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def create_model_resource(model_name, provider_info, credentials, model_id)
|
|
191
|
+
# Check if model already exists
|
|
192
|
+
begin
|
|
193
|
+
ctx.client.get_resource('LanguageModel', model_name, ctx.namespace)
|
|
194
|
+
CLI::Formatters::ProgressFormatter.error("Model '#{model_name}' already exists in cluster '#{ctx.name}'")
|
|
195
|
+
puts
|
|
196
|
+
puts "Use 'aictl model inspect #{model_name}' to view details"
|
|
197
|
+
puts "Use 'aictl model edit #{model_name}' to modify it"
|
|
198
|
+
return false
|
|
199
|
+
rescue K8s::Error::NotFound
|
|
200
|
+
# Expected - model doesn't exist yet
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
CLI::Formatters::ProgressFormatter.with_spinner("Creating model '#{model_name}'") do
|
|
204
|
+
# Create API key secret if provided
|
|
205
|
+
if credentials[:api_key]
|
|
206
|
+
secret_name = "#{model_name}-api-key"
|
|
207
|
+
secret = {
|
|
208
|
+
'apiVersion' => 'v1',
|
|
209
|
+
'kind' => 'Secret',
|
|
210
|
+
'metadata' => {
|
|
211
|
+
'name' => secret_name,
|
|
212
|
+
'namespace' => ctx.namespace
|
|
213
|
+
},
|
|
214
|
+
'type' => 'Opaque',
|
|
215
|
+
'stringData' => {
|
|
216
|
+
'api-key' => credentials[:api_key]
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
ctx.client.apply_resource(secret)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Create LanguageModel resource
|
|
223
|
+
resource = Kubernetes::ResourceBuilder.language_model(
|
|
224
|
+
model_name,
|
|
225
|
+
provider: provider_info[:provider_key],
|
|
226
|
+
model: model_id,
|
|
227
|
+
endpoint: provider_info[:endpoint],
|
|
228
|
+
cluster: ctx.namespace
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Add API key reference if secret was created
|
|
232
|
+
if credentials[:api_key]
|
|
233
|
+
resource['spec']['apiKeySecret'] = {
|
|
234
|
+
'name' => "#{model_name}-api-key",
|
|
235
|
+
'key' => 'api-key'
|
|
236
|
+
}
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
ctx.client.apply_resource(resource)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
true
|
|
243
|
+
rescue StandardError => e
|
|
244
|
+
CLI::Formatters::ProgressFormatter.error("Failed to create model: #{e.message}")
|
|
245
|
+
false
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def show_success(model_name, model_id, provider_info)
|
|
249
|
+
puts
|
|
250
|
+
puts pastel.yellow.bold('Model Details:')
|
|
251
|
+
puts " Name: #{model_name}"
|
|
252
|
+
puts " Provider: #{provider_info[:display_name]}"
|
|
253
|
+
puts " Model: #{model_id}"
|
|
254
|
+
puts " Endpoint: #{provider_info[:endpoint]}" if provider_info[:endpoint]
|
|
255
|
+
puts " Cluster: #{ctx.name}"
|
|
256
|
+
puts
|
|
257
|
+
puts pastel.bold('Next steps:')
|
|
258
|
+
puts ' 1. Use this model in an agent:'
|
|
259
|
+
puts " #{pastel.dim("aictl agent create --model #{model_name}")}"
|
|
260
|
+
puts ' 2. View model details:'
|
|
261
|
+
puts " #{pastel.dim("aictl model inspect #{model_name}")}"
|
|
262
|
+
puts
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
# rubocop:enable Metrics/ClassLength, Metrics/MethodLength, Naming/PredicateMethod
|
|
266
|
+
end
|
|
267
|
+
end
|