maglev-rb 0.1.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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +408 -0
  4. data/README.zh-CN.md +408 -0
  5. data/lib/generators/maglev/install/install_generator.rb +70 -0
  6. data/lib/maglev/active_record_extension.rb +100 -0
  7. data/lib/maglev/adapters/ruby_llm_attachment_extractor.rb +15 -0
  8. data/lib/maglev/adapters/ruby_llm_embedding.rb +22 -0
  9. data/lib/maglev/adapters/ruby_llm_generation.rb +22 -0
  10. data/lib/maglev/adapters/ruby_llm_provider.rb +64 -0
  11. data/lib/maglev/answerer.rb +58 -0
  12. data/lib/maglev/attachment_extractor.rb +106 -0
  13. data/lib/maglev/authorization.rb +43 -0
  14. data/lib/maglev/chunk.rb +14 -0
  15. data/lib/maglev/chunker.rb +58 -0
  16. data/lib/maglev/configuration.rb +76 -0
  17. data/lib/maglev/content_source_graph.rb +66 -0
  18. data/lib/maglev/context_assembler.rb +94 -0
  19. data/lib/maglev/context_preview.rb +13 -0
  20. data/lib/maglev/dependency_graph.rb +129 -0
  21. data/lib/maglev/embedding_adapter.rb +9 -0
  22. data/lib/maglev/errors.rb +21 -0
  23. data/lib/maglev/extracted_document.rb +36 -0
  24. data/lib/maglev/generation_adapter.rb +9 -0
  25. data/lib/maglev/indexer.rb +175 -0
  26. data/lib/maglev/knowledge_config.rb +136 -0
  27. data/lib/maglev/prompt_builder.rb +27 -0
  28. data/lib/maglev/provider_call.rb +49 -0
  29. data/lib/maglev/provider_configuration.rb +18 -0
  30. data/lib/maglev/railtie.rb +19 -0
  31. data/lib/maglev/reindex_job.rb +16 -0
  32. data/lib/maglev/response.rb +26 -0
  33. data/lib/maglev/retriever.rb +89 -0
  34. data/lib/maglev/schema_compiler.rb +51 -0
  35. data/lib/maglev/search_result.rb +22 -0
  36. data/lib/maglev/snapshot.rb +14 -0
  37. data/lib/maglev/snapshot_builder.rb +204 -0
  38. data/lib/maglev/vector_stores/base.rb +31 -0
  39. data/lib/maglev/vector_stores/document.rb +39 -0
  40. data/lib/maglev/vector_stores/memory.rb +76 -0
  41. data/lib/maglev/vector_stores/pgvector.rb +94 -0
  42. data/lib/maglev/version.rb +5 -0
  43. data/lib/maglev.rb +37 -0
  44. data/lib/tasks/maglev.rake +31 -0
  45. metadata +202 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 34583270cc6f3febaac2fbe823e4d8f55d1f8ec15594410c011bada98535c102
4
+ data.tar.gz: e1a9c7a3db4c9d40e6fc38be340ba5b9baac12410a5a560f1556b439de2d1b93
5
+ SHA512:
6
+ metadata.gz: a3c193d43ba5cc89e444b079b765aa9044b63bbc960911e6c187550070e0fee149a5d9726f2c1c434f6127f6f74e094b12d12a3d158ee73417f40cb198496e47
7
+ data.tar.gz: 4d9d4f863ec0f910150edb2110d1bbb785941453e340fb6b73528c974c73537e5435f0d01d950140f0af58ce8b7c82c795f285f0ca4845b875d5486a6f750276
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Maglev contributors
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,408 @@
1
+ # Maglev
2
+
3
+ [English](README.md) | [简体中文](README.zh-CN.md)
4
+
5
+ [![CI](https://github.com/benjis/maglev/actions/workflows/ci.yml/badge.svg)](https://github.com/benjis/maglev/actions/workflows/ci.yml)
6
+ [![Ruby](https://img.shields.io/badge/Ruby-3.2%2B-CC342D.svg)](https://www.ruby-lang.org/)
7
+ [![Rails](https://img.shields.io/badge/Rails-7.1%20%7C%208.0-D30001.svg)](https://rubyonrails.org/)
8
+ [![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE.txt)
9
+
10
+ **Give your Rails models semantic memory, without building a second application beside Rails.**
11
+
12
+ Maglev is a Rails-native semantic knowledge layer for ActiveRecord object graphs. Declare which parts of your domain are safe and useful to understand, and Maglev turns records, relationships, attachments, and rich text into searchable knowledge. It keeps that knowledge fresh through normal Rails lifecycle hooks and gives you semantic search and grounded answers through familiar model APIs.
13
+
14
+ ```ruby
15
+ Product.search("laptops with battery or usability complaints")
16
+
17
+ response = Product.ask("What recurring product issues should we investigate?", user: current_user)
18
+ response.text
19
+ response.sources # the ActiveRecord records and chunks behind the answer
20
+ ```
21
+
22
+ Maglev is deliberately focused on retrieval-augmented generation (RAG). ActiveRecord remains the source of truth for exact filters, joins, reporting, and aggregation; Maglev handles questions expressed in human language.
23
+
24
+ ## Why Maglev?
25
+
26
+ - **Rails-native:** a gem and Railtie, not a separate service, engine, or API.
27
+ - **Model-driven:** declare knowledge next to the ActiveRecord model that owns it.
28
+ - **Graph-aware:** traverse direct, `has_many :through`, and polymorphic associations with explicit depth and record limits.
29
+ - **Fresh by default:** reindex owners after declared records, direct associations, attachments, or Action Text content change.
30
+ - **Grounded:** answers are generated only from retrieved context and include their sources.
31
+ - **Production-minded:** authorization hooks, content limits, sanitization, retries, instrumentation, and idempotent reindexing are built in.
32
+ - **Extensible:** use the default PostgreSQL/pgvector store or implement the small vector-store contract.
33
+
34
+ ## Quick Start
35
+
36
+ ### 1. Install prerequisites
37
+
38
+ Maglev requires Ruby 3.2+, Rails 7.1 or 8.0, PostgreSQL, and the
39
+ [`pgvector`](https://github.com/pgvector/pgvector) extension.
40
+
41
+ Add Maglev to your application:
42
+
43
+ ```ruby
44
+ # Gemfile
45
+ gem "maglev-rb"
46
+ ```
47
+
48
+ ```bash
49
+ bundle install
50
+ bin/rails generate maglev:install --embedding-dimensions=1536
51
+ bin/rails db:migrate
52
+ ```
53
+
54
+ The generator writes `--embedding-dimensions` to both `config/initializers/maglev.rb` and the `maglev_chunks` vector column, and includes a cosine HNSW index.
55
+
56
+ ### 2. Configure your provider
57
+
58
+ ```ruby
59
+ # config/initializers/maglev.rb
60
+ Maglev.configure do |config|
61
+ config.embedding_provider do |provider|
62
+ provider.url = "http://localhost:11434/v1"
63
+ provider.api_key = ENV["LOCAL_EMBEDDING_API_KEY"]
64
+ provider.model = "Qwen3-Embedding-0.6B-8bit"
65
+ provider.dimensions = 1024
66
+ end
67
+
68
+ config.generation_provider do |provider|
69
+ provider.url = "https://api.deepseek.com/v1"
70
+ provider.api_key = Rails.application.credentials.dig(:deepseek_api_key)
71
+ provider.model = "deepseek-chat"
72
+ end
73
+
74
+ config.chunk_size = 1000
75
+ end
76
+ ```
77
+
78
+ Embedding and generation endpoints are independent and may use different URLs, API keys, and models. The default provider bridge expects OpenAI-compatible HTTP endpoints. Applications can still inject custom Maglev adapters for other protocols.
79
+
80
+ For an existing installation, change the configured dimensions and the database vector column together. Maglev checks their consistency before requesting an embedding.
81
+
82
+ ### 3. Declare model knowledge
83
+
84
+ ```ruby
85
+ class Product < ApplicationRecord
86
+ has_many :reviews, inverse_of: :product
87
+ has_many :product_categories, inverse_of: :product
88
+ has_many :categories, through: :product_categories
89
+ has_many_attached :images
90
+ has_rich_text :description
91
+
92
+ has_knowledge do
93
+ expose :name, :sku, :price, :status
94
+ tags :product
95
+
96
+ include_related :reviews, depth: 1, limit: 10
97
+ include_related :categories, depth: 1, limit: 10, inverse: :products
98
+
99
+ expose_attached :images
100
+ expose_rich_text :description
101
+ end
102
+ end
103
+
104
+ class Review < ApplicationRecord
105
+ belongs_to :product, inverse_of: :reviews
106
+
107
+ has_knowledge do
108
+ expose :rating, :title, :body
109
+ end
110
+ end
111
+ ```
112
+
113
+ Only explicitly exposed fields and content sources enter Maglev's knowledge snapshot. Relation `limit` bounds the number of records per association. Relation `depth` bounds association hops: `depth: 1` includes the directly related record but does not expand that record's relations. `config.max_relation_depth` is the hard root-to-leaf ceiling for every snapshot. Related models define their own exposed knowledge, so sensitive join-model fields are not flattened accidentally.
114
+
115
+ ### 4. Index existing records
116
+
117
+ New and updated records enqueue `Maglev::ReindexJob` automatically. Backfill existing data once after installation:
118
+
119
+ ```bash
120
+ bin/rails maglev:reindex[Product]
121
+ # or every model that declares has_knowledge
122
+ bin/rails maglev:reindex_all
123
+ ```
124
+
125
+ Make sure your Active Job backend is running in production.
126
+
127
+ ### 5. Search and ask
128
+
129
+ ```ruby
130
+ results = Product.search(
131
+ "laptops with battery or usability complaints",
132
+ limit: 10,
133
+ user: current_user
134
+ )
135
+
136
+ results.each do |result|
137
+ result.owner # => Product
138
+ result.content # retrieved snapshot chunk
139
+ result.source # => "snapshot"
140
+ result.distance # cosine distance; lower is closer
141
+ result.similarity # normalized convenience score
142
+ end
143
+ ```
144
+
145
+ Generate an answer grounded in the retrieved records:
146
+
147
+ ```ruby
148
+ response = Product.ask(
149
+ "What recurring product issues should the merchandising team investigate?",
150
+ limit: 5,
151
+ user: current_user
152
+ )
153
+
154
+ response.text
155
+ response.sources # owner, chunk, distance, and the exact retrieved content
156
+ response.metadata
157
+ ```
158
+
159
+ For example, an answer might summarize a loud fan, intermittent trackpad responsiveness, and lower-than-advertised battery life from retrieved review content. Treat it as a summary of the retrieved context—not an aggregate over every product—and use `response.sources` to show the evidence behind each conclusion.
160
+
161
+ Instance-level questions stay scoped to one owner:
162
+
163
+ ```ruby
164
+ product.ask("Summarize the reported strengths and weaknesses of this product.", user: current_user)
165
+ ```
166
+
167
+ Maglev also follows declared relationships when Rails data changes. Moving a review queues reindexing for both the previous and current product after commit, so their searchable knowledge stays current:
168
+
169
+ ```ruby
170
+ review.update!(product: replacement_product)
171
+ # Maglev::ReindexJob is queued for both affected products.
172
+ ```
173
+
174
+ When retrieval yields no usable context, Maglev returns a deterministic insufficient-context response instead of asking the model to improvise.
175
+
176
+ ## How It Works
177
+
178
+ ```mermaid
179
+ flowchart LR
180
+ A["ActiveRecord object graph"] --> B["Explicit knowledge schema"]
181
+ B --> C["Bounded, sanitized snapshot"]
182
+ C --> D["Chunking + embeddings"]
183
+ D --> E["Vector store<br/>pgvector by default"]
184
+ Q["search / ask"] --> F["Semantic retrieval"]
185
+ E --> F
186
+ F --> G["Context assembly"]
187
+ G --> H["Grounded generation"]
188
+ H --> I["Response + sources"]
189
+ J["Rails commits and content changes"] --> K["Active Job reindex"]
190
+ K --> C
191
+ ```
192
+
193
+ 1. `has_knowledge` compiles an explicit schema for a model and its declared relationships.
194
+ 2. Maglev builds a deterministic text snapshot from allowed attributes, related records, attachments, rich text, and tags.
195
+ 3. The snapshot is split into bounded chunks and embedded through the configured adapter.
196
+ 4. Chunks are upserted into a vector store. The default store persists them in PostgreSQL using pgvector.
197
+ 5. `search` embeds the query and performs cosine nearest-neighbor retrieval.
198
+ 6. `ask` assembles retrieved chunks within context budgets, builds a grounded prompt, and returns the answer with source metadata.
199
+ 7. Rails callbacks propagate declared record changes through the graph and enqueue reindexing for affected owners.
200
+
201
+ Maglev does not duplicate your relational model. Vector documents point back to their ActiveRecord owners; your application remains responsible for transactions, business rules, and structured queries.
202
+
203
+ ## Knowledge Sources
204
+
205
+ ### ActiveRecord graphs
206
+
207
+ `include_related` supports bounded traversal across ordinary associations, `has_many :through`, and polymorphic relationships. Use `inverse:` when Maglev cannot infer how changes on a related model should find their owning knowledge record.
208
+
209
+ ```ruby
210
+ has_knowledge do
211
+ include_related :tickets, depth: 2, limit: 25
212
+ include_related :events, depth: 1, limit: 20, inverse: :eventable
213
+ end
214
+ ```
215
+
216
+ When a related record moves between owners, Maglev reindexes both the previous and current owner.
217
+
218
+ Creating, deleting, or reassigning a join row changes a `has_many :through` relationship without changing the related record itself. After such join-model changes, explicitly enqueue or run owner reindexing from your application.
219
+
220
+ ### Active Storage and Action Text
221
+
222
+ ```ruby
223
+ has_knowledge do
224
+ expose_attached :contracts, :brief
225
+ expose_rich_text :notes
226
+ end
227
+ ```
228
+
229
+ HTML and Action Text content are sanitized before indexing. Attachments are constrained by content type, byte size, and extracted character count. Changes to declared attachments and rich text trigger owner reindexing.
230
+
231
+ ### Inspect before indexing
232
+
233
+ The developer experience APIs let you inspect exactly what a model exposes without calling an embedding or generation provider:
234
+
235
+ ```ruby
236
+ Customer.maglev_schema
237
+ customer.maglev_snapshot
238
+
239
+ preview = customer.maglev_context_preview(
240
+ question: "Why is this customer at risk?"
241
+ )
242
+ preview.text
243
+ preview.metadata # includes provider_calls: 0
244
+ ```
245
+
246
+ ## Authorization
247
+
248
+ Maglev is policy-library agnostic. Configure a small adapter to apply your application's authorization rules during retrieval and answering:
249
+
250
+ ```ruby
251
+ class MaglevAuthorization
252
+ def scope(model:, user:)
253
+ model.accessible_by(user)
254
+ end
255
+
256
+ def authorize(record:, user:)
257
+ record.account_id == user.account_id
258
+ end
259
+ end
260
+
261
+ Maglev.configure do |config|
262
+ config.authorization_adapter = MaglevAuthorization.new
263
+ end
264
+ ```
265
+
266
+ The adapter must implement:
267
+
268
+ - `scope(model:, user:)`, returning the records visible to the user.
269
+ - `authorize(record:, user:)`, returning `false` to deny a record.
270
+
271
+ Without an adapter, all records are allowed. Pass `user:` consistently anywhere retrieval must be scoped.
272
+
273
+ `customer.explain` is a convenience API for applications without user-scoped authorization; use `customer.ask(Maglev.configuration.explain_question, user: current_user)` when a user context is required. Authorization scopes apply to the default pgvector retrieval path. See [Vector Stores](#vector-stores) before combining direct `search` calls with a custom store.
274
+
275
+ ## Configuration
276
+
277
+ ```ruby
278
+ Maglev.configure do |config|
279
+ config.embedding_provider do |provider|
280
+ provider.url = ENV.fetch("MAGLEV_EMBEDDING_URL", "https://api.openai.com/v1")
281
+ provider.api_key = ENV["MAGLEV_EMBEDDING_API_KEY"]
282
+ provider.model = "text-embedding-3-small"
283
+ provider.dimensions = 1536
284
+ end
285
+
286
+ config.generation_provider do |provider|
287
+ provider.url = ENV.fetch("MAGLEV_GENERATION_URL", "https://api.openai.com/v1")
288
+ provider.api_key = ENV["MAGLEV_GENERATION_API_KEY"]
289
+ provider.model = "gpt-4.1-mini"
290
+ end
291
+
292
+ config.chunk_size = 1000
293
+
294
+ config.context_max_characters = 4000
295
+ config.context_per_owner_characters = 1200
296
+ config.max_relation_depth = 3
297
+
298
+ config.attachment_allowed_content_types = [
299
+ "text/plain",
300
+ "text/markdown",
301
+ "text/html"
302
+ ]
303
+ config.attachment_max_bytes = 5 * 1024 * 1024
304
+ config.attachment_max_characters = 20_000
305
+
306
+ config.provider_max_attempts = 2
307
+ config.provider_timeout = 30
308
+ config.logger = Rails.logger
309
+ end
310
+ ```
311
+
312
+ `provider_timeout` applies to each provider attempt. Timed-out attempts are retryable and count toward `provider_max_attempts`.
313
+
314
+ Inject custom `embedding_adapter`, `generation_adapter`, `attachment_extractor`, `authorization_adapter`, or `source_redactor` objects when your application needs different provider or policy behavior. Tests can use deterministic adapters without making network calls.
315
+
316
+ ## Vector Stores
317
+
318
+ PostgreSQL with pgvector is the default production path. Maglev also exposes a compact backend contract for applications that need another store:
319
+
320
+ ```ruby
321
+ class MyVectorStore < Maglev::VectorStores::Base
322
+ def upsert(documents:)
323
+ # Persist or replace documents by document.id
324
+ end
325
+
326
+ def search(vector:, filters:, limit:)
327
+ # Return nearest Maglev::VectorStores::Document objects
328
+ end
329
+
330
+ def delete(ids:)
331
+ # Delete documents with these stable IDs
332
+ end
333
+
334
+ def delete_by_owner(owner_type:, owner_id:)
335
+ # Delete every document belonging to this owner
336
+ end
337
+
338
+ def healthcheck = :ok
339
+ def capabilities = {metadata_filtering: true}
340
+ end
341
+
342
+ Maglev.configure do |config|
343
+ config.vector_store = MyVectorStore.new
344
+ end
345
+ ```
346
+
347
+ `Maglev::VectorStores::Memory` is useful for tests and local experiments. Custom stores should preserve document metadata filtering and stable document identity semantics.
348
+
349
+ Custom vector stores currently receive model and owner metadata filters, but not the configured authorization scope. `ask` still authorizes retrieved owners individually; direct `search(..., user:)` with a custom store must not be treated as authorization-filtered. Apply tenant or policy filtering inside the custom store, or use the default pgvector path when authorization-scoped search is required.
350
+
351
+ ## Operations and Observability
352
+
353
+ ```bash
354
+ bin/rails maglev:status
355
+ bin/rails maglev:reindex[Customer]
356
+ bin/rails maglev:reindex_all
357
+ ```
358
+
359
+ Reindexing is safe to repeat: unchanged chunks are reused and obsolete chunks are removed. Maglev emits ActiveSupport notifications for indexing start/success/failure, retrieval, generation, and provider retries:
360
+
361
+ ```ruby
362
+ ActiveSupport::Notifications.subscribe(/\Amaglev\./) do |name, start, finish, id, payload|
363
+ Rails.logger.info(
364
+ event: name,
365
+ duration_ms: ((finish - start) * 1000).round(1),
366
+ **payload
367
+ )
368
+ end
369
+ ```
370
+
371
+ ## Security and Boundaries
372
+
373
+ - Treat extracted content as untrusted context. Maglev sanitizes supported HTML sources, but authorization and model exposure remain application responsibilities.
374
+ - Expose only fields and content sources that are appropriate to send to your configured providers.
375
+ - Default attachment limits are 5 MiB and 20,000 extracted characters; the default allowlist covers plain text, Markdown, HTML, and XHTML.
376
+ - The generated v1 migration uses `bigint` owner IDs. UUID-backed owners require a customized migration.
377
+ - Maglev is RAG-only. It does not generate SQL, answer aggregate questions through database computation, expose REST endpoints, provide an admin UI, run agents, or manage conversation memory.
378
+
379
+ ## Compatibility
380
+
381
+ The CI matrix tests:
382
+
383
+ | | Supported |
384
+ |---|---|
385
+ | Ruby | 3.2, 3.3 |
386
+ | Rails | 7.1, 8.0 |
387
+ | Database | PostgreSQL with pgvector |
388
+
389
+ Versions outside this matrix may work but are not currently guaranteed.
390
+
391
+ ## Development
392
+
393
+ ```bash
394
+ bundle install
395
+
396
+ # PostgreSQL + pgvector integration tests
397
+ MAGLEV_REQUIRE_POSTGRESQL=true MAGLEV_DATABASE=maglev_test bundle exec rspec
398
+
399
+ bundle exec standardrb
400
+ bundle exec rubocop
401
+ bundle exec rake build
402
+ ```
403
+
404
+ The default test suite uses deterministic fake adapters and never calls a live LLM or embedding provider.
405
+
406
+ ## License
407
+
408
+ Maglev is available as open source under the [MIT License](LICENSE.txt).