console_agent 0.2.0 → 0.3.0
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/README.md +117 -231
- data/app/controllers/console_agent/application_controller.rb +21 -0
- data/app/controllers/console_agent/sessions_controller.rb +16 -0
- data/app/helpers/console_agent/sessions_helper.rb +42 -0
- data/app/models/console_agent/session.rb +23 -0
- data/app/views/console_agent/sessions/index.html.erb +57 -0
- data/app/views/console_agent/sessions/show.html.erb +56 -0
- data/app/views/layouts/console_agent/application.html.erb +83 -0
- data/config/routes.rb +4 -0
- data/lib/console_agent/configuration.rb +8 -4
- data/lib/console_agent/console_methods.rb +125 -5
- data/lib/console_agent/context_builder.rb +12 -132
- data/lib/console_agent/engine.rb +5 -0
- data/lib/console_agent/executor.rb +19 -1
- data/lib/console_agent/repl.rb +299 -34
- data/lib/console_agent/session_logger.rb +79 -0
- data/lib/console_agent/tools/registry.rb +156 -2
- data/lib/console_agent/version.rb +1 -1
- data/lib/console_agent.rb +125 -3
- data/lib/generators/console_agent/templates/initializer.rb +14 -6
- metadata +11 -1
|
@@ -6,9 +6,10 @@ module ConsoleAgent
|
|
|
6
6
|
attr_reader :definitions
|
|
7
7
|
|
|
8
8
|
# Tools that should never be cached (side effects or user interaction)
|
|
9
|
-
NO_CACHE = %w[ask_user save_memory delete_memory].freeze
|
|
9
|
+
NO_CACHE = %w[ask_user save_memory delete_memory execute_plan].freeze
|
|
10
10
|
|
|
11
|
-
def initialize
|
|
11
|
+
def initialize(executor: nil)
|
|
12
|
+
@executor = executor
|
|
12
13
|
@definitions = []
|
|
13
14
|
@handlers = {}
|
|
14
15
|
@cache = {}
|
|
@@ -180,6 +181,7 @@ module ConsoleAgent
|
|
|
180
181
|
)
|
|
181
182
|
|
|
182
183
|
register_memory_tools
|
|
184
|
+
register_execute_plan
|
|
183
185
|
end
|
|
184
186
|
|
|
185
187
|
def register_memory_tools
|
|
@@ -232,6 +234,158 @@ module ConsoleAgent
|
|
|
232
234
|
)
|
|
233
235
|
end
|
|
234
236
|
|
|
237
|
+
def register_execute_plan
|
|
238
|
+
return unless @executor
|
|
239
|
+
|
|
240
|
+
register(
|
|
241
|
+
name: 'execute_plan',
|
|
242
|
+
description: 'Execute a multi-step plan. Each step has a description and Ruby code. The plan is shown to the user for approval, then each step is executed in order. After each step executes, its return value is stored as step1, step2, etc. Use these variables in later steps to reference earlier results (e.g. `token = step1`).',
|
|
243
|
+
parameters: {
|
|
244
|
+
'type' => 'object',
|
|
245
|
+
'properties' => {
|
|
246
|
+
'steps' => {
|
|
247
|
+
'type' => 'array',
|
|
248
|
+
'description' => 'Ordered list of steps to execute',
|
|
249
|
+
'items' => {
|
|
250
|
+
'type' => 'object',
|
|
251
|
+
'properties' => {
|
|
252
|
+
'description' => { 'type' => 'string', 'description' => 'What this step does' },
|
|
253
|
+
'code' => { 'type' => 'string', 'description' => 'Ruby code to execute' }
|
|
254
|
+
},
|
|
255
|
+
'required' => %w[description code]
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
'required' => ['steps']
|
|
260
|
+
},
|
|
261
|
+
handler: ->(args) { execute_plan(args['steps'] || []) }
|
|
262
|
+
)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def execute_plan(steps)
|
|
266
|
+
return 'No steps provided.' if steps.nil? || steps.empty?
|
|
267
|
+
|
|
268
|
+
auto = ConsoleAgent.configuration.auto_execute
|
|
269
|
+
|
|
270
|
+
# Display full plan
|
|
271
|
+
$stdout.puts
|
|
272
|
+
$stdout.puts "\e[36m Plan (#{steps.length} steps):\e[0m"
|
|
273
|
+
steps.each_with_index do |step, i|
|
|
274
|
+
$stdout.puts "\e[36m #{i + 1}. #{step['description']}\e[0m"
|
|
275
|
+
$stdout.puts highlight_plan_code(step['code'])
|
|
276
|
+
end
|
|
277
|
+
$stdout.puts
|
|
278
|
+
|
|
279
|
+
# Ask for plan approval (unless auto-execute)
|
|
280
|
+
skip_confirmations = auto
|
|
281
|
+
unless auto
|
|
282
|
+
$stdout.print "\e[33m Accept plan? [y/N/a(uto)] \e[0m"
|
|
283
|
+
answer = $stdin.gets.to_s.strip.downcase
|
|
284
|
+
case answer
|
|
285
|
+
when 'a', 'auto'
|
|
286
|
+
skip_confirmations = true
|
|
287
|
+
when 'y', 'yes'
|
|
288
|
+
# proceed with per-step confirmation
|
|
289
|
+
else
|
|
290
|
+
$stdout.puts "\e[33m Plan declined.\e[0m"
|
|
291
|
+
feedback = ask_feedback("What would you like changed?")
|
|
292
|
+
return "User declined the plan. Feedback: #{feedback}"
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Execute steps one by one
|
|
297
|
+
results = []
|
|
298
|
+
steps.each_with_index do |step, i|
|
|
299
|
+
$stdout.puts
|
|
300
|
+
$stdout.puts "\e[36m Step #{i + 1}/#{steps.length}: #{step['description']}\e[0m"
|
|
301
|
+
$stdout.puts "\e[33m # Code:\e[0m"
|
|
302
|
+
$stdout.puts highlight_plan_code(step['code'])
|
|
303
|
+
|
|
304
|
+
# Per-step confirmation (unless auto-execute or plan-level auto)
|
|
305
|
+
unless skip_confirmations
|
|
306
|
+
$stdout.print "\e[33m Run? [y/N/edit] \e[0m"
|
|
307
|
+
step_answer = $stdin.gets.to_s.strip.downcase
|
|
308
|
+
|
|
309
|
+
case step_answer
|
|
310
|
+
when 'e', 'edit'
|
|
311
|
+
edited = edit_step_code(step['code'])
|
|
312
|
+
if edited && edited != step['code']
|
|
313
|
+
$stdout.puts "\e[33m # Edited code:\e[0m"
|
|
314
|
+
$stdout.puts highlight_plan_code(edited)
|
|
315
|
+
$stdout.print "\e[33m Run edited code? [y/N] \e[0m"
|
|
316
|
+
confirm = $stdin.gets.to_s.strip.downcase
|
|
317
|
+
unless confirm == 'y' || confirm == 'yes'
|
|
318
|
+
feedback = ask_feedback("What would you like changed?")
|
|
319
|
+
results << "Step #{i + 1}: User declined after edit. Feedback: #{feedback}"
|
|
320
|
+
break
|
|
321
|
+
end
|
|
322
|
+
step['code'] = edited
|
|
323
|
+
end
|
|
324
|
+
when 'y', 'yes'
|
|
325
|
+
# proceed
|
|
326
|
+
else
|
|
327
|
+
feedback = ask_feedback("What would you like changed?")
|
|
328
|
+
results << "Step #{i + 1}: User declined. Feedback: #{feedback}"
|
|
329
|
+
break
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
exec_result = @executor.execute(step['code'])
|
|
334
|
+
# Make result available as step1, step2, etc. for subsequent steps
|
|
335
|
+
@executor.binding_context.local_variable_set(:"step#{i + 1}", exec_result)
|
|
336
|
+
output = @executor.last_output
|
|
337
|
+
|
|
338
|
+
step_report = "Step #{i + 1} (#{step['description']}):\n"
|
|
339
|
+
if output && !output.strip.empty?
|
|
340
|
+
step_report += "Output: #{output.strip}\n"
|
|
341
|
+
end
|
|
342
|
+
step_report += "Return value: #{exec_result.inspect}"
|
|
343
|
+
results << step_report
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
results.join("\n\n")
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def highlight_plan_code(code)
|
|
350
|
+
if coderay_available?
|
|
351
|
+
CodeRay.scan(code, :ruby).terminal.gsub(/^/, ' ')
|
|
352
|
+
else
|
|
353
|
+
code.split("\n").map { |l| " \e[37m#{l}\e[0m" }.join("\n")
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def edit_step_code(code)
|
|
358
|
+
require 'tempfile'
|
|
359
|
+
editor = ENV['EDITOR'] || 'vi'
|
|
360
|
+
tmpfile = Tempfile.new(['console_agent_step', '.rb'])
|
|
361
|
+
tmpfile.write(code)
|
|
362
|
+
tmpfile.flush
|
|
363
|
+
system("#{editor} #{tmpfile.path}")
|
|
364
|
+
File.read(tmpfile.path).strip
|
|
365
|
+
rescue => e
|
|
366
|
+
$stderr.puts "\e[31m Editor error: #{e.message}\e[0m"
|
|
367
|
+
code
|
|
368
|
+
ensure
|
|
369
|
+
tmpfile.close! if tmpfile
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def coderay_available?
|
|
373
|
+
return @coderay_available unless @coderay_available.nil?
|
|
374
|
+
@coderay_available = begin
|
|
375
|
+
require 'coderay'
|
|
376
|
+
true
|
|
377
|
+
rescue LoadError
|
|
378
|
+
false
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def ask_feedback(prompt)
|
|
383
|
+
$stdout.print "\e[36m #{prompt} > \e[0m"
|
|
384
|
+
feedback = $stdin.gets
|
|
385
|
+
return '(no feedback provided)' if feedback.nil?
|
|
386
|
+
feedback.strip.empty? ? '(no feedback provided)' : feedback.strip
|
|
387
|
+
end
|
|
388
|
+
|
|
235
389
|
def ask_user(question)
|
|
236
390
|
$stdout.puts "\e[36m ? #{question}\e[0m"
|
|
237
391
|
$stdout.print "\e[36m > \e[0m"
|
data/lib/console_agent.rb
CHANGED
|
@@ -45,6 +45,14 @@ module ConsoleAgent
|
|
|
45
45
|
@logger = log
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
def current_user
|
|
49
|
+
@current_user
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def current_user=(name)
|
|
53
|
+
@current_user = name
|
|
54
|
+
end
|
|
55
|
+
|
|
48
56
|
def status
|
|
49
57
|
c = configuration
|
|
50
58
|
key = c.resolved_api_key
|
|
@@ -59,18 +67,132 @@ module ConsoleAgent
|
|
|
59
67
|
lines << " Provider: #{c.provider}"
|
|
60
68
|
lines << " Model: #{c.resolved_model}"
|
|
61
69
|
lines << " API key: #{masked_key}"
|
|
62
|
-
lines << " Context mode: #{c.context_mode}"
|
|
63
70
|
lines << " Max tokens: #{c.max_tokens}"
|
|
64
71
|
lines << " Temperature: #{c.temperature}"
|
|
65
72
|
lines << " Timeout: #{c.timeout}s"
|
|
66
|
-
lines << " Max tool rounds:#{c.max_tool_rounds}"
|
|
73
|
+
lines << " Max tool rounds:#{c.max_tool_rounds}"
|
|
67
74
|
lines << " Auto-execute: #{c.auto_execute}"
|
|
68
75
|
lines << " Memories: #{c.memories_enabled}"
|
|
76
|
+
lines << " Session logging:#{session_table_status}"
|
|
69
77
|
lines << " Debug: #{c.debug}"
|
|
70
78
|
$stdout.puts lines.join("\n")
|
|
71
79
|
nil
|
|
72
80
|
end
|
|
81
|
+
|
|
82
|
+
def setup!
|
|
83
|
+
conn = session_connection
|
|
84
|
+
table = 'console_agent_sessions'
|
|
85
|
+
|
|
86
|
+
if conn.table_exists?(table)
|
|
87
|
+
$stdout.puts "\e[32mConsoleAgent: #{table} already exists. Run ConsoleAgent.teardown! first to recreate.\e[0m"
|
|
88
|
+
else
|
|
89
|
+
conn.create_table(table) do |t|
|
|
90
|
+
t.text :query, null: false
|
|
91
|
+
t.text :conversation, null: false
|
|
92
|
+
t.integer :input_tokens, default: 0
|
|
93
|
+
t.integer :output_tokens, default: 0
|
|
94
|
+
t.string :user_name, limit: 255
|
|
95
|
+
t.string :mode, limit: 20, null: false
|
|
96
|
+
t.text :code_executed
|
|
97
|
+
t.text :code_output
|
|
98
|
+
t.text :code_result
|
|
99
|
+
t.text :console_output
|
|
100
|
+
t.boolean :executed, default: false
|
|
101
|
+
t.string :provider, limit: 50
|
|
102
|
+
t.string :model, limit: 100
|
|
103
|
+
t.string :name, limit: 255
|
|
104
|
+
t.integer :duration_ms
|
|
105
|
+
t.datetime :created_at, null: false
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
conn.add_index(table, :created_at)
|
|
109
|
+
conn.add_index(table, :user_name)
|
|
110
|
+
conn.add_index(table, :name)
|
|
111
|
+
|
|
112
|
+
$stdout.puts "\e[32mConsoleAgent: created #{table} table.\e[0m"
|
|
113
|
+
end
|
|
114
|
+
rescue => e
|
|
115
|
+
$stderr.puts "\e[31mConsoleAgent setup failed: #{e.class}: #{e.message}\e[0m"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def migrate!
|
|
119
|
+
conn = session_connection
|
|
120
|
+
table = 'console_agent_sessions'
|
|
121
|
+
|
|
122
|
+
unless conn.table_exists?(table)
|
|
123
|
+
$stderr.puts "\e[33mConsoleAgent: #{table} does not exist. Run ConsoleAgent.setup! first.\e[0m"
|
|
124
|
+
return
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
migrations = []
|
|
128
|
+
|
|
129
|
+
unless conn.column_exists?(table, :name)
|
|
130
|
+
conn.add_column(table, :name, :string, limit: 255)
|
|
131
|
+
conn.add_index(table, :name) unless conn.index_exists?(table, :name)
|
|
132
|
+
migrations << 'name'
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
if migrations.empty?
|
|
136
|
+
$stdout.puts "\e[32mConsoleAgent: #{table} is up to date.\e[0m"
|
|
137
|
+
else
|
|
138
|
+
$stdout.puts "\e[32mConsoleAgent: added columns: #{migrations.join(', ')}.\e[0m"
|
|
139
|
+
end
|
|
140
|
+
rescue => e
|
|
141
|
+
$stderr.puts "\e[31mConsoleAgent migrate failed: #{e.class}: #{e.message}\e[0m"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def teardown!
|
|
145
|
+
conn = session_connection
|
|
146
|
+
table = 'console_agent_sessions'
|
|
147
|
+
|
|
148
|
+
unless conn.table_exists?(table)
|
|
149
|
+
$stdout.puts "\e[33mConsoleAgent: #{table} does not exist, nothing to remove.\e[0m"
|
|
150
|
+
return
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
count = conn.select_value("SELECT COUNT(*) FROM #{conn.quote_table_name(table)}")
|
|
154
|
+
$stdout.print "\e[33mDrop #{table} (#{count} sessions)? [y/N] \e[0m"
|
|
155
|
+
answer = $stdin.gets.to_s.strip.downcase
|
|
156
|
+
|
|
157
|
+
unless answer == 'y' || answer == 'yes'
|
|
158
|
+
$stdout.puts "\e[33mCancelled.\e[0m"
|
|
159
|
+
return
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
conn.drop_table(table)
|
|
163
|
+
$stdout.puts "\e[32mConsoleAgent: dropped #{table}.\e[0m"
|
|
164
|
+
rescue => e
|
|
165
|
+
$stderr.puts "\e[31mConsoleAgent teardown failed: #{e.class}: #{e.message}\e[0m"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
private
|
|
169
|
+
|
|
170
|
+
def session_table_status
|
|
171
|
+
return 'disabled' unless configuration.session_logging
|
|
172
|
+
conn = session_connection
|
|
173
|
+
if conn.table_exists?('console_agent_sessions')
|
|
174
|
+
count = conn.select_value("SELECT COUNT(*) FROM #{conn.quote_table_name('console_agent_sessions')}")
|
|
175
|
+
"\e[32m#{count} sessions\e[0m"
|
|
176
|
+
else
|
|
177
|
+
"\e[33mtable missing (run ConsoleAgent.setup!)\e[0m"
|
|
178
|
+
end
|
|
179
|
+
rescue
|
|
180
|
+
"\e[33munavailable\e[0m"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def session_connection
|
|
184
|
+
klass = configuration.connection_class
|
|
185
|
+
if klass
|
|
186
|
+
klass = Object.const_get(klass) if klass.is_a?(String)
|
|
187
|
+
klass.connection
|
|
188
|
+
else
|
|
189
|
+
ActiveRecord::Base.connection
|
|
190
|
+
end
|
|
191
|
+
end
|
|
73
192
|
end
|
|
74
193
|
end
|
|
75
194
|
|
|
76
|
-
|
|
195
|
+
if defined?(Rails::Railtie)
|
|
196
|
+
require 'console_agent/railtie'
|
|
197
|
+
require 'console_agent/engine'
|
|
198
|
+
end
|
|
@@ -17,12 +17,7 @@ ConsoleAgent.configure do |config|
|
|
|
17
17
|
# Auto-execute generated code without confirmation (use with caution!)
|
|
18
18
|
config.auto_execute = false
|
|
19
19
|
|
|
20
|
-
#
|
|
21
|
-
# :smart - (default) minimal system prompt, LLM uses tools to fetch schema/model/code details on demand
|
|
22
|
-
# :full - sends all schema, models, and routes in the system prompt every time
|
|
23
|
-
config.context_mode = :smart
|
|
24
|
-
|
|
25
|
-
# Max tool-use rounds per query in :smart mode (safety cap)
|
|
20
|
+
# Max tool-use rounds per query (safety cap)
|
|
26
21
|
config.max_tool_rounds = 10
|
|
27
22
|
|
|
28
23
|
# HTTP timeout in seconds
|
|
@@ -30,4 +25,17 @@ ConsoleAgent.configure do |config|
|
|
|
30
25
|
|
|
31
26
|
# Debug mode: prints full API requests/responses and tool calls to stderr
|
|
32
27
|
# config.debug = true
|
|
28
|
+
|
|
29
|
+
# Session logging: persist AI sessions to the database
|
|
30
|
+
# Run ConsoleAgent.setup! in the Rails console to create the table
|
|
31
|
+
config.session_logging = true
|
|
32
|
+
|
|
33
|
+
# Database connection for ConsoleAgent tables (default: ActiveRecord::Base)
|
|
34
|
+
# Set to a class that responds to .connection if tables live on a different DB
|
|
35
|
+
# config.connection_class = Sharding::CentralizedModel
|
|
36
|
+
|
|
37
|
+
# Admin UI credentials (mount ConsoleAgent::Engine => '/console_agent' in routes.rb)
|
|
38
|
+
# When nil, no authentication is required (convenient for development)
|
|
39
|
+
# config.admin_username = 'admin'
|
|
40
|
+
# config.admin_password = 'changeme'
|
|
33
41
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: console_agent
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Cortfr
|
|
@@ -88,16 +88,26 @@ extra_rdoc_files: []
|
|
|
88
88
|
files:
|
|
89
89
|
- LICENSE
|
|
90
90
|
- README.md
|
|
91
|
+
- app/controllers/console_agent/application_controller.rb
|
|
92
|
+
- app/controllers/console_agent/sessions_controller.rb
|
|
93
|
+
- app/helpers/console_agent/sessions_helper.rb
|
|
94
|
+
- app/models/console_agent/session.rb
|
|
95
|
+
- app/views/console_agent/sessions/index.html.erb
|
|
96
|
+
- app/views/console_agent/sessions/show.html.erb
|
|
97
|
+
- app/views/layouts/console_agent/application.html.erb
|
|
98
|
+
- config/routes.rb
|
|
91
99
|
- lib/console_agent.rb
|
|
92
100
|
- lib/console_agent/configuration.rb
|
|
93
101
|
- lib/console_agent/console_methods.rb
|
|
94
102
|
- lib/console_agent/context_builder.rb
|
|
103
|
+
- lib/console_agent/engine.rb
|
|
95
104
|
- lib/console_agent/executor.rb
|
|
96
105
|
- lib/console_agent/providers/anthropic.rb
|
|
97
106
|
- lib/console_agent/providers/base.rb
|
|
98
107
|
- lib/console_agent/providers/openai.rb
|
|
99
108
|
- lib/console_agent/railtie.rb
|
|
100
109
|
- lib/console_agent/repl.rb
|
|
110
|
+
- lib/console_agent/session_logger.rb
|
|
101
111
|
- lib/console_agent/storage/base.rb
|
|
102
112
|
- lib/console_agent/storage/file_storage.rb
|
|
103
113
|
- lib/console_agent/tools/code_tools.rb
|