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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -8
  3. data/CHANGELOG.md +14 -0
  4. data/CI_STATUS.md +56 -0
  5. data/Gemfile.lock +2 -2
  6. data/Makefile +22 -6
  7. data/lib/language_operator/agent/base.rb +10 -6
  8. data/lib/language_operator/agent/executor.rb +19 -97
  9. data/lib/language_operator/agent/safety/ast_validator.rb +62 -43
  10. data/lib/language_operator/agent/safety/safe_executor.rb +27 -2
  11. data/lib/language_operator/agent/scheduler.rb +60 -0
  12. data/lib/language_operator/agent/task_executor.rb +548 -0
  13. data/lib/language_operator/agent.rb +90 -27
  14. data/lib/language_operator/cli/base_command.rb +117 -0
  15. data/lib/language_operator/cli/commands/agent.rb +339 -407
  16. data/lib/language_operator/cli/commands/cluster.rb +274 -290
  17. data/lib/language_operator/cli/commands/install.rb +110 -119
  18. data/lib/language_operator/cli/commands/model.rb +284 -184
  19. data/lib/language_operator/cli/commands/persona.rb +218 -284
  20. data/lib/language_operator/cli/commands/quickstart.rb +4 -5
  21. data/lib/language_operator/cli/commands/status.rb +31 -35
  22. data/lib/language_operator/cli/commands/system.rb +221 -233
  23. data/lib/language_operator/cli/commands/tool.rb +356 -422
  24. data/lib/language_operator/cli/commands/use.rb +19 -22
  25. data/lib/language_operator/cli/helpers/resource_dependency_checker.rb +0 -18
  26. data/lib/language_operator/cli/wizards/quickstart_wizard.rb +0 -1
  27. data/lib/language_operator/client/config.rb +20 -21
  28. data/lib/language_operator/config.rb +115 -3
  29. data/lib/language_operator/constants.rb +54 -0
  30. data/lib/language_operator/dsl/agent_context.rb +7 -7
  31. data/lib/language_operator/dsl/agent_definition.rb +111 -26
  32. data/lib/language_operator/dsl/config.rb +30 -66
  33. data/lib/language_operator/dsl/main_definition.rb +114 -0
  34. data/lib/language_operator/dsl/schema.rb +84 -43
  35. data/lib/language_operator/dsl/task_definition.rb +315 -0
  36. data/lib/language_operator/dsl.rb +0 -1
  37. data/lib/language_operator/instrumentation/task_tracer.rb +285 -0
  38. data/lib/language_operator/logger.rb +4 -4
  39. data/lib/language_operator/synthesis_test_harness.rb +324 -0
  40. data/lib/language_operator/templates/examples/agent_synthesis.tmpl +26 -8
  41. data/lib/language_operator/templates/schema/CHANGELOG.md +26 -0
  42. data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
  43. data/lib/language_operator/templates/schema/agent_dsl_schema.json +84 -42
  44. data/lib/language_operator/type_coercion.rb +250 -0
  45. data/lib/language_operator/ux/base.rb +81 -0
  46. data/lib/language_operator/ux/concerns/README.md +155 -0
  47. data/lib/language_operator/ux/concerns/headings.rb +90 -0
  48. data/lib/language_operator/ux/concerns/input_validation.rb +146 -0
  49. data/lib/language_operator/ux/concerns/provider_helpers.rb +167 -0
  50. data/lib/language_operator/ux/create_agent.rb +252 -0
  51. data/lib/language_operator/ux/create_model.rb +267 -0
  52. data/lib/language_operator/ux/quickstart.rb +594 -0
  53. data/lib/language_operator/version.rb +1 -1
  54. data/lib/language_operator.rb +2 -0
  55. data/requirements/ARCHITECTURE.md +1 -0
  56. data/requirements/SCRATCH.md +153 -0
  57. data/requirements/dsl.md +0 -0
  58. data/requirements/features +1 -0
  59. data/requirements/personas +1 -0
  60. data/requirements/proposals +1 -0
  61. data/requirements/tasks/iterate.md +14 -15
  62. data/requirements/tasks/optimize.md +13 -4
  63. data/synth/001/Makefile +90 -0
  64. data/synth/001/agent.rb +26 -0
  65. data/synth/001/agent.yaml +7 -0
  66. data/synth/001/output.log +44 -0
  67. data/synth/Makefile +39 -0
  68. data/synth/README.md +342 -0
  69. metadata +37 -10
  70. data/lib/language_operator/dsl/workflow_definition.rb +0 -259
  71. 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