relay.app 0.6.0 → 0.7.1
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 +4 -0
- data/README.md +51 -52
- data/app/models/model_record.rb +0 -1
- data/app/routes/websocket/connection.rb +11 -11
- data/app/tools/relay_knowledge.rb +14 -13
- data/lib/relay/jukebox.rb +2 -1
- data/lib/relay/version.rb +1 -1
- data/lib/relay.rb +15 -8
- data/libexec/relay/configure +2 -2
- metadata +6 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5496c45e309886d6da018a08c25a794226fa02f3f88133d338d9503e5b7f756a
|
|
4
|
+
data.tar.gz: ba68687dbe30954d16000ba1b9d0e3dd361b01c174c65fbc99eecc2dbbf4988a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eba7c940580c7524494409c9e6ee4c21b82bfe835f8877fe1d7b5a2a928f537e90f1cb3d155333d345b153aa027e798c4e5803b9dd0970fb296eb2701463e9f2
|
|
7
|
+
data.tar.gz: 67b645677cdf7ef749cd03b8515ded7a26bcd47fbffdd447d3d912f685c16419750c06ade9608317bc4f527234fe068ce8e21913c868f99cb8fff496d562751e
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
|
@@ -1,66 +1,64 @@
|
|
|
1
1
|
## About
|
|
2
2
|
|
|
3
|
-
Relay is a self-hostable LLM environment
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
Relay is a self-hostable, hackable LLM web environment that can be extended
|
|
4
|
+
with your own tools and skills that live in your `${HOME}` directory.
|
|
5
|
+
It is for programmers, AI engineers, hackers, and anyone who wants
|
|
6
|
+
their own AI environment with the option to extend it with code.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
## Setup
|
|
10
|
+
|
|
11
|
+
It is simple to setup and get started. The application is
|
|
12
|
+
distributed as a RubyGem. It has a minimal set of dependencies -
|
|
13
|
+
built on Roda, Sequel, Falcon, [llm.rb](https://github.com/llmrb/llm.rb),
|
|
7
14
|
HTMX and web sockets.
|
|
8
15
|
|
|
9
|
-
|
|
10
|
-
add your own tools to `~/.config/relay/tools` which is a neat way to extend the
|
|
11
|
-
environment with your own functionality. The database uses SQLite3 to keep things
|
|
12
|
-
simple - the goal is to have something you can setup in under two minutes.
|
|
16
|
+

|
|
13
17
|
|
|
14
|
-
##
|
|
18
|
+
## Appearance
|
|
15
19
|
|
|
16
|
-
####
|
|
20
|
+
#### Sign-in
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+

|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
gem install relay.app
|
|
22
|
-
```
|
|
24
|
+
#### Chat
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
http://localhost:9292.
|
|
26
|
+

|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
relay setup
|
|
29
|
-
relay start
|
|
30
|
-
```
|
|
28
|
+
#### MCP
|
|
31
29
|
|
|
32
|
-
|
|
30
|
+

|
|
33
31
|
|
|
34
|
-
* Install and setup in 2 minutes
|
|
35
|
-
* Localize your chats and mcp settings to your user account
|
|
36
|
-
* Connect to multiple providers (OpenAI, xAI, Anthropic, Google, DeepSeek, zAI)
|
|
37
|
-
* Connect to MCP servers
|
|
38
|
-
* Cancel in-flight requests and tool execution cleanly
|
|
39
|
-
* Run tools concurrently
|
|
40
|
-
* Make it yours: extend and customize with your own tools and system prompt
|
|
41
|
-
* Lightweight architecture
|
|
42
32
|
|
|
43
|
-
##
|
|
33
|
+
## Getting started
|
|
44
34
|
|
|
45
|
-
|
|
35
|
+
#### Install
|
|
46
36
|
|
|
47
|
-
|
|
37
|
+
Install the gem
|
|
48
38
|
|
|
49
|
-
|
|
39
|
+
```sh
|
|
40
|
+
gem install relay.app
|
|
41
|
+
```
|
|
50
42
|
|
|
51
|
-
|
|
43
|
+
#### Configure
|
|
52
44
|
|
|
53
|
-
|
|
45
|
+
Interactive setup
|
|
54
46
|
|
|
55
|
-
|
|
47
|
+
```sh
|
|
48
|
+
relay setup
|
|
49
|
+
```
|
|
56
50
|
|
|
57
|
-
|
|
51
|
+
#### Serve
|
|
58
52
|
|
|
59
|
-
|
|
53
|
+
Start the server, and visit http://localhost:9292
|
|
60
54
|
|
|
61
|
-
|
|
55
|
+
```sh
|
|
56
|
+
relay start
|
|
57
|
+
```
|
|
62
58
|
|
|
63
|
-
##
|
|
59
|
+
## Tools
|
|
60
|
+
|
|
61
|
+
#### How do I add my own tool?
|
|
64
62
|
|
|
65
63
|
Before running `relay start` you should add `~/.config/relay/tools/<yourtool>.rb`.
|
|
66
64
|
The tool will be automatically made available to the LLM. This is how a tool
|
|
@@ -82,18 +80,18 @@ class Shell < LLM::Tool
|
|
|
82
80
|
end
|
|
83
81
|
```
|
|
84
82
|
|
|
85
|
-
|
|
83
|
+
#### Wait, what is a tool?
|
|
86
84
|
|
|
87
85
|
A tool contains a name, a description, and optional parameters. It is attached
|
|
88
86
|
to a method, and that method that can be called. The model or LLM decides when
|
|
89
|
-
and how to call a tool. A tool can
|
|
90
|
-
|
|
91
|
-
you can think of. They're a powerful way to extend the capabilities of an LLM.
|
|
87
|
+
and how to call a tool. A tool can extend the abilities of the LLM with
|
|
88
|
+
your own code that could can search the web, read documentation, etc.
|
|
92
89
|
|
|
93
90
|
An MCP server can also expose pre-packaged tools, and those can be especially
|
|
94
|
-
|
|
91
|
+
useful for talking to GitHub, your own Forgejo instance or any other
|
|
92
|
+
kind of MCP server.
|
|
95
93
|
|
|
96
|
-
|
|
94
|
+
#### What are the default tools?
|
|
97
95
|
|
|
98
96
|
The `relay-knowledge` tool returns documentation for both Relay
|
|
99
97
|
and [llm.rb](https://github.com/llmrb/llm.rb) - ask about either
|
|
@@ -106,14 +104,15 @@ can be played inline in the chat, and you can also add your own
|
|
|
106
104
|
songs or remove existing ones through the same tools. The only
|
|
107
105
|
requirement is that it is a YouTube URL.
|
|
108
106
|
|
|
109
|
-
##
|
|
107
|
+
## Costs
|
|
108
|
+
|
|
109
|
+
#### What provider is the best value?
|
|
110
110
|
|
|
111
|
-
DeepSeek.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
that a 1M context window costs 14 cents or so.
|
|
111
|
+
DeepSeek. <br>
|
|
112
|
+
Hard to beat it on price. <br>
|
|
113
|
+
Recent models have a context window of 1M.
|
|
115
114
|
|
|
116
|
-
|
|
115
|
+
#### What about self-hosting with Ollama ?
|
|
117
116
|
|
|
118
117
|
[llm.rb](https://github.com/llmrb/llm.rb#readme) provides support ollama, llama.cpp,
|
|
119
118
|
and any OpenAI-compatible endpoint. But Relay does not surface it as a feature. I haven't
|
data/app/models/model_record.rb
CHANGED
|
@@ -73,14 +73,14 @@ class Relay::Routes::Websocket
|
|
|
73
73
|
file = attachment_from_payload(payload) || attachment.consume
|
|
74
74
|
prompt = build_prompt(ctx, payload["message"], file)
|
|
75
75
|
return if prompt.empty?
|
|
76
|
-
vars[:messages].concat [{role: :user, content: prompt}, {role: :assistant, content: +""}]
|
|
77
|
-
write(conn, fragment(:status, status_bar(status: "Thinking...", ctx:)))
|
|
78
|
-
write(conn, fragment(:remove_empty_state)) if vars[:messages].length == 2
|
|
79
|
-
write(conn, fragment(:append_message, message: vars[:messages][-2]))
|
|
80
|
-
write(conn, fragment(:append_message, message: vars[:messages][-1]))
|
|
81
|
-
write(conn, fragment(:input))
|
|
82
76
|
yield_tools(ctx) do |tools|
|
|
83
77
|
params[:tools] = tools
|
|
78
|
+
vars[:messages].concat [{role: :user, content: prompt}, {role: :assistant, content: +""}]
|
|
79
|
+
write(conn, fragment(:status, status_bar(status: "Thinking...", ctx:)))
|
|
80
|
+
write(conn, fragment(:remove_empty_state)) if vars[:messages].length == 2
|
|
81
|
+
write(conn, fragment(:append_message, message: vars[:messages][-2]))
|
|
82
|
+
write(conn, fragment(:append_message, message: vars[:messages][-1]))
|
|
83
|
+
write(conn, fragment(:input))
|
|
84
84
|
wait_with_heartbeat(conn, proc { talk(ctx, prompt, params) })
|
|
85
85
|
resolve_functions(ctx, conn, params)
|
|
86
86
|
end
|
|
@@ -121,11 +121,11 @@ class Relay::Routes::Websocket
|
|
|
121
121
|
# The WebSocket connection object
|
|
122
122
|
# @return [void]
|
|
123
123
|
def resolve_functions(ctx, conn, params)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
while ctx.functions?
|
|
125
|
+
returns = wait_with_heartbeat(conn, proc { ctx.wait(:task) })
|
|
126
|
+
break if returns.empty?
|
|
127
|
+
write(conn, fragment(:status, status_bar(status: tool_status(ctx.functions), ctx:))) if ctx.functions?
|
|
128
|
+
wait_with_heartbeat(conn, proc { ctx.talk(returns, params) })
|
|
129
129
|
end
|
|
130
130
|
end
|
|
131
131
|
|
|
@@ -10,24 +10,26 @@ module Relay::Tools
|
|
|
10
10
|
include Relay::Tool
|
|
11
11
|
|
|
12
12
|
name "relay-knowledge"
|
|
13
|
-
description "Returns Relay
|
|
14
|
-
|
|
13
|
+
description "Returns Relay, llm.rb or mruby-llm documentation"
|
|
14
|
+
parameter :topic, Enum["relay", "llm.rb", "mruby-llm"], "The knowledge topic"
|
|
15
|
+
required %i[topic]
|
|
15
16
|
|
|
16
17
|
##
|
|
17
18
|
# Provides the Relay documentation
|
|
18
19
|
# @return [Hash]
|
|
19
20
|
def call(topic:)
|
|
20
21
|
case topic
|
|
21
|
-
when "relay" then {directions:, documentation:
|
|
22
|
-
when "llm.rb" then {directions:, documentation:
|
|
22
|
+
when "relay" then {directions:, documentation: fetch(relay_resources)}
|
|
23
|
+
when "llm.rb" then {directions:, documentation: fetch(llmrb_resources)}
|
|
24
|
+
when "mruby-llm" then {directions:, documentation: fetch(mruby_llm_resources)}
|
|
23
25
|
else {error: "unknown topic: #{topic}"}
|
|
24
26
|
end
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
private
|
|
28
30
|
|
|
29
|
-
def
|
|
30
|
-
|
|
31
|
+
def fetch(resources)
|
|
32
|
+
resources.each_with_object({}) do |(key, url), h|
|
|
31
33
|
res = Net::HTTP.get_response URI.parse(url)
|
|
32
34
|
h[key] = res.body
|
|
33
35
|
end
|
|
@@ -37,13 +39,6 @@ module Relay::Tools
|
|
|
37
39
|
{"readme" => "https://raw.githubusercontent.com/llmrb/relay/refs/heads/main/README.md"}
|
|
38
40
|
end
|
|
39
41
|
|
|
40
|
-
def llmrb_documentation
|
|
41
|
-
llmrb_resources.each_with_object({}) do |(key, url), h|
|
|
42
|
-
res = Net::HTTP.get_response URI.parse(url)
|
|
43
|
-
h[key] = res.body
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
42
|
def llmrb_resources
|
|
48
43
|
{
|
|
49
44
|
"readme" => "https://raw.githubusercontent.com/llmrb/llm.rb/refs/heads/main/README.md",
|
|
@@ -52,6 +47,12 @@ module Relay::Tools
|
|
|
52
47
|
}
|
|
53
48
|
end
|
|
54
49
|
|
|
50
|
+
def mruby_llm_resources
|
|
51
|
+
{
|
|
52
|
+
"readme" => "https://raw.githubusercontent.com/llmrb/mruby-llm/refs/heads/main/README.md"
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
55
56
|
def directions
|
|
56
57
|
"Reference links from the associated document in your response"
|
|
57
58
|
end
|
data/lib/relay/jukebox.rb
CHANGED
|
@@ -63,7 +63,8 @@ module Relay
|
|
|
63
63
|
def extract_youtube_id(uri)
|
|
64
64
|
path = uri.path.to_s
|
|
65
65
|
return path.split("/").reject(&:empty?).last if path.start_with?("/embed/", "/shorts/")
|
|
66
|
-
|
|
66
|
+
form = URI.decode_www_form(uri.query.to_s)
|
|
67
|
+
(form.to_h["v"] || []).first
|
|
67
68
|
end
|
|
68
69
|
|
|
69
70
|
def songs
|
data/lib/relay/version.rb
CHANGED
data/lib/relay.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Relay
|
|
4
4
|
require "fileutils"
|
|
5
|
-
gem "llm.rb", "=
|
|
5
|
+
gem "llm.rb", "= 11.0.0"
|
|
6
6
|
|
|
7
7
|
require_relative "relay/version"
|
|
8
8
|
require_relative "relay/cache"
|
|
@@ -17,12 +17,16 @@ module Relay
|
|
|
17
17
|
require_relative "relay/reloader"
|
|
18
18
|
|
|
19
19
|
PROVIDERS = {
|
|
20
|
-
"anthropic" => -> { LLM.anthropic(key: ENV["ANTHROPIC_SECRET"]) },
|
|
21
|
-
"deepseek" => -> { LLM.deepseek(key: ENV["DEEPSEEK_SECRET"]) },
|
|
22
|
-
"google" => -> { LLM.google(key: ENV["GOOGLE_SECRET"]) },
|
|
23
|
-
"openai" => -> { LLM.openai(key: ENV["OPENAI_SECRET"]) },
|
|
24
|
-
"xai" => -> { LLM.xai(key: ENV["XAI_SECRET"]) },
|
|
25
|
-
"bedrock" => ->
|
|
20
|
+
"anthropic" => -> { ENV["ANTHROPIC_SECRET"].nil? ? nil : LLM.anthropic(key: ENV["ANTHROPIC_SECRET"]) },
|
|
21
|
+
"deepseek" => -> { ENV["DEEPSEEK_SECRET"].nil? ? nil : LLM.deepseek(key: ENV["DEEPSEEK_SECRET"]) },
|
|
22
|
+
"google" => -> { ENV["GOOGLE_SECRET"].nil? ? nil : LLM.google(key: ENV["GOOGLE_SECRET"]) },
|
|
23
|
+
"openai" => -> { ENV["OPENAI_SECRET"].nil? ? nil : LLM.openai(key: ENV["OPENAI_SECRET"]) },
|
|
24
|
+
"xai" => -> { ENV["XAI_SECRET"].nil? ? nul : LLM.xai(key: ENV["XAI_SECRET"]) },
|
|
25
|
+
"bedrock" => -> do
|
|
26
|
+
(ENV["AWS_ACCESS_KEY_ID"].nil? || ENV["AWS_SECRET_ACCESS_KEY"].nil?) ?
|
|
27
|
+
nil :
|
|
28
|
+
LLM.bedrock(access_key_id: ENV["AWS_ACCESS_KEY_ID"], secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"])
|
|
29
|
+
end
|
|
26
30
|
}.freeze
|
|
27
31
|
private_constant :PROVIDERS
|
|
28
32
|
|
|
@@ -41,7 +45,10 @@ module Relay
|
|
|
41
45
|
# Returns all known providers
|
|
42
46
|
# @return [LLM::Object]
|
|
43
47
|
def self.providers
|
|
44
|
-
@providers ||= LLM::Object
|
|
48
|
+
@providers ||= LLM::Object
|
|
49
|
+
.from(PROVIDERS)
|
|
50
|
+
.transform_values!(&:call)
|
|
51
|
+
.compact
|
|
45
52
|
end
|
|
46
53
|
|
|
47
54
|
##
|
data/libexec/relay/configure
CHANGED
|
@@ -34,8 +34,8 @@ PROVIDERS = [
|
|
|
34
34
|
),
|
|
35
35
|
LLM::Object.from(
|
|
36
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)
|
|
37
|
+
LLM::Object.from(label: "[Bedrock] AWS access key ID", key: "AWS_ACCESS_KEY_ID", aliases: [], secret: false),
|
|
38
|
+
LLM::Object.from(label: "[Bedrock] AWS secret access key", key: "AWS_SECRET_ACCESS_KEY", aliases: [], secret: true)
|
|
39
39
|
]
|
|
40
40
|
)
|
|
41
41
|
].freeze
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: relay.app
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Antar Azri
|
|
@@ -86,14 +86,14 @@ dependencies:
|
|
|
86
86
|
requirements:
|
|
87
87
|
- - '='
|
|
88
88
|
- !ruby/object:Gem::Version
|
|
89
|
-
version:
|
|
89
|
+
version: 11.0.0
|
|
90
90
|
type: :runtime
|
|
91
91
|
prerelease: false
|
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
|
93
93
|
requirements:
|
|
94
94
|
- - '='
|
|
95
95
|
- !ruby/object:Gem::Version
|
|
96
|
-
version:
|
|
96
|
+
version: 11.0.0
|
|
97
97
|
- !ruby/object:Gem::Dependency
|
|
98
98
|
name: net-http-persistent
|
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -318,9 +318,7 @@ dependencies:
|
|
|
318
318
|
- - ">="
|
|
319
319
|
- !ruby/object:Gem::Version
|
|
320
320
|
version: '0'
|
|
321
|
-
description:
|
|
322
|
-
set up and get running in under 2 minutes. Extend it with your own tools, connect
|
|
323
|
-
MCP servers, and run an AI workspace on your own infrastructure.
|
|
321
|
+
description: Ruby's hackable AI web environment
|
|
324
322
|
email:
|
|
325
323
|
- azantar@proton.me
|
|
326
324
|
- 0x1eef@hardenedbsd.org
|
|
@@ -461,7 +459,7 @@ files:
|
|
|
461
459
|
- public/js/relay.js.map
|
|
462
460
|
- public/stylesheets/application.css
|
|
463
461
|
- public/stylesheets/application.css.map
|
|
464
|
-
homepage: https://github.com/llmrb/relay
|
|
462
|
+
homepage: https://github.com/llmrb/relay.app
|
|
465
463
|
licenses:
|
|
466
464
|
- 0BSD
|
|
467
465
|
metadata: {}
|
|
@@ -481,5 +479,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
481
479
|
requirements: []
|
|
482
480
|
rubygems_version: 4.0.6
|
|
483
481
|
specification_version: 4
|
|
484
|
-
summary:
|
|
482
|
+
summary: Ruby's hackable AI web environment
|
|
485
483
|
test_files: []
|