rails_console_ai 0.35.0 → 0.36.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/CHANGELOG.md +9 -0
- data/README.md +56 -2
- data/app/helpers/rails_console_ai/sessions_helper.rb +11 -11
- data/lib/generators/rails_console_ai/templates/initializer.rb +7 -1
- data/lib/rails_console_ai/channel/console.rb +120 -15
- data/lib/rails_console_ai/configuration.rb +44 -5
- data/lib/rails_console_ai/conversation_engine.rb +135 -35
- data/lib/rails_console_ai/line_editor.rb +142 -0
- data/lib/rails_console_ai/providers/base.rb +6 -1
- data/lib/rails_console_ai/providers/local.rb +16 -36
- data/lib/rails_console_ai/providers/openai.rb +30 -9
- data/lib/rails_console_ai/providers/openrouter.rb +100 -0
- data/lib/rails_console_ai/slack_bot.rb +18 -11
- data/lib/rails_console_ai/slash_commands.rb +149 -0
- data/lib/rails_console_ai/sub_agent.rb +7 -2
- data/lib/rails_console_ai/tools/registry.rb +2 -2
- data/lib/rails_console_ai/version.rb +1 -1
- data/lib/rails_console_ai.rb +1 -0
- metadata +4 -1
|
@@ -737,6 +737,7 @@ module RailsConsoleAi
|
|
|
737
737
|
|
|
738
738
|
lines = ["*Cost estimate:*"]
|
|
739
739
|
total_cost = 0.0
|
|
740
|
+
has_reported_cost = false
|
|
740
741
|
|
|
741
742
|
token_usage.each do |model, usage|
|
|
742
743
|
pricing = Configuration.pricing_for(model, cache_ttl: RailsConsoleAi.configuration.resolved_cache_ttl)
|
|
@@ -744,18 +745,23 @@ module RailsConsoleAi
|
|
|
744
745
|
input_str = "in: #{usage[:input]}"
|
|
745
746
|
output_str = "out: #{usage[:output]}"
|
|
746
747
|
|
|
747
|
-
|
|
748
|
-
|
|
748
|
+
reported_cost = usage[:cost]
|
|
749
|
+
if reported_cost && reported_cost > 0
|
|
750
|
+
has_reported_cost = true
|
|
751
|
+
total_cost += reported_cost
|
|
752
|
+
cache_read = usage[:cache_read] || 0
|
|
753
|
+
cache_write = usage[:cache_write] || 0
|
|
754
|
+
cache_str = ""
|
|
755
|
+
cache_str = " cache r: #{cache_read} w: #{cache_write}" if cache_read > 0 || cache_write > 0
|
|
756
|
+
lines << " `#{model}`: #{input_str} #{output_str}#{cache_str} $#{'%.4f' % reported_cost}"
|
|
757
|
+
elsif pricing
|
|
758
|
+
cost = Configuration.estimate_cost(model,
|
|
759
|
+
input: usage[:input], output: usage[:output],
|
|
760
|
+
cache_read: usage[:cache_read] || 0, cache_write: usage[:cache_write] || 0,
|
|
761
|
+
cache_ttl: RailsConsoleAi.configuration.resolved_cache_ttl)
|
|
762
|
+
total_cost += cost if cost
|
|
749
763
|
cache_read = usage[:cache_read] || 0
|
|
750
764
|
cache_write = usage[:cache_write] || 0
|
|
751
|
-
if (cache_read > 0 || cache_write > 0) && pricing[:cache_read]
|
|
752
|
-
# input_tokens excludes cached tokens — bill each bucket at its own
|
|
753
|
-
# rate rather than discounting cache_read out of input (see
|
|
754
|
-
# ConversationEngine#display_cost_summary).
|
|
755
|
-
cost += cache_read * pricing[:cache_read]
|
|
756
|
-
cost += cache_write * pricing[:cache_write]
|
|
757
|
-
end
|
|
758
|
-
total_cost += cost
|
|
759
765
|
cache_str = ""
|
|
760
766
|
cache_str = " cache r: #{cache_read} w: #{cache_write}" if cache_read > 0 || cache_write > 0
|
|
761
767
|
lines << " `#{model}`: #{input_str} #{output_str}#{cache_str} ~$#{'%.2f' % cost}"
|
|
@@ -764,7 +770,8 @@ module RailsConsoleAi
|
|
|
764
770
|
end
|
|
765
771
|
end
|
|
766
772
|
|
|
767
|
-
|
|
773
|
+
label = has_reported_cost ? "Total:" : "Total: ~"
|
|
774
|
+
lines << "*#{label}$#{'%.2f' % total_cost}*"
|
|
768
775
|
lines.join("\n")
|
|
769
776
|
end
|
|
770
777
|
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
require 'rails_console_ai/skill_loader'
|
|
2
|
+
require 'rails_console_ai/agent_loader'
|
|
3
|
+
|
|
4
|
+
module RailsConsoleAi
|
|
5
|
+
# The union of everything a user can invoke by typing "/" in interactive mode:
|
|
6
|
+
# the fixed REPL commands, plus every activatable skill and agent, each exposed
|
|
7
|
+
# under a slug derived from its display name ("Restart user trial" -> /restart-user-trial).
|
|
8
|
+
#
|
|
9
|
+
# Backs three surfaces that must agree with each other: the "/" listing, the
|
|
10
|
+
# completion candidates offered by the line editor, and the dispatcher that
|
|
11
|
+
# decides what a typed slash command actually does.
|
|
12
|
+
class SlashCommands
|
|
13
|
+
Command = Struct.new(:slug, :name, :description, :kind, :record, keyword_init: true) do
|
|
14
|
+
def builtin?; kind == :builtin; end
|
|
15
|
+
def skill?; kind == :skill; end
|
|
16
|
+
def agent?; kind == :agent; end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
KIND_LABELS = { builtin: 'command', skill: 'skill', agent: 'agent' }.freeze
|
|
20
|
+
|
|
21
|
+
# Slug => one-line help. Order is the order they render in the listing.
|
|
22
|
+
# Descriptions that depend on live state (auto-execute on/off, safe mode)
|
|
23
|
+
# are filled in by the channel at render time; these are the static fallbacks.
|
|
24
|
+
BUILTINS = {
|
|
25
|
+
'auto' => 'Toggle auto-execute',
|
|
26
|
+
'danger' => 'Toggle safe mode',
|
|
27
|
+
'safe' => 'Show safety guard status',
|
|
28
|
+
'model' => 'Show provider, model, and pricing info',
|
|
29
|
+
'think' => 'Switch to thinking model',
|
|
30
|
+
'unthink' => 'Switch back to default model',
|
|
31
|
+
'compact' => 'Summarize conversation to reduce context',
|
|
32
|
+
'usage' => 'Show session token totals',
|
|
33
|
+
'cost' => 'Show cost estimate by model',
|
|
34
|
+
'name' => 'Name this session for easy resume',
|
|
35
|
+
'context' => 'Show conversation history sent to the LLM',
|
|
36
|
+
'system' => 'Show the system prompt',
|
|
37
|
+
'expand' => 'Show full omitted output',
|
|
38
|
+
'debug' => 'Toggle debug summaries',
|
|
39
|
+
'retry' => 'Re-execute the last code block'
|
|
40
|
+
}.freeze
|
|
41
|
+
|
|
42
|
+
# "Restart user trial" -> restart-user-trial.
|
|
43
|
+
#
|
|
44
|
+
# Close to SkillLoader#skill_key / AgentLoader#agent_key, but kinder to names
|
|
45
|
+
# that aren't plain prose: those two drop "/" and case boundaries outright,
|
|
46
|
+
# which turns "Approve/Reject ChangeApprovals" into the unreadable
|
|
47
|
+
# "approvereject-changeapprovals". Here every boundary a human would read as
|
|
48
|
+
# a word break becomes a dash. The slug is only ever a handle for typing and
|
|
49
|
+
# dispatch — file lookup still goes through the loaders — so the two are free
|
|
50
|
+
# to differ.
|
|
51
|
+
def self.slugify(name)
|
|
52
|
+
name.to_s.strip
|
|
53
|
+
.gsub(%r{[/_\s]+}, '-') # separators
|
|
54
|
+
.gsub(/([a-z0-9])([A-Z])/, '\1-\2') # fooBar -> foo-Bar
|
|
55
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1-\2') # HTTPToken -> HTTP-Token
|
|
56
|
+
.downcase
|
|
57
|
+
.gsub(/[^a-z0-9-]/, '')
|
|
58
|
+
.gsub(/-+/, '-')
|
|
59
|
+
.sub(/\A-/, '').sub(/-\z/, '')
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def initialize(skill_loader: nil, agent_loader: nil)
|
|
63
|
+
@skill_loader = skill_loader
|
|
64
|
+
@agent_loader = agent_loader
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Loading skills and agents touches the DB and the filesystem, and completion
|
|
68
|
+
# runs on every keystroke — so the list is built once and reused until something
|
|
69
|
+
# that could have changed it (a save_skill/save_agent tool call) finishes.
|
|
70
|
+
def refresh!
|
|
71
|
+
@commands = nil
|
|
72
|
+
self
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def commands
|
|
76
|
+
@commands ||= build
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def skills
|
|
80
|
+
commands.select(&:skill?)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def agents
|
|
84
|
+
commands.select(&:agent?)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Completion candidates, already "/"-prefixed.
|
|
88
|
+
def candidates
|
|
89
|
+
commands.map { |c| "/#{c.slug}" }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# [slug, kind label] pairs for the line editor. The label is what lets a Tab
|
|
93
|
+
# list say which entries are built-in commands and which are skills or agents.
|
|
94
|
+
def completion_entries
|
|
95
|
+
commands.map { |c| ["/#{c.slug}", KIND_LABELS[c.kind]] }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def find(slug)
|
|
99
|
+
slug = slug.to_s.sub(%r{\A/}, '').downcase
|
|
100
|
+
commands.find { |c| c.slug == slug }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def build
|
|
106
|
+
builtins = BUILTINS.map do |slug, desc|
|
|
107
|
+
Command.new(slug: slug, name: slug, description: desc, kind: :builtin, record: nil)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
taken = builtins.map(&:slug)
|
|
111
|
+
|
|
112
|
+
skills = safe(:skills) do
|
|
113
|
+
skill_loader.load_activatable_skills.filter_map do |s|
|
|
114
|
+
slug = self.class.slugify(s['name'])
|
|
115
|
+
next if slug.empty? || taken.include?(slug)
|
|
116
|
+
taken << slug
|
|
117
|
+
Command.new(slug: slug, name: s['name'], description: s['description'], kind: :skill, record: s)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
agents = safe(:agents) do
|
|
122
|
+
agent_loader.load_activatable_agents.filter_map do |a|
|
|
123
|
+
slug = self.class.slugify(a['name'])
|
|
124
|
+
next if slug.empty? || taken.include?(slug)
|
|
125
|
+
taken << slug
|
|
126
|
+
Command.new(slug: slug, name: a['name'], description: a['description'], kind: :agent, record: a)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
builtins + skills.sort_by(&:slug) + agents.sort_by(&:slug)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# A broken skill file or an unmigrated database must not take down the prompt.
|
|
134
|
+
def safe(what)
|
|
135
|
+
yield
|
|
136
|
+
rescue => e
|
|
137
|
+
RailsConsoleAi.logger.warn("RailsConsoleAi: failed to load #{what} for slash commands: #{e.message}")
|
|
138
|
+
[]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def skill_loader
|
|
142
|
+
@skill_loader ||= SkillLoader.new
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def agent_loader
|
|
146
|
+
@agent_loader ||= AgentLoader.new
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -10,7 +10,7 @@ module RailsConsoleAi
|
|
|
10
10
|
LARGE_OUTPUT_THRESHOLD = 10_000
|
|
11
11
|
LARGE_OUTPUT_PREVIEW_CHARS = 8_000
|
|
12
12
|
|
|
13
|
-
attr_reader :input_tokens, :output_tokens, :model_used
|
|
13
|
+
attr_reader :input_tokens, :output_tokens, :model_used, :cost
|
|
14
14
|
|
|
15
15
|
def initialize(task:, agent_config:, binding_context:, parent_channel:, executor:,
|
|
16
16
|
output_payload: nil, output_local_name: :output)
|
|
@@ -23,6 +23,7 @@ module RailsConsoleAi
|
|
|
23
23
|
@output_local_name = output_local_name
|
|
24
24
|
@input_tokens = 0
|
|
25
25
|
@output_tokens = 0
|
|
26
|
+
@cost = 0
|
|
26
27
|
@model_used = nil
|
|
27
28
|
end
|
|
28
29
|
|
|
@@ -83,6 +84,7 @@ module RailsConsoleAi
|
|
|
83
84
|
end
|
|
84
85
|
@input_tokens += result.input_tokens || 0
|
|
85
86
|
@output_tokens += result.output_tokens || 0
|
|
87
|
+
@cost += result.cost || 0
|
|
86
88
|
|
|
87
89
|
break if channel.cancelled?
|
|
88
90
|
break unless result.tool_use?
|
|
@@ -152,6 +154,7 @@ module RailsConsoleAi
|
|
|
152
154
|
result = provider.chat_with_tools(messages, tools: tools, system_prompt: system_prompt)
|
|
153
155
|
@input_tokens += result.input_tokens || 0
|
|
154
156
|
@output_tokens += result.output_tokens || 0
|
|
157
|
+
@cost += result.cost || 0
|
|
155
158
|
end
|
|
156
159
|
|
|
157
160
|
text = result&.text.to_s
|
|
@@ -181,7 +184,7 @@ module RailsConsoleAi
|
|
|
181
184
|
config = RailsConsoleAi.configuration
|
|
182
185
|
model_override = @agent_config['model'] || config.sub_agent_model
|
|
183
186
|
|
|
184
|
-
if model_override
|
|
187
|
+
p = if model_override
|
|
185
188
|
config_dup = config.dup
|
|
186
189
|
config_dup.model = model_override
|
|
187
190
|
@model_used = model_override
|
|
@@ -190,6 +193,8 @@ module RailsConsoleAi
|
|
|
190
193
|
@model_used = config.resolved_model
|
|
191
194
|
Providers.build(config)
|
|
192
195
|
end
|
|
196
|
+
p.routing_session_id = SecureRandom.hex(8) if p.respond_to?(:routing_session_id=)
|
|
197
|
+
p
|
|
193
198
|
end
|
|
194
199
|
|
|
195
200
|
def build_system_prompt
|
|
@@ -381,7 +381,7 @@ module RailsConsoleAi
|
|
|
381
381
|
# the live database. Scoped via thread-local (explore_output runs synchronously),
|
|
382
382
|
# so the parent session's guard state is untouched.
|
|
383
383
|
result = RailsConsoleAi.configuration.safety_guards.with_database_blocked { sub.run }
|
|
384
|
-
@last_sub_agent_usage = { input: sub.input_tokens, output: sub.output_tokens, model: sub.model_used }
|
|
384
|
+
@last_sub_agent_usage = { input: sub.input_tokens, output: sub.output_tokens, model: sub.model_used, cost: sub.cost }
|
|
385
385
|
"Exploration result (#{sub.input_tokens + sub.output_tokens} tokens used, #{payload.length} chars explored):\n#{result}"
|
|
386
386
|
end
|
|
387
387
|
|
|
@@ -418,7 +418,7 @@ module RailsConsoleAi
|
|
|
418
418
|
executor: @executor
|
|
419
419
|
)
|
|
420
420
|
result = sub.run
|
|
421
|
-
@last_sub_agent_usage = { input: sub.input_tokens, output: sub.output_tokens, model: sub.model_used }
|
|
421
|
+
@last_sub_agent_usage = { input: sub.input_tokens, output: sub.output_tokens, model: sub.model_used, cost: sub.cost }
|
|
422
422
|
"Sub-agent result (#{sub.input_tokens + sub.output_tokens} tokens used):\n#{result}"
|
|
423
423
|
end
|
|
424
424
|
|
data/lib/rails_console_ai.rb
CHANGED
|
@@ -125,6 +125,7 @@ module RailsConsoleAi
|
|
|
125
125
|
lines << " Model: #{c.resolved_model}"
|
|
126
126
|
lines << " API key: #{masked_key}"
|
|
127
127
|
lines << " Local URL: #{c.local_url}" if c.provider == :local
|
|
128
|
+
lines << " OpenRouter URL: #{c.openrouter_url}" if c.openrouter_url
|
|
128
129
|
lines << " Max tokens: #{c.max_tokens || '(auto)'}"
|
|
129
130
|
lines << " Temperature: #{c.temperature}"
|
|
130
131
|
lines << " Timeout: #{c.timeout}s"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_console_ai
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.36.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Cortfr
|
|
@@ -166,18 +166,21 @@ files:
|
|
|
166
166
|
- lib/rails_console_ai/conversation_engine.rb
|
|
167
167
|
- lib/rails_console_ai/engine.rb
|
|
168
168
|
- lib/rails_console_ai/executor.rb
|
|
169
|
+
- lib/rails_console_ai/line_editor.rb
|
|
169
170
|
- lib/rails_console_ai/prefixed_io.rb
|
|
170
171
|
- lib/rails_console_ai/providers/anthropic.rb
|
|
171
172
|
- lib/rails_console_ai/providers/base.rb
|
|
172
173
|
- lib/rails_console_ai/providers/bedrock.rb
|
|
173
174
|
- lib/rails_console_ai/providers/local.rb
|
|
174
175
|
- lib/rails_console_ai/providers/openai.rb
|
|
176
|
+
- lib/rails_console_ai/providers/openrouter.rb
|
|
175
177
|
- lib/rails_console_ai/railtie.rb
|
|
176
178
|
- lib/rails_console_ai/repl.rb
|
|
177
179
|
- lib/rails_console_ai/safety_guards.rb
|
|
178
180
|
- lib/rails_console_ai/session_logger.rb
|
|
179
181
|
- lib/rails_console_ai/skill_loader.rb
|
|
180
182
|
- lib/rails_console_ai/slack_bot.rb
|
|
183
|
+
- lib/rails_console_ai/slash_commands.rb
|
|
181
184
|
- lib/rails_console_ai/storage/base.rb
|
|
182
185
|
- lib/rails_console_ai/storage/database_storage.rb
|
|
183
186
|
- lib/rails_console_ai/storage/file_storage.rb
|