maglev-rb 0.1.1 → 0.2.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 +4 -4
- data/CHANGELOG.md +46 -0
- data/README.ja.md +618 -0
- data/README.md +533 -248
- data/README.zh-CN.md +457 -250
- data/lib/generators/maglev/install/install_generator.rb +20 -0
- data/lib/generators/maglev/upgrade_index_version/upgrade_index_version_generator.rb +27 -0
- data/lib/generators/maglev/upgrade_source_identity/upgrade_source_identity_generator.rb +52 -0
- data/lib/maglev/active_record_extension.rb +141 -24
- data/lib/maglev/adapters/faraday_client.rb +94 -0
- data/lib/maglev/adapters/faraday_embedding.rb +51 -0
- data/lib/maglev/adapters/faraday_generation.rb +49 -0
- data/lib/maglev/adapters/faraday_planner.rb +88 -0
- data/lib/maglev/answerer.rb +30 -12
- data/lib/maglev/chunker.rb +39 -4
- data/lib/maglev/configuration.rb +66 -1
- data/lib/maglev/content_source_graph.rb +17 -11
- data/lib/maglev/dependency_graph.rb +72 -13
- data/lib/maglev/embedding_adapter.rb +10 -0
- data/lib/maglev/hybrid_candidate_set.rb +25 -0
- data/lib/maglev/hybrid_coordinator.rb +112 -0
- data/lib/maglev/hybrid_result.rb +25 -0
- data/lib/maglev/index_diagnostics.rb +83 -0
- data/lib/maglev/index_identity.rb +70 -0
- data/lib/maglev/index_state.rb +9 -0
- data/lib/maglev/indexer.rb +185 -35
- data/lib/maglev/knowledge_config.rb +27 -5
- data/lib/maglev/knowledge_registry.rb +33 -0
- data/lib/maglev/planner.rb +172 -0
- data/lib/maglev/planner_adapter.rb +25 -0
- data/lib/maglev/planner_evaluation.rb +49 -0
- data/lib/maglev/query_compiler.rb +197 -0
- data/lib/maglev/query_ir.rb +143 -0
- data/lib/maglev/query_validator.rb +311 -0
- data/lib/maglev/railtie.rb +9 -0
- data/lib/maglev/registry.rb +72 -0
- data/lib/maglev/reindex_job.rb +34 -2
- data/lib/maglev/relation_order.rb +16 -0
- data/lib/maglev/request.rb +22 -0
- data/lib/maglev/request_executor.rb +101 -0
- data/lib/maglev/resource_config.rb +222 -0
- data/lib/maglev/response.rb +2 -2
- data/lib/maglev/result.rb +30 -0
- data/lib/maglev/retrieval_outcome.rb +52 -0
- data/lib/maglev/retrieval_result.rb +25 -0
- data/lib/maglev/retriever.rb +282 -27
- data/lib/maglev/router.rb +77 -0
- data/lib/maglev/routing_adapter.rb +25 -0
- data/lib/maglev/schema_compiler.rb +17 -4
- data/lib/maglev/schema_snapshot.rb +159 -0
- data/lib/maglev/search_result.rb +7 -3
- data/lib/maglev/snapshot.rb +21 -1
- data/lib/maglev/snapshot_budget.rb +57 -0
- data/lib/maglev/snapshot_builder.rb +89 -11
- data/lib/maglev/source_extractor.rb +43 -0
- data/lib/maglev/source_fragment.rb +9 -0
- data/lib/maglev/structured_answer_composer.rb +67 -0
- data/lib/maglev/structured_evidence_builder.rb +56 -0
- data/lib/maglev/structured_executor.rb +157 -0
- data/lib/maglev/structured_result.rb +97 -0
- data/lib/maglev/trace.rb +56 -0
- data/lib/maglev/vector_stores/base.rb +12 -0
- data/lib/maglev/vector_stores/document.rb +14 -5
- data/lib/maglev/vector_stores/document_id.rb +27 -0
- data/lib/maglev/vector_stores/memory.rb +68 -6
- data/lib/maglev/vector_stores/metadata_filter.rb +55 -0
- data/lib/maglev/vector_stores/pgvector.rb +94 -7
- data/lib/maglev/version.rb +1 -1
- data/lib/maglev-rb.rb +3 -0
- data/lib/maglev.rb +36 -3
- data/lib/tasks/maglev.rake +43 -0
- metadata +71 -11
- data/lib/maglev/adapters/ruby_llm_attachment_extractor.rb +0 -15
- data/lib/maglev/adapters/ruby_llm_embedding.rb +0 -22
- data/lib/maglev/adapters/ruby_llm_generation.rb +0 -22
- data/lib/maglev/adapters/ruby_llm_provider.rb +0 -64
data/README.md
CHANGED
|
@@ -1,48 +1,108 @@
|
|
|
1
1
|
# Maglev
|
|
2
2
|
|
|
3
|
-
[English](README.md) | [简体中文](README.zh-CN.md)
|
|
3
|
+
[English](README.md) | [简体中文](README.zh-CN.md) | [日本語](README.ja.md)
|
|
4
4
|
|
|
5
5
|
[](https://github.com/benjis/maglev/actions/workflows/ci.yml)
|
|
6
|
-
[](https://www.ruby-lang.org/)
|
|
7
7
|
[](https://rubyonrails.org/)
|
|
8
8
|
[](LICENSE.txt)
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Maglev 0.2 is a Rails-native, read-only knowledge and query layer for
|
|
11
|
+
ActiveRecord applications. It lets an application answer natural-language
|
|
12
|
+
questions through three explicit routes:
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
- **Structured:** question → validated Query IR → composable
|
|
15
|
+
`ActiveRecord::Relation` or bounded aggregate.
|
|
16
|
+
- **RAG:** question → authorized semantic retrieval → optional grounded answer.
|
|
17
|
+
- **Hybrid:** one of two fixed workflows combining structured filtering and RAG
|
|
18
|
+
evidence.
|
|
19
|
+
|
|
20
|
+
Models expose only an application-defined allowlist. Structured compilation
|
|
21
|
+
starts from the caller's base relation and can only narrow it. RAG retrieval and
|
|
22
|
+
answer generation remain separable.
|
|
13
23
|
|
|
14
24
|
```ruby
|
|
15
|
-
|
|
25
|
+
# Exact database question
|
|
26
|
+
resource_authorizer = ->(_entry, user) { user.account_id == current_account.id }
|
|
27
|
+
result = current_account.invoices.maglev_request(
|
|
28
|
+
"Open invoices over $500",
|
|
29
|
+
mode: :structured,
|
|
30
|
+
planner_adapter: planner,
|
|
31
|
+
authorizer: resource_authorizer,
|
|
32
|
+
user: current_user
|
|
33
|
+
)
|
|
16
34
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
35
|
+
# Inspectable semantic evidence, without generation
|
|
36
|
+
retrieval = SupportTicket.retrieve("customers blocked during cancellation", user: current_user)
|
|
37
|
+
|
|
38
|
+
# Grounded answer
|
|
39
|
+
answer = SupportTicket.ask("What cancellation problems recur?", user: current_user)
|
|
20
40
|
```
|
|
21
41
|
|
|
22
|
-
|
|
42
|
+
## Start here: choose the data path
|
|
43
|
+
|
|
44
|
+
You do not need to understand Maglev's whole architecture before using it. Start
|
|
45
|
+
with the kind of answer your application needs:
|
|
46
|
+
|
|
47
|
+
| User question | Data needed | Declare | Call |
|
|
48
|
+
| --- | --- | --- | --- |
|
|
49
|
+
| “How many invoices are overdue?” | Exact columns, filters, counts | `queryable` | `maglev_request(..., mode: :structured)` |
|
|
50
|
+
| “What problems do customers describe when cancelling?” | Free text, comments, attachments | `knowledge` | `retrieve` or `ask` |
|
|
51
|
+
| “Which open tickets mention being charged twice?” | Exact status plus semantic text | Both | `maglev_request(..., mode: :hybrid)` |
|
|
52
|
+
|
|
53
|
+
The mental model is deliberately small:
|
|
54
|
+
|
|
55
|
+
1. `maglev_resource :support_tickets` gives a model a stable name in Maglev.
|
|
56
|
+
2. `queryable` allowlists what a planner may ask ActiveRecord to filter, sort,
|
|
57
|
+
join, or aggregate.
|
|
58
|
+
3. `knowledge` selects what becomes searchable semantic evidence.
|
|
59
|
+
4. The caller still supplies authorization and, for structured work, the base
|
|
60
|
+
`ActiveRecord::Relation`. Maglev never grants itself broader access.
|
|
23
61
|
|
|
24
62
|
## Why Maglev?
|
|
25
63
|
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
|
|
64
|
+
- A normal Ruby gem with `Maglev::Railtie`, not a Rails Engine or separate API.
|
|
65
|
+
- ActiveRecord-first structured queries; no unrestricted SQL or Ruby generation.
|
|
66
|
+
- Explicit registry for queryable fields, associations, scopes, aggregates, and
|
|
67
|
+
knowledge sources.
|
|
68
|
+
- Tenant and authorization constraints carried by application-owned relations.
|
|
69
|
+
- Source-aware RAG with pgvector, normalized similarity, deterministic budgets,
|
|
70
|
+
and inspectable evidence.
|
|
71
|
+
- Immutable plans/results and redacted trace metadata.
|
|
72
|
+
- Deterministic fake adapters for tests; no live provider calls in the default suite.
|
|
33
73
|
|
|
34
|
-
##
|
|
74
|
+
## Architecture
|
|
35
75
|
|
|
36
|
-
|
|
76
|
+
```mermaid
|
|
77
|
+
flowchart TD
|
|
78
|
+
Q["Question + registered scope"] --> R["Router"]
|
|
79
|
+
R --> S["Structured planner"]
|
|
80
|
+
R --> G["RAG retrieval"]
|
|
81
|
+
R --> H["Fixed hybrid coordinator"]
|
|
82
|
+
S --> I["Untrusted Query IR"]
|
|
83
|
+
I --> V["Deterministic validator"]
|
|
84
|
+
V --> C["Compiler on supplied base relation"]
|
|
85
|
+
C --> E["Read-only bounded execution"]
|
|
86
|
+
G --> X["Authorized semantic evidence"]
|
|
87
|
+
H --> E
|
|
88
|
+
H --> X
|
|
89
|
+
E --> O["Evidence + Result"]
|
|
90
|
+
X --> O
|
|
91
|
+
O --> A["Optional grounded answer"]
|
|
92
|
+
Q --> T["Redacted trace"]
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The registry is an authority boundary. A schema snapshot contains only the
|
|
96
|
+
registered resources authorized for one request, never record values. Provider
|
|
97
|
+
output is untrusted until deterministic validation succeeds.
|
|
37
98
|
|
|
38
|
-
|
|
39
|
-
[`pgvector`](https://github.com/pgvector/pgvector) extension.
|
|
99
|
+
## Installation
|
|
40
100
|
|
|
41
|
-
|
|
101
|
+
Maglev requires Ruby 3.3+, Rails 7.1 or 8.0, PostgreSQL, and pgvector.
|
|
42
102
|
|
|
43
103
|
```ruby
|
|
44
104
|
# Gemfile
|
|
45
|
-
gem "maglev-rb"
|
|
105
|
+
gem "maglev-rb", "~> 0.2.1"
|
|
46
106
|
```
|
|
47
107
|
|
|
48
108
|
```bash
|
|
@@ -51,358 +111,583 @@ bin/rails generate maglev:install --embedding-dimensions=1536
|
|
|
51
111
|
bin/rails db:migrate
|
|
52
112
|
```
|
|
53
113
|
|
|
54
|
-
The generator
|
|
114
|
+
The generator creates an initializer, the `maglev_chunks` table, source and
|
|
115
|
+
tenant metadata, an HNSW cosine index, and `maglev_index_states` diagnostics.
|
|
116
|
+
Review generated migrations when owner primary keys are UUIDs.
|
|
117
|
+
|
|
118
|
+
## Configuration
|
|
55
119
|
|
|
56
|
-
|
|
120
|
+
The built-in embedding and generation clients use OpenAI-compatible HTTP
|
|
121
|
+
endpoints. The planner defaults to the OpenAI `json_schema` response format;
|
|
122
|
+
providers without that capability may use the less constrained `json_object`
|
|
123
|
+
format. Embedding and generation may use different providers.
|
|
57
124
|
|
|
58
125
|
```ruby
|
|
59
|
-
# config/initializers/maglev.rb
|
|
60
126
|
Maglev.configure do |config|
|
|
61
127
|
config.embedding_provider do |provider|
|
|
62
|
-
provider.url = "
|
|
63
|
-
provider.api_key = ENV["
|
|
64
|
-
provider.model = "
|
|
65
|
-
provider.dimensions =
|
|
128
|
+
provider.url = ENV.fetch("MAGLEV_EMBEDDING_URL", "https://api.openai.com/v1")
|
|
129
|
+
provider.api_key = ENV["MAGLEV_EMBEDDING_API_KEY"]
|
|
130
|
+
provider.model = "text-embedding-3-small"
|
|
131
|
+
provider.dimensions = 1536
|
|
66
132
|
end
|
|
67
133
|
|
|
68
134
|
config.generation_provider do |provider|
|
|
69
|
-
provider.url = "https://api.
|
|
70
|
-
provider.api_key =
|
|
71
|
-
provider.model = "
|
|
135
|
+
provider.url = ENV.fetch("MAGLEV_GENERATION_URL", "https://api.openai.com/v1")
|
|
136
|
+
provider.api_key = ENV["MAGLEV_GENERATION_API_KEY"]
|
|
137
|
+
provider.model = "gpt-4.1-mini"
|
|
72
138
|
end
|
|
73
139
|
|
|
140
|
+
config.planner_adapter = Maglev::Adapters::FaradayPlanner.new
|
|
141
|
+
# For providers without json_schema support:
|
|
142
|
+
# config.planner_adapter = Maglev::Adapters::FaradayPlanner.new(response_format: :json_object)
|
|
143
|
+
config.routing_adapter = MyRoutingAdapter.new # required only for mode: :auto
|
|
144
|
+
|
|
74
145
|
config.chunk_size = 1000
|
|
146
|
+
config.minimum_similarity = nil
|
|
147
|
+
config.retrieval_max_candidates = 1000
|
|
148
|
+
config.context_max_characters = 4000
|
|
149
|
+
config.context_per_owner_characters = 1200
|
|
150
|
+
|
|
151
|
+
config.snapshot_attribute_max_characters = 20_000
|
|
152
|
+
config.snapshot_related_record_max_characters = 50_000
|
|
153
|
+
config.snapshot_max_characters = 100_000
|
|
154
|
+
config.snapshot_max_chunks = 100
|
|
155
|
+
|
|
156
|
+
config.structured_query_timeout = 5
|
|
157
|
+
config.structured_evidence_max_rows = 100
|
|
158
|
+
config.structured_evidence_max_bytes = 32_768
|
|
75
159
|
end
|
|
76
160
|
```
|
|
77
161
|
|
|
78
|
-
|
|
162
|
+
Use custom `embedding_adapter`, `generation_adapter`, `planner_adapter`,
|
|
163
|
+
`routing_adapter`, `attachment_extractor`, or `authorization_adapter` objects
|
|
164
|
+
when the built-in protocols do not fit.
|
|
165
|
+
|
|
166
|
+
## Registering a resource
|
|
79
167
|
|
|
80
|
-
|
|
168
|
+
`maglev_resource` is the primary v0.2 DSL. Structured and knowledge capabilities
|
|
169
|
+
are independent and may be declared together or separately.
|
|
81
170
|
|
|
82
|
-
###
|
|
171
|
+
### A fully annotated structured resource
|
|
83
172
|
|
|
84
173
|
```ruby
|
|
85
|
-
class
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
174
|
+
class Invoice < ApplicationRecord
|
|
175
|
+
# Normal Rails declarations remain the source of truth.
|
|
176
|
+
belongs_to :account
|
|
177
|
+
scope :due_before, ->(date) { where(due_on: ..date) }
|
|
178
|
+
|
|
179
|
+
# :invoices is the stable identifier used by Maglev plans and requests.
|
|
180
|
+
maglev_resource :invoices do
|
|
181
|
+
# Help the planner understand what this resource represents.
|
|
182
|
+
description "Invoices belonging to an authorized account"
|
|
183
|
+
synonyms "bills"
|
|
184
|
+
|
|
185
|
+
# This block controls structured ActiveRecord queries only.
|
|
186
|
+
queryable do
|
|
187
|
+
# Allow exact filters/sorts on these real database columns.
|
|
188
|
+
# enum also tells the planner which status values are valid.
|
|
189
|
+
field :status, enum: %w[draft open paid void]
|
|
190
|
+
field :amount, description: "Invoice total in the account currency"
|
|
191
|
+
field :due_on, synonyms: ["deadline"]
|
|
192
|
+
field :paid_at
|
|
193
|
+
|
|
194
|
+
# Explicitly deny sensitive columns, even if a planner asks for them.
|
|
195
|
+
prohibit :number, :internal_note
|
|
196
|
+
|
|
197
|
+
# Permit a registered join to the separately registered :accounts resource.
|
|
198
|
+
association :account, resource: :accounts
|
|
199
|
+
|
|
200
|
+
# Permit this existing Rails scope and describe its typed argument.
|
|
201
|
+
scope :due_before,
|
|
202
|
+
parameters: {date: {type: :date, required: true}}
|
|
203
|
+
|
|
204
|
+
# Allow only these aggregate functions/columns.
|
|
205
|
+
aggregates count: true, sum: [:amount], average: [:amount]
|
|
206
|
+
|
|
207
|
+
# Per-resource ceilings applied in addition to global/request limits.
|
|
208
|
+
limits rows: 50, operations: 8, joins: 1
|
|
209
|
+
|
|
210
|
+
# The caller must authorize this resource for every structured plan.
|
|
211
|
+
authorization :required
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# This separate block controls semantic indexing and RAG.
|
|
215
|
+
# Repeating a field here does not broaden structured query permissions.
|
|
216
|
+
knowledge do
|
|
217
|
+
expose :status, :amount, :due_on, :paid_at
|
|
218
|
+
end
|
|
101
219
|
end
|
|
102
220
|
end
|
|
221
|
+
```
|
|
103
222
|
|
|
104
|
-
|
|
105
|
-
|
|
223
|
+
Nothing is exposed implicitly. `authorization :required` means the resource is
|
|
224
|
+
omitted from a request schema snapshot unless the caller authorizes it.
|
|
225
|
+
`allow_unscoped_model_queries` is opt-in and should be reserved for genuinely
|
|
226
|
+
public data.
|
|
106
227
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
228
|
+
`queryable` defines only the constrained ActiveRecord contract. `knowledge`
|
|
229
|
+
defines only RAG indexing and retrieval sources. `maglev_resource` is the
|
|
230
|
+
unified resource declaration and may contain either block or both. Models that
|
|
231
|
+
do not declare `knowledge` cannot use `search`, `retrieve`, `ask`, snapshots, or
|
|
232
|
+
indexing callbacks.
|
|
112
233
|
|
|
113
|
-
|
|
234
|
+
Read the declaration as an allowlist, not as a schema dump. Columns that are not
|
|
235
|
+
listed in `field` cannot appear in Query IR. Values that are not listed in
|
|
236
|
+
`expose` (or another knowledge source) do not enter the semantic snapshot.
|
|
114
237
|
|
|
115
|
-
###
|
|
238
|
+
### Why a field may appear in both blocks
|
|
116
239
|
|
|
117
|
-
|
|
240
|
+
The same column can serve two different jobs:
|
|
118
241
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
```
|
|
242
|
+
- `field :status` lets structured queries apply an exact condition such as
|
|
243
|
+
`status = "open"`.
|
|
244
|
+
- `expose :status` writes `status: open` into the indexed snapshot so retrieved
|
|
245
|
+
evidence keeps useful context.
|
|
124
246
|
|
|
125
|
-
|
|
247
|
+
Declaring one never implies the other. Put identifiers, dates, enums, and money
|
|
248
|
+
in `queryable` when exactness matters. Put prose, descriptions, comments,
|
|
249
|
+
resolution notes, and attachment text in `knowledge` when meaning matters. Use
|
|
250
|
+
both for contextual fields such as status, priority, or product area.
|
|
126
251
|
|
|
127
|
-
###
|
|
252
|
+
### A realistic RAG resource
|
|
128
253
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
user: current_user
|
|
134
|
-
)
|
|
254
|
+
RAG is most useful when the answer is present in human language rather than a
|
|
255
|
+
single database column. In this example, structured queries can find open/high
|
|
256
|
+
priority tickets, while RAG can understand phrases such as “charged twice” in
|
|
257
|
+
ticket bodies, comments, resolution notes, and attached logs.
|
|
135
258
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
259
|
+
```ruby
|
|
260
|
+
class SupportTicket < ApplicationRecord
|
|
261
|
+
belongs_to :account
|
|
262
|
+
has_many :comments
|
|
263
|
+
has_many_attached :files
|
|
264
|
+
has_rich_text :resolution
|
|
265
|
+
|
|
266
|
+
maglev_resource :support_tickets do
|
|
267
|
+
description "Customer support requests and their investigation evidence"
|
|
268
|
+
|
|
269
|
+
queryable do
|
|
270
|
+
# Good structured fields: exact, typed, and useful for filtering.
|
|
271
|
+
field :status, enum: %w[open pending resolved closed]
|
|
272
|
+
field :priority, enum: %w[low normal high urgent]
|
|
273
|
+
field :product_area
|
|
274
|
+
field :created_at
|
|
275
|
+
prohibit :requester_email, :internal_notes
|
|
276
|
+
limits rows: 100, operations: 8, joins: 1
|
|
277
|
+
authorization :required
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
knowledge do
|
|
281
|
+
# Prose carries the semantic meaning that exact filters cannot capture.
|
|
282
|
+
expose :subject, :body
|
|
283
|
+
|
|
284
|
+
# These overlap with queryable fields to preserve context in evidence.
|
|
285
|
+
expose :status, :priority, :product_area
|
|
286
|
+
|
|
287
|
+
# Include a bounded, deterministic slice of related conversation.
|
|
288
|
+
include_related :comments, depth: 1, limit: 20,
|
|
289
|
+
order: {created_at: :desc}
|
|
290
|
+
|
|
291
|
+
# Extract supported attachment text and Action Text content.
|
|
292
|
+
expose_attached :files
|
|
293
|
+
expose_rich_text :resolution
|
|
294
|
+
end
|
|
295
|
+
end
|
|
142
296
|
end
|
|
143
297
|
```
|
|
144
298
|
|
|
145
|
-
|
|
299
|
+
Related models used by `include_related` must declare their own
|
|
300
|
+
`maglev_resource ... knowledge` block. To create a knowledge-only resource,
|
|
301
|
+
simply omit `queryable`; to create a structured-only resource, omit `knowledge`.
|
|
302
|
+
|
|
303
|
+
Once records are indexed, these calls answer different questions:
|
|
146
304
|
|
|
147
305
|
```ruby
|
|
148
|
-
|
|
149
|
-
|
|
306
|
+
# Semantic matches only. No generation call is made.
|
|
307
|
+
evidence = SupportTicket.retrieve(
|
|
308
|
+
"customers say they were charged twice after cancellation",
|
|
309
|
+
limit: 10,
|
|
310
|
+
user: current_user
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# A prose answer grounded in the selected ticket evidence.
|
|
314
|
+
answer = SupportTicket.ask(
|
|
315
|
+
"What double-charge patterns recur after cancellation?",
|
|
150
316
|
limit: 5,
|
|
151
317
|
user: current_user
|
|
152
318
|
)
|
|
153
319
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
320
|
+
# Exact database filtering followed by semantic retrieval inside that set.
|
|
321
|
+
result = current_account.support_tickets.maglev_request(
|
|
322
|
+
"Open urgent tickets where customers describe being charged twice",
|
|
323
|
+
mode: :hybrid,
|
|
324
|
+
hybrid_plan: :structured_first,
|
|
325
|
+
planner_adapter: planner,
|
|
326
|
+
authorizer: resource_authorizer,
|
|
327
|
+
user: current_user
|
|
328
|
+
)
|
|
157
329
|
```
|
|
158
330
|
|
|
159
|
-
|
|
331
|
+
The default attachment extractor supports plain text, Markdown, HTML, and XHTML.
|
|
332
|
+
PDF, Office, OCR, image, audio, and video parsing require an application-owned
|
|
333
|
+
extractor. All snapshots, relations, attachments, and chunks have hard budgets.
|
|
160
334
|
|
|
161
|
-
|
|
335
|
+
Inspect exposure without calling a provider:
|
|
162
336
|
|
|
163
337
|
```ruby
|
|
164
|
-
|
|
338
|
+
SupportTicket.maglev_schema
|
|
339
|
+
ticket.maglev_snapshot
|
|
340
|
+
ticket.maglev_context_preview(question: "Why is this unresolved?")
|
|
341
|
+
ticket.maglev_index_status
|
|
165
342
|
```
|
|
166
343
|
|
|
167
|
-
|
|
344
|
+
### DSL reference
|
|
168
345
|
|
|
169
|
-
|
|
170
|
-
review.update!(product: replacement_product)
|
|
171
|
-
# Maglev::ReindexJob is queued for both affected products.
|
|
172
|
-
```
|
|
346
|
+
Resource-level methods:
|
|
173
347
|
|
|
174
|
-
|
|
348
|
+
| DSL | Purpose |
|
|
349
|
+
| --- | --- |
|
|
350
|
+
| `maglev_resource :identifier` | Registers one stable resource identifier for the model. |
|
|
351
|
+
| `description "..."` | Human-readable planner context; never contains record values. |
|
|
352
|
+
| `synonyms "...", "..."` | Alternate names the planner may use for the resource. |
|
|
353
|
+
| `queryable { ... }` | Declares the structured ActiveRecord capability. May appear once. |
|
|
354
|
+
| `knowledge { ... }` | Declares the RAG/indexing capability. May appear once. |
|
|
175
355
|
|
|
176
|
-
|
|
356
|
+
`queryable` methods:
|
|
177
357
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
358
|
+
| DSL | Purpose and options |
|
|
359
|
+
| --- | --- |
|
|
360
|
+
| `field :name` | Allowlists a real column. Options: `description:`, `synonyms:`, `enum:`, and `sensitive:`. A sensitive field is excluded from the planner schema. |
|
|
361
|
+
| `prohibit :a, :b` | Explicitly denies real columns. A field cannot be both allowed and prohibited. |
|
|
362
|
+
| `association :account, resource: :accounts` | Allows a registered ActiveRecord association path. Options: `description:` and `synonyms:`. The target resource must also be registered. |
|
|
363
|
+
| `scope :due_before, parameters: {...}` | Allows one existing model scope. Parameter metadata supports `type`, `required`, `nullable`, `enum_values`, `minimum`, and `maximum`. Arbitrary scopes are never callable. |
|
|
364
|
+
| `aggregates count: true, sum: [:amount]` | Allows `count`, `sum`, `average`, `minimum`, or `maximum`; field lists restrict numeric/value aggregates. |
|
|
365
|
+
| `limits rows:, operations:, joins:` | Adds positive per-resource ceilings. Effective limits are the strictest configured limits. |
|
|
366
|
+
| `authorization :required` | Default. The resource enters a schema snapshot only when `authorizer` approves it. |
|
|
367
|
+
| `authorization :public` | Marks the resource schema public; record access must still respect the supplied relation. |
|
|
368
|
+
| `allow_unscoped_model_queries true` | Allows structured requests without a supplied base relation. Off by default; use only for genuinely public data. |
|
|
369
|
+
|
|
370
|
+
Scope parameter `type` accepts `:string`, `:integer`, `:float`, `:decimal`,
|
|
371
|
+
`:boolean`, `:date`, `:datetime`, `:timestamp`, or `:time`. Unsupported types
|
|
372
|
+
are rejected when the resource is registered.
|
|
192
373
|
|
|
193
|
-
|
|
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.
|
|
374
|
+
`knowledge` methods:
|
|
200
375
|
|
|
201
|
-
|
|
376
|
+
| DSL | Purpose and options |
|
|
377
|
+
| --- | --- |
|
|
378
|
+
| `expose :subject, :body` | Adds selected non-nil model attributes to the searchable snapshot. |
|
|
379
|
+
| `hide :internal_notes` | Explicitly documents attributes that must not be exposed; an attribute cannot be both exposed and hidden. |
|
|
380
|
+
| `tags :support, :customer` | Adds fixed classification labels to every snapshot for this model. |
|
|
381
|
+
| `include_related :comments, depth:, limit:` | Adds bounded related-record snapshots. Optional `inverse:` resolves non-obvious reverse associations; `order:` accepts a column or `{column: :asc/:desc}`. |
|
|
382
|
+
| `expose_attached :files` | Adds text extracted from named Active Storage attachments. |
|
|
383
|
+
| `expose_rich_text :resolution` | Adds plain text from named Action Text attributes. |
|
|
202
384
|
|
|
203
|
-
|
|
385
|
+
Nothing is inferred from database visibility. DSL declarations are validated at
|
|
386
|
+
registration time, and unknown fields, associations, scopes, or attachments
|
|
387
|
+
raise `Maglev::ConfigurationError`.
|
|
204
388
|
|
|
205
|
-
|
|
389
|
+
## Structured queries
|
|
206
390
|
|
|
207
|
-
|
|
391
|
+
Planning and execution are deliberately separate.
|
|
208
392
|
|
|
209
393
|
```ruby
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
394
|
+
base = current_account.invoices.where(archived: false)
|
|
395
|
+
|
|
396
|
+
plan = Maglev.plan(
|
|
397
|
+
"Open invoices over $500 due this month",
|
|
398
|
+
resource: :invoices,
|
|
399
|
+
base_relation: base,
|
|
400
|
+
authorizer: ->(entry, user) { user.account_id == current_account.id },
|
|
401
|
+
user: current_user,
|
|
402
|
+
constraints: {rows: 25, operations: 8, joins: 1},
|
|
403
|
+
adapter: planner
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
plan.status # :ready, :clarification_required, :unsupported, :invalid
|
|
407
|
+
plan.ir # immutable Maglev::QueryIR::Query when ready
|
|
408
|
+
plan.explanation
|
|
409
|
+
plan.policy_limits # effective rows/operations/joins/complexity limits
|
|
410
|
+
plan.evidence_requirements
|
|
411
|
+
plan.trace_id
|
|
412
|
+
|
|
413
|
+
result = Maglev.execute(plan)
|
|
414
|
+
result.status # :succeeded
|
|
415
|
+
result.kind # :relation or :aggregate
|
|
416
|
+
result.value # protected ActiveRecord::Relation or bounded scalar
|
|
417
|
+
result.evidence # bounded Maglev::StructuredEvidence
|
|
418
|
+
result.render
|
|
214
419
|
```
|
|
215
420
|
|
|
216
|
-
|
|
421
|
+
Record relations stay lazy and composable until read. Reads execute in a
|
|
422
|
+
read-only PostgreSQL transaction with `statement_timeout`; returned records are
|
|
423
|
+
read-only and bulk writes are rejected.
|
|
424
|
+
|
|
425
|
+
Query IR v1 supports registered scopes; `eq`, `not_eq`, `gt`, `gte`, `lt`,
|
|
426
|
+
`lte`, `in`, `not_in`, `is_null`, `is_not_null`, and `between`; joins up to two
|
|
427
|
+
levels; sort, distinct, limit; and count, sum, average, minimum, and maximum.
|
|
428
|
+
It cannot contain SQL, Arel, Ruby, arbitrary methods, writes, locks, nested
|
|
429
|
+
boolean groups, windows, subqueries, `HAVING`, or planner-defined tools.
|
|
217
430
|
|
|
218
|
-
|
|
431
|
+
## RAG: search, retrieve, and ask
|
|
219
432
|
|
|
220
|
-
|
|
433
|
+
These APIs remain directly available on knowledge-enabled models.
|
|
221
434
|
|
|
222
435
|
```ruby
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
436
|
+
matches = SupportTicket.search(
|
|
437
|
+
"cancellation failures",
|
|
438
|
+
limit: 10,
|
|
439
|
+
minimum_similarity: 0.65,
|
|
440
|
+
user: current_user
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
matches.first.owner
|
|
444
|
+
matches.first.source_identity
|
|
445
|
+
matches.first.source_type
|
|
446
|
+
matches.first.similarity
|
|
227
447
|
```
|
|
228
448
|
|
|
229
|
-
|
|
449
|
+
`search` returns at most one `SearchResult` per owner. For full retrieval
|
|
450
|
+
diagnostics without generation, use `retrieve`:
|
|
451
|
+
|
|
452
|
+
```ruby
|
|
453
|
+
retrieval = SupportTicket.retrieve(
|
|
454
|
+
"cancellation failures",
|
|
455
|
+
limit: 10,
|
|
456
|
+
chunks_per_owner: 2,
|
|
457
|
+
user: current_user
|
|
458
|
+
)
|
|
230
459
|
|
|
231
|
-
|
|
460
|
+
retrieval.considered
|
|
461
|
+
retrieval.selected
|
|
462
|
+
retrieval.rejected
|
|
463
|
+
retrieval.context
|
|
464
|
+
retrieval.budgets
|
|
465
|
+
retrieval.reasons
|
|
466
|
+
retrieval.timings
|
|
467
|
+
retrieval.trace_id
|
|
468
|
+
```
|
|
232
469
|
|
|
233
|
-
|
|
470
|
+
Generate only when a prose answer is needed:
|
|
234
471
|
|
|
235
472
|
```ruby
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
473
|
+
answer = SupportTicket.ask(
|
|
474
|
+
"Which cancellation failures recur?",
|
|
475
|
+
limit: 5,
|
|
476
|
+
chunks_per_owner: 2,
|
|
477
|
+
minimum_similarity: 0.65,
|
|
478
|
+
user: current_user
|
|
241
479
|
)
|
|
242
|
-
|
|
243
|
-
|
|
480
|
+
|
|
481
|
+
answer.text
|
|
482
|
+
answer.sources
|
|
483
|
+
answer.metadata
|
|
244
484
|
```
|
|
245
485
|
|
|
246
|
-
|
|
486
|
+
If no evidence survives authorization, similarity, or context budgets, `ask`
|
|
487
|
+
returns deterministic insufficient context without calling generation.
|
|
247
488
|
|
|
248
|
-
|
|
489
|
+
## Unified requests and routing
|
|
249
490
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
def scope(model:, user:)
|
|
253
|
-
model.accessible_by(user)
|
|
254
|
-
end
|
|
491
|
+
Use `Maglev.request`, `Model.maglev_request`, or
|
|
492
|
+
`relation.maglev_request` when one result envelope and route selection are useful.
|
|
255
493
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
494
|
+
```ruby
|
|
495
|
+
result = current_account.invoices.maglev_request(
|
|
496
|
+
"How many open invoices are overdue?",
|
|
497
|
+
mode: :structured,
|
|
498
|
+
planner_adapter: planner,
|
|
499
|
+
authorizer: resource_authorizer,
|
|
500
|
+
user: current_user
|
|
501
|
+
)
|
|
260
502
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
503
|
+
result.status
|
|
504
|
+
result.route
|
|
505
|
+
result.kind
|
|
506
|
+
result.value
|
|
507
|
+
result.evidence
|
|
508
|
+
result.warnings
|
|
509
|
+
result.trace_id
|
|
510
|
+
result.confidence
|
|
511
|
+
result.reasons
|
|
512
|
+
result.metadata
|
|
264
513
|
```
|
|
265
514
|
|
|
266
|
-
|
|
515
|
+
Modes are `:structured`, `:rag`, `:hybrid`, and `:auto`. Explicit modes never
|
|
516
|
+
call the routing classifier. Automatic routing requires `routing_adapter` and
|
|
517
|
+
receives only bounded capability summaries—not record values or source text.
|
|
518
|
+
Every application-level request must name resources/models or supply a base
|
|
519
|
+
relation; Maglev never scans all application models.
|
|
267
520
|
|
|
268
|
-
|
|
269
|
-
|
|
521
|
+
A routing adapter implements `classify(question:, capabilities:)` and returns
|
|
522
|
+
`{"route" => "structured", "confidence" => 0.9, "reasons" => ["exact fields"]}`
|
|
523
|
+
or another valid route. Confidence is advisory and never grants access.
|
|
270
524
|
|
|
271
|
-
|
|
525
|
+
For a generated RAG answer through the unified API, pass `answer: true`.
|
|
526
|
+
Otherwise the RAG route returns `kind: :semantic_matches`.
|
|
272
527
|
|
|
273
|
-
|
|
528
|
+
## Hybrid workflows
|
|
274
529
|
|
|
275
|
-
|
|
530
|
+
Hybrid mode supports exactly two fixed shapes and requires one registered
|
|
531
|
+
resource with both queryable and knowledge capabilities.
|
|
276
532
|
|
|
277
533
|
```ruby
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
|
534
|
+
result = current_account.support_tickets.maglev_request(
|
|
535
|
+
"Open tickets that mention cancellation",
|
|
536
|
+
mode: :hybrid,
|
|
537
|
+
hybrid_plan: :structured_first, # or :rag_first
|
|
538
|
+
planner_adapter: planner,
|
|
539
|
+
authorizer: resource_authorizer,
|
|
540
|
+
candidate_limit: 100,
|
|
541
|
+
user: current_user
|
|
542
|
+
)
|
|
293
543
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
|
544
|
+
result.kind # :hybrid_answer
|
|
545
|
+
result.value.records
|
|
546
|
+
result.evidence # structured/RAG provenance envelopes
|
|
547
|
+
result.metadata[:plan_shape]
|
|
548
|
+
result.metadata[:operations]
|
|
310
549
|
```
|
|
311
550
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
551
|
+
Structured-first filters records before retrieval. RAG-first retrieves candidate
|
|
552
|
+
owners and then verifies them through the authorized relation. Candidate handoff
|
|
553
|
+
uses typed primary keys, is bounded, and reapplies registration, tenant, base
|
|
554
|
+
relation, and authorization constraints at every stage. There are no loops or
|
|
555
|
+
autonomous tool calls.
|
|
315
556
|
|
|
316
|
-
##
|
|
557
|
+
## Authorization and tenancy
|
|
317
558
|
|
|
318
|
-
|
|
559
|
+
The base relation is the structured and hybrid authority boundary:
|
|
319
560
|
|
|
320
561
|
```ruby
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
562
|
+
# Plain tenant scope
|
|
563
|
+
current_account.invoices.maglev_request(
|
|
564
|
+
question, mode: :structured, authorizer: resource_authorizer, user: current_user
|
|
565
|
+
)
|
|
325
566
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
567
|
+
# Pundit
|
|
568
|
+
policy_scope(Invoice).maglev_request(
|
|
569
|
+
question, mode: :structured, authorizer: resource_authorizer, user: current_user
|
|
570
|
+
)
|
|
329
571
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
572
|
+
# CanCanCan
|
|
573
|
+
Invoice.accessible_by(current_ability).maglev_request(
|
|
574
|
+
question, mode: :structured, authorizer: resource_authorizer, user: current_user
|
|
575
|
+
)
|
|
576
|
+
```
|
|
333
577
|
|
|
334
|
-
|
|
335
|
-
# Delete every document belonging to this owner
|
|
336
|
-
end
|
|
578
|
+
RAG authorization uses an optional adapter:
|
|
337
579
|
|
|
338
|
-
|
|
339
|
-
|
|
580
|
+
```ruby
|
|
581
|
+
class MaglevAuthorization
|
|
582
|
+
def scope(model:, user:) = model.where(account_id: user.account_id)
|
|
583
|
+
def authorize(record:, user:) = record.account_id == user.account_id
|
|
340
584
|
end
|
|
341
585
|
|
|
342
586
|
Maglev.configure do |config|
|
|
343
|
-
config.
|
|
587
|
+
config.authorization_adapter = MaglevAuthorization.new
|
|
588
|
+
config.tenant_id_resolver = lambda do |record: nil, user: nil|
|
|
589
|
+
(record || user)&.account_id&.to_s
|
|
590
|
+
end
|
|
344
591
|
end
|
|
345
592
|
```
|
|
346
593
|
|
|
347
|
-
|
|
594
|
+
Without an RAG authorization adapter, records are allowed by default. Configure
|
|
595
|
+
one and pass `user:` for every user-scoped retrieval.
|
|
348
596
|
|
|
349
|
-
|
|
597
|
+
Authorization filters are pushed into storage when supported and every hydrated
|
|
598
|
+
record is rechecked. Authorization scopes exceeding 1,000 owner IDs fail closed.
|
|
350
599
|
|
|
351
|
-
##
|
|
600
|
+
## Indexing, upgrades, and operations
|
|
352
601
|
|
|
353
602
|
```bash
|
|
354
603
|
bin/rails maglev:status
|
|
355
|
-
bin/rails maglev:reindex[
|
|
604
|
+
bin/rails maglev:reindex[SupportTicket]
|
|
356
605
|
bin/rails maglev:reindex_all
|
|
606
|
+
bin/rails maglev:evaluate_planner
|
|
357
607
|
```
|
|
358
608
|
|
|
359
|
-
|
|
609
|
+
Callbacks enqueue `Maglev::ReindexJob` after relevant commits. Indexing is
|
|
610
|
+
idempotent, reuses unchanged chunks, atomically replaces one owner's searchable
|
|
611
|
+
generation, and records safe status/failure diagnostics.
|
|
360
612
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
end
|
|
613
|
+
Applications upgrading from 0.1.x should follow [CHANGELOG.md](CHANGELOG.md):
|
|
614
|
+
|
|
615
|
+
```bash
|
|
616
|
+
bin/rails generate maglev:upgrade_index_version
|
|
617
|
+
bin/rails generate maglev:upgrade_source_identity
|
|
618
|
+
bin/rails db:migrate
|
|
619
|
+
bin/rails maglev:reindex_all
|
|
369
620
|
```
|
|
370
621
|
|
|
371
|
-
|
|
622
|
+
Review generated migrations, migrate the vector column separately when embedding
|
|
623
|
+
dimensions change, rebuild the HNSW index, and complete a full reindex. Old rows
|
|
624
|
+
remain unavailable until they carry the current index identity.
|
|
372
625
|
|
|
373
|
-
|
|
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.
|
|
626
|
+
### Index identity and safe replacement
|
|
378
627
|
|
|
379
|
-
|
|
628
|
+
Every chunk stores an `index_version`. Fingerprint format version 1 uses the
|
|
629
|
+
`maglev-index` namespace and covers the embedding model/dimensions, adapter ID
|
|
630
|
+
and version, chunking algorithm/size, and `application_index_version`. Custom
|
|
631
|
+
embedding adapters implement `maglev_adapter_id` and `maglev_adapter_version`,
|
|
632
|
+
or configure `embedding_adapter_id` and `embedding_adapter_version`.
|
|
380
633
|
|
|
381
|
-
The
|
|
634
|
+
The `upgrade_index_version` migration intentionally adds a nullable
|
|
635
|
+
`index_version`. Legacy rows are unavailable until a full reindex gives them the
|
|
636
|
+
current identity. Migrate the vector dimension before reindexing. A failed owner
|
|
637
|
+
replacement preserves the previous complete generation.
|
|
382
638
|
|
|
383
|
-
|
|
384
|
-
|---|---|
|
|
385
|
-
| Ruby | 3.2, 3.3 |
|
|
386
|
-
| Rails | 7.1, 8.0 |
|
|
387
|
-
| Database | PostgreSQL with pgvector |
|
|
639
|
+
## Vector-store contract
|
|
388
640
|
|
|
389
|
-
|
|
641
|
+
PostgreSQL/pgvector is the production default; `Maglev::VectorStores::Memory` is
|
|
642
|
+
for tests and local experiments. A custom store implements `fetch(ids:)`,
|
|
643
|
+
`upsert(documents:)`, `search(vector:, filters:, limit:)`, `delete(ids:)`,
|
|
644
|
+
`delete_by_owner(owner_type:, owner_id:)`, atomic
|
|
645
|
+
`replace_owner(owner_type:, owner_id:, documents:)`, `healthcheck`, and
|
|
646
|
+
`capabilities`.
|
|
390
647
|
|
|
391
|
-
|
|
648
|
+
`delete_by_owner` followed by `upsert` is not an atomic replacement. Concurrent
|
|
649
|
+
replacement and deletion for one owner must be linearizable, and failed
|
|
650
|
+
replacement must preserve the previous complete generation.
|
|
392
651
|
|
|
393
|
-
|
|
394
|
-
bundle install
|
|
652
|
+
## Traces, evidence, and safety boundaries
|
|
395
653
|
|
|
396
|
-
|
|
397
|
-
|
|
654
|
+
Maglev results carry bounded evidence and a trace ID. Traces record identifiers,
|
|
655
|
+
decisions, operation names, limits, safe timings, warnings, and error classes.
|
|
656
|
+
They exclude record values, source text, prompts, secrets, and raw provider
|
|
657
|
+
payloads by default. The host application owns audit persistence and retention.
|
|
398
658
|
|
|
659
|
+
Maglev 0.2 does **not** provide:
|
|
660
|
+
|
|
661
|
+
- natural-language writes or mutations;
|
|
662
|
+
- unrestricted SQL, Ruby, Arel, scopes, or code execution;
|
|
663
|
+
- autonomous/iterative agents;
|
|
664
|
+
- a Rails Engine, REST API, admin UI, or mandatory frontend;
|
|
665
|
+
- built-in PDF/Office/OCR/audio parsing;
|
|
666
|
+
- streaming or conversation memory;
|
|
667
|
+
- Qdrant or another required external vector service.
|
|
668
|
+
|
|
669
|
+
Retrieved documents are evidence, never instructions that can change route,
|
|
670
|
+
permissions, Query IR, or execution policy.
|
|
671
|
+
|
|
672
|
+
## Runtime support and development
|
|
673
|
+
|
|
674
|
+
| Component | Supported |
|
|
675
|
+
| --- | --- |
|
|
676
|
+
| Ruby | 3.3, 4.0 |
|
|
677
|
+
| Rails | 7.1, 8.0 |
|
|
678
|
+
| Database | PostgreSQL with pgvector |
|
|
679
|
+
|
|
680
|
+
```bash
|
|
681
|
+
bundle exec rspec
|
|
399
682
|
bundle exec standardrb
|
|
400
683
|
bundle exec rubocop
|
|
401
684
|
bundle exec rake build
|
|
685
|
+
bundle exec rake maglev:release_audit
|
|
402
686
|
```
|
|
403
687
|
|
|
404
|
-
The default
|
|
688
|
+
The default suite uses deterministic fakes and never calls a live LLM or
|
|
689
|
+
embedding provider.
|
|
405
690
|
|
|
406
691
|
## License
|
|
407
692
|
|
|
408
|
-
Maglev is available
|
|
693
|
+
Maglev is available under the [MIT License](LICENSE.txt).
|