parse-stack-next 5.6.0 → 5.7.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 +489 -0
- data/README.md +378 -6
- data/docs/caching.md +748 -0
- data/examples/basic_client.rb +3 -3
- data/examples/basic_server.rb +3 -3
- data/examples/live_query_listener.rb +12 -12
- data/examples/rag_chatbot.rb +14 -14
- data/examples/transaction_example.rb +44 -45
- data/examples/webhook_server.rb +3 -3
- data/lib/parse/access.rb +493 -0
- data/lib/parse/acl_scope.rb +125 -52
- data/lib/parse/agent/approval_gate.rb +0 -0
- data/lib/parse/agent/cancellation_token.rb +3 -3
- data/lib/parse/agent/constraint_translator.rb +4 -4
- data/lib/parse/agent/describe.rb +34 -34
- data/lib/parse/agent/errors.rb +9 -9
- data/lib/parse/agent/mcp_client.rb +61 -59
- data/lib/parse/agent/mcp_dispatcher.rb +89 -101
- data/lib/parse/agent/mcp_rack_app.rb +92 -93
- data/lib/parse/agent/mcp_server.rb +1 -1
- data/lib/parse/agent/mcp_subscriptions.rb +22 -22
- data/lib/parse/agent/metadata_audit.rb +1 -2
- data/lib/parse/agent/metadata_dsl.rb +7 -7
- data/lib/parse/agent/metadata_registry.rb +12 -12
- data/lib/parse/agent/prompt_hardening.rb +4 -4
- data/lib/parse/agent/prompts.rb +10 -10
- data/lib/parse/agent/result_formatter.rb +4 -4
- data/lib/parse/agent/tools.rb +493 -400
- data/lib/parse/agent.rb +157 -116
- data/lib/parse/api/hooks.rb +1 -1
- data/lib/parse/api/server.rb +2 -2
- data/lib/parse/api/users.rb +2 -2
- data/lib/parse/atlas_search/index_manager.rb +1 -1
- data/lib/parse/atlas_search/session.rb +40 -218
- data/lib/parse/atlas_search.rb +181 -60
- data/lib/parse/authorization.rb +466 -0
- data/lib/parse/cache/invalidation.rb +219 -0
- data/lib/parse/cache/keyspace.rb +306 -0
- data/lib/parse/cache/moneta_surface.rb +126 -0
- data/lib/parse/cache/pool.rb +48 -5
- data/lib/parse/cache/redis.rb +425 -10
- data/lib/parse/cache/scoped_view.rb +443 -0
- data/lib/parse/cache/sub_cache.rb +264 -0
- data/lib/parse/cache/upstream_roles.rb +230 -0
- data/lib/parse/client/authentication.rb +1 -1
- data/lib/parse/client/body_builder.rb +1 -2
- data/lib/parse/client/caching.rb +84 -5
- data/lib/parse/client.rb +327 -189
- data/lib/parse/clp_scope.rb +225 -28
- data/lib/parse/console.rb +3 -3
- data/lib/parse/embeddings/batch_embedder.rb +3 -3
- data/lib/parse/embeddings/cache.rb +17 -17
- data/lib/parse/embeddings/cohere.rb +21 -22
- data/lib/parse/embeddings/image_fetch.rb +22 -21
- data/lib/parse/embeddings/jina.rb +21 -21
- data/lib/parse/embeddings/local_http.rb +6 -7
- data/lib/parse/embeddings/media_file.rb +5 -4
- data/lib/parse/embeddings/openai.rb +5 -5
- data/lib/parse/embeddings/provider.rb +1 -1
- data/lib/parse/embeddings/qwen.rb +10 -10
- data/lib/parse/embeddings/spend_cap.rb +2 -3
- data/lib/parse/embeddings/streaming_body.rb +11 -11
- data/lib/parse/embeddings/video_source.rb +7 -6
- data/lib/parse/embeddings/voyage.rb +59 -61
- data/lib/parse/embeddings.rb +28 -27
- data/lib/parse/graphql/type_generator.rb +1 -1
- data/lib/parse/graphql.rb +5 -5
- data/lib/parse/live_query/client.rb +1 -1
- data/lib/parse/live_query.rb +1 -1
- data/lib/parse/lock.rb +15 -18
- data/lib/parse/lock_backend.rb +1 -1
- data/lib/parse/lookup_rewriter.rb +6 -6
- data/lib/parse/model/associations/belongs_to.rb +4 -0
- data/lib/parse/model/associations/collection_proxy.rb +3 -0
- data/lib/parse/model/associations/has_many.rb +4 -0
- data/lib/parse/model/classes/role.rb +227 -28
- data/lib/parse/model/classes/user.rb +60 -1
- data/lib/parse/model/clp.rb +4 -4
- data/lib/parse/model/core/actions.rb +366 -113
- data/lib/parse/model/core/create_lock.rb +0 -2
- data/lib/parse/model/core/describe.rb +57 -57
- data/lib/parse/model/core/embed_managed.rb +21 -23
- data/lib/parse/model/core/fetching.rb +6 -0
- data/lib/parse/model/core/indexing.rb +14 -14
- data/lib/parse/model/core/parse_reference.rb +1 -1
- data/lib/parse/model/core/properties.rb +6 -2
- data/lib/parse/model/core/querying.rb +1 -1
- data/lib/parse/model/core/schema.rb +2 -2
- data/lib/parse/model/core/search_indexing.rb +2 -2
- data/lib/parse/model/core/vector_searchable.rb +4 -5
- data/lib/parse/model/file.rb +21 -15
- data/lib/parse/model/geojson.rb +2 -2
- data/lib/parse/model/geopoint.rb +1 -0
- data/lib/parse/model/object.rb +136 -66
- data/lib/parse/model/pointer.rb +2 -0
- data/lib/parse/model/polygon.rb +3 -6
- data/lib/parse/model/push.rb +2 -2
- data/lib/parse/model/vector.rb +3 -1
- data/lib/parse/mongodb.rb +512 -282
- data/lib/parse/pipeline_security.rb +3 -0
- data/lib/parse/query/constraints.rb +46 -46
- data/lib/parse/query.rb +146 -59
- data/lib/parse/retrieval/agent_tool.rb +34 -20
- data/lib/parse/retrieval/chunk.rb +1 -0
- data/lib/parse/retrieval/reranker/cohere.rb +11 -11
- data/lib/parse/retrieval/reranker.rb +3 -4
- data/lib/parse/retrieval/retriever.rb +4 -6
- data/lib/parse/schema/index_migrator.rb +26 -26
- data/lib/parse/schema/search_index_migrator.rb +19 -19
- data/lib/parse/stack/tasks.rb +6 -6
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +10 -10
- data/lib/parse/vector_search/hybrid.rb +66 -41
- data/lib/parse/vector_search.rb +14 -11
- data/lib/parse/webhooks/payload.rb +1 -0
- data/lib/parse/webhooks/registration.rb +4 -4
- data/lib/parse/webhooks/trigger_audit.rb +50 -49
- data/lib/parse/webhooks.rb +120 -5
- data/parse-stack-next.gemspec +6 -6
- metadata +10 -1
data/lib/parse/query.rb
CHANGED
|
@@ -795,10 +795,9 @@ module Parse
|
|
|
795
795
|
# @param amount [Integer] The number of records to skip.
|
|
796
796
|
# @return [self]
|
|
797
797
|
def skip(amount)
|
|
798
|
-
coerced =
|
|
799
|
-
|
|
800
|
-
when
|
|
801
|
-
when Numeric then amount.to_i
|
|
798
|
+
coerced = case amount
|
|
799
|
+
when nil then 0
|
|
800
|
+
when Numeric then amount.to_i
|
|
802
801
|
when String
|
|
803
802
|
unless amount =~ /\A-?\d+\z/
|
|
804
803
|
raise ArgumentError,
|
|
@@ -889,6 +888,7 @@ module Parse
|
|
|
889
888
|
# reader and returns the current hint value.
|
|
890
889
|
# @return [String, nil, self]
|
|
891
890
|
HINT_UNSET = :_hint_unset_ # @!visibility private
|
|
891
|
+
|
|
892
892
|
def hint(index_name = HINT_UNSET)
|
|
893
893
|
return @hint if index_name.equal?(HINT_UNSET)
|
|
894
894
|
@hint = index_name
|
|
@@ -1004,8 +1004,7 @@ module Parse
|
|
|
1004
1004
|
# `:bodyEmbedding`) depending on whether Query.format_field has
|
|
1005
1005
|
# already run. Resolve both shapes against the local set.
|
|
1006
1006
|
operand_sym = constraint.operand.to_sym
|
|
1007
|
-
local_field =
|
|
1008
|
-
if vec_fields.key?(operand_sym)
|
|
1007
|
+
local_field = if vec_fields.key?(operand_sym)
|
|
1009
1008
|
operand_sym
|
|
1010
1009
|
elsif klass.respond_to?(:field_map)
|
|
1011
1010
|
klass.field_map.find { |_local, remote| remote.to_sym == operand_sym }&.first
|
|
@@ -1136,7 +1135,7 @@ module Parse
|
|
|
1136
1135
|
# Explicit opt-in to direct MongoDB
|
|
1137
1136
|
if mongo_direct
|
|
1138
1137
|
return distinct_direct(field, return_pointers: return_pointers, order: order,
|
|
1139
|
-
|
|
1138
|
+
**mongo_direct_auth_kwargs)
|
|
1140
1139
|
end
|
|
1141
1140
|
|
|
1142
1141
|
# Auto-route to mongo-direct when the compiled where contains a
|
|
@@ -1144,7 +1143,7 @@ module Parse
|
|
|
1144
1143
|
if requires_mongo_direct?
|
|
1145
1144
|
assert_mongo_direct_routable!
|
|
1146
1145
|
return distinct_direct(field, return_pointers: return_pointers, order: order,
|
|
1147
|
-
|
|
1146
|
+
**mongo_direct_auth_kwargs)
|
|
1148
1147
|
end
|
|
1149
1148
|
|
|
1150
1149
|
# Auto-route scoped queries (session_token / acl_user / acl_role) to
|
|
@@ -1155,7 +1154,7 @@ module Parse
|
|
|
1155
1154
|
# `$group`, so distinct values reflect only ACL-readable rows.
|
|
1156
1155
|
if distinct_query_is_scoped? && defined?(Parse::MongoDB) && Parse::MongoDB.enabled?
|
|
1157
1156
|
return distinct_direct(field, return_pointers: return_pointers, order: order,
|
|
1158
|
-
|
|
1157
|
+
**mongo_direct_auth_kwargs)
|
|
1159
1158
|
end
|
|
1160
1159
|
|
|
1161
1160
|
if field.nil? || !field.respond_to?(:to_s) || field.is_a?(Hash) || field.is_a?(Array)
|
|
@@ -1295,8 +1294,8 @@ module Parse
|
|
|
1295
1294
|
|
|
1296
1295
|
# Execute aggregation
|
|
1297
1296
|
aggregation = Aggregation.new(self, pipeline, verbose: @verbose_aggregate,
|
|
1298
|
-
|
|
1299
|
-
|
|
1297
|
+
mongo_direct: use_mongo_direct,
|
|
1298
|
+
allow_internal_fields: uses_internal_fields)
|
|
1300
1299
|
response = aggregation.execute!
|
|
1301
1300
|
|
|
1302
1301
|
# Extract count from aggregation result
|
|
@@ -1420,8 +1419,7 @@ module Parse
|
|
|
1420
1419
|
@results = nil if @limit != fetch_count
|
|
1421
1420
|
@limit = fetch_count
|
|
1422
1421
|
else
|
|
1423
|
-
fetch_count =
|
|
1424
|
-
case limit_or_constraints
|
|
1422
|
+
fetch_count = case limit_or_constraints
|
|
1425
1423
|
when Numeric then limit_or_constraints.to_i
|
|
1426
1424
|
when String
|
|
1427
1425
|
unless limit_or_constraints =~ /\A-?\d+\z/
|
|
@@ -1910,8 +1908,7 @@ module Parse
|
|
|
1910
1908
|
# @param user [Parse::User, Parse::Pointer] the principal to scope by.
|
|
1911
1909
|
# @return [self]
|
|
1912
1910
|
def scope_to_user(user)
|
|
1913
|
-
raise ArgumentError, "[Parse::Query] scope_to_user requires a Parse::User or User Pointer."
|
|
1914
|
-
unless user.respond_to?(:id) && user.id.is_a?(String)
|
|
1911
|
+
raise ArgumentError, "[Parse::Query] scope_to_user requires a Parse::User or User Pointer." unless user.respond_to?(:id) && user.id.is_a?(String)
|
|
1915
1912
|
@acl_user = user
|
|
1916
1913
|
self
|
|
1917
1914
|
end
|
|
@@ -2021,11 +2018,11 @@ module Parse
|
|
|
2021
2018
|
# path would have sent; the SDK should not force callers to repeat
|
|
2022
2019
|
# `use_master_key: true` on every direct query.
|
|
2023
2020
|
client_has_master_key = begin
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2021
|
+
c = client
|
|
2022
|
+
c.respond_to?(:master_key) && !c.master_key.to_s.empty?
|
|
2023
|
+
rescue StandardError
|
|
2024
|
+
false
|
|
2025
|
+
end
|
|
2029
2026
|
server_mode_master = (use_master_key != false) && !Parse.client_mode && client_has_master_key
|
|
2030
2027
|
unless use_master_key || server_mode_master || @acl_user || @acl_role || has_session || has_ambient_session
|
|
2031
2028
|
raise MongoDirectRequired,
|
|
@@ -2070,6 +2067,30 @@ module Parse
|
|
|
2070
2067
|
# * Otherwise (master-key path) → forward `master: true`.
|
|
2071
2068
|
# @!visibility private
|
|
2072
2069
|
def mongo_direct_auth_kwargs
|
|
2070
|
+
# Every branch below is merged with the query's own client, so a query
|
|
2071
|
+
# built from a non-default client authorizes against THAT client rather
|
|
2072
|
+
# than silently falling back to Parse.client at the ACLScope boundary.
|
|
2073
|
+
# Note that a client is bound with `query.client = other`, NOT by
|
|
2074
|
+
# passing `client:` to `Post.query(...)`, which builds a constraint on
|
|
2075
|
+
# a field literally named `client` and silently matches nothing.
|
|
2076
|
+
mongo_direct_client_kwarg.merge(mongo_direct_scope_kwargs)
|
|
2077
|
+
end
|
|
2078
|
+
|
|
2079
|
+
# @!visibility private
|
|
2080
|
+
# @return [Hash] `{ client: ... }`, or empty when this query uses the
|
|
2081
|
+
# default client. Omitted rather than passed as the default client so
|
|
2082
|
+
# the downstream fallback stays observable in one place.
|
|
2083
|
+
def mongo_direct_client_kwarg
|
|
2084
|
+
resolved = client
|
|
2085
|
+
return {} if resolved.nil?
|
|
2086
|
+
return {} if Parse::Client.client? && resolved.equal?(Parse::Client.client)
|
|
2087
|
+
{ client: resolved }
|
|
2088
|
+
rescue StandardError
|
|
2089
|
+
{}
|
|
2090
|
+
end
|
|
2091
|
+
|
|
2092
|
+
# @!visibility private
|
|
2093
|
+
def mongo_direct_scope_kwargs
|
|
2073
2094
|
if @acl_user
|
|
2074
2095
|
# Pre-resolved User pointer. Hand it to Parse::ACLScope as
|
|
2075
2096
|
# acl_user: so the same three-layer simulation runs (top-level
|
|
@@ -2113,9 +2134,29 @@ module Parse
|
|
|
2113
2134
|
# @return [Hash] zero-or-one-of { session_token:/master:/acl_user:/acl_role: }
|
|
2114
2135
|
# @!visibility private
|
|
2115
2136
|
def atlas_search_auth_kwargs(options)
|
|
2137
|
+
# The client travels with the identity on this path too. Atlas Search
|
|
2138
|
+
# runs its own pipelines against the driver rather than going through
|
|
2139
|
+
# Parse::MongoDB.aggregate, so a `q.client = other; q.atlas_search(...)`
|
|
2140
|
+
# would otherwise resolve and read as the default application while the
|
|
2141
|
+
# query it came from was bound to another.
|
|
2142
|
+
#
|
|
2143
|
+
# An explicitly passed client wins, matching how the identity kwargs
|
|
2144
|
+
# below behave, and is dropped when it is already the default so the
|
|
2145
|
+
# common single-application call carries nothing extra.
|
|
2146
|
+
client_kwarg = if options.key?(:client)
|
|
2147
|
+
{ client: options[:client] }
|
|
2148
|
+
else
|
|
2149
|
+
mongo_direct_client_kwarg
|
|
2150
|
+
end
|
|
2151
|
+
|
|
2116
2152
|
explicit = %i[session_token master acl_user acl_role].select { |k| options.key?(k) }
|
|
2117
|
-
return explicit.to_h { |k| [k, options[k]] } if explicit.any?
|
|
2153
|
+
return client_kwarg.merge(explicit.to_h { |k| [k, options[k]] }) if explicit.any?
|
|
2154
|
+
|
|
2155
|
+
client_kwarg.merge(atlas_search_scope_kwargs)
|
|
2156
|
+
end
|
|
2118
2157
|
|
|
2158
|
+
# @!visibility private
|
|
2159
|
+
def atlas_search_scope_kwargs
|
|
2119
2160
|
if @acl_user
|
|
2120
2161
|
{ acl_user: @acl_user }
|
|
2121
2162
|
elsif @acl_role
|
|
@@ -2201,7 +2242,7 @@ module Parse
|
|
|
2201
2242
|
# @raise [Parse::MongoDB::ExecutionTimeout] if the query exceeds max_time_ms
|
|
2202
2243
|
# @note This is a read-only operation. Direct MongoDB queries cannot modify data.
|
|
2203
2244
|
# @see Parse::MongoDB.configure
|
|
2204
|
-
def results_direct(raw: false, max_time_ms: nil, session_token: nil, master: nil, acl_user: nil, acl_role: nil, &block)
|
|
2245
|
+
def results_direct(raw: false, max_time_ms: nil, session_token: nil, master: nil, acl_user: nil, acl_role: nil, client: nil, &block)
|
|
2205
2246
|
require_relative "mongodb"
|
|
2206
2247
|
Parse::MongoDB.require_gem!
|
|
2207
2248
|
|
|
@@ -2211,6 +2252,17 @@ module Parse
|
|
|
2211
2252
|
"Call Parse::MongoDB.configure(uri: 'mongodb://...', enabled: true) first."
|
|
2212
2253
|
end
|
|
2213
2254
|
|
|
2255
|
+
# A query already owns a client. Defaulting to it means a query built
|
|
2256
|
+
# from a secondary client does not have to repeat itself, and cannot
|
|
2257
|
+
# silently authorize against the default application by omission.
|
|
2258
|
+
#
|
|
2259
|
+
# Resolved defensively: `#client` lazily constructs the DEFAULT client
|
|
2260
|
+
# and raises ConnectionError when none is configured. Argument
|
|
2261
|
+
# validation below must still be able to raise ArgumentError in a
|
|
2262
|
+
# process that never called Parse.setup, so a missing client is left
|
|
2263
|
+
# nil here and resolved at the authorization boundary instead.
|
|
2264
|
+
client ||= (self.client rescue nil)
|
|
2265
|
+
|
|
2214
2266
|
# Build the aggregation pipeline for direct MongoDB execution
|
|
2215
2267
|
pipeline = build_direct_mongodb_pipeline
|
|
2216
2268
|
|
|
@@ -2225,9 +2277,9 @@ module Parse
|
|
|
2225
2277
|
if session_token.nil? && master.nil? && acl_user.nil? && acl_role.nil?
|
|
2226
2278
|
auth = mongo_direct_auth_kwargs
|
|
2227
2279
|
session_token = auth[:session_token]
|
|
2228
|
-
master
|
|
2229
|
-
acl_user
|
|
2230
|
-
acl_role
|
|
2280
|
+
master = auth[:master]
|
|
2281
|
+
acl_user = auth[:acl_user]
|
|
2282
|
+
acl_role = auth[:acl_role]
|
|
2231
2283
|
end
|
|
2232
2284
|
|
|
2233
2285
|
# Execute the aggregation directly on MongoDB. The pipeline was built
|
|
@@ -2245,6 +2297,7 @@ module Parse
|
|
|
2245
2297
|
master: master,
|
|
2246
2298
|
acl_user: acl_user,
|
|
2247
2299
|
acl_role: acl_role,
|
|
2300
|
+
client: client,
|
|
2248
2301
|
read_preference: @read_preference,
|
|
2249
2302
|
hint: @hint)
|
|
2250
2303
|
|
|
@@ -2291,8 +2344,7 @@ module Parse
|
|
|
2291
2344
|
limit_or_constraints = 1
|
|
2292
2345
|
end
|
|
2293
2346
|
|
|
2294
|
-
count =
|
|
2295
|
-
case limit_or_constraints
|
|
2347
|
+
count = case limit_or_constraints
|
|
2296
2348
|
when Numeric then limit_or_constraints.to_i
|
|
2297
2349
|
when String
|
|
2298
2350
|
unless limit_or_constraints =~ /\A-?\d+\z/
|
|
@@ -2335,7 +2387,7 @@ module Parse
|
|
|
2335
2387
|
# @raise [Parse::MongoDB::NotEnabled] if direct MongoDB is not configured
|
|
2336
2388
|
# @note This is a read-only operation. Direct MongoDB queries cannot modify data.
|
|
2337
2389
|
# @see Parse::MongoDB.configure
|
|
2338
|
-
def count_direct(session_token: nil, master: nil, acl_user: nil, acl_role: nil)
|
|
2390
|
+
def count_direct(session_token: nil, master: nil, acl_user: nil, acl_role: nil, client: nil)
|
|
2339
2391
|
require_relative "mongodb"
|
|
2340
2392
|
Parse::MongoDB.require_gem!
|
|
2341
2393
|
|
|
@@ -2345,6 +2397,17 @@ module Parse
|
|
|
2345
2397
|
"Call Parse::MongoDB.configure(uri: 'mongodb://...', enabled: true) first."
|
|
2346
2398
|
end
|
|
2347
2399
|
|
|
2400
|
+
# A query already owns a client. Defaulting to it means a query built
|
|
2401
|
+
# from a secondary client does not have to repeat itself, and cannot
|
|
2402
|
+
# silently authorize against the default application by omission.
|
|
2403
|
+
#
|
|
2404
|
+
# Resolved defensively: `#client` lazily constructs the DEFAULT client
|
|
2405
|
+
# and raises ConnectionError when none is configured. Argument
|
|
2406
|
+
# validation below must still be able to raise ArgumentError in a
|
|
2407
|
+
# process that never called Parse.setup, so a missing client is left
|
|
2408
|
+
# nil here and resolved at the authorization boundary instead.
|
|
2409
|
+
client ||= (self.client rescue nil)
|
|
2410
|
+
|
|
2348
2411
|
# Build the aggregation pipeline for direct MongoDB execution
|
|
2349
2412
|
pipeline = build_direct_mongodb_pipeline
|
|
2350
2413
|
|
|
@@ -2359,9 +2422,9 @@ module Parse
|
|
|
2359
2422
|
if session_token.nil? && master.nil? && acl_user.nil? && acl_role.nil?
|
|
2360
2423
|
auth = mongo_direct_auth_kwargs
|
|
2361
2424
|
session_token = auth[:session_token]
|
|
2362
|
-
master
|
|
2363
|
-
acl_user
|
|
2364
|
-
acl_role
|
|
2425
|
+
master = auth[:master]
|
|
2426
|
+
acl_user = auth[:acl_user]
|
|
2427
|
+
acl_role = auth[:acl_role]
|
|
2365
2428
|
end
|
|
2366
2429
|
|
|
2367
2430
|
# SDK-built pipeline only — see results_direct for rationale.
|
|
@@ -2373,6 +2436,7 @@ module Parse
|
|
|
2373
2436
|
master: master,
|
|
2374
2437
|
acl_user: acl_user,
|
|
2375
2438
|
acl_role: acl_role,
|
|
2439
|
+
client: client,
|
|
2376
2440
|
read_preference: @read_preference,
|
|
2377
2441
|
hint: @hint)
|
|
2378
2442
|
|
|
@@ -2400,7 +2464,8 @@ module Parse
|
|
|
2400
2464
|
# @note This is a read-only operation. Direct MongoDB queries cannot modify data.
|
|
2401
2465
|
# @see Parse::MongoDB.configure
|
|
2402
2466
|
def distinct_direct(field, return_pointers: false, order: nil,
|
|
2403
|
-
|
|
2467
|
+
session_token: nil, master: nil, acl_user: nil, acl_role: nil,
|
|
2468
|
+
client: nil)
|
|
2404
2469
|
require_relative "mongodb"
|
|
2405
2470
|
Parse::MongoDB.require_gem!
|
|
2406
2471
|
|
|
@@ -2410,6 +2475,17 @@ module Parse
|
|
|
2410
2475
|
"Call Parse::MongoDB.configure(uri: 'mongodb://...', enabled: true) first."
|
|
2411
2476
|
end
|
|
2412
2477
|
|
|
2478
|
+
# A query already owns a client. Defaulting to it means a query built
|
|
2479
|
+
# from a secondary client does not have to repeat itself, and cannot
|
|
2480
|
+
# silently authorize against the default application by omission.
|
|
2481
|
+
#
|
|
2482
|
+
# Resolved defensively: `#client` lazily constructs the DEFAULT client
|
|
2483
|
+
# and raises ConnectionError when none is configured. Argument
|
|
2484
|
+
# validation below must still be able to raise ArgumentError in a
|
|
2485
|
+
# process that never called Parse.setup, so a missing client is left
|
|
2486
|
+
# nil here and resolved at the authorization boundary instead.
|
|
2487
|
+
client ||= (self.client rescue nil)
|
|
2488
|
+
|
|
2413
2489
|
if field.nil? || !field.respond_to?(:to_s) || field.is_a?(Hash) || field.is_a?(Array)
|
|
2414
2490
|
raise ArgumentError, "Invalid field name passed to `distinct_direct`."
|
|
2415
2491
|
end
|
|
@@ -2446,9 +2522,9 @@ module Parse
|
|
|
2446
2522
|
if session_token.nil? && master.nil? && acl_user.nil? && acl_role.nil?
|
|
2447
2523
|
auth = mongo_direct_auth_kwargs
|
|
2448
2524
|
session_token = auth[:session_token]
|
|
2449
|
-
master
|
|
2450
|
-
acl_user
|
|
2451
|
-
acl_role
|
|
2525
|
+
master = auth[:master]
|
|
2526
|
+
acl_user = auth[:acl_user]
|
|
2527
|
+
acl_role = auth[:acl_role]
|
|
2452
2528
|
end
|
|
2453
2529
|
raw_results = Parse::MongoDB.aggregate(@table, pipeline,
|
|
2454
2530
|
allow_internal_fields: true,
|
|
@@ -2457,7 +2533,8 @@ module Parse
|
|
|
2457
2533
|
session_token: session_token,
|
|
2458
2534
|
master: master,
|
|
2459
2535
|
acl_user: acl_user,
|
|
2460
|
-
acl_role: acl_role
|
|
2536
|
+
acl_role: acl_role,
|
|
2537
|
+
client: client)
|
|
2461
2538
|
|
|
2462
2539
|
# Extract values from results
|
|
2463
2540
|
values = raw_results.map { |doc| doc["value"] }.compact
|
|
@@ -2484,10 +2561,11 @@ module Parse
|
|
|
2484
2561
|
# @return [Array] array of distinct values, with pointer fields as Parse::Pointer objects
|
|
2485
2562
|
# @see #distinct_direct
|
|
2486
2563
|
def distinct_direct_pointers(field, order: nil,
|
|
2487
|
-
|
|
2564
|
+
session_token: nil, master: nil, acl_user: nil, acl_role: nil,
|
|
2565
|
+
client: nil)
|
|
2488
2566
|
distinct_direct(field, return_pointers: true, order: order,
|
|
2489
|
-
|
|
2490
|
-
|
|
2567
|
+
session_token: session_token, master: master,
|
|
2568
|
+
acl_user: acl_user, acl_role: acl_role, client: client)
|
|
2491
2569
|
end
|
|
2492
2570
|
|
|
2493
2571
|
#----------------------------------------------------------------
|
|
@@ -2615,6 +2693,10 @@ module Parse
|
|
|
2615
2693
|
options[:class_name] = @table
|
|
2616
2694
|
options[:limit] = limit
|
|
2617
2695
|
options[:skip] = skip_val
|
|
2696
|
+
# The query's client travels with its identity here too. Without it a
|
|
2697
|
+
# `q.client = other` query reached Atlas Search as the default
|
|
2698
|
+
# application. See #atlas_search_auth_kwargs.
|
|
2699
|
+
options.merge!(mongo_direct_client_kwarg) unless options.key?(:client)
|
|
2618
2700
|
# Forward the query's read_preference (set via `#read_pref`).
|
|
2619
2701
|
# Without this, Atlas Search calls reached through the Query
|
|
2620
2702
|
# bridge silently fall back to the client default even though
|
|
@@ -2682,6 +2764,10 @@ module Parse
|
|
|
2682
2764
|
# Use query limit if set and no explicit limit provided
|
|
2683
2765
|
options[:limit] ||= (@limit.is_a?(Numeric) && @limit > 0 ? @limit : 10)
|
|
2684
2766
|
options[:class_name] = @table
|
|
2767
|
+
# The query's client travels with its identity here too. Without it a
|
|
2768
|
+
# `q.client = other` query reached Atlas Search as the default
|
|
2769
|
+
# application. See #atlas_search_auth_kwargs.
|
|
2770
|
+
options.merge!(mongo_direct_client_kwarg) unless options.key?(:client)
|
|
2685
2771
|
# Forward the query's read_preference (set via `#read_pref`).
|
|
2686
2772
|
# See #atlas_search for the parity rationale.
|
|
2687
2773
|
if @read_preference && !options.key?(:read_preference)
|
|
@@ -2739,6 +2825,10 @@ module Parse
|
|
|
2739
2825
|
options[:limit] ||= (@limit.is_a?(Numeric) && @limit > 0 ? @limit : 100)
|
|
2740
2826
|
options[:skip] ||= (@skip > 0 ? @skip : 0)
|
|
2741
2827
|
options[:class_name] = @table
|
|
2828
|
+
# The query's client travels with its identity here too. Without it a
|
|
2829
|
+
# `q.client = other` query reached Atlas Search as the default
|
|
2830
|
+
# application. See #atlas_search_auth_kwargs.
|
|
2831
|
+
options.merge!(mongo_direct_client_kwarg) unless options.key?(:client)
|
|
2742
2832
|
# Forward the query's read_preference (set via `#read_pref`).
|
|
2743
2833
|
# See #atlas_search for the parity rationale.
|
|
2744
2834
|
if @read_preference && !options.key?(:read_preference)
|
|
@@ -3692,8 +3782,8 @@ module Parse
|
|
|
3692
3782
|
end
|
|
3693
3783
|
|
|
3694
3784
|
Aggregation.new(self, complete_pipeline, verbose: verbose, mongo_direct: use_mongo_direct || false,
|
|
3695
|
-
|
|
3696
|
-
|
|
3785
|
+
allow_internal_fields: uses_internal_fields,
|
|
3786
|
+
raw_values: raw_values, raw_field_names: raw_field_names)
|
|
3697
3787
|
end
|
|
3698
3788
|
|
|
3699
3789
|
# Apply the direct-MongoDB stage converter to every stage in a pipeline.
|
|
@@ -3796,7 +3886,7 @@ module Parse
|
|
|
3796
3886
|
|
|
3797
3887
|
# Create Aggregation directly to avoid double-applying constraints
|
|
3798
3888
|
Aggregation.new(self, pipeline, verbose: verbose, mongo_direct: use_mongo_direct || false,
|
|
3799
|
-
|
|
3889
|
+
allow_internal_fields: uses_internal_fields)
|
|
3800
3890
|
end
|
|
3801
3891
|
|
|
3802
3892
|
private
|
|
@@ -3976,7 +4066,7 @@ module Parse
|
|
|
3976
4066
|
# Create Aggregation directly to avoid double-applying constraints
|
|
3977
4067
|
# The aggregate() method would redundantly add where constraints again
|
|
3978
4068
|
Aggregation.new(self, pipeline, verbose: @verbose_aggregate, mongo_direct: use_mongo_direct,
|
|
3979
|
-
|
|
4069
|
+
allow_internal_fields: uses_internal_fields)
|
|
3980
4070
|
end
|
|
3981
4071
|
|
|
3982
4072
|
# Check if the pipeline references internal Parse fields that require MongoDB direct access
|
|
@@ -4330,6 +4420,7 @@ module Parse
|
|
|
4330
4420
|
end
|
|
4331
4421
|
@keys.uniq!
|
|
4332
4422
|
end
|
|
4423
|
+
|
|
4333
4424
|
private :merge_includes_into_keys!
|
|
4334
4425
|
|
|
4335
4426
|
# Builds Parse::Pointer objects based on the set of Parse JSON hashes in an array.
|
|
@@ -6057,7 +6148,7 @@ module Parse
|
|
|
6057
6148
|
# auth data) is what `allow_internal_fields` relaxes, so it must never be
|
|
6058
6149
|
# set on a pipeline that interpolates user input. Defaults to `false`.
|
|
6059
6150
|
def initialize(query, pipeline, verbose: nil, mongo_direct: false, max_time_ms: nil,
|
|
6060
|
-
|
|
6151
|
+
raw_values: false, raw_field_names: false, allow_internal_fields: false)
|
|
6061
6152
|
@query = query
|
|
6062
6153
|
@pipeline = pipeline
|
|
6063
6154
|
@cached_response = nil
|
|
@@ -6127,7 +6218,7 @@ module Parse
|
|
|
6127
6218
|
# count_direct / distinct_direct).
|
|
6128
6219
|
hint = @query.instance_variable_get(:@hint)
|
|
6129
6220
|
Parse::MongoDB.aggregate(table, @pipeline, max_time_ms: max_time_ms, hint: hint,
|
|
6130
|
-
|
|
6221
|
+
allow_internal_fields: @allow_internal_fields, **auth_kwargs)
|
|
6131
6222
|
end
|
|
6132
6223
|
|
|
6133
6224
|
# Returns processed results from the aggregation.
|
|
@@ -6339,8 +6430,7 @@ module Parse
|
|
|
6339
6430
|
# @example Groups with the most members first
|
|
6340
6431
|
# Document.group_by(:category).order(size: :desc).list
|
|
6341
6432
|
def order(spec)
|
|
6342
|
-
target, direction =
|
|
6343
|
-
case spec
|
|
6433
|
+
target, direction = case spec
|
|
6344
6434
|
when Symbol
|
|
6345
6435
|
[:key, spec]
|
|
6346
6436
|
when Hash
|
|
@@ -6841,8 +6931,7 @@ module Parse
|
|
|
6841
6931
|
# `count` field (for :value / :size).
|
|
6842
6932
|
def sort_stage
|
|
6843
6933
|
return nil unless @sort_target
|
|
6844
|
-
field =
|
|
6845
|
-
case @sort_target
|
|
6934
|
+
field = case @sort_target
|
|
6846
6935
|
when :key then "_id"
|
|
6847
6936
|
when :value then "count"
|
|
6848
6937
|
when :size then "__order_size"
|
|
@@ -7012,13 +7101,13 @@ module Parse
|
|
|
7012
7101
|
# @return [String] the default second-column header
|
|
7013
7102
|
def default_value_header
|
|
7014
7103
|
case @operation&.to_s
|
|
7015
|
-
when "count"
|
|
7016
|
-
when "sum"
|
|
7104
|
+
when "count" then "Count"
|
|
7105
|
+
when "sum" then "Sum"
|
|
7017
7106
|
when "average", "avg" then "Average"
|
|
7018
|
-
when "min"
|
|
7019
|
-
when "max"
|
|
7020
|
-
when "list"
|
|
7021
|
-
else
|
|
7107
|
+
when "min" then "Min"
|
|
7108
|
+
when "max" then "Max"
|
|
7109
|
+
when "list" then "Items"
|
|
7110
|
+
else "Count"
|
|
7022
7111
|
end
|
|
7023
7112
|
end
|
|
7024
7113
|
|
|
@@ -7193,8 +7282,7 @@ module Parse
|
|
|
7193
7282
|
# @example Busiest day first
|
|
7194
7283
|
# Post.group_by_date(:created_at, :day).order(value: :desc).count
|
|
7195
7284
|
def order(spec)
|
|
7196
|
-
target, direction =
|
|
7197
|
-
case spec
|
|
7285
|
+
target, direction = case spec
|
|
7198
7286
|
when Symbol
|
|
7199
7287
|
[:key, spec]
|
|
7200
7288
|
when Hash
|
|
@@ -7626,8 +7714,7 @@ module Parse
|
|
|
7626
7714
|
# `.order(...)` has been called.
|
|
7627
7715
|
def sort_stage
|
|
7628
7716
|
field = @sort_target == :value ? "count" : "_id"
|
|
7629
|
-
dir =
|
|
7630
|
-
if @sort_target.nil?
|
|
7717
|
+
dir = if @sort_target.nil?
|
|
7631
7718
|
1
|
|
7632
7719
|
else
|
|
7633
7720
|
@sort_direction == :desc ? -1 : 1
|
|
@@ -27,6 +27,20 @@ module Parse
|
|
|
27
27
|
module AgentTool
|
|
28
28
|
module_function
|
|
29
29
|
|
|
30
|
+
# The agent's auth kwargs for the direct path, including its client
|
|
31
|
+
# when the agent can supply one.
|
|
32
|
+
#
|
|
33
|
+
# Agents are duck-typed at this boundary, so an agent-shaped object
|
|
34
|
+
# that predates `direct_auth_kwargs` still works and simply resolves as
|
|
35
|
+
# an unidentified caller, which Parse::MongoDB's binding guard already
|
|
36
|
+
# models. Requiring the new method of every stand-in would break each
|
|
37
|
+
# one that has not been updated, to gain a field that is optional by
|
|
38
|
+
# construction.
|
|
39
|
+
def retrieval_auth_kwargs(agent)
|
|
40
|
+
return agent.direct_auth_kwargs if agent.respond_to?(:direct_auth_kwargs)
|
|
41
|
+
agent.acl_scope_kwargs
|
|
42
|
+
end
|
|
43
|
+
|
|
30
44
|
# Upper bound on `k` (mirrors the registered parameter schema).
|
|
31
45
|
MAX_K = 20
|
|
32
46
|
# Default neighbour count for the agent tool. Intentionally lower than
|
|
@@ -60,18 +74,18 @@ module Parse
|
|
|
60
74
|
# token budget trims the result, `budget_truncated: true` and
|
|
61
75
|
# `budget_dropped: <n>` are added.
|
|
62
76
|
def semantic_search(agent, class_name: nil, query: nil, k: DEFAULT_K,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
77
|
+
filter: nil, vector_filter: nil, text_field: nil,
|
|
78
|
+
chunk_size: nil, chunk_overlap: nil, chunk_by: nil,
|
|
79
|
+
max_chunks_per_document: nil, max_total_tokens: nil,
|
|
80
|
+
# Back-compat / ergonomic aliases for direct callers:
|
|
81
|
+
# `klass:`/`class:` for class_name, and the chunker's
|
|
82
|
+
# own `size:`/`overlap:`/`by:` names.
|
|
83
|
+
klass: nil, size: nil, overlap: nil, by: nil,
|
|
70
84
|
**rest)
|
|
71
|
-
class_name
|
|
72
|
-
chunk_size
|
|
85
|
+
class_name ||= klass || rest.delete(:class)
|
|
86
|
+
chunk_size ||= size
|
|
73
87
|
chunk_overlap ||= overlap
|
|
74
|
-
chunk_by
|
|
88
|
+
chunk_by ||= by
|
|
75
89
|
|
|
76
90
|
klass = Parse::Agent::MetadataRegistry.resolve_searchable!(class_name)
|
|
77
91
|
cname = klass.parse_class
|
|
@@ -126,7 +140,7 @@ module Parse
|
|
|
126
140
|
tenant_scope: scope,
|
|
127
141
|
score_quantize: score_quantize,
|
|
128
142
|
source_transform: source_projector(agent, cname, scope),
|
|
129
|
-
**agent
|
|
143
|
+
**retrieval_auth_kwargs(agent),
|
|
130
144
|
)
|
|
131
145
|
end
|
|
132
146
|
|
|
@@ -352,15 +366,15 @@ module Parse
|
|
|
352
366
|
PARAMETERS = {
|
|
353
367
|
"type" => "object",
|
|
354
368
|
"properties" => {
|
|
355
|
-
"class_name"
|
|
356
|
-
"query"
|
|
357
|
-
"k"
|
|
358
|
-
"filter"
|
|
369
|
+
"class_name" => { "type" => "string", "description" => "Parse class name (must be agent_searchable)." },
|
|
370
|
+
"query" => { "type" => "string", "description" => "Natural-language query." },
|
|
371
|
+
"k" => { "type" => "integer", "default" => DEFAULT_K, "minimum" => 1, "maximum" => MAX_K },
|
|
372
|
+
"filter" => { "type" => "object", "description" => "Post-search field filter (allowlisted fields only)." },
|
|
359
373
|
"vector_filter" => { "type" => "object", "description" => "Atlas pre-search filter (allowlisted fields only)." },
|
|
360
|
-
"text_field"
|
|
361
|
-
"chunk_size"
|
|
374
|
+
"text_field" => { "type" => "string", "description" => "Which embedded text source to chunk and return as content. Required only when the class embeds more than one text field; must name one of those sources." },
|
|
375
|
+
"chunk_size" => { "type" => "integer", "description" => "Override chunk window size." },
|
|
362
376
|
"chunk_overlap" => { "type" => "integer", "description" => "Override chunk overlap." },
|
|
363
|
-
"chunk_by"
|
|
377
|
+
"chunk_by" => { "type" => "string", "enum" => %w[chars tokens], "description" => "Chunk unit." },
|
|
364
378
|
"max_chunks_per_document" => { "type" => "integer", "minimum" => 1, "description" => "Cap on chunks emitted per matched document." },
|
|
365
379
|
"max_total_tokens" => { "type" => "integer", "minimum" => 0, "description" => "Ceiling on total returned chunk-content tokens (approx chars/4). Trims lowest-ranked chunks first and sets budget_truncated. 0 disables." },
|
|
366
380
|
},
|
|
@@ -379,8 +393,8 @@ module Parse
|
|
|
379
393
|
"items" => {
|
|
380
394
|
"type" => "object",
|
|
381
395
|
"properties" => {
|
|
382
|
-
"id"
|
|
383
|
-
"score"
|
|
396
|
+
"id" => { "type" => "string" },
|
|
397
|
+
"score" => { "type" => %w[number null] },
|
|
384
398
|
"content" => { "type" => "string" },
|
|
385
399
|
"metadata" => { "type" => "object" },
|
|
386
400
|
},
|
|
@@ -35,10 +35,10 @@ module Parse
|
|
|
35
35
|
class BadRequestError < Error; end
|
|
36
36
|
|
|
37
37
|
DEFAULT_BASE_URL = "https://api.cohere.com/v2"
|
|
38
|
-
DEFAULT_MODEL
|
|
39
|
-
DEFAULT_TIMEOUT
|
|
38
|
+
DEFAULT_MODEL = "rerank-v3.5"
|
|
39
|
+
DEFAULT_TIMEOUT = 30
|
|
40
40
|
DEFAULT_OPEN_TIMEOUT = 5
|
|
41
|
-
DEFAULT_MAX_RETRIES
|
|
41
|
+
DEFAULT_MAX_RETRIES = 2
|
|
42
42
|
|
|
43
43
|
# Cohere documents a cap of 1000 documents per rerank call; the
|
|
44
44
|
# {Base::MAX_DOCUMENTS} cap (1000) already enforces this.
|
|
@@ -75,7 +75,7 @@ module Parse
|
|
|
75
75
|
|
|
76
76
|
def inspect
|
|
77
77
|
"#<#{self.class} model=#{@model.inspect} base=#{safe_base_host.inspect} " \
|
|
78
|
-
|
|
78
|
+
"retries=#{@max_retries} api_key=[REDACTED]>"
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
protected
|
|
@@ -83,10 +83,10 @@ module Parse
|
|
|
83
83
|
def rerank_scores(query, documents, top_n)
|
|
84
84
|
require_faraday!
|
|
85
85
|
body = {
|
|
86
|
-
"model"
|
|
87
|
-
"query"
|
|
86
|
+
"model" => @model,
|
|
87
|
+
"query" => query,
|
|
88
88
|
"documents" => documents,
|
|
89
|
-
"top_n"
|
|
89
|
+
"top_n" => top_n,
|
|
90
90
|
}
|
|
91
91
|
payload = post_rerank(body)
|
|
92
92
|
extract_results!(payload, documents.length)
|
|
@@ -158,9 +158,9 @@ module Parse
|
|
|
158
158
|
require_faraday!
|
|
159
159
|
headers = {
|
|
160
160
|
"Authorization" => "Bearer #{@api_key}",
|
|
161
|
-
"Content-Type"
|
|
162
|
-
"Accept"
|
|
163
|
-
"User-Agent"
|
|
161
|
+
"Content-Type" => "application/json",
|
|
162
|
+
"Accept" => "application/json",
|
|
163
|
+
"User-Agent" => "parse-stack-reranker/#{Parse::Stack::VERSION rescue "0"}",
|
|
164
164
|
}
|
|
165
165
|
# base_url must end with a trailing slash so Faraday resolves the
|
|
166
166
|
# relative "rerank" path under /v2/ rather than replacing it.
|
|
@@ -177,7 +177,7 @@ module Parse
|
|
|
177
177
|
end
|
|
178
178
|
|
|
179
179
|
def backoff_seconds(attempt)
|
|
180
|
-
[0.5 * (2**(attempt - 1)), 30.0].min
|
|
180
|
+
[0.5 * (2 ** (attempt - 1)), 30.0].min
|
|
181
181
|
end
|
|
182
182
|
|
|
183
183
|
def retry_after_seconds(response)
|
|
@@ -100,11 +100,10 @@ module Parse
|
|
|
100
100
|
|
|
101
101
|
def normalize_results(pairs, doc_count, top_n)
|
|
102
102
|
results = Array(pairs).map do |p|
|
|
103
|
-
idx, score =
|
|
104
|
-
case p
|
|
103
|
+
idx, score = case p
|
|
105
104
|
when Result then [p.index, p.relevance_score]
|
|
106
|
-
when Array
|
|
107
|
-
when Hash
|
|
105
|
+
when Array then [p[0], p[1]]
|
|
106
|
+
when Hash then [p[:index] || p["index"], p[:relevance_score] || p["relevance_score"]]
|
|
108
107
|
else
|
|
109
108
|
raise InvalidResponseError, "#{self.class}: unexpected rerank result element #{p.inspect}."
|
|
110
109
|
end
|