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.
- checksums.yaml +4 -4
- data/.rubocop.yml +125 -0
- data/CHANGELOG.md +88 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +284 -0
- data/LICENSE +229 -21
- data/Makefile +82 -0
- data/README.md +3 -11
- data/Rakefile +63 -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/SCHEMA_VERSION.md +250 -0
- data/docs/dsl/agent-reference.md +604 -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/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 +127 -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 +1205 -0
- data/lib/language_operator/cli/commands/cluster.rb +371 -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 +393 -0
- data/lib/language_operator/cli/commands/quickstart.rb +22 -0
- data/lib/language_operator/cli/commands/status.rb +143 -0
- data/lib/language_operator/cli/commands/system.rb +772 -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 +77 -0
- data/lib/language_operator/cli/formatters/log_formatter.rb +288 -0
- data/lib/language_operator/cli/formatters/progress_formatter.rb +49 -0
- data/lib/language_operator/cli/formatters/status_formatter.rb +37 -0
- data/lib/language_operator/cli/formatters/table_formatter.rb +163 -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/pastel_helper.rb +24 -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 +236 -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/schema.rb +1102 -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 +161 -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/templates/README.md +23 -0
- data/lib/language_operator/templates/examples/agent_synthesis.tmpl +115 -0
- data/lib/language_operator/templates/examples/persona_distillation.tmpl +19 -0
- data/lib/language_operator/templates/schema/.gitkeep +0 -0
- data/lib/language_operator/templates/schema/CHANGELOG.md +93 -0
- data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +306 -0
- data/lib/language_operator/templates/schema/agent_dsl_schema.json +452 -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 +507 -20
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'thor'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require_relative 'commands/cluster'
|
|
6
|
+
require_relative 'commands/use'
|
|
7
|
+
require_relative 'commands/agent'
|
|
8
|
+
require_relative 'commands/status'
|
|
9
|
+
require_relative 'commands/persona'
|
|
10
|
+
require_relative 'commands/tool'
|
|
11
|
+
require_relative 'commands/model'
|
|
12
|
+
require_relative 'commands/quickstart'
|
|
13
|
+
require_relative 'commands/install'
|
|
14
|
+
require_relative 'commands/system'
|
|
15
|
+
require_relative 'formatters/progress_formatter'
|
|
16
|
+
require_relative '../config/cluster_config'
|
|
17
|
+
require_relative '../kubernetes/client'
|
|
18
|
+
|
|
19
|
+
module LanguageOperator
|
|
20
|
+
module CLI
|
|
21
|
+
# Main CLI class for aictl command
|
|
22
|
+
#
|
|
23
|
+
# Provides commands for creating, running, and managing language-operator resources.
|
|
24
|
+
class Main < Thor
|
|
25
|
+
def self.exit_on_failure?
|
|
26
|
+
true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
desc 'status', 'Show system status and overview'
|
|
30
|
+
def status
|
|
31
|
+
Commands::Status.new.invoke(:overview)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
desc 'version', 'Show aictl and operator version'
|
|
35
|
+
def version
|
|
36
|
+
puts "aictl v#{LanguageOperator::VERSION}"
|
|
37
|
+
puts
|
|
38
|
+
|
|
39
|
+
# Try to get operator version from current cluster
|
|
40
|
+
current_cluster = Config::ClusterConfig.current_cluster
|
|
41
|
+
if current_cluster
|
|
42
|
+
cluster_config = Config::ClusterConfig.get_cluster(current_cluster)
|
|
43
|
+
begin
|
|
44
|
+
k8s = Kubernetes::Client.new(
|
|
45
|
+
kubeconfig: cluster_config[:kubeconfig],
|
|
46
|
+
context: cluster_config[:context]
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if k8s.operator_installed?
|
|
50
|
+
operator_version = k8s.operator_version || 'unknown'
|
|
51
|
+
puts "Operator: v#{operator_version}"
|
|
52
|
+
puts "Cluster: #{current_cluster}"
|
|
53
|
+
|
|
54
|
+
# Check compatibility (simple version check)
|
|
55
|
+
# In the future, this could be more sophisticated
|
|
56
|
+
puts
|
|
57
|
+
Formatters::ProgressFormatter.success('Versions are compatible')
|
|
58
|
+
else
|
|
59
|
+
Formatters::ProgressFormatter.warn("Operator not installed in cluster '#{current_cluster}'")
|
|
60
|
+
end
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
Formatters::ProgressFormatter.error("Could not connect to cluster: #{e.message}")
|
|
63
|
+
end
|
|
64
|
+
else
|
|
65
|
+
puts 'No cluster selected'
|
|
66
|
+
puts
|
|
67
|
+
puts 'Select a cluster to check operator version:'
|
|
68
|
+
puts ' aictl use <cluster>'
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
desc 'cluster SUBCOMMAND ...ARGS', 'Manage language clusters'
|
|
73
|
+
subcommand 'cluster', Commands::Cluster
|
|
74
|
+
|
|
75
|
+
desc 'use CLUSTER', 'Switch to a different cluster context'
|
|
76
|
+
def use(cluster_name)
|
|
77
|
+
Commands::Use.new.switch(cluster_name)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
desc 'agent SUBCOMMAND ...ARGS', 'Manage autonomous agents'
|
|
81
|
+
subcommand 'agent', Commands::Agent
|
|
82
|
+
|
|
83
|
+
desc 'persona SUBCOMMAND ...ARGS', 'Manage agent personas'
|
|
84
|
+
subcommand 'persona', Commands::Persona
|
|
85
|
+
|
|
86
|
+
desc 'tool SUBCOMMAND ...ARGS', 'Manage MCP tools'
|
|
87
|
+
subcommand 'tool', Commands::Tool
|
|
88
|
+
|
|
89
|
+
desc 'model SUBCOMMAND ...ARGS', 'Manage language models'
|
|
90
|
+
subcommand 'model', Commands::Model
|
|
91
|
+
|
|
92
|
+
desc 'system SUBCOMMAND ...ARGS', 'System commands for schema and metadata'
|
|
93
|
+
subcommand 'system', Commands::System
|
|
94
|
+
|
|
95
|
+
desc 'quickstart', 'Interactive setup wizard for first-time users'
|
|
96
|
+
def quickstart
|
|
97
|
+
Commands::Quickstart.new.invoke(:start)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
desc 'install', 'Install the language-operator using Helm'
|
|
101
|
+
long_desc Commands::Install.long_desc_for(:install)
|
|
102
|
+
option :values, type: :string, desc: 'Path to custom Helm values file'
|
|
103
|
+
option :namespace, type: :string, default: Commands::Install::DEFAULT_NAMESPACE, desc: 'Kubernetes namespace'
|
|
104
|
+
option :version, type: :string, desc: 'Specific chart version to install'
|
|
105
|
+
option :dry_run, type: :boolean, default: false, desc: 'Preview installation without applying'
|
|
106
|
+
option :wait, type: :boolean, default: true, desc: 'Wait for deployment to complete'
|
|
107
|
+
option :create_namespace, type: :boolean, default: true, desc: 'Create namespace if it does not exist'
|
|
108
|
+
def install
|
|
109
|
+
Commands::Install.new([], options).install
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
desc 'upgrade', 'Upgrade the language-operator using Helm'
|
|
113
|
+
long_desc Commands::Install.long_desc_for(:upgrade)
|
|
114
|
+
option :values, type: :string, desc: 'Path to custom Helm values file'
|
|
115
|
+
option :namespace, type: :string, default: Commands::Install::DEFAULT_NAMESPACE, desc: 'Kubernetes namespace'
|
|
116
|
+
option :version, type: :string, desc: 'Specific chart version to upgrade to'
|
|
117
|
+
option :dry_run, type: :boolean, default: false, desc: 'Preview upgrade without applying'
|
|
118
|
+
option :wait, type: :boolean, default: true, desc: 'Wait for deployment to complete'
|
|
119
|
+
def upgrade
|
|
120
|
+
Commands::Install.new([], options).upgrade
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
desc 'uninstall', 'Uninstall the language-operator using Helm'
|
|
124
|
+
long_desc Commands::Install.long_desc_for(:uninstall)
|
|
125
|
+
option :namespace, type: :string, default: Commands::Install::DEFAULT_NAMESPACE, desc: 'Kubernetes namespace'
|
|
126
|
+
option :force, type: :boolean, default: false, desc: 'Skip confirmation prompt'
|
|
127
|
+
def uninstall
|
|
128
|
+
Commands::Install.new([], options).uninstall
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
desc 'completion SHELL', 'Install shell completion for aictl (bash, zsh, fish)'
|
|
132
|
+
long_desc <<-DESC
|
|
133
|
+
Install shell completion for aictl. Supports bash, zsh, and fish.
|
|
134
|
+
|
|
135
|
+
Examples:
|
|
136
|
+
aictl completion bash
|
|
137
|
+
aictl completion zsh
|
|
138
|
+
aictl completion fish
|
|
139
|
+
|
|
140
|
+
Manual installation:
|
|
141
|
+
bash: Add to ~/.bashrc:
|
|
142
|
+
source <(aictl completion bash --stdout)
|
|
143
|
+
|
|
144
|
+
zsh: Add to ~/.zshrc:
|
|
145
|
+
source <(aictl completion zsh --stdout)
|
|
146
|
+
|
|
147
|
+
fish: Run once:
|
|
148
|
+
aictl completion fish | source
|
|
149
|
+
DESC
|
|
150
|
+
option :stdout, type: :boolean, desc: 'Print completion script to stdout instead of installing'
|
|
151
|
+
def completion(shell)
|
|
152
|
+
case shell.downcase
|
|
153
|
+
when 'bash'
|
|
154
|
+
install_bash_completion
|
|
155
|
+
when 'zsh'
|
|
156
|
+
install_zsh_completion
|
|
157
|
+
when 'fish'
|
|
158
|
+
install_fish_completion
|
|
159
|
+
else
|
|
160
|
+
Formatters::ProgressFormatter.error("Unsupported shell: #{shell}")
|
|
161
|
+
puts
|
|
162
|
+
puts 'Supported shells: bash, zsh, fish'
|
|
163
|
+
exit 1
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
|
|
169
|
+
def install_bash_completion
|
|
170
|
+
completion_file = File.expand_path('../../completions/aictl.bash', __dir__)
|
|
171
|
+
|
|
172
|
+
if options[:stdout]
|
|
173
|
+
puts File.read(completion_file)
|
|
174
|
+
return
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
target = File.expand_path('~/.bash_completion.d/aictl')
|
|
178
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
179
|
+
FileUtils.cp(completion_file, target)
|
|
180
|
+
|
|
181
|
+
Formatters::ProgressFormatter.success('Bash completion installed')
|
|
182
|
+
puts
|
|
183
|
+
puts 'Add to your ~/.bashrc:'
|
|
184
|
+
puts ' [ -f ~/.bash_completion.d/aictl ] && source ~/.bash_completion.d/aictl'
|
|
185
|
+
puts
|
|
186
|
+
puts 'Then reload your shell:'
|
|
187
|
+
puts ' source ~/.bashrc'
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def install_zsh_completion
|
|
191
|
+
completion_file = File.expand_path('../../completions/_aictl', __dir__)
|
|
192
|
+
|
|
193
|
+
if options[:stdout]
|
|
194
|
+
puts File.read(completion_file)
|
|
195
|
+
return
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Check if user has a custom fpath directory
|
|
199
|
+
fpath_dir = File.expand_path('~/.zsh/completions')
|
|
200
|
+
FileUtils.mkdir_p(fpath_dir)
|
|
201
|
+
|
|
202
|
+
target = File.join(fpath_dir, '_aictl')
|
|
203
|
+
FileUtils.cp(completion_file, target)
|
|
204
|
+
|
|
205
|
+
Formatters::ProgressFormatter.success('Zsh completion installed')
|
|
206
|
+
puts
|
|
207
|
+
puts 'Add to your ~/.zshrc (before compinit):'
|
|
208
|
+
puts ' fpath=(~/.zsh/completions $fpath)'
|
|
209
|
+
puts ' autoload -Uz compinit && compinit'
|
|
210
|
+
puts
|
|
211
|
+
puts 'Then reload your shell:'
|
|
212
|
+
puts ' source ~/.zshrc'
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def install_fish_completion
|
|
216
|
+
completion_file = File.expand_path('../../completions/aictl.fish', __dir__)
|
|
217
|
+
|
|
218
|
+
if options[:stdout]
|
|
219
|
+
puts File.read(completion_file)
|
|
220
|
+
return
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
target = File.expand_path('~/.config/fish/completions/aictl.fish')
|
|
224
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
225
|
+
FileUtils.cp(completion_file, target)
|
|
226
|
+
|
|
227
|
+
Formatters::ProgressFormatter.success('Fish completion installed')
|
|
228
|
+
puts
|
|
229
|
+
puts 'Reload completions:'
|
|
230
|
+
puts ' fish_update_completions'
|
|
231
|
+
puts
|
|
232
|
+
puts 'Or restart your fish shell'
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
apiVersion: langop.io/v1alpha1
|
|
2
|
+
kind: LanguageTool
|
|
3
|
+
metadata:
|
|
4
|
+
name: <%= name %>
|
|
5
|
+
namespace: <%= namespace %>
|
|
6
|
+
spec:
|
|
7
|
+
type: <%= type || 'mcp' %>
|
|
8
|
+
image: <%= image || "changeme/#{name}-tool:latest" %>
|
|
9
|
+
imagePullPolicy: Always
|
|
10
|
+
deploymentMode: <%= deployment_mode || 'sidecar' %>
|
|
11
|
+
port: <%= port || 8080 %>
|
|
12
|
+
|
|
13
|
+
resources:
|
|
14
|
+
requests:
|
|
15
|
+
cpu: 50m
|
|
16
|
+
memory: 64Mi
|
|
17
|
+
limits:
|
|
18
|
+
cpu: 200m
|
|
19
|
+
memory: 256Mi
|
|
20
|
+
|
|
21
|
+
<% if egress && !egress.empty? -%>
|
|
22
|
+
egress:
|
|
23
|
+
<% egress.each do |rule| -%>
|
|
24
|
+
- description: <%= rule['description'] %>
|
|
25
|
+
<% if rule['cidr'] -%>
|
|
26
|
+
cidr: <%= rule['cidr'] %>
|
|
27
|
+
<% end -%>
|
|
28
|
+
<% if rule['dns'] -%>
|
|
29
|
+
dns:
|
|
30
|
+
<% rule['dns'].each do |dns| -%>
|
|
31
|
+
- "<%= dns %>"
|
|
32
|
+
<% end -%>
|
|
33
|
+
<% end -%>
|
|
34
|
+
<% if rule['ports'] -%>
|
|
35
|
+
ports:
|
|
36
|
+
<% rule['ports'].each do |port_rule| -%>
|
|
37
|
+
- port: <%= port_rule['port'] %>
|
|
38
|
+
protocol: <%= port_rule['protocol'] %>
|
|
39
|
+
<% end -%>
|
|
40
|
+
<% end -%>
|
|
41
|
+
<% end -%>
|
|
42
|
+
<% else -%>
|
|
43
|
+
egress: []
|
|
44
|
+
<% end -%>
|
|
45
|
+
|
|
46
|
+
<% if rbac -%>
|
|
47
|
+
rbac:
|
|
48
|
+
<% if rbac['clusterRole'] -%>
|
|
49
|
+
clusterRole:
|
|
50
|
+
rules:
|
|
51
|
+
<% rbac['clusterRole']['rules'].each do |rule| -%>
|
|
52
|
+
- apiGroups:
|
|
53
|
+
<% rule['apiGroups'].each do |group| -%>
|
|
54
|
+
- <%= group %>
|
|
55
|
+
<% end -%>
|
|
56
|
+
resources:
|
|
57
|
+
<% rule['resources'].each do |resource| -%>
|
|
58
|
+
- <%= resource %>
|
|
59
|
+
<% end -%>
|
|
60
|
+
verbs:
|
|
61
|
+
<% rule['verbs'].each do |verb| -%>
|
|
62
|
+
- <%= verb %>
|
|
63
|
+
<% end -%>
|
|
64
|
+
<% end -%>
|
|
65
|
+
<% end -%>
|
|
66
|
+
<% end -%>
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tty-prompt'
|
|
4
|
+
require 'pastel'
|
|
5
|
+
require_relative '../helpers/schedule_builder'
|
|
6
|
+
|
|
7
|
+
module LanguageOperator
|
|
8
|
+
module CLI
|
|
9
|
+
module Wizards
|
|
10
|
+
# Interactive wizard for creating agents
|
|
11
|
+
class AgentWizard
|
|
12
|
+
attr_reader :prompt, :pastel
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@prompt = TTY::Prompt.new
|
|
16
|
+
@pastel = Pastel.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Run the wizard and return the generated description
|
|
20
|
+
def run
|
|
21
|
+
show_welcome
|
|
22
|
+
|
|
23
|
+
# Step 1: Get task description
|
|
24
|
+
task = ask_task_description
|
|
25
|
+
|
|
26
|
+
# Step 2: Determine schedule
|
|
27
|
+
schedule_info = ask_schedule
|
|
28
|
+
|
|
29
|
+
# Step 3: Tool detection and configuration
|
|
30
|
+
tools_config = configure_tools(task)
|
|
31
|
+
|
|
32
|
+
# Step 4: Preview and confirm
|
|
33
|
+
description = build_description(task, schedule_info, tools_config)
|
|
34
|
+
|
|
35
|
+
show_preview(description, schedule_info, tools_config)
|
|
36
|
+
|
|
37
|
+
return nil unless confirm_creation?
|
|
38
|
+
|
|
39
|
+
description
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def show_welcome
|
|
45
|
+
puts
|
|
46
|
+
puts pastel.cyan('╭────────────────────────────────────────╮')
|
|
47
|
+
puts pastel.cyan('│ Let\'s create your agent! 🤖 │')
|
|
48
|
+
puts pastel.cyan('╰────────────────────────────────────────╯')
|
|
49
|
+
puts
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def ask_task_description
|
|
53
|
+
prompt.ask('What should your agent do?') do |q|
|
|
54
|
+
q.required true
|
|
55
|
+
q.validate(/\w+/)
|
|
56
|
+
q.messages[:valid?] = 'Please describe a task (cannot be empty)'
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def ask_schedule
|
|
61
|
+
puts
|
|
62
|
+
schedule_type = prompt.select('How often should it run?') do |menu|
|
|
63
|
+
menu.choice 'Every day at a specific time', :daily
|
|
64
|
+
menu.choice 'Every few minutes/hours', :interval
|
|
65
|
+
menu.choice 'Continuously (whenever something changes)', :continuous
|
|
66
|
+
menu.choice 'Only when I trigger it manually', :manual
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
case schedule_type
|
|
70
|
+
when :daily
|
|
71
|
+
ask_daily_schedule
|
|
72
|
+
when :interval
|
|
73
|
+
ask_interval_schedule
|
|
74
|
+
when :continuous
|
|
75
|
+
{ type: :continuous, description: 'continuously' }
|
|
76
|
+
when :manual
|
|
77
|
+
{ type: :manual, description: 'on manual trigger' }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def ask_daily_schedule
|
|
82
|
+
puts
|
|
83
|
+
time_input = prompt.ask('What time each day? (e.g., 4pm, 9:30am, 16:00):') do |q|
|
|
84
|
+
q.required true
|
|
85
|
+
q.validate(lambda do |input|
|
|
86
|
+
Helpers::ScheduleBuilder.parse_time(input)
|
|
87
|
+
true
|
|
88
|
+
rescue ArgumentError
|
|
89
|
+
false
|
|
90
|
+
end)
|
|
91
|
+
q.messages[:valid?] = 'Invalid time format. Try: 4pm, 9:30am, or 16:00'
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
time_24h = Helpers::ScheduleBuilder.parse_time(time_input)
|
|
95
|
+
cron = Helpers::ScheduleBuilder.daily_cron(time_24h)
|
|
96
|
+
|
|
97
|
+
{
|
|
98
|
+
type: :daily,
|
|
99
|
+
time: time_24h,
|
|
100
|
+
cron: cron,
|
|
101
|
+
description: "daily at #{time_input}"
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def ask_interval_schedule
|
|
106
|
+
puts
|
|
107
|
+
interval = prompt.ask('How often?', convert: :int) do |q|
|
|
108
|
+
q.required true
|
|
109
|
+
q.validate(->(v) { v.to_i.positive? })
|
|
110
|
+
q.messages[:valid?] = 'Please enter a positive number'
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
unit = prompt.select('Minutes, hours, or days?', %w[minutes hours days])
|
|
114
|
+
|
|
115
|
+
cron = Helpers::ScheduleBuilder.interval_cron(interval, unit)
|
|
116
|
+
|
|
117
|
+
{
|
|
118
|
+
type: :interval,
|
|
119
|
+
interval: interval,
|
|
120
|
+
unit: unit,
|
|
121
|
+
cron: cron,
|
|
122
|
+
description: "every #{interval} #{unit}"
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def configure_tools(task_description)
|
|
127
|
+
detected = detect_tools(task_description)
|
|
128
|
+
config = {}
|
|
129
|
+
|
|
130
|
+
return config if detected.empty?
|
|
131
|
+
|
|
132
|
+
puts
|
|
133
|
+
puts "I detected these tools: #{pastel.yellow(detected.join(', '))}"
|
|
134
|
+
puts
|
|
135
|
+
|
|
136
|
+
detected.each do |tool|
|
|
137
|
+
case tool
|
|
138
|
+
when 'email'
|
|
139
|
+
config[:email] = ask_email_config
|
|
140
|
+
when 'google-sheets', 'spreadsheet'
|
|
141
|
+
config[:spreadsheet] = ask_spreadsheet_config
|
|
142
|
+
when 'slack'
|
|
143
|
+
config[:slack] = ask_slack_config
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
config[:tools] = detected
|
|
148
|
+
config
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def detect_tools(description)
|
|
152
|
+
tools = []
|
|
153
|
+
text = description.downcase
|
|
154
|
+
|
|
155
|
+
tools << 'email' if text.match?(/email|mail|send.*message/i)
|
|
156
|
+
tools << 'google-sheets' if text.match?(/spreadsheet|sheet|excel|csv/i)
|
|
157
|
+
tools << 'slack' if text.match?(/slack/i)
|
|
158
|
+
tools << 'github' if text.match?(/github|git|repo/i)
|
|
159
|
+
|
|
160
|
+
tools
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def ask_email_config
|
|
164
|
+
email = prompt.ask('Your email for notifications:') do |q|
|
|
165
|
+
q.required true
|
|
166
|
+
q.validate(/\A[\w+\-.]+@[a-z\d-]+(\.[a-z\d-]+)*\.[a-z]+\z/i)
|
|
167
|
+
q.messages[:valid?] = 'Please enter a valid email address'
|
|
168
|
+
end
|
|
169
|
+
{ address: email }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def ask_spreadsheet_config
|
|
173
|
+
url = prompt.ask('Spreadsheet URL:') do |q|
|
|
174
|
+
q.required true
|
|
175
|
+
q.validate(%r{^https?://}i)
|
|
176
|
+
q.messages[:valid?] = 'Please enter a valid URL'
|
|
177
|
+
end
|
|
178
|
+
{ url: url }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def ask_slack_config
|
|
182
|
+
channel = prompt.ask('Slack channel (e.g., #general):') do |q|
|
|
183
|
+
q.required true
|
|
184
|
+
end
|
|
185
|
+
{ channel: channel }
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def build_description(task, schedule_info, tools_config)
|
|
189
|
+
parts = [task]
|
|
190
|
+
|
|
191
|
+
# Add schedule information
|
|
192
|
+
case schedule_info[:type]
|
|
193
|
+
when :daily
|
|
194
|
+
parts << schedule_info[:description]
|
|
195
|
+
when :interval
|
|
196
|
+
parts << schedule_info[:description]
|
|
197
|
+
when :continuous
|
|
198
|
+
# "continuously" might already be implied in task
|
|
199
|
+
parts << 'continuously' unless task.downcase.include?('continuous')
|
|
200
|
+
when :manual
|
|
201
|
+
# Manual trigger doesn't need to be in description
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Add tool-specific details
|
|
205
|
+
parts << "and email me at #{tools_config[:email][:address]}" if tools_config[:email]
|
|
206
|
+
|
|
207
|
+
if tools_config[:spreadsheet] && !task.include?('http')
|
|
208
|
+
# Replace generic "spreadsheet" with specific URL if not already present
|
|
209
|
+
parts << "using spreadsheet at #{tools_config[:spreadsheet][:url]}"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
parts << "and send results to #{tools_config[:slack][:channel]}" if tools_config[:slack]
|
|
213
|
+
|
|
214
|
+
parts.join(' ')
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def show_preview(description, schedule_info, tools_config)
|
|
218
|
+
puts
|
|
219
|
+
puts pastel.cyan('╭─ Preview ──────────────────────────────╮')
|
|
220
|
+
puts '│'
|
|
221
|
+
puts "│ #{pastel.bold('Task:')} #{description}"
|
|
222
|
+
|
|
223
|
+
if schedule_info[:type] == :manual
|
|
224
|
+
puts "│ #{pastel.bold('Mode:')} Manual trigger"
|
|
225
|
+
else
|
|
226
|
+
schedule_text = schedule_info[:description] || 'on demand'
|
|
227
|
+
puts "│ #{pastel.bold('Schedule:')} #{schedule_text}"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
puts "│ #{pastel.bold('Cron:')} #{pastel.dim(schedule_info[:cron])}" if schedule_info[:cron]
|
|
231
|
+
|
|
232
|
+
puts "│ #{pastel.bold('Tools:')} #{tools_config[:tools].join(', ')}" if tools_config[:tools]&.any?
|
|
233
|
+
|
|
234
|
+
puts '│'
|
|
235
|
+
puts pastel.cyan('╰────────────────────────────────────────╯')
|
|
236
|
+
puts
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def confirm_creation?
|
|
240
|
+
puts
|
|
241
|
+
prompt.yes?('Create this agent?')
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|