{"id":49172496,"url":"https://github.com/thoughtbot/ruby_llm-top_secret","last_synced_at":"2026-06-22T06:03:57.571Z","repository":{"id":356639499,"uuid":"1209618021","full_name":"thoughtbot/ruby_llm-top_secret","owner":"thoughtbot","description":"Automatically filter sensitive information from RubyLLM conversations using Top Secret.","archived":false,"fork":false,"pushed_at":"2026-04-14T18:45:20.000Z","size":48,"stargazers_count":13,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-21T19:05:00.958Z","etag":null,"topics":["anonymization","data-anon","data-obfuscation","data-privacy","data-redaction","llm-tools","named-entity-recognition","ner","personal-identifiable-information","pii","pii-detection","privacy","redaction","ruby","ruby-llm","rubyllm"],"latest_commit_sha":null,"homepage":"https://thoughtbot.com/blog/ruby-llm-top-secret","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/thoughtbot.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":"CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"thoughtbot"}},"created_at":"2026-04-13T15:55:52.000Z","updated_at":"2026-06-09T07:49:56.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/thoughtbot/ruby_llm-top_secret","commit_stats":null,"previous_names":["thoughtbot/ruby_llm-top_secret"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/thoughtbot/ruby_llm-top_secret","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thoughtbot%2Fruby_llm-top_secret","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thoughtbot%2Fruby_llm-top_secret/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thoughtbot%2Fruby_llm-top_secret/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thoughtbot%2Fruby_llm-top_secret/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/thoughtbot","download_url":"https://codeload.github.com/thoughtbot/ruby_llm-top_secret/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thoughtbot%2Fruby_llm-top_secret/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34636427,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-22T02:00:06.391Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["anonymization","data-anon","data-obfuscation","data-privacy","data-redaction","llm-tools","named-entity-recognition","ner","personal-identifiable-information","pii","pii-detection","privacy","redaction","ruby","ruby-llm","rubyllm"],"created_at":"2026-04-22T20:00:44.258Z","updated_at":"2026-06-22T06:03:57.567Z","avatar_url":"https://github.com/thoughtbot.png","language":"Ruby","funding_links":["https://github.com/sponsors/thoughtbot"],"categories":["Ruby"],"sub_categories":[],"readme":"# RubyLLM::TopSecret\n\n[![Ruby](https://github.com/thoughtbot/ruby_llm-top_secret/actions/workflows/main.yml/badge.svg)](https://github.com/thoughtbot/ruby_llm-top_secret/actions/workflows/main.yml)\n\nFilter sensitive information from [RubyLLM](https://rubyllm.com) conversations using [Top Secret](https://github.com/thoughtbot/top_secret).\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem \"ruby_llm-top_secret\"\n```\n\nThen run:\n\n```bash\nbundle install\n```\n\n## Usage\n\nRequiring 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`.\n\n```ruby\nRubyLLM::TopSecret.with_filtering do\n  chat = RubyLLM.chat\n  response = chat.ask(\"My name is Ralph and my email is ralph@thoughtbot.com\")\n\n  # The provider receives: \"My name is [PERSON_1] and my email is [EMAIL_1]\"\n  # The response comes back with placeholders restored:\n  puts response.content\n  # =\u003e \"Nice to meet you, Ralph!\"\nend\n```\n\nWithout `with_filtering`, conversations behave normally with no filtering overhead.\n\n### How it works\n\n1. Wrap your conversation in `RubyLLM::TopSecret.with_filtering`\n2. Before sending to the provider, all messages are filtered using `TopSecret::Text.filter_all`\n3. The provider only sees placeholders like `[PERSON_1]` and `[EMAIL_1]`\n4. The response is restored using `TopSecret::FilteredText.restore`\n5. Original message content is always preserved locally\n\nFiltering state is thread-isolated, so concurrent requests in a web server won't interfere with each other.\n\n### Rails integration\n\nIn 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.\n\n```ruby\nclass Chat \u003c ApplicationRecord\n  acts_as_chat\n  acts_as_filtered_chat\nend\n```\n\nEvery call to `complete` on this model will filter automatically. The restored (not filtered) response is what gets saved to your database.\n\n\u003e [!NOTE]\n\u003e 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.\n\n#### Per-chat filtering\n\nTo 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.\n\n1. Add a boolean column to your chats table\n\n    ```\n    rails generate migration AddFilteredToChats filtered:boolean\n    ```\n\n2. Pass `if: :filtered?` to `acts_as_chat`\n\n    ```ruby\n    class Chat \u003c ApplicationRecord\n      acts_as_chat\n      acts_as_filtered_chat if: :filtered?\n    end\n    ```\n\n\u003e [!NOTE]\n\u003e 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:\n\n```ruby\nacts_as_filtered_chat if: -\u003e { filtered? }\n```\n\n### Error handling\n\nErrors from Top Secret (filtering or restoring failures) are wrapped in `RubyLLM::TopSecret::Error`. Errors from RubyLLM itself (API failures, etc.) are passed through unchanged.\n\n```ruby\nRubyLLM::TopSecret.with_filtering do\n  chat.ask(\"Hello\")\nrescue RubyLLM::TopSecret::Error =\u003e e\n  # Top Secret failed (e.g., NER model missing)\nrescue RubyLLM::Error =\u003e e\n  # RubyLLM API error\nend\n```\n\n## Development\n\nAfter 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.\n\nTo 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)\n\n## Contributing\n\n[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).\n\nPlease 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.\n\n## License\n\nruby_llm-top_secret is Copyright (c) thoughtbot, inc.\nIt is free software, and may be redistributed\nunder the terms specified in the [LICENSE] file.\n\n[LICENSE]: /LICENSE.txt\n\n\u003c!-- START /templates/footer.md --\u003e\n\n## About thoughtbot\n\n![thoughtbot](https://thoughtbot.com/thoughtbot-logo-for-readmes.svg)\n\nThis repo is maintained and funded by thoughtbot, inc.\nThe names and logos for thoughtbot are trademarks of thoughtbot, inc.\n\nWe love open source software!\nSee [our other projects][community].\nWe are [available for hire][hire].\n\n[community]: https://thoughtbot.com/community?utm_source=github\n[hire]: https://thoughtbot.com/hire-us?utm_source=github\n\n\u003c!-- END /templates/footer.md --\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthoughtbot%2Fruby_llm-top_secret","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthoughtbot%2Fruby_llm-top_secret","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthoughtbot%2Fruby_llm-top_secret/lists"}