attocode-code-intel 0.1.0__py3-none-any.whl
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.
- attocode_code_intel-0.1.0.dist-info/METADATA +221 -0
- attocode_code_intel-0.1.0.dist-info/RECORD +345 -0
- attocode_code_intel-0.1.0.dist-info/WHEEL +4 -0
- attocode_code_intel-0.1.0.dist-info/entry_points.txt +2 -0
- attocode_code_intel-0.1.0.dist-info/licenses/LICENSE +21 -0
- attocode_intel/GUIDELINES.md +432 -0
- attocode_intel/__init__.py +52 -0
- attocode_intel/_internal/__init__.py +1 -0
- attocode_intel/_internal/errors/__init__.py +126 -0
- attocode_intel/_internal/integrations/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/architecture_drift.py +354 -0
- attocode_intel/_internal/integrations/context/ast_chunker.py +202 -0
- attocode_intel/_internal/integrations/context/ast_service.py +1483 -0
- attocode_intel/_internal/integrations/context/blob_oid.py +423 -0
- attocode_intel/_internal/integrations/context/cache_manifest.py +221 -0
- attocode_intel/_internal/integrations/context/cas.py +466 -0
- attocode_intel/_internal/integrations/context/code_analyzer.py +528 -0
- attocode_intel/_internal/integrations/context/codebase_ast.py +1461 -0
- attocode_intel/_internal/integrations/context/codebase_context.py +1736 -0
- attocode_intel/_internal/integrations/context/cross_mode.py +263 -0
- attocode_intel/_internal/integrations/context/cross_references.py +616 -0
- attocode_intel/_internal/integrations/context/embedding_rotation.py +597 -0
- attocode_intel/_internal/integrations/context/embeddings.py +282 -0
- attocode_intel/_internal/integrations/context/frecency.py +427 -0
- attocode_intel/_internal/integrations/context/fuzzy.py +320 -0
- attocode_intel/_internal/integrations/context/hierarchical_explorer.py +331 -0
- attocode_intel/_internal/integrations/context/hydration.py +90 -0
- attocode_intel/_internal/integrations/context/index_repair.py +87 -0
- attocode_intel/_internal/integrations/context/index_store.py +586 -0
- attocode_intel/_internal/integrations/context/memory_store.py +602 -0
- attocode_intel/_internal/integrations/context/module_resolution.py +52 -0
- attocode_intel/_internal/integrations/context/query_constraints.py +361 -0
- attocode_intel/_internal/integrations/context/query_history.py +417 -0
- attocode_intel/_internal/integrations/context/reranker.py +351 -0
- attocode_intel/_internal/integrations/context/semantic_search.py +2931 -0
- attocode_intel/_internal/integrations/context/syntax_evidence.py +94 -0
- attocode_intel/_internal/integrations/context/systemone_ranker.py +245 -0
- attocode_intel/_internal/integrations/context/temporal_coupling.py +411 -0
- attocode_intel/_internal/integrations/context/trigram_index.py +820 -0
- attocode_intel/_internal/integrations/context/trigram_regex.py +171 -0
- attocode_intel/_internal/integrations/context/ts_parser.py +2199 -0
- attocode_intel/_internal/integrations/context/vector_store.py +795 -0
- attocode_intel/_internal/integrations/feature_flags/__init__.py +357 -0
- attocode_intel/_internal/integrations/lsp/__init__.py +1 -0
- attocode_intel/_internal/integrations/lsp/client.py +1140 -0
- attocode_intel/_internal/integrations/mcp/__init__.py +1 -0
- attocode_intel/_internal/integrations/mcp/client.py +253 -0
- attocode_intel/_internal/integrations/security/__init__.py +1 -0
- attocode_intel/_internal/integrations/security/custom_rules.py +176 -0
- attocode_intel/_internal/integrations/security/dataflow.py +522 -0
- attocode_intel/_internal/integrations/security/dependency_audit.py +303 -0
- attocode_intel/_internal/integrations/security/matcher.py +44 -0
- attocode_intel/_internal/integrations/security/patterns.py +724 -0
- attocode_intel/_internal/integrations/security/scanner.py +351 -0
- attocode_intel/_internal/integrations/utilities/__init__.py +1 -0
- attocode_intel/_internal/integrations/utilities/ignore.py +64 -0
- attocode_intel/_internal/integrations/utilities/token_estimate.py +51 -0
- attocode_intel/_internal/tricks/__init__.py +1 -0
- attocode_intel/_internal/tricks/recursive_context.py +357 -0
- attocode_intel/_internal/types/__init__.py +1 -0
- attocode_intel/_internal/types/events.py +289 -0
- attocode_intel/_internal/types/severity.py +15 -0
- attocode_intel/_shared.py +341 -0
- attocode_intel/analysis_coverage.py +56 -0
- attocode_intel/api/__init__.py +1 -0
- attocode_intel/api/access.py +32 -0
- attocode_intel/api/app.py +247 -0
- attocode_intel/api/auth/__init__.py +128 -0
- attocode_intel/api/auth/api_keys.py +61 -0
- attocode_intel/api/auth/context.py +34 -0
- attocode_intel/api/auth/jwt.py +66 -0
- attocode_intel/api/auth/oauth.py +146 -0
- attocode_intel/api/auth/passwords.py +15 -0
- attocode_intel/api/deps.py +351 -0
- attocode_intel/api/knowledge_bridge.py +13 -0
- attocode_intel/api/mcp_transport.py +51 -0
- attocode_intel/api/middleware.py +411 -0
- attocode_intel/api/models.py +698 -0
- attocode_intel/api/providers/__init__.py +1 -0
- attocode_intel/api/providers/db_provider.py +1106 -0
- attocode_intel/api/providers/local_provider.py +476 -0
- attocode_intel/api/providers/remote_provider.py +740 -0
- attocode_intel/api/repository_source.py +21 -0
- attocode_intel/api/routes/__init__.py +1 -0
- attocode_intel/api/routes/_pin_helper.py +158 -0
- attocode_intel/api/routes/activity.py +148 -0
- attocode_intel/api/routes/adr.py +102 -0
- attocode_intel/api/routes/analysis.py +515 -0
- attocode_intel/api/routes/api_keys.py +170 -0
- attocode_intel/api/routes/auth.py +428 -0
- attocode_intel/api/routes/branches.py +262 -0
- attocode_intel/api/routes/cross_repo_search.py +198 -0
- attocode_intel/api/routes/embeddings.py +386 -0
- attocode_intel/api/routes/files.py +132 -0
- attocode_intel/api/routes/files_v2.py +388 -0
- attocode_intel/api/routes/gc.py +322 -0
- attocode_intel/api/routes/git_v2.py +433 -0
- attocode_intel/api/routes/graph.py +174 -0
- attocode_intel/api/routes/graph_viz.py +160 -0
- attocode_intel/api/routes/health.py +62 -0
- attocode_intel/api/routes/history.py +175 -0
- attocode_intel/api/routes/jobs.py +84 -0
- attocode_intel/api/routes/learning.py +205 -0
- attocode_intel/api/routes/lsp.py +272 -0
- attocode_intel/api/routes/notify.py +623 -0
- attocode_intel/api/routes/operations.py +27 -0
- attocode_intel/api/routes/orgs.py +924 -0
- attocode_intel/api/routes/preferences.py +100 -0
- attocode_intel/api/routes/presence.py +42 -0
- attocode_intel/api/routes/projects.py +116 -0
- attocode_intel/api/routes/repos.py +47 -0
- attocode_intel/api/routes/search.py +180 -0
- attocode_intel/api/routes/snapshots.py +614 -0
- attocode_intel/api/routes/webhooks.py +313 -0
- attocode_intel/api/routes/websocket.py +116 -0
- attocode_intel/api/static/graph.html +239 -0
- attocode_intel/api/utils.py +96 -0
- attocode_intel/artifacts/__init__.py +45 -0
- attocode_intel/artifacts/hashing.py +230 -0
- attocode_intel/artifacts/provenance.py +190 -0
- attocode_intel/artifacts/retrieval_pin.py +159 -0
- attocode_intel/audit.py +59 -0
- attocode_intel/bug_finder.py +206 -0
- attocode_intel/bundle.py +80 -0
- attocode_intel/catalog.py +242 -0
- attocode_intel/change_review.py +75 -0
- attocode_intel/cli.py +3062 -0
- attocode_intel/community.py +63 -0
- attocode_intel/confidence/__init__.py +144 -0
- attocode_intel/confidence/jev.py +234 -0
- attocode_intel/confidence/llm.py +330 -0
- attocode_intel/confidence/redact.py +44 -0
- attocode_intel/confidence/settings.py +74 -0
- attocode_intel/config.py +256 -0
- attocode_intel/crypto.py +31 -0
- attocode_intel/db/__init__.py +7 -0
- attocode_intel/db/base.py +33 -0
- attocode_intel/db/engine.py +61 -0
- attocode_intel/db/models.py +529 -0
- attocode_intel/entrypoint.py +185 -0
- attocode_intel/focused_evidence.py +130 -0
- attocode_intel/freshness.py +154 -0
- attocode_intel/gateway.py +734 -0
- attocode_intel/git/__init__.py +7 -0
- attocode_intel/git/credentials.py +75 -0
- attocode_intel/git/manager.py +493 -0
- attocode_intel/git/models.py +96 -0
- attocode_intel/git/storage.py +98 -0
- attocode_intel/graph_query_parser.py +761 -0
- attocode_intel/helpers.py +712 -0
- attocode_intel/indexing/__init__.py +29 -0
- attocode_intel/indexing/debouncer.py +136 -0
- attocode_intel/indexing/delta_indexer.py +151 -0
- attocode_intel/indexing/full_indexer.py +323 -0
- attocode_intel/indexing/incremental.py +240 -0
- attocode_intel/indexing/parser.py +403 -0
- attocode_intel/indexing/summarizer.py +264 -0
- attocode_intel/installer.py +1896 -0
- attocode_intel/knowledge.py +269 -0
- attocode_intel/local_knowledge.py +77 -0
- attocode_intel/migrations/alembic.ini +36 -0
- attocode_intel/migrations/env.py +71 -0
- attocode_intel/migrations/script.py.mako +27 -0
- attocode_intel/migrations/versions/001_initial_schema.py +219 -0
- attocode_intel/migrations/versions/002_branch_version_and_embeddings.py +61 -0
- attocode_intel/migrations/versions/003_embedding_uniqueness_and_cascade.py +51 -0
- attocode_intel/migrations/versions/004_audit_events.py +38 -0
- attocode_intel/migrations/versions/005_user_preferences.py +30 -0
- attocode_intel/migrations/versions/006_google_oauth.py +27 -0
- attocode_intel/migrations/versions/007_pgvector_embeddings.py +28 -0
- attocode_intel/migrations/versions/008_symbol_references.py +37 -0
- attocode_intel/migrations/versions/009_webhook_secret_encryption.py +25 -0
- attocode_intel/migrations/versions/010_add_commits_table.py +40 -0
- attocode_intel/migrations/versions/011_commit_changed_files.py +25 -0
- attocode_intel/migrations/versions/012_branch_merged_at.py +25 -0
- attocode_intel/migrations/versions/013_revoked_tokens.py +32 -0
- attocode_intel/migrations/versions/014_blame_hunks.py +40 -0
- attocode_intel/migrations/versions/015_commit_file_stats.py +40 -0
- attocode_intel/migrations/versions/016_embedding_provenance_columns.py +179 -0
- attocode_intel/migrations/versions/017_provenance_table.py +102 -0
- attocode_intel/migrations/versions/018_repo_snapshots.py +178 -0
- attocode_intel/migrations/versions/019_symref_caller.py +42 -0
- attocode_intel/migrations/versions/020_shared_knowledge.py +29 -0
- attocode_intel/onboarding.py +258 -0
- attocode_intel/output.py +178 -0
- attocode_intel/precision.py +133 -0
- attocode_intel/presence.py +160 -0
- attocode_intel/probe.py +110 -0
- attocode_intel/project_dir.py +97 -0
- attocode_intel/pubsub.py +113 -0
- attocode_intel/query_ranking.py +242 -0
- attocode_intel/readiness.py +972 -0
- attocode_intel/redis.py +51 -0
- attocode_intel/reference_pages.py +60 -0
- attocode_intel/remote.py +131 -0
- attocode_intel/repo_ranker.py +246 -0
- attocode_intel/request_context.py +82 -0
- attocode_intel/rules/__init__.py +28 -0
- attocode_intel/rules/ci.py +320 -0
- attocode_intel/rules/combinators.py +293 -0
- attocode_intel/rules/enricher.py +127 -0
- attocode_intel/rules/evolution.py +452 -0
- attocode_intel/rules/executor.py +531 -0
- attocode_intel/rules/filters/__init__.py +5 -0
- attocode_intel/rules/filters/pipeline.py +151 -0
- attocode_intel/rules/formatter.py +198 -0
- attocode_intel/rules/hygiene.py +242 -0
- attocode_intel/rules/importers/__init__.py +1 -0
- attocode_intel/rules/importers/semgrep.py +382 -0
- attocode_intel/rules/loader.py +292 -0
- attocode_intel/rules/marketplace.py +375 -0
- attocode_intel/rules/metavar.py +181 -0
- attocode_intel/rules/model.py +317 -0
- attocode_intel/rules/packs/__init__.py +1 -0
- attocode_intel/rules/packs/community/_license_templates/Apache-2.0.txt +176 -0
- attocode_intel/rules/packs/community/_license_templates/MIT.txt +21 -0
- attocode_intel/rules/packs/community/bandit-python/LICENSE +176 -0
- attocode_intel/rules/packs/community/bandit-python/NOTICE +13 -0
- attocode_intel/rules/packs/community/bandit-python/PORTING.md +23 -0
- attocode_intel/rules/packs/community/bandit-python/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B105-hardcoded-password.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B303-weak-md5.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B304-weak-des.yaml +20 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B307-use-of-eval.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B324-weak-sha1.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B501-no-cert-validation.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B602-shell-true.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B608-sqli.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/LICENSE +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/NOTICE +13 -0
- attocode_intel/rules/packs/community/eslint-typescript/PORTING.md +23 -0
- attocode_intel/rules/packs/community/eslint-typescript/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/eqeqeq.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-eval.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-implied-eval.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-new-func.yaml +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-throw-literal.yaml +19 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-var.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-with.yaml +18 -0
- attocode_intel/rules/packs/community/gosec-go/LICENSE +176 -0
- attocode_intel/rules/packs/community/gosec-go/NOTICE +13 -0
- attocode_intel/rules/packs/community/gosec-go/PORTING.md +23 -0
- attocode_intel/rules/packs/community/gosec-go/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G101-hardcoded-credentials.yaml +22 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G102-bind-all-interfaces.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G103-unsafe-block.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G201-sql-format.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G304-file-inclusion.yaml +23 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G401-weak-cipher.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G402-tls-min-version.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G501-md5-import.yaml +21 -0
- attocode_intel/rules/packs/examples/cpp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/cpp/rules/security.yaml +90 -0
- attocode_intel/rules/packs/examples/cpp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/csharp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/csharp/rules/security.yaml +69 -0
- attocode_intel/rules/packs/examples/csharp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/go/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/go/rules/performance.yaml +136 -0
- attocode_intel/rules/packs/examples/go/rules/security.yaml +48 -0
- attocode_intel/rules/packs/examples/go/rules/structural.yaml +58 -0
- attocode_intel/rules/packs/examples/go/taint/sanitizers.yaml +24 -0
- attocode_intel/rules/packs/examples/go/taint/sinks.yaml +51 -0
- attocode_intel/rules/packs/examples/go/taint/sources.yaml +26 -0
- attocode_intel/rules/packs/examples/java/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/java/rules/correctness.yaml +55 -0
- attocode_intel/rules/packs/examples/java/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/examples/java/taint/sinks.yaml +37 -0
- attocode_intel/rules/packs/examples/java/taint/sources.yaml +21 -0
- attocode_intel/rules/packs/examples/kotlin/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/kotlin/rules/correctness.yaml +50 -0
- attocode_intel/rules/packs/examples/kotlin/rules/structural.yaml +38 -0
- attocode_intel/rules/packs/examples/php/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/php/rules/security.yaml +66 -0
- attocode_intel/rules/packs/examples/php/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/python/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/python/rules/performance.yaml +90 -0
- attocode_intel/rules/packs/examples/python/rules/structural.yaml +55 -0
- attocode_intel/rules/packs/examples/ruby/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/ruby/rules/security.yaml +73 -0
- attocode_intel/rules/packs/examples/ruby/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/rust/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/rust/rules/correctness.yaml +51 -0
- attocode_intel/rules/packs/examples/rust/rules/structural.yaml +49 -0
- attocode_intel/rules/packs/examples/typescript/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/typescript/rules/performance.yaml +70 -0
- attocode_intel/rules/packs/examples/typescript/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/pack_loader.py +244 -0
- attocode_intel/rules/plugins/__init__.py +1 -0
- attocode_intel/rules/plugins/plugin_loader.py +132 -0
- attocode_intel/rules/profiling.py +332 -0
- attocode_intel/rules/registry.py +213 -0
- attocode_intel/rules/sarif.py +187 -0
- attocode_intel/rules/synthesis.py +420 -0
- attocode_intel/rules/testing.py +326 -0
- attocode_intel/server.py +734 -0
- attocode_intel/service.py +3416 -0
- attocode_intel/sharing.py +57 -0
- attocode_intel/storage/__init__.py +6 -0
- attocode_intel/storage/branch_overlay.py +352 -0
- attocode_intel/storage/content_store.py +220 -0
- attocode_intel/storage/dependency_store.py +143 -0
- attocode_intel/storage/diff_engine.py +187 -0
- attocode_intel/storage/embedding_store.py +594 -0
- attocode_intel/storage/provenance_store.py +137 -0
- attocode_intel/storage/security_scanner_db.py +164 -0
- attocode_intel/storage/symbol_store.py +160 -0
- attocode_intel/structural_evidence.py +144 -0
- attocode_intel/symbol_inspection.py +141 -0
- attocode_intel/symbol_links.py +167 -0
- attocode_intel/telemetry.py +42 -0
- attocode_intel/test_ranking.py +73 -0
- attocode_intel/testing/__init__.py +43 -0
- attocode_intel/testing/fixtures.py +122 -0
- attocode_intel/testing/helpers.py +75 -0
- attocode_intel/testing/mocks.py +99 -0
- attocode_intel/tools/__init__.py +2 -0
- attocode_intel/tools/adr_tools.py +628 -0
- attocode_intel/tools/analysis_tools.py +382 -0
- attocode_intel/tools/composite_tools.py +695 -0
- attocode_intel/tools/cross_mode_tools.py +173 -0
- attocode_intel/tools/dead_code_tools.py +582 -0
- attocode_intel/tools/distill_tools.py +353 -0
- attocode_intel/tools/frecency_tools.py +220 -0
- attocode_intel/tools/fuzzy_tools.py +255 -0
- attocode_intel/tools/history_tools.py +486 -0
- attocode_intel/tools/learning_tools.py +222 -0
- attocode_intel/tools/lsp_tools.py +218 -0
- attocode_intel/tools/maintenance_tools.py +1662 -0
- attocode_intel/tools/navigation_tools.py +220 -0
- attocode_intel/tools/overlay_tools.py +540 -0
- attocode_intel/tools/pin_store.py +432 -0
- attocode_intel/tools/pin_tools.py +266 -0
- attocode_intel/tools/query_constraints_tools.py +262 -0
- attocode_intel/tools/query_history_tools.py +158 -0
- attocode_intel/tools/readiness_tools.py +44 -0
- attocode_intel/tools/rule_tools.py +992 -0
- attocode_intel/tools/search_tools.py +770 -0
- attocode_intel/tools/snapshot_tools.py +624 -0
- attocode_intel/workers/__init__.py +1 -0
- attocode_intel/workers/job_utils.py +111 -0
- attocode_intel/workers/jobs.py +573 -0
- attocode_intel/workers/run.py +30 -0
- attocode_intel/workers/settings.py +61 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""Cross-encoder reranking for search results.
|
|
2
|
+
|
|
3
|
+
Provides optional reranking using a cross-encoder model to improve
|
|
4
|
+
precision after initial retrieval. Gracefully degrades if the model
|
|
5
|
+
is not available.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import hmac
|
|
12
|
+
import logging
|
|
13
|
+
import math
|
|
14
|
+
import re
|
|
15
|
+
import threading
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Protocol
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Singleton cache for the cross-encoder model
|
|
23
|
+
_reranker_instance: CrossEncoderReranker | None = None
|
|
24
|
+
_reranker_lock = threading.Lock()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_reranker(
|
|
28
|
+
model_name: str = "cross-encoder/ms-marco-MiniLM-L-6-v2",
|
|
29
|
+
) -> CrossEncoderReranker:
|
|
30
|
+
"""Get or create the singleton CrossEncoderReranker instance."""
|
|
31
|
+
global _reranker_instance
|
|
32
|
+
if _reranker_instance is not None and _reranker_instance._model_name == model_name:
|
|
33
|
+
return _reranker_instance
|
|
34
|
+
with _reranker_lock:
|
|
35
|
+
# Double-check after acquiring lock
|
|
36
|
+
if _reranker_instance is not None and _reranker_instance._model_name == model_name:
|
|
37
|
+
return _reranker_instance
|
|
38
|
+
_reranker_instance = CrossEncoderReranker(model_name=model_name)
|
|
39
|
+
return _reranker_instance
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CrossEncoderReranker:
|
|
43
|
+
"""Optional cross-encoder reranking for search results.
|
|
44
|
+
|
|
45
|
+
Uses a lightweight cross-encoder model to score (query, document) pairs
|
|
46
|
+
for more precise relevance ranking. Falls back to returning candidates
|
|
47
|
+
unchanged if the model cannot be loaded.
|
|
48
|
+
|
|
49
|
+
Usage::
|
|
50
|
+
|
|
51
|
+
reranker = get_reranker()
|
|
52
|
+
reranked = reranker.rerank(
|
|
53
|
+
query="authentication middleware",
|
|
54
|
+
candidates=[("id1", "text1", 0.8), ("id2", "text2", 0.6)],
|
|
55
|
+
top_k=10,
|
|
56
|
+
)
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self, model_name: str = "cross-encoder/ms-marco-MiniLM-L-6-v2",
|
|
61
|
+
) -> None:
|
|
62
|
+
self._model_name = model_name
|
|
63
|
+
self._model: Any = None
|
|
64
|
+
self._available = False
|
|
65
|
+
self._load_attempted = False
|
|
66
|
+
self._load_lock = threading.Lock()
|
|
67
|
+
|
|
68
|
+
def _ensure_loaded(self) -> bool:
|
|
69
|
+
"""Lazy-load the cross-encoder model. Returns True if available."""
|
|
70
|
+
if self._load_attempted:
|
|
71
|
+
return self._available
|
|
72
|
+
with self._load_lock:
|
|
73
|
+
if self._load_attempted:
|
|
74
|
+
return self._available
|
|
75
|
+
self._load_attempted = True
|
|
76
|
+
try:
|
|
77
|
+
from sentence_transformers import CrossEncoder
|
|
78
|
+
|
|
79
|
+
self._model = CrossEncoder(self._model_name)
|
|
80
|
+
self._available = True
|
|
81
|
+
logger.info(
|
|
82
|
+
"Cross-encoder reranker loaded: %s", self._model_name,
|
|
83
|
+
)
|
|
84
|
+
except ImportError:
|
|
85
|
+
logger.warning(
|
|
86
|
+
"sentence-transformers not installed; "
|
|
87
|
+
"cross-encoder reranking disabled. "
|
|
88
|
+
"Install with: pip install sentence-transformers",
|
|
89
|
+
)
|
|
90
|
+
self._available = False
|
|
91
|
+
except Exception:
|
|
92
|
+
logger.warning(
|
|
93
|
+
"Failed to load cross-encoder model %s; "
|
|
94
|
+
"reranking disabled",
|
|
95
|
+
self._model_name,
|
|
96
|
+
exc_info=True,
|
|
97
|
+
)
|
|
98
|
+
self._available = False
|
|
99
|
+
return self._available
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def is_available(self) -> bool:
|
|
103
|
+
"""Check if the reranker model is loaded and usable."""
|
|
104
|
+
return self._available
|
|
105
|
+
|
|
106
|
+
def rerank(
|
|
107
|
+
self,
|
|
108
|
+
query: str,
|
|
109
|
+
candidates: list[tuple[str, str, float]],
|
|
110
|
+
top_k: int = 10,
|
|
111
|
+
) -> list[tuple[str, str, float]]:
|
|
112
|
+
"""Rerank candidates using the cross-encoder model.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
query: The search query.
|
|
116
|
+
candidates: List of (id, text, original_score) tuples.
|
|
117
|
+
top_k: Number of results to return after reranking.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Reranked list of (id, text, reranker_score) tuples,
|
|
121
|
+
or the original candidates (truncated to top_k) if
|
|
122
|
+
reranking is not available.
|
|
123
|
+
"""
|
|
124
|
+
if not candidates:
|
|
125
|
+
return []
|
|
126
|
+
|
|
127
|
+
if not self._ensure_loaded():
|
|
128
|
+
# Graceful degradation: return candidates as-is
|
|
129
|
+
return candidates[:top_k]
|
|
130
|
+
|
|
131
|
+
# Build (query, document) pairs for cross-encoder scoring
|
|
132
|
+
pairs = [(query, text) for _, text, _ in candidates]
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
scores = self._model.predict(pairs)
|
|
136
|
+
except Exception:
|
|
137
|
+
logger.warning(
|
|
138
|
+
"Cross-encoder prediction failed; returning original ranking",
|
|
139
|
+
exc_info=True,
|
|
140
|
+
)
|
|
141
|
+
return candidates[:top_k]
|
|
142
|
+
|
|
143
|
+
# Attach scores and sort descending
|
|
144
|
+
scored = [
|
|
145
|
+
(cid, text, float(score))
|
|
146
|
+
for (cid, text, _), score in zip(candidates, scores, strict=False)
|
|
147
|
+
]
|
|
148
|
+
scored.sort(key=lambda x: x[2], reverse=True)
|
|
149
|
+
return scored[:top_k]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def model_tree_sha256(model_path: str | Path) -> str:
|
|
153
|
+
"""Fingerprint a locally installed model, including every file's contents.
|
|
154
|
+
|
|
155
|
+
The digest is for deployment pinning, not for use on a search request. The
|
|
156
|
+
caller can compute it once when installing weights and configure that value
|
|
157
|
+
as ``expected_sha256``. Symlinks are rejected so the fingerprint cannot
|
|
158
|
+
silently depend on files outside the chosen model directory.
|
|
159
|
+
"""
|
|
160
|
+
path = Path(model_path)
|
|
161
|
+
if not path.is_absolute() or not path.is_dir() or path.is_symlink():
|
|
162
|
+
raise ValueError("model_path must be an existing absolute directory")
|
|
163
|
+
digest = hashlib.sha256()
|
|
164
|
+
files = sorted(path.rglob("*"))
|
|
165
|
+
if not files or not any(item.is_file() for item in files):
|
|
166
|
+
raise ValueError("model_path contains no files")
|
|
167
|
+
for item in files:
|
|
168
|
+
if item.is_symlink():
|
|
169
|
+
raise ValueError("model_path must not contain symlinks")
|
|
170
|
+
if not item.is_file():
|
|
171
|
+
continue
|
|
172
|
+
relative = item.relative_to(path).as_posix().encode("utf-8")
|
|
173
|
+
digest.update(len(relative).to_bytes(8, "big"))
|
|
174
|
+
digest.update(relative)
|
|
175
|
+
with item.open("rb") as source:
|
|
176
|
+
while chunk := source.read(1024 * 1024):
|
|
177
|
+
digest.update(chunk)
|
|
178
|
+
return digest.hexdigest()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@dataclass(frozen=True)
|
|
182
|
+
class LocalRerankOutcome:
|
|
183
|
+
"""Result plus a compact, source-free fallback reason for provenance."""
|
|
184
|
+
|
|
185
|
+
candidates: list[tuple[str, str, float]]
|
|
186
|
+
reranked: bool
|
|
187
|
+
fallback_reason: str | None = None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
RankingOutcome = LocalRerankOutcome
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class RankingProvider(Protocol):
|
|
194
|
+
"""Shared shortlist contract for local and SystemOne-compatible adapters."""
|
|
195
|
+
|
|
196
|
+
max_candidates: int
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def status(self) -> str: ...
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def is_available(self) -> bool: ...
|
|
203
|
+
|
|
204
|
+
def rerank_result(
|
|
205
|
+
self, query: str, candidates: list[tuple[str, str, float]], top_k: int = 10,
|
|
206
|
+
) -> RankingOutcome: ...
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class LocalCrossEncoderReranker:
|
|
210
|
+
"""Opt-in offline reranking with preinstalled, fingerprint-pinned weights.
|
|
211
|
+
|
|
212
|
+
Construction never loads weights. ``start_prewarm`` does that in a daemon
|
|
213
|
+
worker; until it completes, search calls use the original order. At most
|
|
214
|
+
one prediction runs at a time, and a timed-out prediction keeps the slot
|
|
215
|
+
until it finishes, preventing an unbounded queue of background work.
|
|
216
|
+
This class deliberately does not replace the legacy lazy-loading reranker.
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
def __init__(
|
|
220
|
+
self,
|
|
221
|
+
model_path: str | Path,
|
|
222
|
+
expected_sha256: str,
|
|
223
|
+
*,
|
|
224
|
+
max_candidates: int = 24,
|
|
225
|
+
max_query_chars: int = 512,
|
|
226
|
+
max_excerpt_chars: int = 2048,
|
|
227
|
+
timeout_seconds: float = 1.5,
|
|
228
|
+
) -> None:
|
|
229
|
+
path = Path(model_path)
|
|
230
|
+
if not path.is_absolute():
|
|
231
|
+
raise ValueError("model_path must be absolute")
|
|
232
|
+
if re.fullmatch(r"[0-9a-fA-F]{64}", expected_sha256) is None:
|
|
233
|
+
raise ValueError("expected_sha256 must be a SHA-256 hex digest")
|
|
234
|
+
if min(max_candidates, max_query_chars, max_excerpt_chars) < 1:
|
|
235
|
+
raise ValueError("input bounds must be positive")
|
|
236
|
+
if timeout_seconds <= 0:
|
|
237
|
+
raise ValueError("timeout_seconds must be positive")
|
|
238
|
+
self.model_path = path
|
|
239
|
+
self.expected_sha256 = expected_sha256.lower()
|
|
240
|
+
self.max_candidates = max_candidates
|
|
241
|
+
self.max_query_chars = max_query_chars
|
|
242
|
+
self.max_excerpt_chars = max_excerpt_chars
|
|
243
|
+
self.timeout_seconds = timeout_seconds
|
|
244
|
+
self._model: Any = None
|
|
245
|
+
self._status = "cold"
|
|
246
|
+
self._state_lock = threading.Lock()
|
|
247
|
+
self._prediction_lock = threading.Lock()
|
|
248
|
+
|
|
249
|
+
@property
|
|
250
|
+
def status(self) -> str:
|
|
251
|
+
"""One of ``cold``, ``loading``, ``ready``, or ``failed``."""
|
|
252
|
+
with self._state_lock:
|
|
253
|
+
return self._status
|
|
254
|
+
|
|
255
|
+
@property
|
|
256
|
+
def is_available(self) -> bool:
|
|
257
|
+
return self.status == "ready"
|
|
258
|
+
|
|
259
|
+
def start_prewarm(self) -> bool:
|
|
260
|
+
"""Begin one asynchronous, offline load. Return False if already begun."""
|
|
261
|
+
with self._state_lock:
|
|
262
|
+
if self._status != "cold":
|
|
263
|
+
return False
|
|
264
|
+
self._status = "loading"
|
|
265
|
+
threading.Thread(target=self._prewarm, name="local-reranker-prewarm", daemon=True).start()
|
|
266
|
+
return True
|
|
267
|
+
|
|
268
|
+
def _prewarm(self) -> None:
|
|
269
|
+
try:
|
|
270
|
+
actual_sha256 = model_tree_sha256(self.model_path)
|
|
271
|
+
if not hmac.compare_digest(actual_sha256, self.expected_sha256):
|
|
272
|
+
raise ValueError("model fingerprint mismatch")
|
|
273
|
+
from sentence_transformers import CrossEncoder
|
|
274
|
+
|
|
275
|
+
model = CrossEncoder(
|
|
276
|
+
str(self.model_path),
|
|
277
|
+
local_files_only=True,
|
|
278
|
+
trust_remote_code=False,
|
|
279
|
+
)
|
|
280
|
+
except Exception:
|
|
281
|
+
# Never log a source snippet or a model's local path.
|
|
282
|
+
logger.warning("Local cross-encoder prewarm failed; deterministic ranking remains active")
|
|
283
|
+
with self._state_lock:
|
|
284
|
+
self._status = "failed"
|
|
285
|
+
return
|
|
286
|
+
with self._state_lock:
|
|
287
|
+
self._model = model
|
|
288
|
+
self._status = "ready"
|
|
289
|
+
|
|
290
|
+
def rerank_result(
|
|
291
|
+
self,
|
|
292
|
+
query: str,
|
|
293
|
+
candidates: list[tuple[str, str, float]],
|
|
294
|
+
top_k: int = 10,
|
|
295
|
+
) -> LocalRerankOutcome:
|
|
296
|
+
"""Rerank bounded pairs, or immediately return deterministic order."""
|
|
297
|
+
fallback = candidates[:max(0, top_k)]
|
|
298
|
+
if top_k <= 0 or not candidates:
|
|
299
|
+
return LocalRerankOutcome(fallback, False, "empty")
|
|
300
|
+
if not query.strip():
|
|
301
|
+
return LocalRerankOutcome(fallback, False, "empty_query")
|
|
302
|
+
if not self.is_available:
|
|
303
|
+
return LocalRerankOutcome(fallback, False, self.status)
|
|
304
|
+
if not self._prediction_lock.acquire(blocking=False):
|
|
305
|
+
return LocalRerankOutcome(fallback, False, "busy")
|
|
306
|
+
|
|
307
|
+
bounded = candidates[: self.max_candidates]
|
|
308
|
+
pairs = [
|
|
309
|
+
(query[: self.max_query_chars], excerpt[: self.max_excerpt_chars])
|
|
310
|
+
for _, excerpt, _ in bounded
|
|
311
|
+
]
|
|
312
|
+
done = threading.Event()
|
|
313
|
+
output: list[Any] = []
|
|
314
|
+
|
|
315
|
+
def predict() -> None:
|
|
316
|
+
try:
|
|
317
|
+
output.append(self._model.predict(pairs, batch_size=8, show_progress_bar=False))
|
|
318
|
+
except Exception:
|
|
319
|
+
output.append(None)
|
|
320
|
+
finally:
|
|
321
|
+
self._prediction_lock.release()
|
|
322
|
+
done.set()
|
|
323
|
+
|
|
324
|
+
threading.Thread(target=predict, name="local-reranker-predict", daemon=True).start()
|
|
325
|
+
if not done.wait(self.timeout_seconds):
|
|
326
|
+
return LocalRerankOutcome(fallback, False, "timeout")
|
|
327
|
+
scores = output[0] if output else None
|
|
328
|
+
try:
|
|
329
|
+
if scores is None or len(scores) != len(bounded):
|
|
330
|
+
raise ValueError("invalid score count")
|
|
331
|
+
numeric_scores = [float(score) for score in scores]
|
|
332
|
+
if not all(math.isfinite(score) for score in numeric_scores):
|
|
333
|
+
raise ValueError("non-finite score")
|
|
334
|
+
except (TypeError, ValueError, OverflowError):
|
|
335
|
+
return LocalRerankOutcome(fallback, False, "prediction_failed")
|
|
336
|
+
scored = [
|
|
337
|
+
(candidate_id, excerpt, score)
|
|
338
|
+
for (candidate_id, excerpt, _), score in zip(bounded, numeric_scores, strict=True)
|
|
339
|
+
]
|
|
340
|
+
scored.sort(key=lambda item: item[2], reverse=True)
|
|
341
|
+
# Never drop an unscored tail if a caller requests more than the cap.
|
|
342
|
+
return LocalRerankOutcome((scored + candidates[self.max_candidates :])[:top_k], True)
|
|
343
|
+
|
|
344
|
+
def rerank(
|
|
345
|
+
self,
|
|
346
|
+
query: str,
|
|
347
|
+
candidates: list[tuple[str, str, float]],
|
|
348
|
+
top_k: int = 10,
|
|
349
|
+
) -> list[tuple[str, str, float]]:
|
|
350
|
+
"""Compatibility-shaped wrapper for callers that only need results."""
|
|
351
|
+
return self.rerank_result(query, candidates, top_k).candidates
|