rogalik 0.1.0.pre

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c19fc61a1d0ec42f6e90c0515c3b00f0cda64542ac45fb3cde71c278b509f4c1
4
+ data.tar.gz: 1fb7b63be6faf752e50979ddef46057c7b332edfc5df1354192a64b0da9b8fd6
5
+ SHA512:
6
+ metadata.gz: 59cbac4797105f2665976d616e1f4ec3eefc5c281f312501d12e18b7fef3699f5be52d817e4adb05a896eecc9a82d389fae112c1edba17345751feee2452f118
7
+ data.tar.gz: 9220dbc41a429211061bcb759db30b1aade1e41ec0e36d8ea1c6367e50fa3ce748d4abdd0c5394e3bd4ea46606ea0e979fd0cb30e7afbcc94f5e237856370b9a
@@ -0,0 +1,37 @@
1
+ name: Publish on Main Merge
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+
14
+ - name: Set up Ruby
15
+ uses: ruby/setup-ruby@v1
16
+ with:
17
+ ruby-version: '3.3'
18
+
19
+ - name: Publish if Version Bumped
20
+ env:
21
+ GEM_HOST_API_KEY: "${{ secrets.RUBYGEMS_API_KEY }}"
22
+ run: |
23
+ GEM_NAME=$(ruby -r ./lib/rogalik/version.rb -e "puts Rogalik::VERSION")
24
+
25
+ # Check if version already exists on RubyGems
26
+ if gem search -e ^rogalik$ -v "$GEM_NAME" | grep -q "$GEM_NAME"; then
27
+ echo "Version $GEM_NAME already published to RubyGems. Skipping push."
28
+ else
29
+ echo "New version detected ($GEM_NAME). Publishing to RubyGems..."
30
+ mkdir -p ~/.gem
31
+ echo "---" > ~/.gem/credentials
32
+ echo ":rubygems_api_key: ${GEM_HOST_API_KEY}" > ~/.gem/credentials
33
+ chmod 0600 ~/.gem/credentials
34
+
35
+ gem build *.gemspec
36
+ gem push *.gem
37
+ fi
data/.rubocop.yml ADDED
@@ -0,0 +1,2 @@
1
+ inherit_gem:
2
+ rubocop-rails-omakase: rubocop.yml
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 4.0.6
@@ -0,0 +1,132 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, caste, color, religion, or sexual
10
+ identity and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the overall
26
+ community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or advances of
31
+ any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email address,
35
+ without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official email address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ [INSERT CONTACT METHOD].
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series of
86
+ actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or permanent
93
+ ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within the
113
+ community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.1, available at
119
+ [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120
+
121
+ Community Impact Guidelines were inspired by
122
+ [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123
+
124
+ For answers to common questions about this code of conduct, see the FAQ at
125
+ [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126
+ [https://www.contributor-covenant.org/translations][translations].
127
+
128
+ [homepage]: https://www.contributor-covenant.org
129
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130
+ [Mozilla CoC]: https://github.com/mozilla/diversity
131
+ [FAQ]: https://www.contributor-covenant.org/faq
132
+ [translations]: https://www.contributor-covenant.org/translations
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Umidjon Ustabaev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # VectorRecord
2
+
3
+ TODO: Delete this and the text below, and describe your gem
4
+
5
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/active_vector`. To experiment with that code, run `bin/console` for an interactive prompt.
6
+
7
+ ## Installation
8
+
9
+ TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
10
+
11
+ Install the gem and add to the application's Gemfile by executing:
12
+
13
+ ```bash
14
+ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
15
+ ```
16
+
17
+ If bundler is not being used to manage dependencies, install the gem by executing:
18
+
19
+ ```bash
20
+ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ 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.
30
+
31
+ 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).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/vector_record. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/active_vector/blob/master/CODE_OF_CONDUCT.md).
36
+
37
+ ## License
38
+
39
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
40
+
41
+ ## Code of Conduct
42
+
43
+ Everyone interacting in the VectorRecord project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/active_vector/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ module Rogalik
6
+ # Holds all configuration for the Rogalik library.
7
+ #
8
+ # Access the singleton via {Rogalik.configuration} and mutate it
9
+ # through the {Rogalik.configure} block.
10
+ #
11
+ # @example
12
+ # Rogalik.configure do |config|
13
+ # config.embeddings.provider = "openai"
14
+ # config.embeddings.api_token = "sk-..."
15
+ # config.vector_store.adapter = :pgvector
16
+ # end
17
+ class Configuration
18
+ # Configuration for the embedding provider.
19
+ #
20
+ # @example
21
+ # config.embeddings.provider = "gemini"
22
+ # config.embeddings.model_name = "models/gemini-embedding-2-preview"
23
+ # config.embeddings.api_token = "my-token"
24
+
25
+ # A lightweight dynamic configuration struct backed by a Hash.
26
+ #
27
+ # Subclasses (or instances used directly) gain arbitrary getter/setter
28
+ # pairs without declaring each attribute explicitly. Useful for ad-hoc
29
+ # config namespaces where the key set is not known ahead of time.
30
+ #
31
+ # @example
32
+ # cfg = BaseConfig.new
33
+ # cfg.timeout = 30
34
+ # cfg.timeout # => 30
35
+ # cfg.to_h # => { timeout: 30 }
36
+ class BaseConfig
37
+ # @param settings [Hash] optional initial key/value pairs
38
+ def initialize(settings = {})
39
+ @settings = settings.transform_keys(&:to_sym)
40
+ end
41
+
42
+ # Supports +key+ (getter) and +key=+ (setter) for any symbol name.
43
+ def method_missing(name, *args, &block)
44
+ key = name.to_s.delete_suffix("=").to_sym
45
+
46
+ if name.to_s.end_with?("=")
47
+ @settings[key] = args.first
48
+ else
49
+ @settings.fetch(key, nil)
50
+ end
51
+ end
52
+
53
+ # @param include_private [Boolean]
54
+ # @return [Boolean]
55
+ def respond_to_missing?(name, include_private = false)
56
+ key = name.to_s.delete_suffix("=").to_sym
57
+ @settings.key?(key) || name.to_s.end_with?("=") || super
58
+ end
59
+
60
+ # Returns the underlying settings as a plain Hash.
61
+ #
62
+ # @return [Hash{Symbol => Object}]
63
+ def to_h
64
+ @settings.dup
65
+ end
66
+
67
+ # @return [String] JSON representation of the settings hash
68
+ def to_json(*args)
69
+ @settings.to_json(*args)
70
+ end
71
+
72
+ # @return [String]
73
+ def to_s
74
+ @settings.to_s
75
+ end
76
+
77
+ # @return [String]
78
+ def inspect
79
+ "#<#{self.class.name} #{@settings.inspect}>"
80
+ end
81
+ end
82
+
83
+ # Configuration for the embedding provider.
84
+ #
85
+ # @example
86
+ # config.embeddings.api_token = "sk-..."
87
+ # config.embeddings.model = "text-embedding-ada-002"
88
+ class Embeddings < BaseConfig; end
89
+
90
+ # Configuration for the vector store backend.
91
+ #
92
+ # @example
93
+ # config.vector_store.adapter = :pgvector
94
+ # config.vector_store.options = { connection_string: "postgres://..." }
95
+ class VectorStore < BaseConfig; end
96
+
97
+ # @return [Embeddings]
98
+ attr_reader :embeddings
99
+
100
+ # @return [VectorStore]
101
+ attr_reader :vector_store
102
+
103
+ # @return [Logger]
104
+ attr_reader :logger
105
+
106
+ def initialize
107
+ @embeddings = Embeddings.new
108
+ @vector_store = VectorStore.new
109
+ @logger = Logger.new($stdout)
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rogalik
4
+ module Plugins
5
+ module Core
6
+ # Instance methods mixed into {Rogalik::Pipeline::Chunker}.
7
+ module ChunkerMethods
8
+ # Splits documents into smaller chunks suitable for embedding.
9
+ #
10
+ # @return [void]
11
+ def chunk
12
+ print("Chunker...")
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rogalik
4
+ module Plugins
5
+ module Core
6
+ # Mixin that wires the configured embeddings provider into any class that includes it.
7
+ #
8
+ # When included, {.included} reads the active provider from
9
+ # {Rogalik.configuration} and delegates to {Rogalik::Embeddings.load},
10
+ # which requires the provider file and mixes in its +ProviderMethods+.
11
+ # The including class therefore gains the full embedding interface
12
+ # (+embed_text+, +embed_documents+, etc.) without knowing which
13
+ # provider is in use.
14
+ #
15
+ # @example
16
+ # # config/initializer
17
+ # Rogalik.configure { |c| c.embeddings.provider = :open_ai }
18
+ #
19
+ # class MyEmbedder
20
+ # include Rogalik::Plugins::Core::EmbeddingsMethods
21
+ # # => now has #embed_text, #embed_documents from OpenAIEmbeddings::ProviderMethods
22
+ # end
23
+ module EmbeddingsMethods
24
+ def embed_documents(_documents)
25
+ raise NotImplementedError, "Embeddings plugins must be included"
26
+ end
27
+
28
+ def embed_text(_text)
29
+ raise NotImplementedError, "Embeddings plugins must be included"
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rogalik
4
+ module Plugins
5
+ module Core
6
+ # Instance methods mixed into Rogalik::Pipeline::Loader.
7
+ #
8
+ # Provides file-based document loading that yields {Document} objects to
9
+ # the pipeline. Each loader is initialized with a source path and a logger,
10
+ # then drives the pipeline by calling {#load}.
11
+ #
12
+ # @example
13
+ # class MyLoader
14
+ # include Rogalik::Plugins::Core::LoaderMethods
15
+ # end
16
+ #
17
+ # MyLoader.new("path/to/file.txt", logger).load { |doc| process(doc) }
18
+ module LoaderMethods
19
+ attr_reader :source, :logger
20
+
21
+ # @param source [String] path to the file to load
22
+ # @param logger [Logger] logger instance for progress output
23
+ def initialize(source, logger)
24
+ @source = source
25
+ @logger = logger
26
+ end
27
+
28
+ # Reads the source file and yields a single {Document} to the caller.
29
+ #
30
+ # @yieldparam document [Document] the loaded document
31
+ # @return [void]
32
+ def load
33
+ logger.info("Loading data...")
34
+ File.open(source, "r") do |file|
35
+ yield Pipeline::Document.new(
36
+ id: nil,
37
+ source: source,
38
+ page_content: file.read,
39
+ metadata: { source: source }
40
+ )
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rogalik
4
+ module Plugins
5
+ module Core
6
+ # Instance methods mixed into {Rogalik::Pipeline::PiiDetector}.
7
+ module PiiDetectorMethods
8
+ # Anonymizes personally identifiable information in documents before embedding.
9
+ #
10
+ # @return [void]
11
+ def anonymize
12
+ print("PiiDetector...")
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rogalik
4
+ module Plugins
5
+ module Core
6
+ # Instance methods mixed into {Rogalik::Pipeline::VectorStore}.
7
+ module VectorStoreMethods
8
+ # Persists embedded documents to the vector store.
9
+ #
10
+ # @return [void]
11
+ def add_documents
12
+ print("VectorStore...")
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rogalik
4
+ module Plugins
5
+ # Core plugin — provides default implementations for every pipeline stage.
6
+ #
7
+ # This module is automatically loaded by {Rogalik::Pipeline} and serves
8
+ # as the baseline behaviour that other plugins can override or extend.
9
+ #
10
+ # Sub-modules are kept in +plugins/core/+ and mixed into their respective
11
+ # stage classes by {Rogalik::Pipeline.plugin}.
12
+ module Core
13
+ Rogalik::Pipeline.register_plugin(:core, self)
14
+ end
15
+ end
16
+ end
17
+
18
+ require_relative "core/loader_methods"
19
+ require_relative "core/embeddings_methods"
20
+ require_relative "core/vector_store_methods"
21
+ require_relative "core/chunker_methods"
22
+ require_relative "core/pii_detector_methods"
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+
5
+ module Rogalik
6
+ module Plugins
7
+ module GeminiEmbeddings
8
+ Rogalik::Pipeline.register_plugin(:gemini_embeddings, self)
9
+ module EmbeddingsMethods
10
+ BASE_URL = "https://generativelanguage.googleapis.com"
11
+ private_constant :BASE_URL
12
+
13
+ # @return [String] Gemini API key
14
+ attr_reader :api_key
15
+
16
+ # @return [String] embedding model name
17
+ attr_reader :model
18
+
19
+ # @return [String] base URL for the Gemini API
20
+ attr_reader :base_url
21
+
22
+ # @param config [Rogalik::Configuration::Embeddings]
23
+ def initialize(config: Rogalik.configuration.embeddings)
24
+ api_key = config.api_key || ENV["GEMINI_API_KEY"]
25
+ raise ArgumentError, "Gemini API key is required, set through env GEMINI_API_KEY or config" if api_key.nil?
26
+ raise ArgumentError, "Gemini model name is required" if config.model.nil?
27
+
28
+ @api_key = api_key
29
+ @model = config.model
30
+ @base_url = config.base_url || ENV["GEMINI_BASE_URL"] || BASE_URL
31
+ end
32
+
33
+ # Returns a memoized Faraday client configured for the Gemini API.
34
+ #
35
+ # @return [Faraday::Connection]
36
+ def client
37
+ @client ||= Faraday.new(url: base_url) do |con|
38
+ con.request :json
39
+ con.response :json
40
+ con.headers["Content-Type"] = "application/json"
41
+ con.headers["x-goog-api-key"] = api_key
42
+ end
43
+ end
44
+
45
+ # Generates embeddings for each document and assigns them in place.
46
+ # Uses the batchEmbedContents endpoint to embed all documents in a single request.
47
+ #
48
+ # @param documents [Array<Pipeline::Document>] documents whose +page_content+ will be embedded
49
+ # @return [Array<Pipeline::Document>] the same documents with +embeddings+ populated
50
+ def embed_documents(documents)
51
+ response = client.post(batch_endpoint) do |req|
52
+ req.body = {
53
+ requests: documents.map do |doc|
54
+ { model: "models/#{model}", content: { parts: [ { text: doc.page_content } ] } }
55
+ end
56
+ }
57
+ end
58
+
59
+ raise StandardError, "Gemini embedding request failed (HTTP #{response.status})" unless response.success?
60
+
61
+ response.body.fetch("embeddings").each_with_index do |embedding, i|
62
+ documents[i].embeddings = embedding["values"]
63
+ end
64
+
65
+ documents
66
+ end
67
+
68
+ # Generates an embedding vector for a single string.
69
+ #
70
+ # @param text [String] the text to embed
71
+ # @return [Array<Float>] the embedding vector
72
+ def embed_text(text)
73
+ response = client.post(embed_endpoint) do |req|
74
+ req.body = { model: "models/#{model}", content: { parts: [ { text: text } ] } }
75
+ end
76
+
77
+ raise StandardError, "Gemini embedding request failed (HTTP #{response.status})" unless response.success?
78
+
79
+ response.body.dig("embedding", "values")
80
+ end
81
+
82
+ private
83
+
84
+ def batch_endpoint
85
+ @batch_endpoint ||= "/v1beta/models/#{model}:batchEmbedContents"
86
+ end
87
+
88
+ def embed_endpoint
89
+ @embed_endpoint ||= "/v1beta/models/#{model}:embedContent"
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openai"
4
+
5
+ module Rogalik
6
+ module Plugins
7
+ module OpenAIEmbeddings
8
+ Rogalik::Pipeline.register_plugin(:open_ai_embeddings, self)
9
+ module EmbeddingsMethods
10
+ # @return [String] OpenAI API key
11
+ attr_reader :api_key
12
+
13
+ # @return [String] embedding model name
14
+ attr_reader :model
15
+
16
+ # @param config [Rogalik::Configuration::Embeddings]]
17
+ def initialize(config = Rogalik.configuration.embeddings)
18
+ api_key = config.api_key || ENV["OPENAI_API_KEY"]
19
+ raise ArgumentError, "OpenAI API key is required, set through env OPENAI_API_KEY or config" if api_key.nil?
20
+ raise ArgumentError, "OpenAI model name is required" if config.model.nil?
21
+
22
+ @api_key = api_key
23
+ @model = config.model
24
+ end
25
+
26
+ # Returns a memoized OpenAI client.
27
+ #
28
+ # @return [OpenAI::Client]
29
+ def client
30
+ @client ||= OpenAI::Client.new(api_key: api_key)
31
+ end
32
+
33
+ # Generates embeddings for each document and assigns them in place.
34
+ #
35
+ # @param documents [Array<Pipeline::Document>] documents whose +page_content+ will be embedded
36
+ # @return [Array<Pipeline::Document>] the same documents with +embeddings+ populated
37
+ def embed_documents(documents)
38
+ response = client.embeddings.create(input: documents.map(&:page_content), model: model)
39
+ embeddings = response.data
40
+
41
+ documents.each_with_index do |doc, index|
42
+ doc.embeddings = embeddings[index].embedding
43
+ end
44
+
45
+ documents
46
+ end
47
+
48
+ # Generates an embedding vector for a single string.
49
+ #
50
+ # @param text [String] the text to embed
51
+ # @return [Array<Float>] the embedding vector
52
+ def embed_text(text)
53
+ response = client.embeddings.create(input: text, model: model)
54
+ response.data.first.embedding
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rogalik
4
+ VERSION = "0.1.0.pre"
5
+ end
data/lib/rogalik.rb ADDED
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rogalik/version"
4
+ require_relative "rogalik/configuration"
5
+
6
+ # Top-level namespace for the Rogalik library.
7
+ module Rogalik
8
+ # Base error class for all Rogalik exceptions.
9
+ class Error < StandardError; end
10
+ # Represents a single loaded document with its content and provenance.
11
+ Document = Struct.new(:id, :page_content, :source, :metadata, :embeddings, keyword_init: true)
12
+
13
+ class << self
14
+ # Returns the global {Configuration} instance, creating it on first call.
15
+ #
16
+ # @return [Configuration]
17
+ def configuration
18
+ @configuration ||= Configuration.new
19
+ end
20
+
21
+ # Yields the global {Configuration} instance for mutation.
22
+ #
23
+ # @yieldparam config [Configuration]
24
+ # @return [void]
25
+ #
26
+ # @example
27
+ # Rogalik.configure do |config|
28
+ # config.embeddings.provider = "openai"
29
+ # config.vector_store.adapter = :qdrant
30
+ # end
31
+ def configure
32
+ yield configuration
33
+ end
34
+
35
+ # Resets the global configuration to defaults. Primarily useful in tests.
36
+ #
37
+ # @return [Configuration]
38
+ def reset_configuration!
39
+ @configuration = Configuration.new
40
+ end
41
+ end
42
+
43
+ # Orchestrates the embedding and storage pipeline.
44
+ #
45
+ # Plugins are registered in {PLUGINS} and must implement the interface
46
+ # defined by their corresponding stage class.
47
+ class Pipeline
48
+ # Registry mapping plugin names to their implementation classes.
49
+ # @return [Hash]
50
+ PLUGINS = {}
51
+
52
+ private_constant :PLUGINS
53
+
54
+ # @abstract Implement to provide a data loader.
55
+ class Loader; end
56
+
57
+ # @abstract Implement to provide an embedding strategy.
58
+ class Embeddings; end
59
+
60
+ # @abstract Implement to provide a vector store strategy.
61
+ class VectorStore; end
62
+
63
+ # @abstract Implement to provide a text chunking strategy.
64
+ class Chunker; end
65
+
66
+ # @abstract Implement to provide PII detection.
67
+ class PiiDetector; end
68
+
69
+ # @param name [Symbol]
70
+ # @pram mod [Module]
71
+ def self.register_plugin(name, mod)
72
+ PLUGINS[name] = mod
73
+ end
74
+
75
+
76
+ # @param mod [Module, Symbol]]
77
+ # @param args [Array]
78
+ #
79
+ # @return [void]
80
+ def self.plugin(mod, ...)
81
+ if mod.is_a?(Symbol)
82
+ require "rogalik/plugins/#{mod}"
83
+ mod = PLUGINS.fetch(mod)
84
+ end
85
+
86
+ mod.before_load(self, ...) if mod.respond_to?(:before_load)
87
+
88
+ if defined?(mod::LoaderMethods)
89
+ self::Loader.include(mod::LoaderMethods)
90
+ end
91
+
92
+ if defined?(mod::EmbeddingsMethods)
93
+ self::Embeddings.include(mod::EmbeddingsMethods)
94
+ end
95
+
96
+ if defined?(mod::VectorStoreMethods)
97
+ self::VectorStore.include(mod::VectorStoreMethods)
98
+ end
99
+
100
+ if defined?(mod::ChunkerMethods)
101
+ self::Chunker.include(mod::ChunkerMethods)
102
+ end
103
+
104
+ if defined?(mod::PiiDetectorMethods)
105
+ self::PiiDetector.include(mod::PiiDetectorMethods)
106
+ end
107
+
108
+ if defined?(mod::LoaderClassMethods)
109
+ self::Loader.extend(mod::LoaderClassMethods)
110
+ end
111
+
112
+ if defined?(mod::EmbeddingsClassMethods)
113
+ self::Embeddings.extend(mod::EmbeddingsClassMethods)
114
+ end
115
+
116
+ if defined?(mod::VectorStoreClassMethods)
117
+ self::VectorStore.extend(mod::VectorStoreClassMethods)
118
+ end
119
+
120
+ if defined?(mod::ChunkerClassMethods)
121
+ self::Chunker.extend(mod::ChunkerClassMethods)
122
+ end
123
+
124
+ if defined?(mod::PiiDetectorClassMethods)
125
+ self::PiiDetector.extend(mod::PiiDetectorClassMethods)
126
+ end
127
+
128
+ mod.after_load(self, ...) if mod.respond_to?(:after_load)
129
+ end
130
+
131
+ plugin(:core)
132
+
133
+ # @param source [String, Pathname]
134
+ # @param logger [Logger]
135
+ #
136
+ # @return [Pipeline]
137
+
138
+ def initialize(source, logger: nil)
139
+ @source = source
140
+ @logger = logger || Rogalik.configuration.logger
141
+ @loader = Loader.new(source, logger)
142
+ @chunker = Chunker.new
143
+ @embeddings = Embeddings.new
144
+ @vector_store = VectorStore.new
145
+ @pii_detector = PiiDetector.new
146
+ end
147
+
148
+ attr_reader :source,
149
+ :logger,
150
+ :loader,
151
+ :chunker,
152
+ :embeddings,
153
+ :vector_store,
154
+ :pii_detector
155
+
156
+ # Executes the pipeline stages in order.
157
+ #
158
+ # @return [void]
159
+ def run
160
+ loader.load
161
+ chunker.chunk
162
+ embeddings.embeddings
163
+ vector_store.add_documents
164
+ pii_detector.anonymize
165
+ end
166
+ end
167
+ end
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rogalik
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.pre
5
+ platform: ruby
6
+ authors:
7
+ - Umidjon Ustabaev
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-08-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: openai
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.62.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.62.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop-rails-omakase
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Bring the power of AI to your Rails app without the boilerplate. Rogalik
56
+ seamlessly synchronizes your Active Record models with vector databases (like pgvector),
57
+ providing an idiomatic, convention-driven API for generating LLM embeddings and
58
+ powering Retrieval-Augmented Generation (RAG).
59
+ email:
60
+ - umidjonustabaev@gmail.com
61
+ executables: []
62
+ extensions: []
63
+ extra_rdoc_files: []
64
+ files:
65
+ - ".github/workflows/gem-push.yml"
66
+ - ".rubocop.yml"
67
+ - ".ruby-version"
68
+ - CODE_OF_CONDUCT.md
69
+ - LICENSE.txt
70
+ - README.md
71
+ - Rakefile
72
+ - lib/rogalik.rb
73
+ - lib/rogalik/configuration.rb
74
+ - lib/rogalik/plugins/core.rb
75
+ - lib/rogalik/plugins/core/chunker_methods.rb
76
+ - lib/rogalik/plugins/core/embeddings_methods.rb
77
+ - lib/rogalik/plugins/core/loader_methods.rb
78
+ - lib/rogalik/plugins/core/pii_detector_methods.rb
79
+ - lib/rogalik/plugins/core/vector_store_methods.rb
80
+ - lib/rogalik/plugins/gemini_embeddings.rb
81
+ - lib/rogalik/plugins/open_ai_embeddings.rb
82
+ - lib/rogalik/version.rb
83
+ homepage: https://github.com/umidjon-ustabaev/rogalik
84
+ licenses:
85
+ - MIT
86
+ metadata:
87
+ allowed_push_host: https://rubygems.org
88
+ homepage_uri: https://github.com/umidjon-ustabaev/rogalik
89
+ source_code_uri: https://github.com/umidjon-ustabaev/rogalik
90
+ post_install_message:
91
+ rdoc_options: []
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: 3.2.0
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ requirements: []
105
+ rubygems_version: 3.5.22
106
+ signing_key:
107
+ specification_version: 4
108
+ summary: Rails-native vector embeddings and RAG integration for Active Record.
109
+ test_files: []