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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -0
  3. data/README.ja.md +618 -0
  4. data/README.md +533 -248
  5. data/README.zh-CN.md +457 -250
  6. data/lib/generators/maglev/install/install_generator.rb +20 -0
  7. data/lib/generators/maglev/upgrade_index_version/upgrade_index_version_generator.rb +27 -0
  8. data/lib/generators/maglev/upgrade_source_identity/upgrade_source_identity_generator.rb +52 -0
  9. data/lib/maglev/active_record_extension.rb +141 -24
  10. data/lib/maglev/adapters/faraday_client.rb +94 -0
  11. data/lib/maglev/adapters/faraday_embedding.rb +51 -0
  12. data/lib/maglev/adapters/faraday_generation.rb +49 -0
  13. data/lib/maglev/adapters/faraday_planner.rb +88 -0
  14. data/lib/maglev/answerer.rb +30 -12
  15. data/lib/maglev/chunker.rb +39 -4
  16. data/lib/maglev/configuration.rb +66 -1
  17. data/lib/maglev/content_source_graph.rb +17 -11
  18. data/lib/maglev/dependency_graph.rb +72 -13
  19. data/lib/maglev/embedding_adapter.rb +10 -0
  20. data/lib/maglev/hybrid_candidate_set.rb +25 -0
  21. data/lib/maglev/hybrid_coordinator.rb +112 -0
  22. data/lib/maglev/hybrid_result.rb +25 -0
  23. data/lib/maglev/index_diagnostics.rb +83 -0
  24. data/lib/maglev/index_identity.rb +70 -0
  25. data/lib/maglev/index_state.rb +9 -0
  26. data/lib/maglev/indexer.rb +185 -35
  27. data/lib/maglev/knowledge_config.rb +27 -5
  28. data/lib/maglev/knowledge_registry.rb +33 -0
  29. data/lib/maglev/planner.rb +172 -0
  30. data/lib/maglev/planner_adapter.rb +25 -0
  31. data/lib/maglev/planner_evaluation.rb +49 -0
  32. data/lib/maglev/query_compiler.rb +197 -0
  33. data/lib/maglev/query_ir.rb +143 -0
  34. data/lib/maglev/query_validator.rb +311 -0
  35. data/lib/maglev/railtie.rb +9 -0
  36. data/lib/maglev/registry.rb +72 -0
  37. data/lib/maglev/reindex_job.rb +34 -2
  38. data/lib/maglev/relation_order.rb +16 -0
  39. data/lib/maglev/request.rb +22 -0
  40. data/lib/maglev/request_executor.rb +101 -0
  41. data/lib/maglev/resource_config.rb +222 -0
  42. data/lib/maglev/response.rb +2 -2
  43. data/lib/maglev/result.rb +30 -0
  44. data/lib/maglev/retrieval_outcome.rb +52 -0
  45. data/lib/maglev/retrieval_result.rb +25 -0
  46. data/lib/maglev/retriever.rb +282 -27
  47. data/lib/maglev/router.rb +77 -0
  48. data/lib/maglev/routing_adapter.rb +25 -0
  49. data/lib/maglev/schema_compiler.rb +17 -4
  50. data/lib/maglev/schema_snapshot.rb +159 -0
  51. data/lib/maglev/search_result.rb +7 -3
  52. data/lib/maglev/snapshot.rb +21 -1
  53. data/lib/maglev/snapshot_budget.rb +57 -0
  54. data/lib/maglev/snapshot_builder.rb +89 -11
  55. data/lib/maglev/source_extractor.rb +43 -0
  56. data/lib/maglev/source_fragment.rb +9 -0
  57. data/lib/maglev/structured_answer_composer.rb +67 -0
  58. data/lib/maglev/structured_evidence_builder.rb +56 -0
  59. data/lib/maglev/structured_executor.rb +157 -0
  60. data/lib/maglev/structured_result.rb +97 -0
  61. data/lib/maglev/trace.rb +56 -0
  62. data/lib/maglev/vector_stores/base.rb +12 -0
  63. data/lib/maglev/vector_stores/document.rb +14 -5
  64. data/lib/maglev/vector_stores/document_id.rb +27 -0
  65. data/lib/maglev/vector_stores/memory.rb +68 -6
  66. data/lib/maglev/vector_stores/metadata_filter.rb +55 -0
  67. data/lib/maglev/vector_stores/pgvector.rb +94 -7
  68. data/lib/maglev/version.rb +1 -1
  69. data/lib/maglev-rb.rb +3 -0
  70. data/lib/maglev.rb +36 -3
  71. data/lib/tasks/maglev.rake +43 -0
  72. metadata +71 -11
  73. data/lib/maglev/adapters/ruby_llm_attachment_extractor.rb +0 -15
  74. data/lib/maglev/adapters/ruby_llm_embedding.rb +0 -22
  75. data/lib/maglev/adapters/ruby_llm_generation.rb +0 -22
  76. data/lib/maglev/adapters/ruby_llm_provider.rb +0 -64
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+ require_relative "knowledge_config"
5
+
6
+ module Maglev
7
+ class ResourceConfig
8
+ SUPPORTED_PARAMETER_TYPES = %i[
9
+ string integer float decimal boolean date datetime timestamp time
10
+ ].freeze
11
+
12
+ Field = Struct.new(:name, :description, :synonyms, :enum_values, :sensitive) do
13
+ def initialize(**attributes)
14
+ attributes[:name] = attributes.fetch(:name).to_s
15
+ attributes[:description] = attributes[:description]&.to_s
16
+ attributes[:synonyms] = Array(attributes[:synonyms]).map(&:to_s).uniq.freeze
17
+ attributes[:enum_values] = Array(attributes[:enum_values]).map(&:to_s).uniq.freeze
18
+ attributes[:sensitive] = !!attributes[:sensitive]
19
+ super
20
+ freeze
21
+ end
22
+ end
23
+
24
+ Association = Struct.new(:name, :resource, :description, :synonyms) do
25
+ def initialize(**attributes)
26
+ attributes[:name] = attributes.fetch(:name).to_s
27
+ attributes[:resource] = attributes.fetch(:resource).to_s
28
+ attributes[:description] = attributes[:description]&.to_s
29
+ attributes[:synonyms] = Array(attributes[:synonyms]).map(&:to_s).uniq.freeze
30
+ super
31
+ freeze
32
+ end
33
+ end
34
+
35
+ Parameter = Struct.new(:type, :required, :nullable, :enum_values, :minimum, :maximum) do
36
+ def initialize(**attributes)
37
+ attributes[:type] = attributes.fetch(:type).to_sym
38
+ unless SUPPORTED_PARAMETER_TYPES.include?(attributes[:type])
39
+ raise ConfigurationError, "Unsupported scope parameter type #{attributes[:type]}"
40
+ end
41
+ attributes[:required] = !!attributes[:required]
42
+ attributes[:nullable] = !!attributes[:nullable]
43
+ attributes[:enum_values] = Array(attributes[:enum_values]).map(&:to_s).uniq.freeze
44
+ super
45
+ freeze
46
+ end
47
+ end
48
+
49
+ Scope = Struct.new(:name, :parameters, :description) do
50
+ def initialize(**attributes)
51
+ attributes[:name] = attributes.fetch(:name).to_s
52
+ attributes[:parameters] = attributes.fetch(:parameters).to_h { |name, parameter| [name.to_s, parameter] }.freeze
53
+ attributes[:description] = attributes[:description]&.to_s
54
+ super
55
+ freeze
56
+ end
57
+ end
58
+
59
+ Queryable = Struct.new(:fields, :prohibited_fields, :associations, :scopes, :aggregates, :limits, :authorization, :allow_unscoped_model_queries) do
60
+ def initialize(**attributes)
61
+ attributes[:fields] = attributes.fetch(:fields).freeze
62
+ attributes[:prohibited_fields] = attributes.fetch(:prohibited_fields).freeze
63
+ attributes[:associations] = attributes.fetch(:associations).freeze
64
+ attributes[:scopes] = attributes.fetch(:scopes).freeze
65
+ attributes[:aggregates] = attributes.fetch(:aggregates).transform_values { |values| (values == true) ? true : Array(values).freeze }.freeze
66
+ attributes[:limits] = attributes.fetch(:limits).freeze
67
+ attributes[:authorization] = attributes.fetch(:authorization)
68
+ attributes[:allow_unscoped_model_queries] = !!attributes[:allow_unscoped_model_queries]
69
+ super
70
+ freeze
71
+ end
72
+ end
73
+
74
+ Entry = Struct.new(:identifier, :model_class, :description, :synonyms, :queryable, :knowledge) do
75
+ def initialize(**attributes)
76
+ attributes[:identifier] = attributes.fetch(:identifier).to_s
77
+ attributes[:description] = attributes[:description]&.to_s
78
+ attributes[:synonyms] = Array(attributes[:synonyms]).map(&:to_s).uniq.freeze
79
+ super
80
+ freeze
81
+ end
82
+ end
83
+
84
+ class Builder
85
+ def initialize(model_class, identifier)
86
+ @model_class = model_class
87
+ @identifier = identifier
88
+ @synonyms = []
89
+ end
90
+
91
+ def build(&block)
92
+ instance_eval(&block) if block
93
+ Entry.new(identifier: @identifier, model_class: @model_class, description: @description,
94
+ synonyms: @synonyms, queryable: @queryable, knowledge: @knowledge)
95
+ end
96
+
97
+ def description(value)
98
+ @description = value.to_s
99
+ end
100
+
101
+ def synonyms(*values)
102
+ @synonyms.concat(values)
103
+ end
104
+
105
+ def queryable(&block)
106
+ raise ConfigurationError, "queryable may only be declared once" if @queryable
107
+
108
+ @queryable = QueryableBuilder.new(@model_class).build(&block)
109
+ end
110
+
111
+ def knowledge(&block)
112
+ raise ConfigurationError, "knowledge may only be declared once" if @knowledge
113
+
114
+ @knowledge = KnowledgeConfig.build(@model_class, &block)
115
+ validate_knowledge_sources!
116
+ end
117
+
118
+ private
119
+
120
+ def validate_knowledge_sources!
121
+ @knowledge.attached_sources.each do |source|
122
+ reflection = @model_class.reflect_on_association("#{source.name}_attachment") ||
123
+ @model_class.reflect_on_association("#{source.name}_attachments")
124
+ raise ConfigurationError, "Unknown attached knowledge source #{@model_class.name}.#{source.name}" unless reflection
125
+ end
126
+ end
127
+ end
128
+
129
+ class QueryableBuilder
130
+ AUTHORIZATION_POLICIES = %i[required public].freeze
131
+ LIMIT_KEYS = %i[rows operations joins].freeze
132
+ AGGREGATES = %i[count sum average minimum maximum].freeze
133
+
134
+ def initialize(model_class)
135
+ @model_class = model_class
136
+ @fields = []
137
+ @prohibited_fields = []
138
+ @associations = []
139
+ @scopes = []
140
+ @aggregates = {}
141
+ @limits = {}
142
+ @authorization = :required
143
+ @allow_unscoped_model_queries = false
144
+ end
145
+
146
+ def build(&block)
147
+ instance_eval(&block) if block
148
+ conflicts = @fields.map(&:name) & @prohibited_fields
149
+ raise ConfigurationError, "Queryable fields cannot be prohibited: #{conflicts.join(", ")}" if conflicts.any?
150
+
151
+ Queryable.new(fields: @fields.uniq(&:name), prohibited_fields: @prohibited_fields.uniq.freeze,
152
+ associations: @associations.uniq(&:name), scopes: @scopes.uniq(&:name),
153
+ aggregates: @aggregates, limits: @limits, authorization: @authorization,
154
+ allow_unscoped_model_queries: @allow_unscoped_model_queries)
155
+ end
156
+
157
+ def field(name, description: nil, synonyms: [], enum: [], sensitive: false)
158
+ normalized = name.to_s
159
+ raise ConfigurationError, "Unknown queryable field #{@model_class.name}.#{normalized}" unless @model_class.attribute_names.include?(normalized)
160
+
161
+ @fields << Field.new(name: normalized, description: description, synonyms: synonyms, enum_values: enum, sensitive: sensitive)
162
+ end
163
+
164
+ def association(name, resource:, description: nil, synonyms: [])
165
+ normalized = name.to_s
166
+ raise ConfigurationError, "Unknown queryable association #{@model_class.name}.#{normalized}" unless @model_class.reflect_on_association(normalized.to_sym)
167
+
168
+ @associations << Association.new(name: normalized, resource: resource, description: description, synonyms: synonyms)
169
+ end
170
+
171
+ def prohibit(*names)
172
+ normalized = names.map(&:to_s)
173
+ unknown = normalized - @model_class.attribute_names
174
+ raise ConfigurationError, "Unknown prohibited field #{@model_class.name}.#{unknown.first}" if unknown.any?
175
+
176
+ @prohibited_fields.concat(normalized)
177
+ end
178
+
179
+ def scope(name, parameters: {}, description: nil)
180
+ normalized = name.to_s
181
+ raise ConfigurationError, "Unknown queryable scope #{@model_class.name}.#{normalized}" unless @model_class.respond_to?(normalized)
182
+
183
+ normalized_parameters = parameters.to_h do |parameter_name, schema|
184
+ schema = schema.transform_keys(&:to_sym)
185
+ [parameter_name, Parameter.new(**schema)]
186
+ end
187
+ @scopes << Scope.new(name: normalized, parameters: normalized_parameters, description: description)
188
+ end
189
+
190
+ def aggregates(**permissions)
191
+ unknown = permissions.keys - AGGREGATES
192
+ raise ConfigurationError, "Unknown aggregates: #{unknown.join(", ")}" if unknown.any?
193
+
194
+ permissions.each do |aggregate, fields|
195
+ normalized = (fields == true) ? true : Array(fields).map(&:to_s).uniq
196
+ unknown_fields = (normalized == true) ? [] : Array(normalized) - @model_class.attribute_names
197
+ raise ConfigurationError, "Unknown aggregate field #{@model_class.name}.#{unknown_fields.first}" if unknown_fields.any?
198
+ @aggregates[aggregate] = normalized
199
+ end
200
+ end
201
+
202
+ def limits(**values)
203
+ unknown = values.keys - LIMIT_KEYS
204
+ raise ConfigurationError, "Unknown query limits: #{unknown.join(", ")}" if unknown.any?
205
+ raise ConfigurationError, "Query limits must be positive integers" unless values.values.all? { |value| value.is_a?(Integer) && value.positive? }
206
+
207
+ @limits.merge!(values)
208
+ end
209
+
210
+ def authorization(policy)
211
+ policy = policy.to_sym
212
+ raise ConfigurationError, "Unknown authorization policy #{policy}" unless AUTHORIZATION_POLICIES.include?(policy)
213
+
214
+ @authorization = policy
215
+ end
216
+
217
+ def allow_unscoped_model_queries(value = true)
218
+ @allow_unscoped_model_queries = value
219
+ end
220
+ end
221
+ end
222
+ end
@@ -4,11 +4,11 @@ module Maglev
4
4
  class Response
5
5
  attr_reader :text, :sources, :metadata
6
6
 
7
- def self.insufficient_context(question:)
7
+ def self.insufficient_context(question:, retrieval_metadata: {})
8
8
  new(
9
9
  text: "Insufficient context to answer the question.",
10
10
  sources: [],
11
- metadata: {question: question, reason: "insufficient_context"}
11
+ metadata: {question: question, reason: "insufficient_context"}.merge(retrieval_metadata)
12
12
  )
13
13
  end
14
14
 
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maglev
4
+ class Result
5
+ STATUSES = %i[succeeded clarification_required unsupported failed].freeze
6
+ KINDS = %i[relation scalar aggregate semantic_matches rag_answer hybrid_answer none].freeze
7
+
8
+ attr_reader :status, :route, :kind, :value, :evidence, :warnings, :trace_id,
9
+ :confidence, :reasons, :metadata
10
+
11
+ def initialize(status:, route:, kind:, trace_id:, value: nil, evidence: nil, warnings: [],
12
+ confidence: nil, reasons: [], metadata: {})
13
+ raise ArgumentError, "invalid result status" unless STATUSES.include?(status)
14
+ raise ArgumentError, "invalid result kind" unless KINDS.include?(kind)
15
+ raise ArgumentError, "only successful results may carry a value" if status != :succeeded && !value.nil?
16
+
17
+ @status = status
18
+ @route = route.to_sym
19
+ @kind = kind
20
+ @value = value
21
+ @evidence = evidence
22
+ @warnings = Array(warnings).map { |warning| warning.to_s.freeze }.freeze
23
+ @trace_id = trace_id.to_s.freeze
24
+ @confidence = confidence
25
+ @reasons = Array(reasons).map { |reason| reason.to_s.freeze }.freeze
26
+ @metadata = metadata.freeze
27
+ freeze
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maglev
4
+ class RetrievalOutcome
5
+ attr_reader :results, :minimum_similarity, :examined_count,
6
+ :accepted_count, :rejected_count, :best_similarity, :considered, :rejected_results,
7
+ :authorization_rejected_count
8
+
9
+ def initialize(results:, minimum_similarity:, examined_count:,
10
+ accepted_count:, rejected_count:, best_similarity:, considered: nil, rejected_results: nil,
11
+ authorization_rejected_count: 0)
12
+ @results = results.freeze
13
+ @minimum_similarity = minimum_similarity
14
+ @examined_count = examined_count
15
+ @accepted_count = accepted_count
16
+ @rejected_count = rejected_count
17
+ @best_similarity = best_similarity
18
+ @considered = (considered || results).freeze
19
+ @rejected_results = (rejected_results || []).freeze
20
+ @authorization_rejected_count = authorization_rejected_count
21
+ freeze
22
+ end
23
+
24
+ def empty_reason
25
+ return nil if @results.any?
26
+
27
+ if @examined_count.positive?
28
+ :threshold_rejected
29
+ else
30
+ :no_candidates
31
+ end
32
+ end
33
+
34
+ def threshold_rejected?
35
+ empty_reason == :threshold_rejected
36
+ end
37
+
38
+ def no_candidates?
39
+ empty_reason == :no_candidates
40
+ end
41
+
42
+ def metadata
43
+ {
44
+ minimum_similarity: minimum_similarity,
45
+ examined_count: examined_count,
46
+ accepted_count: accepted_count,
47
+ rejected_count: rejected_count,
48
+ best_similarity: best_similarity
49
+ }.freeze
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maglev
4
+ class RetrievalResult
5
+ attr_reader :query, :considered, :selected, :rejected, :context, :budgets, :reasons, :timings, :trace_id
6
+
7
+ def initialize(query:, considered:, selected:, rejected:, context:, budgets:, reasons:, timings:, trace_id:)
8
+ @query = query.to_s.freeze
9
+ @considered = considered.freeze
10
+ @selected = selected.freeze
11
+ @rejected = rejected.map { |item| item.freeze }.freeze
12
+ @context = context.to_s.freeze
13
+ @budgets = budgets.freeze
14
+ @reasons = reasons.freeze
15
+ @timings = timings.freeze
16
+ @trace_id = trace_id.to_s.freeze
17
+ freeze
18
+ end
19
+
20
+ def metadata
21
+ {trace_id: trace_id, considered_count: considered.size, selected_count: selected.size,
22
+ rejected_count: rejected.size, budgets: budgets, reasons: reasons, timings: timings}.freeze
23
+ end
24
+ end
25
+ end