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.
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(home, "g")
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
- paths = Dir[File.join(tools_dir, "*.rb")]
174
- paths.concat Dir[File.join(home, "tools", "*.rb")]
175
- paths.sort.each do
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
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require "relay"
4
+ require_relative "../../lib/relay"
5
+ Relay.bootstrap!
5
6
  require File.join(Relay.root, "app", "init")
6
7
  warn "relay: bootstrap the database"
7
8
  Sequel.extension :migration
@@ -1,17 +1,44 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require "relay"
4
+ require_relative "../../lib/relay"
5
+ require "llm"
5
6
  require "fileutils"
6
7
  require "io/console"
7
8
 
8
- PROVIDER_KEYS = {
9
- "OPENAI_SECRET" => "OpenAI API key",
10
- "GOOGLE_SECRET" => "Google API key",
11
- "ANTHROPIC_SECRET" => "Anthropic API key",
12
- "DEEPSEEK_SECRET" => "DeepSeek API key",
13
- "XAI_SECRET" => "xAI API key"
14
- }.freeze
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 ? STDIN.noecho(&:gets).to_s.chomp : STDIN.gets.to_s.chomp
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 = STDIN.gets.to_s.strip
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 = STDIN.noecho(&:gets).to_s.chomp
92
+ password = $stdin.noecho(&:gets).to_s.chomp
57
93
  puts
58
94
  print "Confirm password: "
59
- confirm = STDIN.noecho(&:gets).to_s.chomp
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 STDIN.tty? && $stdout.tty?
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
- PROVIDER_KEYS.each do |key, label|
77
- env[key] = prompt(label, current: env[key], secret: true)
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")
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../../lib/relay"
5
+ require File.join(Relay.root, "app", "init")
6
+ require "irb"
7
+
8
+ IRB.start(__FILE__)
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require "relay"
4
+ require_relative "../../lib/relay"
5
5
  require File.join(Relay.root, "app", "init")
6
6
 
7
7
  warn "relay: download models"
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require "relay"
4
+ require_relative "../../lib/relay"
5
+ Relay.bootstrap!
5
6
  require File.join(Relay.root, "app", "init")
6
7
  Sequel.extension :migration
7
8
  Sequel::Migrator.run(Relay::DB, Relay.migrations_dir)
data/libexec/relay/setup CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require "relay"
4
+ require_relative "../../lib/relay"
5
5
 
6
6
  bootstrap = File.join(Relay.root, "libexec", "relay", "bootstrap")
7
7
  configure = File.join(Relay.root, "libexec", "relay", "configure")
data/libexec/relay/start CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require "relay"
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 _file_upload__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../file_upload */ "./js/file_upload.js");
241
- /* harmony import */ var _controllers_ActivityController__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./controllers/ActivityController */ "./js/lib/controllers/ActivityController.js");
242
- /* harmony import */ var _controllers_ContentController__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./controllers/ContentController */ "./js/lib/controllers/ContentController.js");
243
- /* harmony import */ var _scroll__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../scroll */ "./js/scroll.js");
244
- /* harmony import */ var _timer__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../timer */ "./js/timer.js");
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,_timer__WEBPACK_IMPORTED_MODULE_4__.Timer)(document.getElementById("chatbot-status"))
254
- const activity = (0,_controllers_ActivityController__WEBPACK_IMPORTED_MODULE_1__.ActivityController)({target})
255
- const content = (0,_controllers_ContentController__WEBPACK_IMPORTED_MODULE_2__.ContentController)({target})
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
- let scroll = (0,_scroll__WEBPACK_IMPORTED_MODULE_3__.Scroll)(document.getElementById("chatbot-stream"))
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,_scroll__WEBPACK_IMPORTED_MODULE_3__.Scroll)(stream)
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,_file_upload__WEBPACK_IMPORTED_MODULE_0__.FileUpload)({afterUpload: enhance})
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