ruby_llm-top_secret 0.1.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 +7 -0
- data/CHANGELOG.md +5 -0
- data/CODEOWNERS +15 -0
- data/CODE_OF_CONDUCT.md +6 -0
- data/LICENSE.txt +19 -0
- data/README.md +141 -0
- data/RELEASING.md +43 -0
- data/Rakefile +10 -0
- data/SECURITY.md +15 -0
- data/lib/ruby_llm/top_secret/acts_as_filtered_chat.rb +73 -0
- data/lib/ruby_llm/top_secret/patches/acts_as_chat.rb +30 -0
- data/lib/ruby_llm/top_secret/patches/chat.rb +48 -0
- data/lib/ruby_llm/top_secret/payload.rb +50 -0
- data/lib/ruby_llm/top_secret/railtie.rb +23 -0
- data/lib/ruby_llm/top_secret/version.rb +7 -0
- data/lib/ruby_llm/top_secret.rb +34 -0
- data/sig/ruby_llm/top_secret.rbs +6 -0
- metadata +90 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1796e8abddf5610b5181651f7a68f127d6c943b41446dfb0f8fb5159d46e2201
|
|
4
|
+
data.tar.gz: 3ef4204b57a1e8f2c5d743d5d3ef851fe96785f19d3116f6292e7387314d9ca4
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 471b718cbd0009c3c645111be4baa0aa5d1dd046bbbf9f96b7f3c503d38dc0f2f9a058f77c152ecbf8e00efd87ad6a4da9502f187769fc39d9dfcb46e3396955
|
|
7
|
+
data.tar.gz: b89e53615d7dcff0700665f2c498f7a88dfc17b85a757307d9da9eb0eacac1251f60056858fdfdf0d2be7e7baf8d39e9e3cc519c362c2dd8789034f3b3dd5754
|
data/CHANGELOG.md
ADDED
data/CODEOWNERS
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Lines starting with '#' are comments.
|
|
2
|
+
# Each line is a file pattern followed by one or more owners.
|
|
3
|
+
|
|
4
|
+
# More details are here: https://help.github.com/articles/about-codeowners/
|
|
5
|
+
|
|
6
|
+
# The '*' pattern is global owners.
|
|
7
|
+
|
|
8
|
+
# Order is important. The last matching pattern has the most precedence.
|
|
9
|
+
# The folders are ordered as follows:
|
|
10
|
+
|
|
11
|
+
# In each subsection folders are ordered first by depth, then alphabetically.
|
|
12
|
+
# This should make it easy to add new rules without breaking existing ones.
|
|
13
|
+
|
|
14
|
+
# Global rule:
|
|
15
|
+
* @stevepolitodesign
|
data/CODE_OF_CONDUCT.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) Steve Polito and thoughtbot, inc.
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
|
11
|
+
all copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# RubyLLM::TopSecret
|
|
2
|
+
|
|
3
|
+
[](https://github.com/thoughtbot/ruby_llm-top_secret/actions/workflows/main.yml)
|
|
4
|
+
|
|
5
|
+
Filter sensitive information from [RubyLLM](https://rubyllm.com) conversations using [Top Secret](https://github.com/thoughtbot/top_secret).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add this line to your application's Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem "ruby_llm-top_secret", github: "thoughtbot/ruby_llm-top_secret", branch: "main"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then run:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bundle install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
Requiring the gem patches `RubyLLM::Chat` to support filtering sensitive information before it reaches the LLM provider. Filtering is opt-in per conversation using `with_filtering`.
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
RubyLLM::TopSecret.with_filtering do
|
|
27
|
+
chat = RubyLLM.chat
|
|
28
|
+
response = chat.ask("My name is Ralph and my email is ralph@thoughtbot.com")
|
|
29
|
+
|
|
30
|
+
# The provider receives: "My name is [PERSON_1] and my email is [EMAIL_1]"
|
|
31
|
+
# The response comes back with placeholders restored:
|
|
32
|
+
puts response.content
|
|
33
|
+
# => "Nice to meet you, Ralph!"
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Without `with_filtering`, conversations behave normally with no filtering overhead.
|
|
38
|
+
|
|
39
|
+
### How it works
|
|
40
|
+
|
|
41
|
+
1. Wrap your conversation in `RubyLLM::TopSecret.with_filtering`
|
|
42
|
+
2. Before sending to the provider, all messages are filtered using `TopSecret::Text.filter_all`
|
|
43
|
+
3. The provider only sees placeholders like `[PERSON_1]` and `[EMAIL_1]`
|
|
44
|
+
4. The response is restored using `TopSecret::FilteredText.restore`
|
|
45
|
+
5. Original message content is always preserved locally
|
|
46
|
+
|
|
47
|
+
Filtering state is thread-isolated, so concurrent requests in a web server won't interfere with each other.
|
|
48
|
+
|
|
49
|
+
### Rails integration
|
|
50
|
+
|
|
51
|
+
In Rails apps with `acts_as_chat`, declare `acts_as_filtered_chat` on your model so that filtering works automatically — including from background jobs where a `with_filtering` block isn't possible.
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
class Chat < ApplicationRecord
|
|
55
|
+
acts_as_chat
|
|
56
|
+
acts_as_filtered_chat
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Every call to `complete` on this model will filter automatically. The restored (not filtered) response is what gets saved to your database.
|
|
61
|
+
|
|
62
|
+
> [!NOTE]
|
|
63
|
+
> When filtering is active, the assistant message is written to the database twice — once by RubyLLM's built-in callback (with filtered placeholders), and again by this gem (with restored content). This is a known limitation of the current architecture.
|
|
64
|
+
|
|
65
|
+
#### Per-chat filtering
|
|
66
|
+
|
|
67
|
+
To control filtering per chat, pass an `if:` condition with a Symbol or Proc. The gem does not provide a database column — your application is responsible for storing the decision and exposing it via a method on the model.
|
|
68
|
+
|
|
69
|
+
1. Add a boolean column to your chats table
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
rails generate migration AddFilteredToChats filtered:boolean
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
2. Pass `if: :filtered?` to `acts_as_chat`
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
class Chat < ApplicationRecord
|
|
79
|
+
acts_as_chat
|
|
80
|
+
acts_as_filtered_chat if: :filtered?
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
> [!NOTE]
|
|
85
|
+
> The `if:` option follows the same convention as [Rails callbacks](https://guides.rubyonrails.org/active_record_callbacks.html#conditional-callbacks) — it accepts a Symbol (method name) or a Proc:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
acts_as_filtered_chat if: -> { filtered? }
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Error handling
|
|
92
|
+
|
|
93
|
+
Errors from Top Secret (filtering or restoring failures) are wrapped in `RubyLLM::TopSecret::Error`. Errors from RubyLLM itself (API failures, etc.) are passed through unchanged.
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
RubyLLM::TopSecret.with_filtering do
|
|
97
|
+
chat.ask("Hello")
|
|
98
|
+
rescue RubyLLM::TopSecret::Error => e
|
|
99
|
+
# Top Secret failed (e.g., NER model missing)
|
|
100
|
+
rescue RubyLLM::Error => e
|
|
101
|
+
# RubyLLM API error
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Development
|
|
106
|
+
|
|
107
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
108
|
+
|
|
109
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org)
|
|
110
|
+
|
|
111
|
+
## Contributing
|
|
112
|
+
|
|
113
|
+
[Bug reports](https://github.com/thoughtbot/ruby_llm-top_secret/issues/new?template=bug_report.md) and [pull requests](https://github.com/thoughtbot/ruby_llm-top_secret/pulls) are welcome on GitHub at [https://github.com/thoughtbot/ruby_llm-top_secret](https://github.com/thoughtbot/ruby_llm-top_secret).
|
|
114
|
+
|
|
115
|
+
Please create a [new discussion](https://github.com/thoughtbot/ruby_llm-top_secret/discussions/new?category=ideas) if you want to share ideas for new features.
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
ruby_llm-top_secret is Copyright (c) thoughtbot, inc.
|
|
120
|
+
It is free software, and may be redistributed
|
|
121
|
+
under the terms specified in the [LICENSE] file.
|
|
122
|
+
|
|
123
|
+
[LICENSE]: /LICENSE.txt
|
|
124
|
+
|
|
125
|
+
<!-- START /templates/footer.md -->
|
|
126
|
+
|
|
127
|
+
## About thoughtbot
|
|
128
|
+
|
|
129
|
+

|
|
130
|
+
|
|
131
|
+
This repo is maintained and funded by thoughtbot, inc.
|
|
132
|
+
The names and logos for thoughtbot are trademarks of thoughtbot, inc.
|
|
133
|
+
|
|
134
|
+
We love open source software!
|
|
135
|
+
See [our other projects][community].
|
|
136
|
+
We are [available for hire][hire].
|
|
137
|
+
|
|
138
|
+
[community]: https://thoughtbot.com/community?utm_source=github
|
|
139
|
+
[hire]: https://thoughtbot.com/hire-us?utm_source=github
|
|
140
|
+
|
|
141
|
+
<!-- END /templates/footer.md -->
|
data/RELEASING.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
## 1. Update version file accordingly
|
|
4
|
+
|
|
5
|
+
Include an RC (release candidate) number if appropriate, e.g. 2.0.0.rc.
|
|
6
|
+
|
|
7
|
+
## 2. Update `CHANGELOG.md` to reflect the changes since last release
|
|
8
|
+
|
|
9
|
+
You can copy [GitHub automatically generated release notes] for this step.
|
|
10
|
+
|
|
11
|
+
## 3. Commit changes
|
|
12
|
+
|
|
13
|
+
There shouldn't be code changes, and thus CI doesn't need to run. Add "[ci skip]" to the commit message.
|
|
14
|
+
|
|
15
|
+
## 4. Tag the release: `git tag -s vVERSION`
|
|
16
|
+
|
|
17
|
+
We recommend the [_quick guide on how to sign a release_] from git ready.
|
|
18
|
+
|
|
19
|
+
## 5. Push changes
|
|
20
|
+
|
|
21
|
+
Push the changes with `git push --tags`
|
|
22
|
+
|
|
23
|
+
## 6. Build and publish
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
gem build ruby_llm-top_secret.gemspec
|
|
27
|
+
gem push ruby_llm-top_secret-*.gem
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
If the project is hosted on RubyGems, consider using [RubyGems Trusted Publication] to automatically
|
|
31
|
+
push the release using a GitHub action.
|
|
32
|
+
|
|
33
|
+
## 7. Add a new GitHub release using the recent `CHANGELOG.md` as the content
|
|
34
|
+
|
|
35
|
+
Sample URL: https://github.com/thoughtbot/ruby_llm-top_secret/releases/new?tag=vVERSION
|
|
36
|
+
|
|
37
|
+
## 8. Announce the new release
|
|
38
|
+
|
|
39
|
+
Make sure to say "thank you" to the contributors who helped shape this version!
|
|
40
|
+
|
|
41
|
+
[RubyGems Trusted Publication]: https://github.com/rubygems/release-gem
|
|
42
|
+
[_quick guide on how to sign a release_]: https://gitready.com/advanced/2014/11/02/gpg-sign-releases.html
|
|
43
|
+
[GitHub automatically generated release notes]: https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes#about-automatically-generated-release-notes
|
data/Rakefile
ADDED
data/SECURITY.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
Only the latest version of ruby_llm-top_secret is supported at a given time. If
|
|
6
|
+
you find a security issue with an older version, please try updating to the
|
|
7
|
+
latest version first.
|
|
8
|
+
|
|
9
|
+
If for some reason you can't update to the latest version, please let us know
|
|
10
|
+
your reasons so that we can have a better understanding of your situation.
|
|
11
|
+
|
|
12
|
+
## Reporting a Vulnerability
|
|
13
|
+
|
|
14
|
+
For security inquiries or vulnerability reports, visit
|
|
15
|
+
<https://thoughtbot.com/security>.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/concern"
|
|
4
|
+
|
|
5
|
+
module RubyLLM
|
|
6
|
+
module TopSecret
|
|
7
|
+
# Provides the +acts_as_filtered_chat+ class method for ActiveRecord
|
|
8
|
+
# models that use +acts_as_chat+. Declaring it automatically wraps
|
|
9
|
+
# +complete+ in {RubyLLM::TopSecret.with_filtering}, so filtering
|
|
10
|
+
# works across job boundaries without a manual block.
|
|
11
|
+
#
|
|
12
|
+
# The gem does not provide a database column. Your application is
|
|
13
|
+
# responsible for storing the per-chat filtering decision (e.g. a
|
|
14
|
+
# +filtered+ boolean column) and exposing it via a predicate method.
|
|
15
|
+
#
|
|
16
|
+
# @example Filter all chats for this model
|
|
17
|
+
# class Chat < ApplicationRecord
|
|
18
|
+
# acts_as_chat
|
|
19
|
+
# acts_as_filtered_chat
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# @example Filter per-chat with a symbol condition
|
|
23
|
+
# class Chat < ApplicationRecord
|
|
24
|
+
# acts_as_chat
|
|
25
|
+
# acts_as_filtered_chat if: :filtered?
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# @example Filter per-chat with a Proc condition
|
|
29
|
+
# class Chat < ApplicationRecord
|
|
30
|
+
# acts_as_chat
|
|
31
|
+
# acts_as_filtered_chat if: -> { filtered? }
|
|
32
|
+
# end
|
|
33
|
+
module ActsAsFilteredChat
|
|
34
|
+
extend ActiveSupport::Concern
|
|
35
|
+
|
|
36
|
+
class_methods do
|
|
37
|
+
# Enables automatic filtering for this model's chat completions.
|
|
38
|
+
#
|
|
39
|
+
# @param options [Hash]
|
|
40
|
+
# @option options [Symbol, Proc] :if A method name or lambda that
|
|
41
|
+
# determines whether filtering is active for a given chat instance.
|
|
42
|
+
# When omitted, all chats for this model are filtered.
|
|
43
|
+
def acts_as_filtered_chat(**options)
|
|
44
|
+
class_attribute :filter_condition, instance_writer: false
|
|
45
|
+
self.filter_condition = options[:if]
|
|
46
|
+
|
|
47
|
+
prepend AutoFiltering
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @api private
|
|
53
|
+
module AutoFiltering
|
|
54
|
+
def complete(...)
|
|
55
|
+
condition = filter_condition
|
|
56
|
+
should_filter = if condition
|
|
57
|
+
case condition
|
|
58
|
+
when Symbol then send(condition)
|
|
59
|
+
when Proc then instance_exec(&condition)
|
|
60
|
+
end
|
|
61
|
+
else
|
|
62
|
+
true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
if should_filter
|
|
66
|
+
RubyLLM::TopSecret.with_filtering { super }
|
|
67
|
+
else
|
|
68
|
+
super
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module TopSecret
|
|
5
|
+
module Patches
|
|
6
|
+
# Patch for {RubyLLM::ActiveRecord::ChatMethods#complete} that persists
|
|
7
|
+
# the restored response content to the database.
|
|
8
|
+
#
|
|
9
|
+
# When filtering is active via {RubyLLM::TopSecret.with_filtering}
|
|
10
|
+
# (or automatically via {ActsAsFilteredChat}), the default
|
|
11
|
+
# persistence callback (+on_end_message+) fires inside
|
|
12
|
+
# {Patches::Chat#complete} before restoration occurs, saving filtered
|
|
13
|
+
# content. This patch updates the persisted assistant message with the
|
|
14
|
+
# restored content afterward.
|
|
15
|
+
#
|
|
16
|
+
# @see Patches::Chat
|
|
17
|
+
# @see ActsAsFilteredChat
|
|
18
|
+
module ActsAsChat
|
|
19
|
+
# @see RubyLLM::ActiveRecord::ChatMethods#complete
|
|
20
|
+
def complete(...)
|
|
21
|
+
response = super
|
|
22
|
+
|
|
23
|
+
@message.update!(content: response.content)
|
|
24
|
+
|
|
25
|
+
response
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module TopSecret
|
|
5
|
+
module Patches
|
|
6
|
+
# Middleware that intercepts {RubyLLM::Chat#complete} to filter sensitive
|
|
7
|
+
# information from messages before they are sent to the LLM provider,
|
|
8
|
+
# and restore placeholders in the response with original values.
|
|
9
|
+
#
|
|
10
|
+
# Filtering only runs when opted in via {RubyLLM::TopSecret.with_filtering}.
|
|
11
|
+
# Delegates to {Payload} for filtering and restoring content.
|
|
12
|
+
# Original message content is preserved after the call via +ensure+.
|
|
13
|
+
#
|
|
14
|
+
# @raise [RubyLLM::TopSecret::Error] if filtering or restoring fails
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# RubyLLM::TopSecret.with_filtering do
|
|
18
|
+
# chat = RubyLLM.chat
|
|
19
|
+
# chat.ask("My name is Ralph")
|
|
20
|
+
# # => Provider receives "My name is [PERSON_1]"
|
|
21
|
+
# # => Response "[PERSON_1] is a great name" becomes "Ralph is a great name"
|
|
22
|
+
# end
|
|
23
|
+
module Chat
|
|
24
|
+
# @see RubyLLM::Chat#complete
|
|
25
|
+
def complete(&)
|
|
26
|
+
return super unless RubyLLM::TopSecret.filtering?
|
|
27
|
+
|
|
28
|
+
originals = messages.to_h { |msg| [msg, msg.content] }
|
|
29
|
+
|
|
30
|
+
payload = Payload.new(messages)
|
|
31
|
+
payload.filter
|
|
32
|
+
|
|
33
|
+
response = super
|
|
34
|
+
|
|
35
|
+
response.content = payload.restore(response.content)
|
|
36
|
+
|
|
37
|
+
response
|
|
38
|
+
rescue RubyLLM::Error
|
|
39
|
+
raise
|
|
40
|
+
rescue => e
|
|
41
|
+
raise RubyLLM::TopSecret::Error, e.message
|
|
42
|
+
ensure
|
|
43
|
+
originals&.each { |msg, content| msg.content = content }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module TopSecret
|
|
5
|
+
# Represents the sensitive content flowing between a RubyLLM chat and
|
|
6
|
+
# the LLM provider. Mirrors Top Secret's own vocabulary:
|
|
7
|
+
# {#filter} filters messages before sending, and {#restore} restores
|
|
8
|
+
# placeholders in the response.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# payload = Payload.new(messages)
|
|
12
|
+
# payload.filter
|
|
13
|
+
# # ... send to provider ...
|
|
14
|
+
# restored_content = payload.restore(response.content)
|
|
15
|
+
class Payload
|
|
16
|
+
# @param messages [Array<RubyLLM::Message>]
|
|
17
|
+
def initialize(messages)
|
|
18
|
+
@messages = messages
|
|
19
|
+
@mapping = {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Filters sensitive content from messages in place using
|
|
23
|
+
# {TopSecret::Text.filter_all} for consistent labels across turns.
|
|
24
|
+
# @return [void]
|
|
25
|
+
def filter
|
|
26
|
+
filterable = @messages.reject { |msg| msg.content.nil? || msg.content.empty? }
|
|
27
|
+
contents = filterable.map(&:content)
|
|
28
|
+
batch_result = ::TopSecret::Text.filter_all(contents)
|
|
29
|
+
|
|
30
|
+
filterable.zip(batch_result.items).each { |msg, item| msg.content = item.output }
|
|
31
|
+
@mapping = batch_result.mapping
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Restores filtered placeholders in the response content with original
|
|
35
|
+
# values. Handles both String and Hash (structured output) content.
|
|
36
|
+
#
|
|
37
|
+
# @param content [String, Hash] the response content to restore
|
|
38
|
+
# @return [String, Hash] content with placeholders replaced
|
|
39
|
+
def restore(content)
|
|
40
|
+
if content.is_a?(Hash)
|
|
41
|
+
json = JSON.generate(content)
|
|
42
|
+
restored = ::TopSecret::FilteredText.restore(json, mapping: @mapping)
|
|
43
|
+
JSON.parse(restored.output)
|
|
44
|
+
else
|
|
45
|
+
::TopSecret::FilteredText.restore(content, mapping: @mapping).output
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module TopSecret
|
|
5
|
+
# Automatically prepends {Patches::ActsAsChat} onto
|
|
6
|
+
# {RubyLLM::ActiveRecord::ChatMethods} after RubyLLM's own Railtie
|
|
7
|
+
# has loaded the ActiveRecord integration.
|
|
8
|
+
class Railtie < Rails::Railtie
|
|
9
|
+
initializer "ruby_llm_top_secret.active_record", after: "ruby_llm.active_record" do
|
|
10
|
+
ActiveSupport.on_load :active_record do
|
|
11
|
+
require "ruby_llm/top_secret/patches/acts_as_chat"
|
|
12
|
+
require "ruby_llm/top_secret/acts_as_filtered_chat"
|
|
13
|
+
|
|
14
|
+
RubyLLM::ActiveRecord::ChatMethods.module_eval do
|
|
15
|
+
prepend RubyLLM::TopSecret::Patches::ActsAsChat
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
::ActiveRecord::Base.include RubyLLM::TopSecret::ActsAsFilteredChat
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_llm"
|
|
4
|
+
require "top_secret"
|
|
5
|
+
require_relative "top_secret/version"
|
|
6
|
+
require_relative "top_secret/payload"
|
|
7
|
+
require_relative "top_secret/patches/chat"
|
|
8
|
+
|
|
9
|
+
module RubyLLM
|
|
10
|
+
# Integrates Top Secret with RubyLLM to automatically filter sensitive
|
|
11
|
+
# information before it is sent to LLM providers.
|
|
12
|
+
module TopSecret
|
|
13
|
+
# Base error class for all RubyLLM::TopSecret errors.
|
|
14
|
+
class Error < StandardError; end
|
|
15
|
+
|
|
16
|
+
def self.with_filtering
|
|
17
|
+
was_filtering = Thread.current[:ruby_llm_top_secret_filter]
|
|
18
|
+
Thread.current[:ruby_llm_top_secret_filter] = true
|
|
19
|
+
yield
|
|
20
|
+
ensure
|
|
21
|
+
Thread.current[:ruby_llm_top_secret_filter] = was_filtering
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.filtering?
|
|
25
|
+
Thread.current[:ruby_llm_top_secret_filter] == true
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
RubyLLM::Chat.prepend(RubyLLM::TopSecret::Patches::Chat)
|
|
31
|
+
|
|
32
|
+
if defined?(Rails::Railtie)
|
|
33
|
+
require_relative "top_secret/railtie"
|
|
34
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ruby_llm-top_secret
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Steve Polito
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: ruby_llm
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.7'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.7'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: top_secret
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.0'
|
|
40
|
+
description: A RubyLLM plugin that integrates Top Secret to automatically filter sensitive
|
|
41
|
+
information from LLM interactions.
|
|
42
|
+
email:
|
|
43
|
+
- stevepolito@hey.com
|
|
44
|
+
executables: []
|
|
45
|
+
extensions: []
|
|
46
|
+
extra_rdoc_files: []
|
|
47
|
+
files:
|
|
48
|
+
- CHANGELOG.md
|
|
49
|
+
- CODEOWNERS
|
|
50
|
+
- CODE_OF_CONDUCT.md
|
|
51
|
+
- LICENSE.txt
|
|
52
|
+
- README.md
|
|
53
|
+
- RELEASING.md
|
|
54
|
+
- Rakefile
|
|
55
|
+
- SECURITY.md
|
|
56
|
+
- lib/ruby_llm/top_secret.rb
|
|
57
|
+
- lib/ruby_llm/top_secret/acts_as_filtered_chat.rb
|
|
58
|
+
- lib/ruby_llm/top_secret/patches/acts_as_chat.rb
|
|
59
|
+
- lib/ruby_llm/top_secret/patches/chat.rb
|
|
60
|
+
- lib/ruby_llm/top_secret/payload.rb
|
|
61
|
+
- lib/ruby_llm/top_secret/railtie.rb
|
|
62
|
+
- lib/ruby_llm/top_secret/version.rb
|
|
63
|
+
- sig/ruby_llm/top_secret.rbs
|
|
64
|
+
homepage: https://github.com/thoughtbot/ruby_llm-top_secret
|
|
65
|
+
licenses:
|
|
66
|
+
- MIT
|
|
67
|
+
metadata:
|
|
68
|
+
allowed_push_host: https://rubygems.org
|
|
69
|
+
homepage_uri: https://github.com/thoughtbot/ruby_llm-top_secret
|
|
70
|
+
source_code_uri: https://github.com/thoughtbot/ruby_llm-top_secret
|
|
71
|
+
changelog_uri: https://github.com/thoughtbot/ruby_llm-top_secret/blob/main/CHANGELOG.md
|
|
72
|
+
rubygems_mfa_required: 'true'
|
|
73
|
+
rdoc_options: []
|
|
74
|
+
require_paths:
|
|
75
|
+
- lib
|
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: 3.2.0
|
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
82
|
+
requirements:
|
|
83
|
+
- - ">="
|
|
84
|
+
- !ruby/object:Gem::Version
|
|
85
|
+
version: '0'
|
|
86
|
+
requirements: []
|
|
87
|
+
rubygems_version: 4.0.10
|
|
88
|
+
specification_version: 4
|
|
89
|
+
summary: Filter sensitive information from RubyLLM conversations using Top Secret.
|
|
90
|
+
test_files: []
|