relay.app 0.5.0 → 0.7.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 +43 -0
- data/README.md +52 -53
- data/app/concerns/attachment.rb +2 -0
- data/app/concerns/context.rb +5 -12
- data/app/hooks/require_user.rb +1 -1
- data/app/init/database.rb +1 -1
- data/app/init/router.rb +0 -2
- data/app/init.rb +17 -7
- data/app/models/context.rb +13 -4
- data/app/pages/base.rb +0 -1
- data/app/routes/base.rb +0 -1
- data/app/routes/mcp/base.rb +1 -1
- data/app/routes/websocket/connection.rb +14 -14
- data/app/tools/juke_box.rb +1 -1
- data/app/tools/relay_knowledge.rb +14 -13
- data/app/views/fragments/_input.erb +3 -0
- data/app/views/fragments/_providers.erb +1 -2
- data/bin/relay +9 -17
- data/config.ru +1 -1
- data/lib/relay/jukebox.rb +2 -1
- data/lib/relay/version.rb +1 -1
- data/lib/relay.rb +26 -13
- data/libexec/relay/bootstrap +2 -1
- data/libexec/relay/configure +51 -15
- data/libexec/relay/console +8 -0
- data/libexec/relay/download-models +1 -1
- data/libexec/relay/migrate +2 -1
- data/libexec/relay/setup +1 -1
- data/libexec/relay/start +3 -2
- data/public/js/relay.js +81 -11
- data/public/js/relay.js.map +1 -1
- metadata +24 -11
data/lib/relay.rb
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Relay
|
|
4
|
+
require "fileutils"
|
|
5
|
+
gem "llm.rb", "= 11.0.0"
|
|
6
|
+
|
|
4
7
|
require_relative "relay/version"
|
|
5
8
|
require_relative "relay/cache"
|
|
6
9
|
require_relative "relay/attachment"
|
|
@@ -18,7 +21,8 @@ module Relay
|
|
|
18
21
|
"deepseek" => -> { LLM.deepseek(key: ENV["DEEPSEEK_SECRET"]) },
|
|
19
22
|
"google" => -> { LLM.google(key: ENV["GOOGLE_SECRET"]) },
|
|
20
23
|
"openai" => -> { LLM.openai(key: ENV["OPENAI_SECRET"]) },
|
|
21
|
-
"xai" => -> { LLM.xai(key: ENV["XAI_SECRET"]) }
|
|
24
|
+
"xai" => -> { LLM.xai(key: ENV["XAI_SECRET"]) },
|
|
25
|
+
"bedrock" => -> { LLM.bedrock(access_key_id: ENV["AWS_ACCESS_KEY_ID"], secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"]) }
|
|
22
26
|
}.freeze
|
|
23
27
|
private_constant :PROVIDERS
|
|
24
28
|
|
|
@@ -37,7 +41,7 @@ module Relay
|
|
|
37
41
|
# Returns all known providers
|
|
38
42
|
# @return [LLM::Object]
|
|
39
43
|
def self.providers
|
|
40
|
-
@providers ||= LLM::Object.from(PROVIDERS)
|
|
44
|
+
@providers ||= LLM::Object.from(PROVIDERS).transform_values!(&:call)
|
|
41
45
|
end
|
|
42
46
|
|
|
43
47
|
##
|
|
@@ -81,7 +85,7 @@ module Relay
|
|
|
81
85
|
# Returns the writable Relay home directory
|
|
82
86
|
# @return [String]
|
|
83
87
|
def self.home
|
|
84
|
-
@home ||= ENV["RELAY_HOME"] || File.join(Dir.home, ".relay")
|
|
88
|
+
@home ||= ENV["RELAY_HOME"] || File.join(Dir.home, ".config", "relay")
|
|
85
89
|
end
|
|
86
90
|
|
|
87
91
|
##
|
|
@@ -91,6 +95,21 @@ module Relay
|
|
|
91
95
|
@env_path ||= File.join(home, "env")
|
|
92
96
|
end
|
|
93
97
|
|
|
98
|
+
##
|
|
99
|
+
# Creates the Relay home layout and copies bundled defaults into it.
|
|
100
|
+
# @return [String]
|
|
101
|
+
def self.bootstrap!
|
|
102
|
+
FileUtils.mkdir_p home
|
|
103
|
+
FileUtils.mkdir_p File.join(home, "db")
|
|
104
|
+
FileUtils.mkdir_p File.join(home, "tools")
|
|
105
|
+
FileUtils.mkdir_p images_dir
|
|
106
|
+
FileUtils.mkdir_p logs_dir
|
|
107
|
+
source = File.join(root, "db", "config.yml")
|
|
108
|
+
destination = File.join(home, "db", "config.yml")
|
|
109
|
+
FileUtils.cp(source, destination) if File.exist?(source) && !File.exist?(destination)
|
|
110
|
+
home
|
|
111
|
+
end
|
|
112
|
+
|
|
94
113
|
##
|
|
95
114
|
# @return [Array<String>]
|
|
96
115
|
# Returns the tools directory
|
|
@@ -109,7 +128,7 @@ module Relay
|
|
|
109
128
|
# Returns the path to generated images
|
|
110
129
|
# @return [String]
|
|
111
130
|
def self.images_dir
|
|
112
|
-
@images_dir ||= File.join(
|
|
131
|
+
@images_dir ||= File.join(public_dir, "g")
|
|
113
132
|
end
|
|
114
133
|
|
|
115
134
|
##
|
|
@@ -170,14 +189,8 @@ module Relay
|
|
|
170
189
|
def self.reload
|
|
171
190
|
LLM::Tool.clear_registry!
|
|
172
191
|
Relay.loader.reload if development?
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
load(_1)
|
|
177
|
-
rescue => ex
|
|
178
|
-
warn "tool error\n"
|
|
179
|
-
"#{ex.class}: #{ex.message}\n"
|
|
180
|
-
"#{ex.backtrace.join("\n")}"
|
|
181
|
-
end
|
|
192
|
+
Relay.user_loader.reload if development?
|
|
193
|
+
Relay.user_loader.eager_load
|
|
194
|
+
Relay.loader.eager_load_dir(tools_dir)
|
|
182
195
|
end
|
|
183
196
|
end
|
data/libexec/relay/bootstrap
CHANGED
data/libexec/relay/configure
CHANGED
|
@@ -1,17 +1,44 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
require_relative "../../lib/relay"
|
|
5
|
+
require "llm"
|
|
5
6
|
require "fileutils"
|
|
6
7
|
require "io/console"
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
PROVIDERS = [
|
|
10
|
+
LLM::Object.from(
|
|
11
|
+
fields: [
|
|
12
|
+
LLM::Object.from(label: "OpenAI API key", key: "OPENAI_SECRET", aliases: ["OPENAI_API_KEY"], secret: true)
|
|
13
|
+
]
|
|
14
|
+
),
|
|
15
|
+
LLM::Object.from(
|
|
16
|
+
fields: [
|
|
17
|
+
LLM::Object.from(label: "Google API key", key: "GOOGLE_SECRET", aliases: ["GOOGLE_API_KEY"], secret: true)
|
|
18
|
+
]
|
|
19
|
+
),
|
|
20
|
+
LLM::Object.from(
|
|
21
|
+
fields: [
|
|
22
|
+
LLM::Object.from(label: "Anthropic API key", key: "ANTHROPIC_SECRET", aliases: ["ANTHROPIC_API_KEY"], secret: true)
|
|
23
|
+
]
|
|
24
|
+
),
|
|
25
|
+
LLM::Object.from(
|
|
26
|
+
fields: [
|
|
27
|
+
LLM::Object.from(label: "DeepSeek API key", key: "DEEPSEEK_SECRET", aliases: ["DEEPSEEK_API_KEY"], secret: true)
|
|
28
|
+
]
|
|
29
|
+
),
|
|
30
|
+
LLM::Object.from(
|
|
31
|
+
fields: [
|
|
32
|
+
LLM::Object.from(label: "xAI API key", key: "XAI_SECRET", aliases: ["XAI_API_KEY"], secret: true)
|
|
33
|
+
]
|
|
34
|
+
),
|
|
35
|
+
LLM::Object.from(
|
|
36
|
+
fields: [
|
|
37
|
+
LLM::Object.from(label: "AWS access key ID", key: "AWS_ACCESS_KEY_ID", aliases: [], secret: false),
|
|
38
|
+
LLM::Object.from(label: "AWS secret access key", key: "AWS_SECRET_ACCESS_KEY", aliases: [], secret: true)
|
|
39
|
+
]
|
|
40
|
+
)
|
|
41
|
+
].freeze
|
|
15
42
|
|
|
16
43
|
##
|
|
17
44
|
# utils
|
|
@@ -36,15 +63,24 @@ end
|
|
|
36
63
|
def prompt(label, current: nil, secret: false)
|
|
37
64
|
suffix = current.to_s.empty? ? "" : " [configured]"
|
|
38
65
|
print "#{label}#{suffix}: "
|
|
39
|
-
value = secret ?
|
|
66
|
+
value = secret ? $stdin.noecho(&:gets).to_s.chomp : $stdin.gets.to_s.chomp
|
|
40
67
|
puts if secret
|
|
41
68
|
value.empty? ? current.to_s : value
|
|
42
69
|
end
|
|
43
70
|
|
|
71
|
+
def prompt_provider(env, provider)
|
|
72
|
+
provider.fields.each do |field|
|
|
73
|
+
key = field.key
|
|
74
|
+
aliases = Array(field.aliases)
|
|
75
|
+
current = env[key] || aliases.filter_map { ENV[_1] }.first || ENV[key]
|
|
76
|
+
env[key] = prompt(field.label, current:, secret: field.secret)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
44
80
|
def prompt_required(label)
|
|
45
81
|
loop do
|
|
46
82
|
print "#{label}: "
|
|
47
|
-
value =
|
|
83
|
+
value = $stdin.gets.to_s.strip
|
|
48
84
|
return value unless value.empty?
|
|
49
85
|
warn "#{label} is required."
|
|
50
86
|
end
|
|
@@ -53,10 +89,10 @@ end
|
|
|
53
89
|
def prompt_password
|
|
54
90
|
loop do
|
|
55
91
|
print "Password: "
|
|
56
|
-
password =
|
|
92
|
+
password = $stdin.noecho(&:gets).to_s.chomp
|
|
57
93
|
puts
|
|
58
94
|
print "Confirm password: "
|
|
59
|
-
confirm =
|
|
95
|
+
confirm = $stdin.noecho(&:gets).to_s.chomp
|
|
60
96
|
puts
|
|
61
97
|
return password unless password.empty? || password != confirm
|
|
62
98
|
warn "Passwords must match and cannot be empty."
|
|
@@ -66,15 +102,15 @@ end
|
|
|
66
102
|
##
|
|
67
103
|
# main
|
|
68
104
|
def main
|
|
69
|
-
unless
|
|
105
|
+
unless $stdin.tty? && $stdout.tty?
|
|
70
106
|
warn "relay configure requires an interactive terminal"
|
|
71
107
|
throw(:exit, 1)
|
|
72
108
|
end
|
|
73
109
|
env = load_env
|
|
74
110
|
puts Relay.banner
|
|
75
111
|
puts "Relay configuration\n"
|
|
76
|
-
|
|
77
|
-
|
|
112
|
+
PROVIDERS.each do |provider|
|
|
113
|
+
prompt_provider(env, provider)
|
|
78
114
|
end
|
|
79
115
|
name = prompt_required("Name")
|
|
80
116
|
email = prompt_required("Email")
|
data/libexec/relay/migrate
CHANGED
data/libexec/relay/setup
CHANGED
data/libexec/relay/start
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
require_relative "../../lib/relay"
|
|
5
5
|
require "optparse"
|
|
6
|
+
require "rbconfig"
|
|
6
7
|
|
|
7
8
|
DEFAULT_BIND = "http://0.0.0.0:9292"
|
|
8
9
|
|
|
@@ -22,7 +23,7 @@ def main(argv)
|
|
|
22
23
|
option_parser(bind).parse!(argv)
|
|
23
24
|
bind = argv.shift || bind
|
|
24
25
|
Dir.chdir(Relay.root) do
|
|
25
|
-
exec "falcon", "serve", "--bind", bind
|
|
26
|
+
exec RbConfig.ruby, "-S", "falcon", "serve", "--bind", bind
|
|
26
27
|
end
|
|
27
28
|
end
|
|
28
29
|
|
data/public/js/relay.js
CHANGED
|
@@ -1,6 +1,70 @@
|
|
|
1
1
|
/******/ (() => { // webpackBootstrap
|
|
2
2
|
/******/ var __webpack_modules__ = ({
|
|
3
3
|
|
|
4
|
+
/***/ "./js/draft.js"
|
|
5
|
+
/*!*********************!*\
|
|
6
|
+
!*** ./js/draft.js ***!
|
|
7
|
+
\*********************/
|
|
8
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
9
|
+
|
|
10
|
+
"use strict";
|
|
11
|
+
__webpack_require__.r(__webpack_exports__);
|
|
12
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
13
|
+
/* harmony export */ Draft: () => (/* binding */ Draft)
|
|
14
|
+
/* harmony export */ });
|
|
15
|
+
const Draft = () => {
|
|
16
|
+
const prefix = "relay:draft"
|
|
17
|
+
|
|
18
|
+
const composer = () => {
|
|
19
|
+
return document.getElementById("chat-composer")
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const textarea = () => {
|
|
23
|
+
return composer()?.querySelector("textarea[name='message']")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const key = (form = composer()) => {
|
|
27
|
+
if (!form?.dataset.contextId)
|
|
28
|
+
return null
|
|
29
|
+
return [prefix, form.dataset.contextId].join(":")
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const restore = () => {
|
|
33
|
+
const el = textarea()
|
|
34
|
+
const currentKey = key()
|
|
35
|
+
if (!el || !currentKey)
|
|
36
|
+
return
|
|
37
|
+
const value = localStorage.getItem(currentKey)
|
|
38
|
+
if (value === null)
|
|
39
|
+
return
|
|
40
|
+
el.value = value
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const persist = (el) => {
|
|
44
|
+
const currentKey = key(el?.form)
|
|
45
|
+
if (!currentKey)
|
|
46
|
+
return
|
|
47
|
+
if (el.value.length === 0)
|
|
48
|
+
localStorage.removeItem(currentKey)
|
|
49
|
+
else
|
|
50
|
+
localStorage.setItem(currentKey, el.value)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const clear = (form = composer()) => {
|
|
54
|
+
const currentKey = key(form)
|
|
55
|
+
if (!currentKey)
|
|
56
|
+
return
|
|
57
|
+
localStorage.removeItem(currentKey)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {clear, persist, restore}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
/***/ },
|
|
67
|
+
|
|
4
68
|
/***/ "./js/file_upload.js"
|
|
5
69
|
/*!***************************!*\
|
|
6
70
|
!*** ./js/file_upload.js ***!
|
|
@@ -237,11 +301,13 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
237
301
|
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
238
302
|
/* harmony export */ Relay: () => (/* binding */ Relay)
|
|
239
303
|
/* harmony export */ });
|
|
240
|
-
/* harmony import */ var
|
|
241
|
-
/* harmony import */ var
|
|
242
|
-
/* harmony import */ var
|
|
243
|
-
/* harmony import */ var
|
|
244
|
-
/* harmony import */ var
|
|
304
|
+
/* harmony import */ var _draft__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../draft */ "./js/draft.js");
|
|
305
|
+
/* harmony import */ var _file_upload__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../file_upload */ "./js/file_upload.js");
|
|
306
|
+
/* harmony import */ var _controllers_ActivityController__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./controllers/ActivityController */ "./js/lib/controllers/ActivityController.js");
|
|
307
|
+
/* harmony import */ var _controllers_ContentController__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./controllers/ContentController */ "./js/lib/controllers/ContentController.js");
|
|
308
|
+
/* harmony import */ var _scroll__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../scroll */ "./js/scroll.js");
|
|
309
|
+
/* harmony import */ var _timer__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../timer */ "./js/timer.js");
|
|
310
|
+
|
|
245
311
|
|
|
246
312
|
|
|
247
313
|
|
|
@@ -250,23 +316,25 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
250
316
|
|
|
251
317
|
const Relay = () => {
|
|
252
318
|
const target = document
|
|
253
|
-
const timer = (0,
|
|
254
|
-
const activity = (0,
|
|
255
|
-
const content = (0,
|
|
319
|
+
const timer = (0,_timer__WEBPACK_IMPORTED_MODULE_5__.Timer)(document.getElementById("chatbot-status"))
|
|
320
|
+
const activity = (0,_controllers_ActivityController__WEBPACK_IMPORTED_MODULE_2__.ActivityController)({target})
|
|
321
|
+
const content = (0,_controllers_ContentController__WEBPACK_IMPORTED_MODULE_3__.ContentController)({target})
|
|
256
322
|
const controllers = [activity, content]
|
|
257
|
-
|
|
323
|
+
const draft = (0,_draft__WEBPACK_IMPORTED_MODULE_0__.Draft)()
|
|
324
|
+
let scroll = (0,_scroll__WEBPACK_IMPORTED_MODULE_4__.Scroll)(document.getElementById("chatbot-stream"))
|
|
258
325
|
|
|
259
326
|
const refreshScroll = () => {
|
|
260
327
|
const stream = document.getElementById("chatbot-stream")
|
|
261
328
|
if (!stream)
|
|
262
329
|
return
|
|
263
330
|
if (!scroll || scroll.parentEl !== stream)
|
|
264
|
-
scroll = (0,
|
|
331
|
+
scroll = (0,_scroll__WEBPACK_IMPORTED_MODULE_4__.Scroll)(stream)
|
|
265
332
|
}
|
|
266
333
|
|
|
267
334
|
const enhance = (root = document.body) => {
|
|
268
335
|
refreshScroll()
|
|
269
336
|
controllers.forEach((controller) => controller.enhance(root))
|
|
337
|
+
draft.restore()
|
|
270
338
|
}
|
|
271
339
|
|
|
272
340
|
const syncTimer = () => {
|
|
@@ -277,7 +345,7 @@ const Relay = () => {
|
|
|
277
345
|
timer.handle(status)
|
|
278
346
|
}
|
|
279
347
|
|
|
280
|
-
const fileUpload = (0,
|
|
348
|
+
const fileUpload = (0,_file_upload__WEBPACK_IMPORTED_MODULE_1__.FileUpload)({afterUpload: enhance})
|
|
281
349
|
|
|
282
350
|
const handleOobSwap = (event) => {
|
|
283
351
|
const elt = event.detail.elt || event.target
|
|
@@ -298,6 +366,7 @@ const Relay = () => {
|
|
|
298
366
|
return
|
|
299
367
|
if (fileUpload.blockSubmit(event))
|
|
300
368
|
return
|
|
369
|
+
draft.clear(event.target)
|
|
301
370
|
scroll?.force()
|
|
302
371
|
}
|
|
303
372
|
|
|
@@ -310,6 +379,7 @@ const Relay = () => {
|
|
|
310
379
|
const handleInput = (event) => {
|
|
311
380
|
if (!event.target.matches("#chat-composer textarea"))
|
|
312
381
|
return
|
|
382
|
+
draft.persist(event.target)
|
|
313
383
|
scroll?.force()
|
|
314
384
|
}
|
|
315
385
|
|