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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 51b2db665dd9baa7f6003b0cdd360d07ab3e1cba08f451400d7063f4bce234d5
4
- data.tar.gz: e058de42d7416b06df5e35c605c734d8b7af9f93d7fda9015b81f576cca7d286
3
+ metadata.gz: 5496c45e309886d6da018a08c25a794226fa02f3f88133d338d9503e5b7f756a
4
+ data.tar.gz: ba68687dbe30954d16000ba1b9d0e3dd361b01c174c65fbc99eecc2dbbf4988a
5
5
  SHA512:
6
- metadata.gz: 2efe61c19fb25fcbe28dfc94ad68bcae7fe6927950edf0ab617fe473166590491d465913d185ffa94f35d6e4458b2ed0534afddb71a160faba72445bcb36b674
7
- data.tar.gz: 1185e1e81c2c8180ee17165579400a688fc146af4e40f2cb9d03ed2e149274ff2bbfd7fdd7125c638c995beab6e955b2c06ac63995bfb01d9d093d4bb9158e9d
6
+ metadata.gz: eba7c940580c7524494409c9e6ee4c21b82bfe835f8877fe1d7b5a2a928f537e90f1cb3d155333d345b153aa027e798c4e5803b9dd0970fb296eb2701463e9f2
7
+ data.tar.gz: 67b645677cdf7ef749cd03b8515ded7a26bcd47fbffdd447d3d912f685c16419750c06ade9608317bc4f527234fe068ce8e21913c868f99cb8fff496d562751e
data/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## v0.6.0
6
+
7
+ Changes since v0.5.0.
8
+
5
9
  ### Change
6
10
 
7
11
  * **Move the default Relay home to `~/.config/relay`** <br>
data/README.md CHANGED
@@ -1,66 +1,64 @@
1
1
  ## About
2
2
 
3
- Relay is a self-hostable LLM environment with support for OpenAI, DeepSeek,
4
- Anthropic, xAI and zAI out of the box. It is incredibly simple to setup
5
- and get started. The application is distributed as a RubyGem. It has a minimal
6
- set of dependencies - built on Roda, Sequel, Falcon, [llm.rb](https://github.com/llmrb/llm.rb),
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
- There is support for connecting to MCP servers too - both HTTP and stdio. You can
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
+ ![demo](./demo.gif)
13
17
 
14
- ## Getting started
18
+ ## Appearance
15
19
 
16
- #### Install
20
+ #### Sign-in
17
21
 
18
- Install the gem:
22
+ ![Relay screenshot](./relay3.png)
19
23
 
20
- ```sh
21
- gem install relay.app
22
- ```
24
+ #### Chat
23
25
 
24
- Go through interactive setup, start the server, and visit
25
- http://localhost:9292.
26
+ ![Relay screenshot](./relay1.png)
26
27
 
27
- ```sh
28
- relay setup
29
- relay start
30
- ```
28
+ #### MCP
31
29
 
32
- ## Features
30
+ ![Relay screenshot](./relay2.png)
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
- ## Sounds cool, how does it look?
33
+ ## Getting started
44
34
 
45
- **Sign-in**
35
+ #### Install
46
36
 
47
- ![Relay screenshot](./relay3.png)
37
+ Install the gem
48
38
 
49
- **Chat**
39
+ ```sh
40
+ gem install relay.app
41
+ ```
50
42
 
51
- ![Relay screenshot](./relay1.png)
43
+ #### Configure
52
44
 
53
- **MCP**
45
+ Interactive setup
54
46
 
55
- ![Relay screenshot](./relay2.png)
47
+ ```sh
48
+ relay setup
49
+ ```
56
50
 
57
- ## How easy is it to setup?
51
+ #### Serve
58
52
 
59
- Very easy.
53
+ Start the server, and visit http://localhost:9292
60
54
 
61
- ![demo](./demo.gif)
55
+ ```sh
56
+ relay start
57
+ ```
62
58
 
63
- ## How do I add my own tool?
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
- ## Wait, what is a tool?
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 do anything you can imagine, and it can extend
90
- the abilities of the LLM. Suddenly a LLM can search the web, run code, and anything
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
- powerful for talking to GitHub or your own Forgejo instance.
91
+ useful for talking to GitHub, your own Forgejo instance or any other
92
+ kind of MCP server.
95
93
 
96
- ## What are the default tools?
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
- ## What provider is the best value?
107
+ ## Costs
108
+
109
+ #### What provider is the best value?
110
110
 
111
- DeepSeek. I highly recommend it. The context window is 1M. I have been using it
112
- all the time - especially for Relay development, and despite my heavy usage, it
113
- cost only 80 cents overall. It's almost free. I used it **a lot**. I'd estimate
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
- ## What about Ollama and friends?
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
@@ -21,7 +21,6 @@ module Relay::Models
21
21
  # @return [void]
22
22
  def self.refresh_all
23
23
  Relay.providers.each do |_, provider|
24
- provider = provider.call
25
24
  refresh(provider)
26
25
  rescue LLM::Error
27
26
  next
@@ -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
- return if ctx.functions.empty?
125
- returns = wait_with_heartbeat(conn, ctx.wait(:task))
126
- wait_with_heartbeat(conn, proc { ctx.talk(returns, params) })
127
- if ctx.functions.any?
128
- resolve_functions(ctx, conn, params)
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 or llm.rb documentation so answers can cite project details"
14
- param :topic, Enum["relay", "llm.rb"], "The knowledge topic", required: true
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: relay_documentation}
22
- when "llm.rb" then {directions:, documentation: llmrb_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 relay_documentation
30
- relay_resources.each_with_object({}) do |(key, url), h|
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
- CGI.parse(uri.query.to_s).fetch("v", []).first
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Relay
4
- VERSION = "0.6.0"
4
+ VERSION = "0.7.1"
5
5
  end
data/lib/relay.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Relay
4
4
  require "fileutils"
5
- gem "llm.rb", "= 8.1.0"
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" => -> { LLM.bedrock(access_key_id: ENV["AWS_ACCESS_KEY_ID"], secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"]) }
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.from(PROVIDERS).transform_values!(&:call)
48
+ @providers ||= LLM::Object
49
+ .from(PROVIDERS)
50
+ .transform_values!(&:call)
51
+ .compact
45
52
  end
46
53
 
47
54
  ##
@@ -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.6.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: 8.1.0
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: 8.1.0
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: Relay is a self-hostable LLM environment built on llm.rb that you can
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: Self-hostable LLM environment you can run in under 2 minutes
482
+ summary: Ruby's hackable AI web environment
485
483
  test_files: []