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,198 @@
|
|
|
1
|
+
"""Cross-repository search endpoint — search across all repos in an org."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import uuid
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
from sqlalchemy import select
|
|
11
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
12
|
+
|
|
13
|
+
from attocode_intel.api.auth import resolve_auth
|
|
14
|
+
from attocode_intel.api.auth.context import AuthContext
|
|
15
|
+
from attocode_intel.api.deps import get_db_session
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
router = APIRouter(
|
|
20
|
+
prefix="/api/v2/orgs/{org_id}",
|
|
21
|
+
tags=["cross-repo-search"],
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# --- Request/Response models ---
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CrossRepoSearchRequest(BaseModel):
|
|
29
|
+
query: str
|
|
30
|
+
repo_ids: list[str] = []
|
|
31
|
+
top_k: int = 20
|
|
32
|
+
file_filter: str = ""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CrossRepoSearchResultItem(BaseModel):
|
|
36
|
+
repo_id: str = ""
|
|
37
|
+
repo_name: str = ""
|
|
38
|
+
file_path: str = ""
|
|
39
|
+
score: float = 0.0
|
|
40
|
+
snippet: str = ""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class CrossRepoSearchResponse(BaseModel):
|
|
44
|
+
query: str
|
|
45
|
+
results: list[CrossRepoSearchResultItem]
|
|
46
|
+
total: int
|
|
47
|
+
repos_searched: int = 0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# --- Endpoint ---
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@router.post("/search", response_model=CrossRepoSearchResponse)
|
|
54
|
+
async def cross_repo_search(
|
|
55
|
+
org_id: uuid.UUID,
|
|
56
|
+
req: CrossRepoSearchRequest,
|
|
57
|
+
auth: AuthContext = Depends(resolve_auth),
|
|
58
|
+
session: AsyncSession = Depends(get_db_session),
|
|
59
|
+
) -> CrossRepoSearchResponse:
|
|
60
|
+
"""Semantic search across multiple repositories in an organization.
|
|
61
|
+
|
|
62
|
+
If repo_ids is empty, searches all repos in the org.
|
|
63
|
+
Results are sorted globally by cosine similarity score.
|
|
64
|
+
"""
|
|
65
|
+
from attocode_intel.api.deps import get_config
|
|
66
|
+
from attocode_intel.db.models import Branch, OrgMembership, Repository
|
|
67
|
+
from attocode_intel.storage.branch_overlay import BranchOverlay
|
|
68
|
+
from attocode_intel.storage.embedding_store import EmbeddingStore
|
|
69
|
+
from attocode_intel._internal.integrations.context.embeddings import create_embedding_provider
|
|
70
|
+
|
|
71
|
+
# 1. Verify org membership
|
|
72
|
+
if not auth.user_id:
|
|
73
|
+
raise HTTPException(status_code=403, detail="Authentication required")
|
|
74
|
+
|
|
75
|
+
result = await session.execute(
|
|
76
|
+
select(OrgMembership).where(
|
|
77
|
+
OrgMembership.org_id == org_id,
|
|
78
|
+
OrgMembership.user_id == auth.user_id,
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
membership = result.scalar_one_or_none()
|
|
82
|
+
if membership is None:
|
|
83
|
+
raise HTTPException(status_code=403, detail="Not a member of this organization")
|
|
84
|
+
|
|
85
|
+
# 2. Resolve target repositories
|
|
86
|
+
if req.repo_ids:
|
|
87
|
+
# Filter to specified repos within the org
|
|
88
|
+
repo_uuids = []
|
|
89
|
+
for rid in req.repo_ids:
|
|
90
|
+
try:
|
|
91
|
+
repo_uuids.append(uuid.UUID(rid))
|
|
92
|
+
except ValueError:
|
|
93
|
+
raise HTTPException(
|
|
94
|
+
status_code=400,
|
|
95
|
+
detail=f"Invalid repo_id format: {rid}",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
result = await session.execute(
|
|
99
|
+
select(Repository).where(
|
|
100
|
+
Repository.org_id == org_id,
|
|
101
|
+
Repository.id.in_(repo_uuids),
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
# All repos in the org
|
|
106
|
+
result = await session.execute(
|
|
107
|
+
select(Repository).where(Repository.org_id == org_id)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
repos = result.scalars().all()
|
|
111
|
+
if not repos:
|
|
112
|
+
return CrossRepoSearchResponse(
|
|
113
|
+
query=req.query, results=[], total=0, repos_searched=0,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Build repo_id → name mapping
|
|
117
|
+
repo_name_map: dict[str, str] = {str(r.id): r.name for r in repos}
|
|
118
|
+
|
|
119
|
+
# 3. For each repo, get default branch and resolve manifest
|
|
120
|
+
overlay = BranchOverlay(session)
|
|
121
|
+
branch_manifests: dict[str, dict[str, str]] = {}
|
|
122
|
+
|
|
123
|
+
for repo in repos:
|
|
124
|
+
# Find the default branch
|
|
125
|
+
branch_result = await session.execute(
|
|
126
|
+
select(Branch).where(
|
|
127
|
+
Branch.repo_id == repo.id,
|
|
128
|
+
Branch.name == repo.default_branch,
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
branch = branch_result.scalar_one_or_none()
|
|
132
|
+
if branch is None:
|
|
133
|
+
# Try is_default flag as fallback
|
|
134
|
+
branch_result = await session.execute(
|
|
135
|
+
select(Branch).where(
|
|
136
|
+
Branch.repo_id == repo.id,
|
|
137
|
+
Branch.is_default == True, # noqa: E712
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
branch = branch_result.scalar_one_or_none()
|
|
141
|
+
|
|
142
|
+
if branch is None:
|
|
143
|
+
logger.debug(
|
|
144
|
+
"Skipping repo %s (%s): no default branch found",
|
|
145
|
+
repo.name, repo.id,
|
|
146
|
+
)
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
manifest = await overlay.resolve_manifest(branch.id)
|
|
150
|
+
if manifest:
|
|
151
|
+
branch_manifests[str(repo.id)] = manifest
|
|
152
|
+
|
|
153
|
+
if not branch_manifests:
|
|
154
|
+
return CrossRepoSearchResponse(
|
|
155
|
+
query=req.query, results=[], total=0, repos_searched=0,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# 4. Embed query
|
|
159
|
+
config = get_config()
|
|
160
|
+
provider = create_embedding_provider(config.embedding_model)
|
|
161
|
+
if provider.name == "none":
|
|
162
|
+
raise HTTPException(
|
|
163
|
+
status_code=503,
|
|
164
|
+
detail="No embedding provider available for semantic search",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
query_vectors = provider.embed([req.query])
|
|
168
|
+
query_vector = query_vectors[0]
|
|
169
|
+
|
|
170
|
+
# 5. Single pgvector query across all repos
|
|
171
|
+
store = EmbeddingStore(session)
|
|
172
|
+
results = await store.multi_branch_similarity_search(
|
|
173
|
+
session=session,
|
|
174
|
+
branch_manifests=branch_manifests,
|
|
175
|
+
query_vector=query_vector,
|
|
176
|
+
top_k=req.top_k,
|
|
177
|
+
model=provider.name,
|
|
178
|
+
file_filter=req.file_filter,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# 6. Build response with repo context
|
|
182
|
+
items = [
|
|
183
|
+
CrossRepoSearchResultItem(
|
|
184
|
+
repo_id=r["repo_id"],
|
|
185
|
+
repo_name=repo_name_map.get(r["repo_id"], "unknown"),
|
|
186
|
+
file_path=r["file"],
|
|
187
|
+
score=r["score"],
|
|
188
|
+
snippet=r.get("chunk_text", ""),
|
|
189
|
+
)
|
|
190
|
+
for r in results
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
return CrossRepoSearchResponse(
|
|
194
|
+
query=req.query,
|
|
195
|
+
results=items,
|
|
196
|
+
total=len(items),
|
|
197
|
+
repos_searched=len(branch_manifests),
|
|
198
|
+
)
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"""Embedding and indexing status dashboard endpoints (service mode only)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import uuid
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
from sqlalchemy import func, select
|
|
11
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
12
|
+
|
|
13
|
+
from attocode_intel.api.auth import resolve_auth
|
|
14
|
+
from attocode_intel.api.auth.context import AuthContext
|
|
15
|
+
from attocode_intel.api.deps import get_branch_context, get_db_session
|
|
16
|
+
|
|
17
|
+
router = APIRouter(prefix="/api/v2/projects/{project_id}", tags=["embeddings"])
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# --- Response models ---
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class EmbeddingStatusResponse(BaseModel):
|
|
25
|
+
total_files: int
|
|
26
|
+
embedded_files: int
|
|
27
|
+
coverage_pct: float
|
|
28
|
+
model: str
|
|
29
|
+
provider_available: bool = True
|
|
30
|
+
provider_hint: str = ""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class FileEmbeddingStatus(BaseModel):
|
|
34
|
+
path: str
|
|
35
|
+
content_sha: str
|
|
36
|
+
has_embedding: bool
|
|
37
|
+
chunk_count: int = 0
|
|
38
|
+
last_embedded_at: str | None = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class FileEmbeddingListResponse(BaseModel):
|
|
42
|
+
files: list[FileEmbeddingStatus]
|
|
43
|
+
total: int
|
|
44
|
+
limit: int
|
|
45
|
+
offset: int
|
|
46
|
+
has_more: bool
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class SimilarFileItem(BaseModel):
|
|
50
|
+
file_path: str
|
|
51
|
+
score: float
|
|
52
|
+
snippet: str = ""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class FindSimilarResponse(BaseModel):
|
|
56
|
+
source_file: str
|
|
57
|
+
similar: list[SimilarFileItem]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class IndexingStatusResponse(BaseModel):
|
|
61
|
+
index_status: str
|
|
62
|
+
last_indexed_at: str | None = None
|
|
63
|
+
active_jobs: int = 0
|
|
64
|
+
embedding_coverage_pct: float = 0.0
|
|
65
|
+
total_files: int = 0
|
|
66
|
+
embedded_files: int = 0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# --- Helpers ---
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def _get_repo(project_id: uuid.UUID, session: AsyncSession):
|
|
73
|
+
from attocode_intel.db.models import Repository
|
|
74
|
+
|
|
75
|
+
result = await session.execute(select(Repository).where(Repository.id == project_id))
|
|
76
|
+
repo = result.scalar_one_or_none()
|
|
77
|
+
if repo is None:
|
|
78
|
+
raise HTTPException(status_code=404, detail="Repository not found")
|
|
79
|
+
return repo
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _resolve_model_name(model: str) -> str:
|
|
83
|
+
"""Resolve 'default' to the actual embedding provider name."""
|
|
84
|
+
if model != "default":
|
|
85
|
+
return model
|
|
86
|
+
try:
|
|
87
|
+
from attocode_intel.api.deps import get_config
|
|
88
|
+
from attocode_intel._internal.integrations.context.embeddings import create_embedding_provider
|
|
89
|
+
config = get_config()
|
|
90
|
+
provider = create_embedding_provider(config.embedding_model)
|
|
91
|
+
return provider.name
|
|
92
|
+
except Exception:
|
|
93
|
+
return model
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# --- Endpoints ---
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@router.get("/embeddings/status", response_model=EmbeddingStatusResponse)
|
|
100
|
+
async def get_embedding_status(
|
|
101
|
+
project_id: uuid.UUID,
|
|
102
|
+
ref: str = Query("", description="Git ref. Defaults to default branch."),
|
|
103
|
+
model: str = Query("default", description="Embedding model name."),
|
|
104
|
+
auth: AuthContext = Depends(resolve_auth),
|
|
105
|
+
session: AsyncSession = Depends(get_db_session),
|
|
106
|
+
) -> EmbeddingStatusResponse:
|
|
107
|
+
"""Get embedding coverage for a repo branch."""
|
|
108
|
+
repo = await _get_repo(project_id, session)
|
|
109
|
+
resolved_ref = ref if ref else repo.default_branch
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
branch_ctx = await get_branch_context(project_id, resolved_ref, session)
|
|
113
|
+
except HTTPException:
|
|
114
|
+
return EmbeddingStatusResponse(
|
|
115
|
+
total_files=0, embedded_files=0, coverage_pct=0.0, model=model,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
content_shas = branch_ctx.content_shas
|
|
119
|
+
total = len(content_shas)
|
|
120
|
+
if total == 0:
|
|
121
|
+
return EmbeddingStatusResponse(
|
|
122
|
+
total_files=0, embedded_files=0, coverage_pct=0.0, model=model,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
from attocode_intel.storage.embedding_store import EmbeddingStore
|
|
126
|
+
|
|
127
|
+
model = _resolve_model_name(model)
|
|
128
|
+
store = EmbeddingStore(session)
|
|
129
|
+
embedded_shas = await store.batch_has_embeddings(content_shas, model)
|
|
130
|
+
embedded = len(embedded_shas)
|
|
131
|
+
|
|
132
|
+
response = EmbeddingStatusResponse(
|
|
133
|
+
total_files=total,
|
|
134
|
+
embedded_files=embedded,
|
|
135
|
+
coverage_pct=round(embedded / total * 100, 1) if total > 0 else 0.0,
|
|
136
|
+
model=model,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
from attocode_intel._internal.integrations.context.embeddings import create_embedding_provider
|
|
140
|
+
from attocode_intel.api.deps import get_config
|
|
141
|
+
|
|
142
|
+
config = get_config()
|
|
143
|
+
try:
|
|
144
|
+
provider = create_embedding_provider(config.embedding_model)
|
|
145
|
+
if provider.name == "none":
|
|
146
|
+
response.provider_available = False
|
|
147
|
+
response.provider_hint = (
|
|
148
|
+
"No embedding provider detected. "
|
|
149
|
+
"Install sentence-transformers (`pip install attocode[semantic]`) or set OPENAI_API_KEY."
|
|
150
|
+
)
|
|
151
|
+
except (ImportError, RuntimeError) as exc:
|
|
152
|
+
response.provider_available = False
|
|
153
|
+
response.provider_hint = str(exc)
|
|
154
|
+
|
|
155
|
+
return response
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@router.get("/embeddings/files", response_model=FileEmbeddingListResponse)
|
|
159
|
+
async def get_file_embedding_status(
|
|
160
|
+
project_id: uuid.UUID,
|
|
161
|
+
ref: str = Query("", description="Git ref. Defaults to default branch."),
|
|
162
|
+
model: str = Query("default", description="Embedding model name."),
|
|
163
|
+
limit: int = Query(50, ge=1, le=200),
|
|
164
|
+
offset: int = Query(0, ge=0),
|
|
165
|
+
search: str = Query("", description="Filter files by path substring (case-insensitive)."),
|
|
166
|
+
auth: AuthContext = Depends(resolve_auth),
|
|
167
|
+
session: AsyncSession = Depends(get_db_session),
|
|
168
|
+
) -> FileEmbeddingListResponse:
|
|
169
|
+
"""Get per-file embedding status (paginated)."""
|
|
170
|
+
repo = await _get_repo(project_id, session)
|
|
171
|
+
resolved_ref = ref if ref else repo.default_branch
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
branch_ctx = await get_branch_context(project_id, resolved_ref, session)
|
|
175
|
+
except HTTPException:
|
|
176
|
+
return FileEmbeddingListResponse(
|
|
177
|
+
files=[], total=0, limit=limit, offset=offset, has_more=False,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
manifest = branch_ctx.manifest
|
|
181
|
+
content_shas = branch_ctx.content_shas
|
|
182
|
+
total = len(manifest)
|
|
183
|
+
|
|
184
|
+
# Batch check which SHAs have embeddings
|
|
185
|
+
from attocode_intel.storage.embedding_store import EmbeddingStore
|
|
186
|
+
|
|
187
|
+
model = _resolve_model_name(model)
|
|
188
|
+
store = EmbeddingStore(session)
|
|
189
|
+
stats = await store.batch_embedding_stats(content_shas, model)
|
|
190
|
+
|
|
191
|
+
# Paginate over sorted manifest entries
|
|
192
|
+
sorted_paths = sorted(manifest.keys())
|
|
193
|
+
if search:
|
|
194
|
+
search_lower = search.lower()
|
|
195
|
+
sorted_paths = [p for p in sorted_paths if search_lower in p.lower()]
|
|
196
|
+
total = len(sorted_paths) # recalculate after filtering
|
|
197
|
+
page = sorted_paths[offset:offset + limit]
|
|
198
|
+
|
|
199
|
+
files = []
|
|
200
|
+
for path in page:
|
|
201
|
+
sha = manifest[path]
|
|
202
|
+
sha_stats = stats.get(sha)
|
|
203
|
+
files.append(FileEmbeddingStatus(
|
|
204
|
+
path=path,
|
|
205
|
+
content_sha=sha,
|
|
206
|
+
has_embedding=sha_stats is not None,
|
|
207
|
+
chunk_count=sha_stats["chunk_count"] if sha_stats else 0,
|
|
208
|
+
last_embedded_at=sha_stats["last_embedded"].isoformat() if sha_stats and sha_stats["last_embedded"] else None,
|
|
209
|
+
))
|
|
210
|
+
|
|
211
|
+
return FileEmbeddingListResponse(
|
|
212
|
+
files=files,
|
|
213
|
+
total=total,
|
|
214
|
+
limit=limit,
|
|
215
|
+
offset=offset,
|
|
216
|
+
has_more=(offset + limit < total),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@router.post("/embeddings/similar", response_model=FindSimilarResponse)
|
|
221
|
+
async def find_similar_files(
|
|
222
|
+
project_id: uuid.UUID,
|
|
223
|
+
content_sha: str = Query(..., description="Content SHA of source file"),
|
|
224
|
+
ref: str = Query("", description="Git ref. Defaults to default branch."),
|
|
225
|
+
auth: AuthContext = Depends(resolve_auth),
|
|
226
|
+
session: AsyncSession = Depends(get_db_session),
|
|
227
|
+
) -> FindSimilarResponse:
|
|
228
|
+
"""Find files similar to a given file by its content SHA."""
|
|
229
|
+
repo = await _get_repo(project_id, session)
|
|
230
|
+
resolved_ref = ref if ref else repo.default_branch
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
branch_ctx = await get_branch_context(project_id, resolved_ref, session)
|
|
234
|
+
except HTTPException:
|
|
235
|
+
return FindSimilarResponse(source_file="unknown", similar=[])
|
|
236
|
+
|
|
237
|
+
model = _resolve_model_name("default")
|
|
238
|
+
|
|
239
|
+
from attocode_intel.storage.embedding_store import EmbeddingStore
|
|
240
|
+
|
|
241
|
+
store = EmbeddingStore(session)
|
|
242
|
+
results = await store.find_similar_by_sha(
|
|
243
|
+
branch_ctx.branch_id, content_sha, top_k=10, model=model,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Resolve source file path
|
|
247
|
+
sha_to_path = {sha: path for path, sha in branch_ctx.manifest.items()}
|
|
248
|
+
source_file = sha_to_path.get(content_sha, "unknown")
|
|
249
|
+
|
|
250
|
+
return FindSimilarResponse(
|
|
251
|
+
source_file=source_file,
|
|
252
|
+
similar=[
|
|
253
|
+
SimilarFileItem(
|
|
254
|
+
file_path=r["file"],
|
|
255
|
+
score=r["score"],
|
|
256
|
+
snippet=r.get("chunk_text", "")[:200],
|
|
257
|
+
)
|
|
258
|
+
for r in results
|
|
259
|
+
],
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class GenerateEmbeddingsRequest(BaseModel):
|
|
264
|
+
branch: str = "main"
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class GenerateEmbeddingsResponse(BaseModel):
|
|
268
|
+
job_id: str
|
|
269
|
+
status: str
|
|
270
|
+
message: str
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@router.post("/embeddings/generate", response_model=GenerateEmbeddingsResponse)
|
|
274
|
+
async def trigger_generate_embeddings(
|
|
275
|
+
project_id: uuid.UUID,
|
|
276
|
+
req: GenerateEmbeddingsRequest = GenerateEmbeddingsRequest(),
|
|
277
|
+
auth: AuthContext = Depends(resolve_auth),
|
|
278
|
+
session: AsyncSession = Depends(get_db_session),
|
|
279
|
+
) -> GenerateEmbeddingsResponse:
|
|
280
|
+
"""Trigger embedding generation for a branch.
|
|
281
|
+
|
|
282
|
+
Enqueues an ARQ background job to generate embeddings for all files
|
|
283
|
+
in the specified branch that don't already have them.
|
|
284
|
+
"""
|
|
285
|
+
# Validate that the repository exists (raises 404 otherwise).
|
|
286
|
+
await _get_repo(project_id, session)
|
|
287
|
+
|
|
288
|
+
# Fail fast if no embedding provider is available
|
|
289
|
+
from attocode_intel._internal.integrations.context.embeddings import create_embedding_provider
|
|
290
|
+
from attocode_intel.api.deps import get_config
|
|
291
|
+
|
|
292
|
+
config = get_config()
|
|
293
|
+
try:
|
|
294
|
+
provider = create_embedding_provider(config.embedding_model)
|
|
295
|
+
except (ImportError, RuntimeError) as exc:
|
|
296
|
+
raise HTTPException(status_code=400, detail=str(exc))
|
|
297
|
+
if provider.name == "none":
|
|
298
|
+
raise HTTPException(
|
|
299
|
+
status_code=400,
|
|
300
|
+
detail="No embedding provider available. Install sentence-transformers or set OPENAI_API_KEY.",
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
# Enqueue the ARQ job
|
|
304
|
+
try:
|
|
305
|
+
from arq import create_pool
|
|
306
|
+
from arq.connections import RedisSettings
|
|
307
|
+
|
|
308
|
+
from attocode_intel.api.deps import get_config
|
|
309
|
+
|
|
310
|
+
config = get_config()
|
|
311
|
+
if not config.redis_url:
|
|
312
|
+
raise HTTPException(status_code=503, detail="Redis not configured — cannot enqueue jobs")
|
|
313
|
+
|
|
314
|
+
redis_settings = RedisSettings.from_dsn(config.redis_url)
|
|
315
|
+
pool = await create_pool(redis_settings)
|
|
316
|
+
job = await pool.enqueue_job(
|
|
317
|
+
"generate_embeddings",
|
|
318
|
+
str(project_id),
|
|
319
|
+
req.branch,
|
|
320
|
+
)
|
|
321
|
+
await pool.close()
|
|
322
|
+
|
|
323
|
+
return GenerateEmbeddingsResponse(
|
|
324
|
+
job_id=job.job_id if job else "unknown",
|
|
325
|
+
status="queued",
|
|
326
|
+
message=f"Embedding generation queued for branch '{req.branch}'",
|
|
327
|
+
)
|
|
328
|
+
except HTTPException:
|
|
329
|
+
raise
|
|
330
|
+
except Exception as e:
|
|
331
|
+
logger.exception("Failed to enqueue embedding generation job")
|
|
332
|
+
raise HTTPException(status_code=500, detail=f"Failed to enqueue job: {e}")
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@router.get("/indexing/status", response_model=IndexingStatusResponse)
|
|
336
|
+
async def get_indexing_status(
|
|
337
|
+
project_id: uuid.UUID,
|
|
338
|
+
ref: str = Query("", description="Git ref. Defaults to default branch."),
|
|
339
|
+
auth: AuthContext = Depends(resolve_auth),
|
|
340
|
+
session: AsyncSession = Depends(get_db_session),
|
|
341
|
+
) -> IndexingStatusResponse:
|
|
342
|
+
"""Get combined indexing + embedding progress."""
|
|
343
|
+
from attocode_intel.db.models import IndexingJob
|
|
344
|
+
|
|
345
|
+
repo = await _get_repo(project_id, session)
|
|
346
|
+
|
|
347
|
+
# Count active indexing jobs
|
|
348
|
+
active_result = await session.execute(
|
|
349
|
+
select(func.count()).select_from(
|
|
350
|
+
select(IndexingJob.id).where(
|
|
351
|
+
IndexingJob.repo_id == project_id,
|
|
352
|
+
IndexingJob.status.in_(["queued", "running"]),
|
|
353
|
+
).subquery()
|
|
354
|
+
)
|
|
355
|
+
)
|
|
356
|
+
active_jobs = active_result.scalar() or 0
|
|
357
|
+
|
|
358
|
+
# Get embedding coverage
|
|
359
|
+
resolved_ref = ref if ref else repo.default_branch
|
|
360
|
+
total_files = 0
|
|
361
|
+
embedded_files = 0
|
|
362
|
+
coverage_pct = 0.0
|
|
363
|
+
|
|
364
|
+
try:
|
|
365
|
+
branch_ctx = await get_branch_context(project_id, resolved_ref, session)
|
|
366
|
+
content_shas = branch_ctx.content_shas
|
|
367
|
+
total_files = len(content_shas)
|
|
368
|
+
|
|
369
|
+
if total_files > 0:
|
|
370
|
+
from attocode_intel.storage.embedding_store import EmbeddingStore
|
|
371
|
+
|
|
372
|
+
store = EmbeddingStore(session)
|
|
373
|
+
embedded_shas = await store.batch_has_embeddings(content_shas, _resolve_model_name("default"))
|
|
374
|
+
embedded_files = len(embedded_shas)
|
|
375
|
+
coverage_pct = round(embedded_files / total_files * 100, 1)
|
|
376
|
+
except HTTPException:
|
|
377
|
+
pass
|
|
378
|
+
|
|
379
|
+
return IndexingStatusResponse(
|
|
380
|
+
index_status=repo.index_status,
|
|
381
|
+
last_indexed_at=repo.last_indexed_at.isoformat() if repo.last_indexed_at else None,
|
|
382
|
+
active_jobs=active_jobs,
|
|
383
|
+
embedding_coverage_pct=coverage_pct,
|
|
384
|
+
total_files=total_files,
|
|
385
|
+
embedded_files=embedded_files,
|
|
386
|
+
)
|