ask-rails 0.2.5 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c6e0eb7fb3f6797eb16e096b2f2e5b0c79eb002d697be20df6ac0b2bbe5abf3a
4
- data.tar.gz: 4c6ffe88deca00e9e43472e286fb37630ba30953a5488d577a37ee805ccd7b73
3
+ metadata.gz: 958cbbe10b9ff728c6b73776250c422f0218a42d44a9f2608b95c1cd67e9d5a6
4
+ data.tar.gz: db0a490be3201f02c9f71115bea770368ed37f9ce9fe409580c341a92d9dc3bc
5
5
  SHA512:
6
- metadata.gz: 3aafbab19ef97841a392242282f19886624412ec92602dfc525dcfd2743f05adc523f2604c139e847a9078ec307f596c85005796d8dab484ca32bb84bcd36e3e
7
- data.tar.gz: 0f13102f41d739aa2215684508a8d705df6ef2ad842b2404d4232bf4b689840ec0eca0153ea71f503ce114eadb11bd0396bee6d3d30202e9376caf04ebd506bc
6
+ metadata.gz: 1ab77385fc998e0d433d9875ad791ead151f18a085d2a067e69fca310d0eba999167f3482da09db1a38271a0e58e9235a60e958b414abd7740d026ed4d1c7fd7
7
+ data.tar.gz: 31efdd5a7943822f2a0bc4aae038a1834f9933ecce4664928a11993f2441f30cfa0612dda2edc8729c616106549d13f2b9ea9bdb66757077833dc92b4aa121ad
data/CHANGELOG.md CHANGED
@@ -1,3 +1,31 @@
1
+ ## [0.3.0] — 2026-07-21
2
+
3
+ ### Added
4
+
5
+ - **Admin chat UI** — Mount `Ask::Rails::Engine` at `/ask` and get a working chat interface with SSE streaming. No generator needed — just mount and go.
6
+
7
+ ```
8
+ GET /ask → Chat UI
9
+ POST /ask/sessions → Create new session
10
+ POST /ask/sessions/:id/messages → Send message (SSE streamed)
11
+ GET /ask/sessions/:id/messages → Message history
12
+ GET /ask/sessions/:id/stream → SSE stream for existing session
13
+ ```
14
+
15
+ - **`Ask::Rails::ChatController`** — Full controller with actions: `index`, `create`, `message` (SSE streaming), `stream`, `history`, `destroy`. Supports real-time streaming via `Enumerator` + `text/event-stream`.
16
+
17
+ - **Chat layout and view** — A dark-themed admin chat interface shipped in the gem (not generated). Includes sidebar with session list, message history, SSE streaming, and keyboard navigation.
18
+
19
+ - **`Ask::Rails::Auth`** — Configurable authentication hook. Set `Ask::Rails::Auth.check = -> { ... }` to protect the chat behind your existing auth system. Runs in controller context — `current_user`, `redirect_to`, etc. are available.
20
+
21
+ - **Engine routes** — Routes defined in `config/routes.rb` with proper `Ask::Rails::Engine.routes.draw` isolation.
22
+
23
+ ### Changed
24
+
25
+ - **README** — Rewritten with clear positioning: ask-rails is for internal admin agents. ask-agent is for external-facing agents. Includes comparison table, quick start guide, and tool reference.
26
+ - **Engine** — `isolate_namespace Ask::Rails` for proper route isolation.
27
+ - **Gemspec** — Now includes `app/` and `config/` directories for engine views, controllers, and routes.
28
+
1
29
  ## [0.2.5] - 2026-06-25
2
30
 
3
31
  ### Changed
data/README.md CHANGED
@@ -1,8 +1,22 @@
1
1
  # ask-rails
2
2
 
3
- Rails integration for the ask-rb ecosystem. The only gem a Rails app needs to join
4
- the ask-rb stack — provides a Railtie, AR session persistence, a session factory,
5
- automatic service gem discovery, and generators.
3
+ An admin AI agent for your Rails app. Mount the engine, get a chat interface at `/ask` that can inspect your code, query your database, read logs, and help you debug — all through an authenticated admin UI.
4
+
5
+ ## Who is this for?
6
+
7
+ - **Rails developers** who want an AI co-pilot that understands their app's codebase, schema, routes, and logs
8
+ - **Internal/Admin use only** — the agent has direct access to your database, file system, and shell. Not for external/customer-facing use.
9
+
10
+ For building customer-facing AI agents, use `ask-agent` directly with your own tools and UI.
11
+
12
+ ## What it gives you
13
+
14
+ - **7 Rails-aware tools**: `ReadFile`, `QueryDatabase`, `ReadRoutes`, `ReadModel`, `ReadLog`, `RunCommand`, `SearchCodebase`
15
+ - **Admin chat UI**: Mount the engine, get a working chat at `/ask` with SSE streaming
16
+ - **Auth integration**: Protect `/ask` behind your existing Devise/authentication
17
+ - **AR persistence**: Agent sessions survive server restarts
18
+ - **Service discovery**: Auto-detects installed ask-* service gems
19
+ - **Skills**: Built-in guides for Rails debugging, deployment, and database performance
6
20
 
7
21
  ## Installation
8
22
 
@@ -11,24 +25,94 @@ bundle add ask-rails
11
25
  rails generate ask_rails:install
12
26
  ```
13
27
 
28
+ ## Quick Start
29
+
30
+ Add the engine mount and auth protection to `config/routes.rb`:
31
+
32
+ ```ruby
33
+ # config/routes.rb
34
+ Rails.application.routes.draw do
35
+ # ... your routes ...
36
+
37
+ authenticate :user, ->(u) { u.admin? } do
38
+ mount Ask::Rails::Engine, at: "/ask"
39
+ end
40
+ end
41
+ ```
42
+
43
+ Then visit `/ask` in your browser.
44
+
14
45
  ## Usage
15
46
 
47
+ ### Configuration
48
+
16
49
  ```ruby
17
- # In any Rails context
50
+ # config/initializers/ask_rails.rb
51
+ Ask::Rails.configure do |c|
52
+ c.default_model = "claude-sonnet-4"
53
+ c.max_turns = 50
54
+ end
55
+ ```
56
+
57
+ ### Programmatic Access
58
+
59
+ ```ruby
60
+ # From any controller, view, or job
18
61
  session = Ask::Rails.agent_session
19
62
  session.run("Find all open issues labeled 'bug' in our repo")
20
63
  ```
21
64
 
22
- Service gems like `ask-github`, `ask-slack` are auto-discovered — the agent gets their
23
- context (auth info, quick-start snippets, error guides) in the system prompt automatically.
65
+ ### Route Helpers
24
66
 
25
- ## Development
67
+ ```ruby
68
+ ask_rails.root_path # => /ask
69
+ ask_rails.sessions_path # => /ask/sessions
70
+ ask_rails.session_messages_path(session_id) # => /ask/sessions/:id/messages
71
+ ```
26
72
 
27
- ```bash
28
- bin/setup
29
- bundle exec rake test
73
+ ### Auth
74
+
75
+ By default, the admin chat is unprotected. Add auth in your routes (as shown above) or set a custom check:
76
+
77
+ ```ruby
78
+ # config/initializers/ask_rails.rb
79
+ Ask::Rails::Auth.check = -> {
80
+ redirect_to main_app.login_path unless current_user&.admin?
81
+ }
30
82
  ```
31
83
 
84
+ ## Tools
85
+
86
+ | Tool | What it does |
87
+ |---|---|
88
+ | `ReadFile` | Read any file (relative to `Rails.root`) |
89
+ | `QueryDatabase` | Run read-only SQL (rejects non-SELECT in production) |
90
+ | `ReadModel` | Inspect AR model schema, associations, validations |
91
+ | `ReadRoutes` | View `config/routes.rb` |
92
+ | `ReadLog` | Read log files with level/search filtering |
93
+ | `RunCommand` | Run shell commands in the app root |
94
+ | `SearchCodebase` | Grep the codebase for patterns |
95
+
96
+ ## Engine Routes
97
+
98
+ ```
99
+ GET /ask → Chat UI
100
+ POST /ask/sessions → Create new session
101
+ POST /ask/sessions/:id/messages → Send message (SSE streamed response)
102
+ GET /ask/sessions/:id/messages → Get message history
103
+ GET /ask/sessions/:id/stream → SSE stream for existing session
104
+ ```
105
+
106
+ ## Compared to ask-agent
107
+
108
+ | `ask-agent` | `ask-rails` |
109
+ |---|---|
110
+ | Build external-facing agents | Build an internal admin co-pilot |
111
+ | Bring your own tools | Ships Rails-specific tools |
112
+ | Bring your own UI | Ships an admin chat UI |
113
+ | Any Ruby app | Rails apps only |
114
+ | General purpose | Development, debugging, ops |
115
+
32
116
  ## License
33
117
 
34
118
  MIT
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Ask
6
+ module Rails
7
+ class ChatController < ::ActionController::Base
8
+ layout "ask/rails/application"
9
+
10
+ before_action :authenticate!, unless: -> { Ask::Rails::Auth.check.nil? }
11
+ before_action :set_session, only: [:message, :stream, :history, :show]
12
+
13
+ # GET /ask — main chat interface
14
+ def index
15
+ @sessions = load_sessions
16
+ @active_session = @sessions.first
17
+ render :index
18
+ end
19
+
20
+ # POST /ask/sessions — create a new session
21
+ def create
22
+ session_id = SecureRandom.uuid
23
+
24
+ agent = build_agent_session
25
+ agent.run("Hello") # warm up
26
+ agent.save
27
+
28
+ # Store minimal session metadata
29
+ store_session_metadata(session_id, agent)
30
+
31
+ redirect_to ask_rails.root_path
32
+ end
33
+
34
+ # GET /ask/sessions/:id — show a specific session
35
+ def show
36
+ @messages = load_session_messages(@session_id)
37
+ render json: { id: @session_id, messages: @messages }
38
+ end
39
+
40
+ # GET /ask/sessions — list all sessions
41
+ def index_sessions
42
+ @sessions = load_sessions
43
+ render json: @sessions
44
+ end
45
+
46
+ # POST /ask/sessions/:session_id/messages — send a message, get SSE stream
47
+ def message
48
+ prompt = params[:message].to_s.strip
49
+ return head :bad_request if prompt.empty?
50
+
51
+ response.headers["Content-Type"] = "text/event-stream"
52
+ response.headers["Cache-Control"] = "no-cache"
53
+ response.headers["X-Accel-Buffering"] = "no"
54
+
55
+ # Disable buffering for streaming
56
+ response.headers["Last-Modified"] = Time.now.httpdate
57
+
58
+ self.response_body = Enumerator.new do |yielder|
59
+ agent = resume_or_create_session(@session_id)
60
+
61
+ yielder << "data: #{JSON.generate(type: 'start', session_id: @session_id)}\n\n"
62
+
63
+ agent.run(prompt) do |chunk|
64
+ if chunk.content&.length&.> 0
65
+ yielder << "data: #{JSON.generate(type: 'delta', content: chunk.content)}\n\n"
66
+ end
67
+ if chunk.thinking&.length&.> 0
68
+ yielder << "data: #{JSON.generate(type: 'thinking', content: chunk.thinking)}\n\n"
69
+ end
70
+ end
71
+
72
+ agent.save
73
+ @session = agent
74
+
75
+ yielder << "data: #{JSON.generate(type: 'done', session_id: @session_id)}\n\n"
76
+ end
77
+ end
78
+
79
+ # GET /ask/sessions/:session_id/stream — SSE stream for an existing session
80
+ def stream
81
+ response.headers["Content-Type"] = "text/event-stream"
82
+ response.headers["Cache-Control"] = "no-cache"
83
+ response.headers["X-Accel-Buffering"] = "no"
84
+
85
+ self.response_body = Enumerator.new do |yielder|
86
+ messages = load_session_messages(@session_id)
87
+ yielder << "data: #{JSON.generate(type: 'history', messages: messages)}\n\n"
88
+
89
+ # Keep connection open briefly for potential updates
90
+ sleep 30
91
+ yielder << "data: #{JSON.generate(type: 'keepalive')}\n\n"
92
+ end
93
+ end
94
+
95
+ # GET /ask/sessions/:session_id/messages — get message history as JSON
96
+ def history
97
+ messages = load_session_messages(@session_id)
98
+ render json: messages
99
+ end
100
+
101
+ # DELETE /ask/sessions — destroy all sessions
102
+ def destroy
103
+ persistence = Ask::Rails.configuration.persistence_adapter
104
+ if persistence
105
+ persistence.list.each { |id| persistence.delete(id) }
106
+ end
107
+ redirect_to ask_rails.root_path
108
+ end
109
+
110
+ private
111
+
112
+ def authenticate!
113
+ instance_eval(&Ask::Rails::Auth.check) if Ask::Rails::Auth.check
114
+ end
115
+
116
+ def set_session
117
+ @session_id = params[:session_id] || params[:id]
118
+ head :not_found unless @session_id
119
+ end
120
+
121
+ def build_agent_session(**extra)
122
+ Ask::Rails.agent_session(**extra)
123
+ end
124
+
125
+ def resume_or_create_session(session_id)
126
+ persistence = Ask::Rails.configuration.persistence_adapter
127
+
128
+ if persistence
129
+ data = persistence.load(session_id)
130
+ if data
131
+ session = Ask::Agent::Session.load(session_id, adapter: persistence)
132
+ return session if session
133
+ end
134
+ end
135
+
136
+ Ask::Rails.agent_session(persistence: persistence).tap do |s|
137
+ s.instance_variable_set(:@id, session_id)
138
+ end
139
+ end
140
+
141
+ def load_sessions
142
+ persistence = Ask::Rails.configuration.persistence_adapter
143
+ return [] unless persistence
144
+
145
+ persistence.list.map do |id|
146
+ data = persistence.load(id) || {}
147
+ { id: id, created_at: data.dig(:metadata, :created_at), message_count: (data[:messages] || []).length }
148
+ end.sort_by { |s| s[:created_at] || "" }.reverse
149
+ end
150
+
151
+ def load_session_messages(session_id)
152
+ persistence = Ask::Rails.configuration.persistence_adapter
153
+ return [] unless persistence
154
+
155
+ data = persistence.load(session_id)
156
+ return [] unless data
157
+
158
+ (data[:messages] || []).map { |m|
159
+ { role: m[:role], content: m[:content].to_s.truncate(200) }
160
+ }
161
+ end
162
+
163
+ def store_session_metadata(session_id, agent)
164
+ persistence = Ask::Rails.configuration.persistence_adapter
165
+ return unless persistence
166
+
167
+ persistence.save(session_id, {
168
+ id: session_id,
169
+ messages: agent.messages.map { |m|
170
+ { role: m.role, content: m.content.to_s }
171
+ },
172
+ metadata: {
173
+ model: ask_rails_model,
174
+ created_at: Time.now.iso8601,
175
+ updated_at: Time.now.iso8601
176
+ }
177
+ })
178
+ end
179
+
180
+ def ask_rails_model
181
+ Ask::Rails.configuration.default_model
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,6 @@
1
+ <!-- Chat interface rendered by the layout -->
2
+ <!-- The layout handles the full UI. This view is minimal -->
3
+ <!-- since the layout and JavaScript drive the interface. -->
4
+
5
+ <%= tag.div id: "ask-rails-data",
6
+ data: { sessions: @sessions.to_json, active_session_id: @active_session&.dig(:id) } %>
@@ -0,0 +1,220 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Ask — Rails Agent</title>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <style>
7
+ * { margin: 0; padding: 0; box-sizing: border-box; }
8
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #0d0d0f; color: #e1e1e6; height: 100vh; display: flex; }
9
+ a { color: #8b8bf0; text-decoration: none; }
10
+ a:hover { color: #a0a0ff; }
11
+
12
+ .sidebar { width: 280px; background: #121218; border-right: 1px solid #1e1e28; display: flex; flex-direction: column; flex-shrink: 0; }
13
+ .sidebar-header { padding: 16px; border-bottom: 1px solid #1e1e28; }
14
+ .sidebar-header h1 { font-size: 16px; font-weight: 600; color: #c0c0d0; }
15
+ .sidebar-header p { font-size: 11px; color: #606070; margin-top: 2px; }
16
+ .sidebar-actions { padding: 12px 16px; }
17
+ .sidebar-actions a { display: block; padding: 8px 12px; background: #1a1a24; border-radius: 6px; font-size: 13px; text-align: center; color: #c0c0d0; }
18
+ .sidebar-actions a:hover { background: #222230; }
19
+ .session-list { flex: 1; overflow-y: auto; padding: 8px; }
20
+ .session-item { padding: 10px 12px; border-radius: 6px; font-size: 13px; cursor: pointer; color: #9090a0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
21
+ .session-item:hover { background: #1a1a24; color: #c0c0d0; }
22
+ .session-item.active { background: #1e1e30; color: #d0d0e0; }
23
+ .session-item .time { font-size: 11px; color: #505060; margin-top: 2px; }
24
+ .sidebar-footer { padding: 12px 16px; border-top: 1px solid #1e1e28; font-size: 11px; color: #404050; }
25
+
26
+ .main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
27
+ .chat-header { padding: 14px 20px; border-bottom: 1px solid #1e1e28; font-size: 13px; color: #808090; }
28
+ .messages { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 16px; }
29
+ .message { max-width: 80%; padding: 12px 16px; border-radius: 10px; font-size: 14px; line-height: 1.5; }
30
+ .message.user { background: #1e1e30; align-self: flex-end; border-bottom-right-radius: 4px; }
31
+ .message.assistant { background: #181825; align-self: flex-start; border-bottom-left-radius: 4px; }
32
+ .message.thinking { background: #151520; align-self: flex-start; font-style: italic; color: #707080; font-size: 13px; border-left: 2px solid #404068; }
33
+ .message.system { background: #101018; align-self: center; color: #505060; font-size: 12px; text-align: center; }
34
+ .message .label { font-size: 11px; color: #606070; margin-bottom: 4px; }
35
+ .empty-state { flex: 1; display: flex; align-items: center; justify-content: center; color: #404050; font-size: 14px; text-align: center; padding: 40px; }
36
+ .empty-state p { max-width: 300px; }
37
+
38
+ .input-area { padding: 16px 20px; border-top: 1px solid #1e1e28; }
39
+ .input-row { display: flex; gap: 8px; }
40
+ .input-row input { flex: 1; padding: 10px 14px; background: #181825; border: 1px solid #252535; border-radius: 8px; color: #e1e1e6; font-size: 14px; outline: none; }
41
+ .input-row input:focus { border-color: #404068; }
42
+ .input-row button { padding: 10px 20px; background: #404068; border: none; border-radius: 8px; color: #e1e1e6; font-size: 14px; cursor: pointer; }
43
+ .input-row button:hover { background: #505080; }
44
+ .input-row button:disabled { opacity: 0.5; cursor: default; }
45
+ </style>
46
+ </head>
47
+ <body>
48
+ <div class="sidebar">
49
+ <div class="sidebar-header">
50
+ <h1>Ask</h1>
51
+ <p>Rails Agent</p>
52
+ </div>
53
+ <div class="sidebar-actions">
54
+ <%= button_to "New Session", ask_rails.sessions_path, method: :post, style: "display: none", id: "new-session-form" %>
55
+ <a href="#" onclick="document.getElementById('new-session-form').click(); return false;">+ New Session</a>
56
+ </div>
57
+ <div class="session-list" id="session-list">
58
+ <% @sessions.each do |s| %>
59
+ <div class="session-item<%= s[:id] == (@active_session&.dig(:id) || params[:session_id]) ? ' active' : '' %>" onclick="switchSession('<%= s[:id] %>')">
60
+ Session <%= s[:id].to_s[0..7] %>
61
+ <div class="time"><%= s[:message_count] %> messages</div>
62
+ </div>
63
+ <% end %>
64
+ </div>
65
+ <div class="sidebar-footer">
66
+ Model: <%= Ask::Rails.configuration.default_model %>
67
+ </div>
68
+ </div>
69
+
70
+ <div class="main">
71
+ <div class="chat-header" id="chat-header">
72
+ <% if @active_session %>
73
+ Session <%= @active_session[:id].to_s[0..7] %>
74
+ <% else %>
75
+ Start a new session
76
+ <% end %>
77
+ </div>
78
+ <div class="messages" id="messages">
79
+ <div class="empty-state" id="empty-state">
80
+ <p>Send a message to start talking to the Rails agent.</p>
81
+ </div>
82
+ </div>
83
+ <div class="input-area">
84
+ <div class="input-row">
85
+ <input type="text" id="message-input" placeholder="Ask about your app..." autofocus>
86
+ <button id="send-button" onclick="sendMessage()">Send</button>
87
+ </div>
88
+ </div>
89
+ </div>
90
+
91
+ <script>
92
+ let currentSessionId = '<%= @active_session&.dig(:id) || "" %>';
93
+ let isStreaming = false;
94
+
95
+ function addMessage(role, content) {
96
+ const el = document.getElementById('empty-state');
97
+ if (el) el.remove();
98
+
99
+ const msg = document.createElement('div');
100
+ msg.className = 'message ' + role;
101
+ if (role === 'thinking') {
102
+ msg.innerHTML = '<div class="label">Thinking...</div>' + escapeHtml(content);
103
+ } else {
104
+ msg.innerHTML = (role === 'user' ? '' : '<div class="label">Assistant</div>') + escapeHtml(content);
105
+ }
106
+ document.getElementById('messages').appendChild(msg);
107
+ msg.scrollIntoView({ behavior: 'smooth' });
108
+ return msg;
109
+ }
110
+
111
+ function updateLastMessage(role, content) {
112
+ const msgs = document.getElementById('messages').querySelectorAll('.message.' + role);
113
+ if (msgs.length === 0) return addMessage(role, content);
114
+ const last = msgs[msgs.length - 1];
115
+ const textEl = last.querySelector('div:last-child');
116
+ if (textEl) textEl.textContent += content;
117
+ else last.innerHTML += escapeHtml(content);
118
+ last.scrollIntoView({ behavior: 'smooth' });
119
+ }
120
+
121
+ function escapeHtml(text) {
122
+ const d = document.createElement('div');
123
+ d.textContent = text;
124
+ return d.innerHTML;
125
+ }
126
+
127
+ function sendMessage() {
128
+ const input = document.getElementById('message-input');
129
+ const prompt = input.value.trim();
130
+ if (!prompt || isStreaming) return;
131
+
132
+ input.value = '';
133
+ isStreaming = true;
134
+ document.getElementById('send-button').disabled = true;
135
+
136
+ addMessage('user', prompt);
137
+
138
+ if (!currentSessionId) {
139
+ // Create session first
140
+ fetch('<%= ask_rails.sessions_path %>', { method: 'POST' })
141
+ .then(r => r.text())
142
+ .then(() => {
143
+ // Reload to get the new session
144
+ window.location.reload();
145
+ });
146
+ return;
147
+ }
148
+
149
+ const url = '<%= ask_rails.session_message_path("SESSION_ID") %>'.replace('SESSION_ID', currentSessionId);
150
+
151
+ fetch(url, {
152
+ method: 'POST',
153
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
154
+ body: new URLSearchParams({ message: prompt })
155
+ }).then(response => {
156
+ const reader = response.body.getReader();
157
+ const decoder = new TextDecoder();
158
+ let buffer = '';
159
+
160
+ function processChunk() {
161
+ reader.read().then(({ done, value }) => {
162
+ if (done) {
163
+ isStreaming = false;
164
+ document.getElementById('send-button').disabled = false;
165
+ return;
166
+ }
167
+ buffer += decoder.decode(value, { stream: true });
168
+ const lines = buffer.split('\n');
169
+ buffer = lines.pop() || '';
170
+
171
+ lines.forEach(line => {
172
+ if (line.startsWith('data: ')) {
173
+ try {
174
+ const data = JSON.parse(line.slice(6));
175
+ if (data.type === 'delta') {
176
+ updateLastMessage('assistant', data.content);
177
+ } else if (data.type === 'thinking') {
178
+ addMessage('thinking', data.content);
179
+ } else if (data.type === 'done') {
180
+ isStreaming = false;
181
+ document.getElementById('send-button').disabled = false;
182
+ } else if (data.type === 'start') {
183
+ currentSessionId = data.session_id;
184
+ }
185
+ } catch(e) {}
186
+ }
187
+ });
188
+ processChunk();
189
+ });
190
+ }
191
+ processChunk();
192
+ }).catch(() => {
193
+ isStreaming = false;
194
+ document.getElementById('send-button').disabled = false;
195
+ });
196
+ }
197
+
198
+ document.getElementById('message-input').addEventListener('keydown', function(e) {
199
+ if (e.key === 'Enter') sendMessage();
200
+ });
201
+
202
+ function switchSession(id) {
203
+ currentSessionId = id;
204
+ document.getElementById('empty-state')?.remove();
205
+ document.getElementById('messages').innerHTML = '';
206
+ document.getElementById('chat-header').textContent = 'Session ' + id.substring(0, 8);
207
+
208
+ fetch('<%= ask_rails.session_history_path("SESSION_ID") %>'.replace('SESSION_ID', id))
209
+ .then(r => r.json())
210
+ .then(messages => {
211
+ messages.forEach(m => {
212
+ if (m.role === 'user' || m.role === 'assistant') {
213
+ addMessage(m.role, m.content);
214
+ }
215
+ });
216
+ });
217
+ }
218
+ </script>
219
+ </body>
220
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ Ask::Rails::Engine.routes.draw do
4
+ root to: "chat#index"
5
+
6
+ resources :sessions, only: [:index, :create, :show], controller: "chat" do
7
+ collection do
8
+ delete :destroy
9
+ end
10
+ end
11
+
12
+ post "sessions/:session_id/messages" => "chat#message", as: :session_message
13
+ get "sessions/:session_id/stream" => "chat#stream", as: :session_stream
14
+ get "sessions/:session_id/messages" => "chat#history", as: :session_history
15
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ module Rails
5
+ # Inclusion module for authenticating the admin chat.
6
+ #
7
+ # By default, all ask-rails engine routes are accessible without auth.
8
+ # To protect them, define a +current_user+ method and set the auth
9
+ # check in an initializer:
10
+ #
11
+ # Ask::Rails::Auth.check = -> {
12
+ # redirect_to main_app.login_path unless current_user&.admin?
13
+ # }
14
+ #
15
+ # The proc is evaluated in the controller context, so +redirect_to+,
16
+ # +current_user+, +session+, etc. are all available.
17
+ module Auth
18
+ mattr_accessor :check
19
+ self.check = nil
20
+ end
21
+ end
22
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "railtie"
3
+ require_relative "auth"
4
4
 
5
5
  module Ask
6
6
  module Rails
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module Rails
5
- VERSION = "0.2.5"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
data/lib/ask/rails.rb CHANGED
@@ -67,7 +67,7 @@ end
67
67
  require_relative "rails/version"
68
68
  require_relative "rails/engine"
69
69
  require_relative "rails/configuration"
70
- require_relative "rails/railtie"
70
+ require_relative "rails/auth"
71
71
  require_relative "rails/persistence"
72
72
  require_relative "rails/service_discovery"
73
73
  require_relative "rails/tool"
@@ -78,3 +78,8 @@ require_relative "rails/tools/read_routes"
78
78
  require_relative "rails/tools/query_database"
79
79
  require_relative "rails/tools/read_model"
80
80
  require_relative "rails/tools/read_log"
81
+
82
+ # Railtie is loaded only when Rails is fully available
83
+ if defined?(::Rails::Railtie)
84
+ require_relative "rails/railtie"
85
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ask-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto
@@ -146,8 +146,13 @@ files:
146
146
  - CHANGELOG.md
147
147
  - LICENSE
148
148
  - README.md
149
+ - app/controllers/ask/rails/chat_controller.rb
150
+ - app/views/ask/rails/chat/index.html.erb
151
+ - app/views/layouts/ask/rails/application.html.erb
152
+ - config/routes.rb
149
153
  - lib/ask-rails.rb
150
154
  - lib/ask/rails.rb
155
+ - lib/ask/rails/auth.rb
151
156
  - lib/ask/rails/configuration.rb
152
157
  - lib/ask/rails/engine.rb
153
158
  - lib/ask/rails/persistence.rb