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,160 @@
|
|
|
1
|
+
"""Graph visualization endpoint — D3-compatible JSON for force-directed graphs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from collections import deque
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter, Depends
|
|
9
|
+
|
|
10
|
+
from attocode_intel.api.auth import verify_auth
|
|
11
|
+
from attocode_intel.api.deps import get_service_or_404
|
|
12
|
+
|
|
13
|
+
router = APIRouter(
|
|
14
|
+
prefix="/api/v2/projects/{project_id}/graph-viz",
|
|
15
|
+
tags=["graph-viz"],
|
|
16
|
+
dependencies=[Depends(verify_auth)],
|
|
17
|
+
)
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@router.get("")
|
|
22
|
+
async def get_graph_data(
|
|
23
|
+
project_id: str,
|
|
24
|
+
root: str = "",
|
|
25
|
+
depth: int = 3,
|
|
26
|
+
max_nodes: int = 100,
|
|
27
|
+
) -> dict:
|
|
28
|
+
"""Return graph data in D3 force-directed compatible format.
|
|
29
|
+
|
|
30
|
+
If *root* is provided, performs BFS from that file up to *depth* hops.
|
|
31
|
+
Otherwise returns the top *max_nodes* files ranked by importance.
|
|
32
|
+
Community detection data is included when available.
|
|
33
|
+
"""
|
|
34
|
+
depth = min(max(depth, 1), 6)
|
|
35
|
+
max_nodes = min(max(max_nodes, 10), 500)
|
|
36
|
+
|
|
37
|
+
svc = await get_service_or_404(project_id)
|
|
38
|
+
|
|
39
|
+
# Get underlying data structures
|
|
40
|
+
ast_svc = svc._get_ast_service()
|
|
41
|
+
ctx = svc._get_context_mgr()
|
|
42
|
+
idx = ast_svc._index
|
|
43
|
+
files_list = ctx._files # list[FileInfo]
|
|
44
|
+
|
|
45
|
+
# Build lookup: relative_path -> FileInfo
|
|
46
|
+
file_info: dict[str, object] = {}
|
|
47
|
+
for fi in files_list:
|
|
48
|
+
file_info[fi.relative_path] = fi
|
|
49
|
+
|
|
50
|
+
# Determine which files to include
|
|
51
|
+
selected_files: list[str]
|
|
52
|
+
|
|
53
|
+
if root:
|
|
54
|
+
# BFS from root up to depth hops (both directions)
|
|
55
|
+
rel_root = ast_svc._to_rel(root)
|
|
56
|
+
visited: set[str] = set()
|
|
57
|
+
queue: deque[tuple[str, int]] = deque([(rel_root, 0)])
|
|
58
|
+
order: list[str] = []
|
|
59
|
+
|
|
60
|
+
while queue and len(order) < max_nodes:
|
|
61
|
+
current, d = queue.popleft()
|
|
62
|
+
if current in visited:
|
|
63
|
+
continue
|
|
64
|
+
visited.add(current)
|
|
65
|
+
order.append(current)
|
|
66
|
+
if d < depth:
|
|
67
|
+
# Expand both directions
|
|
68
|
+
for neighbor in sorted(ast_svc.get_dependencies(current)):
|
|
69
|
+
if neighbor not in visited:
|
|
70
|
+
queue.append((neighbor, d + 1))
|
|
71
|
+
for neighbor in sorted(ast_svc.get_dependents(current)):
|
|
72
|
+
if neighbor not in visited:
|
|
73
|
+
queue.append((neighbor, d + 1))
|
|
74
|
+
|
|
75
|
+
selected_files = order
|
|
76
|
+
else:
|
|
77
|
+
# Top files by importance
|
|
78
|
+
ranked = sorted(files_list, key=lambda f: f.importance, reverse=True)
|
|
79
|
+
# Only include files that participate in the dependency graph
|
|
80
|
+
graph_files: set[str] = set()
|
|
81
|
+
graph_files.update(idx.file_dependencies.keys())
|
|
82
|
+
graph_files.update(idx.file_dependents.keys())
|
|
83
|
+
for deps in idx.file_dependencies.values():
|
|
84
|
+
graph_files.update(deps)
|
|
85
|
+
|
|
86
|
+
selected_files = []
|
|
87
|
+
for fi in ranked:
|
|
88
|
+
if fi.relative_path in graph_files:
|
|
89
|
+
selected_files.append(fi.relative_path)
|
|
90
|
+
if len(selected_files) >= max_nodes:
|
|
91
|
+
break
|
|
92
|
+
|
|
93
|
+
# If we still have room, add remaining important files
|
|
94
|
+
if len(selected_files) < max_nodes:
|
|
95
|
+
seen = set(selected_files)
|
|
96
|
+
for fi in ranked:
|
|
97
|
+
if fi.relative_path not in seen:
|
|
98
|
+
selected_files.append(fi.relative_path)
|
|
99
|
+
seen.add(fi.relative_path)
|
|
100
|
+
if len(selected_files) >= max_nodes:
|
|
101
|
+
break
|
|
102
|
+
|
|
103
|
+
selected_set = set(selected_files)
|
|
104
|
+
|
|
105
|
+
# --- Community detection ---
|
|
106
|
+
# Build file -> community_id mapping
|
|
107
|
+
file_to_community: dict[str, int] = {}
|
|
108
|
+
communities_list: list[dict] = []
|
|
109
|
+
try:
|
|
110
|
+
cd = svc.community_detection_data(min_community_size=2, max_communities=30)
|
|
111
|
+
# Handle both standard communities and directory-based fallback modules
|
|
112
|
+
community_entries = cd.get("communities", [])
|
|
113
|
+
if not community_entries and "modules" in cd:
|
|
114
|
+
community_entries = [
|
|
115
|
+
{"id": m["id"], "files": m["files"]}
|
|
116
|
+
for m in cd["modules"]
|
|
117
|
+
]
|
|
118
|
+
for comm in community_entries:
|
|
119
|
+
cid = comm["id"]
|
|
120
|
+
comm_files_in_view = [f for f in comm["files"] if f in selected_set]
|
|
121
|
+
if comm_files_in_view:
|
|
122
|
+
communities_list.append({
|
|
123
|
+
"id": cid,
|
|
124
|
+
"files": comm_files_in_view,
|
|
125
|
+
"size": len(comm_files_in_view),
|
|
126
|
+
})
|
|
127
|
+
for f in comm["files"]:
|
|
128
|
+
file_to_community[f] = cid
|
|
129
|
+
except Exception:
|
|
130
|
+
logger.debug("Community detection unavailable, skipping", exc_info=True)
|
|
131
|
+
|
|
132
|
+
# --- Build nodes ---
|
|
133
|
+
nodes: list[dict] = []
|
|
134
|
+
for path in selected_files:
|
|
135
|
+
fi = file_info.get(path)
|
|
136
|
+
node: dict = {
|
|
137
|
+
"id": path,
|
|
138
|
+
"language": fi.language if fi else "",
|
|
139
|
+
"importance": round(fi.importance, 3) if fi else 0.0,
|
|
140
|
+
"lines": fi.line_count if fi else 0,
|
|
141
|
+
"group": file_to_community.get(path, -1),
|
|
142
|
+
}
|
|
143
|
+
nodes.append(node)
|
|
144
|
+
|
|
145
|
+
# --- Build links ---
|
|
146
|
+
links: list[dict] = []
|
|
147
|
+
for src in selected_files:
|
|
148
|
+
for tgt in idx.file_dependencies.get(src, set()):
|
|
149
|
+
if tgt in selected_set:
|
|
150
|
+
links.append({
|
|
151
|
+
"source": src,
|
|
152
|
+
"target": tgt,
|
|
153
|
+
"type": "imports",
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
"nodes": nodes,
|
|
158
|
+
"links": links,
|
|
159
|
+
"communities": communities_list,
|
|
160
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Health check and metrics endpoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Query
|
|
6
|
+
from starlette.responses import PlainTextResponse
|
|
7
|
+
|
|
8
|
+
router = APIRouter(tags=["health"])
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@router.get("/health")
|
|
12
|
+
async def health() -> dict:
|
|
13
|
+
"""Basic health check."""
|
|
14
|
+
return {"status": "ok"}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@router.get("/ready")
|
|
18
|
+
async def ready() -> dict:
|
|
19
|
+
"""Readiness probe — checks that at least one project or repo is registered."""
|
|
20
|
+
from attocode_intel.api.deps import get_config, list_projects
|
|
21
|
+
|
|
22
|
+
# Check v1 local projects
|
|
23
|
+
projects = list_projects()
|
|
24
|
+
if projects:
|
|
25
|
+
return {"status": "ready", "projects": len(projects)}
|
|
26
|
+
|
|
27
|
+
# In service mode, also check DB repos
|
|
28
|
+
config = get_config()
|
|
29
|
+
if config.is_service_mode:
|
|
30
|
+
from sqlalchemy import func, select
|
|
31
|
+
|
|
32
|
+
from attocode_intel.db.engine import get_session
|
|
33
|
+
from attocode_intel.db.models import Repository
|
|
34
|
+
|
|
35
|
+
async for session in get_session():
|
|
36
|
+
result = await session.execute(select(func.count()).select_from(Repository))
|
|
37
|
+
repo_count = result.scalar() or 0
|
|
38
|
+
if repo_count > 0:
|
|
39
|
+
return {"status": "ready", "repos": repo_count}
|
|
40
|
+
|
|
41
|
+
return {"status": "not_ready", "reason": "no projects registered"}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@router.get("/api/v1/metrics")
|
|
45
|
+
async def metrics(
|
|
46
|
+
format: str = Query("json", description="Response format: 'json' or 'prometheus'"),
|
|
47
|
+
):
|
|
48
|
+
"""Return aggregated query metrics.
|
|
49
|
+
|
|
50
|
+
Supports JSON (default) and Prometheus text exposition format.
|
|
51
|
+
Unauthenticated — intended for monitoring infrastructure.
|
|
52
|
+
"""
|
|
53
|
+
from attocode_intel.api.middleware import metrics_collector
|
|
54
|
+
|
|
55
|
+
if format == "prometheus":
|
|
56
|
+
text = metrics_collector.get_metrics(fmt="prometheus")
|
|
57
|
+
return PlainTextResponse(
|
|
58
|
+
content=text,
|
|
59
|
+
media_type="text/plain; version=0.0.4; charset=utf-8",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return metrics_collector.get_metrics(fmt="json")
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""History endpoints — code evolution and change-history utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Depends, Query
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from attocode_intel.api.auth import verify_auth
|
|
11
|
+
from attocode_intel.api.deps import BranchParam, ensure_branch_supported, get_service_or_404
|
|
12
|
+
from attocode_intel.api.models import ChangeCouplingRequest, MergeRiskRequest, TextResult
|
|
13
|
+
|
|
14
|
+
router_v1 = APIRouter(
|
|
15
|
+
prefix="/api/v1/projects/{project_id}/history",
|
|
16
|
+
tags=["history"],
|
|
17
|
+
dependencies=[Depends(verify_auth)],
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
router_v2 = APIRouter(
|
|
21
|
+
prefix="/api/v2/projects/{project_id}",
|
|
22
|
+
tags=["history-v2"],
|
|
23
|
+
dependencies=[Depends(verify_auth)],
|
|
24
|
+
)
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# --- Response models ---
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FileStatEntry(BaseModel):
|
|
32
|
+
path: str
|
|
33
|
+
added: int = 0
|
|
34
|
+
removed: int = 0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class EvolutionCommit(BaseModel):
|
|
38
|
+
sha: str
|
|
39
|
+
author: str
|
|
40
|
+
email: str
|
|
41
|
+
date: str
|
|
42
|
+
subject: str
|
|
43
|
+
files: list[FileStatEntry] = []
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class CodeEvolutionResponse(BaseModel):
|
|
47
|
+
path: str
|
|
48
|
+
symbol: str = ""
|
|
49
|
+
since: str = ""
|
|
50
|
+
commits: list[EvolutionCommit]
|
|
51
|
+
total: int
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class RecentFileEntry(BaseModel):
|
|
55
|
+
path: str
|
|
56
|
+
commits: int
|
|
57
|
+
added: int
|
|
58
|
+
removed: int
|
|
59
|
+
last_date: str = ""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class RecentChangesResponse(BaseModel):
|
|
63
|
+
days: int
|
|
64
|
+
path: str = ""
|
|
65
|
+
commit_count: int
|
|
66
|
+
total_files_changed: int
|
|
67
|
+
files: list[RecentFileEntry]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# --- Endpoints ---
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@router_v1.get("/evolution", response_model=TextResult)
|
|
74
|
+
async def code_evolution_v1(
|
|
75
|
+
project_id: str,
|
|
76
|
+
path: str = Query(..., description="File path to trace history for"),
|
|
77
|
+
symbol: str = Query("", description="Optional symbol name filter"),
|
|
78
|
+
since: str = Query("", description="Date filter (e.g. '2024-01-01', '3 months ago')"),
|
|
79
|
+
max_results: int = Query(20, ge=1, le=100),
|
|
80
|
+
branch: BranchParam = "",
|
|
81
|
+
) -> TextResult:
|
|
82
|
+
"""Get change history for a file or symbol."""
|
|
83
|
+
ensure_branch_supported(branch)
|
|
84
|
+
svc = await get_service_or_404(project_id)
|
|
85
|
+
return TextResult(result=svc.code_evolution(
|
|
86
|
+
path=path,
|
|
87
|
+
symbol=symbol,
|
|
88
|
+
since=since,
|
|
89
|
+
max_results=max_results,
|
|
90
|
+
))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@router_v1.get("/recent-changes", response_model=TextResult)
|
|
94
|
+
async def recent_changes_v1(
|
|
95
|
+
project_id: str,
|
|
96
|
+
days: int = Query(7, ge=1, le=365),
|
|
97
|
+
path: str = Query("", description="Optional path prefix filter"),
|
|
98
|
+
top_n: int = Query(20, ge=1, le=200),
|
|
99
|
+
branch: BranchParam = "",
|
|
100
|
+
) -> TextResult:
|
|
101
|
+
"""Show recently modified files and change frequency."""
|
|
102
|
+
ensure_branch_supported(branch)
|
|
103
|
+
svc = await get_service_or_404(project_id)
|
|
104
|
+
return TextResult(result=svc.recent_changes(days=days, path=path, top_n=top_n))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@router_v1.post("/change-coupling", response_model=TextResult)
|
|
108
|
+
async def change_coupling_v1(
|
|
109
|
+
project_id: str,
|
|
110
|
+
req: ChangeCouplingRequest,
|
|
111
|
+
branch: BranchParam = "",
|
|
112
|
+
) -> TextResult:
|
|
113
|
+
"""Find files that frequently change together."""
|
|
114
|
+
ensure_branch_supported(branch)
|
|
115
|
+
svc = await get_service_or_404(project_id)
|
|
116
|
+
return TextResult(result=svc.change_coupling(
|
|
117
|
+
file=req.file,
|
|
118
|
+
days=req.days,
|
|
119
|
+
min_coupling=req.min_coupling,
|
|
120
|
+
top_k=req.top_k,
|
|
121
|
+
))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@router_v1.get("/churn-hotspots", response_model=TextResult)
|
|
125
|
+
async def churn_hotspots_v1(
|
|
126
|
+
project_id: str,
|
|
127
|
+
days: int = Query(90, ge=1, le=3650),
|
|
128
|
+
top_n: int = Query(20, ge=1, le=200),
|
|
129
|
+
branch: BranchParam = "",
|
|
130
|
+
) -> TextResult:
|
|
131
|
+
"""Rank high-churn files."""
|
|
132
|
+
ensure_branch_supported(branch)
|
|
133
|
+
svc = await get_service_or_404(project_id)
|
|
134
|
+
return TextResult(result=svc.churn_hotspots(days=days, top_n=top_n))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@router_v1.post("/merge-risk", response_model=TextResult)
|
|
138
|
+
async def merge_risk_v1(
|
|
139
|
+
project_id: str,
|
|
140
|
+
req: MergeRiskRequest,
|
|
141
|
+
branch: BranchParam = "",
|
|
142
|
+
) -> TextResult:
|
|
143
|
+
"""Predict additional files likely to require changes."""
|
|
144
|
+
ensure_branch_supported(branch)
|
|
145
|
+
svc = await get_service_or_404(project_id)
|
|
146
|
+
return TextResult(result=svc.merge_risk(files=req.files, days=req.days))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@router_v2.get("/evolution", response_model=CodeEvolutionResponse)
|
|
150
|
+
async def code_evolution(
|
|
151
|
+
project_id: str,
|
|
152
|
+
path: str = Query(..., description="File path to trace history for"),
|
|
153
|
+
symbol: str = Query("", description="Optional symbol name filter"),
|
|
154
|
+
since: str = Query("", description="Date filter (e.g. '2024-01-01', '3 months ago')"),
|
|
155
|
+
max_results: int = Query(20, ge=1, le=100),
|
|
156
|
+
) -> CodeEvolutionResponse:
|
|
157
|
+
"""Get change history for a file or symbol."""
|
|
158
|
+
svc = await get_service_or_404(project_id)
|
|
159
|
+
data = svc.code_evolution_data(
|
|
160
|
+
path=path, symbol=symbol, since=since, max_results=max_results,
|
|
161
|
+
)
|
|
162
|
+
return CodeEvolutionResponse(**data)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@router_v2.get("/recent-changes", response_model=RecentChangesResponse)
|
|
166
|
+
async def recent_changes(
|
|
167
|
+
project_id: str,
|
|
168
|
+
days: int = Query(7, ge=1, le=365),
|
|
169
|
+
path: str = Query("", description="Optional path prefix filter"),
|
|
170
|
+
top_n: int = Query(20, ge=1, le=200),
|
|
171
|
+
) -> RecentChangesResponse:
|
|
172
|
+
"""Show recently modified files and change frequency."""
|
|
173
|
+
svc = await get_service_or_404(project_id)
|
|
174
|
+
data = svc.recent_changes_data(days=days, path=path, top_n=top_n)
|
|
175
|
+
return RecentChangesResponse(**data)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Job status API endpoints (service mode only)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
from sqlalchemy import select
|
|
10
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
11
|
+
|
|
12
|
+
from attocode_intel.api.auth import resolve_auth
|
|
13
|
+
from attocode_intel.api.auth.context import AuthContext
|
|
14
|
+
from attocode_intel.api.deps import get_db_session
|
|
15
|
+
from attocode_intel.db.models import IndexingJob
|
|
16
|
+
|
|
17
|
+
router = APIRouter(prefix="/api/v1/jobs", tags=["jobs"])
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class JobResponse(BaseModel):
|
|
21
|
+
id: str
|
|
22
|
+
repo_id: str
|
|
23
|
+
job_type: str
|
|
24
|
+
status: str
|
|
25
|
+
branch_name: str | None = None
|
|
26
|
+
progress: dict = {}
|
|
27
|
+
error: str | None = None
|
|
28
|
+
started_at: str | None = None
|
|
29
|
+
completed_at: str | None = None
|
|
30
|
+
created_at: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class JobListResponse(BaseModel):
|
|
34
|
+
jobs: list[JobResponse]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _job_to_response(job: IndexingJob) -> JobResponse:
|
|
38
|
+
return JobResponse(
|
|
39
|
+
id=str(job.id),
|
|
40
|
+
repo_id=str(job.repo_id),
|
|
41
|
+
job_type=job.job_type,
|
|
42
|
+
status=job.status,
|
|
43
|
+
branch_name=job.branch_name,
|
|
44
|
+
progress=job.progress or {},
|
|
45
|
+
error=job.error,
|
|
46
|
+
started_at=job.started_at.isoformat() if job.started_at else None,
|
|
47
|
+
completed_at=job.completed_at.isoformat() if job.completed_at else None,
|
|
48
|
+
created_at=job.created_at.isoformat(),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@router.get("/{job_id}", response_model=JobResponse)
|
|
53
|
+
async def get_job(
|
|
54
|
+
job_id: uuid.UUID,
|
|
55
|
+
auth: AuthContext = Depends(resolve_auth),
|
|
56
|
+
session: AsyncSession = Depends(get_db_session),
|
|
57
|
+
) -> JobResponse:
|
|
58
|
+
"""Get job status by ID."""
|
|
59
|
+
result = await session.execute(
|
|
60
|
+
select(IndexingJob).where(IndexingJob.id == job_id)
|
|
61
|
+
)
|
|
62
|
+
job = result.scalar_one_or_none()
|
|
63
|
+
if job is None:
|
|
64
|
+
raise HTTPException(status_code=404, detail="Job not found")
|
|
65
|
+
return _job_to_response(job)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@router.get("/repo/{repo_id}", response_model=JobListResponse)
|
|
69
|
+
async def list_repo_jobs(
|
|
70
|
+
repo_id: uuid.UUID,
|
|
71
|
+
status: str = "",
|
|
72
|
+
limit: int = 20,
|
|
73
|
+
auth: AuthContext = Depends(resolve_auth),
|
|
74
|
+
session: AsyncSession = Depends(get_db_session),
|
|
75
|
+
) -> JobListResponse:
|
|
76
|
+
"""List jobs for a repository."""
|
|
77
|
+
query = select(IndexingJob).where(IndexingJob.repo_id == repo_id)
|
|
78
|
+
if status:
|
|
79
|
+
query = query.where(IndexingJob.status == status)
|
|
80
|
+
query = query.order_by(IndexingJob.created_at.desc()).limit(limit)
|
|
81
|
+
|
|
82
|
+
result = await session.execute(query)
|
|
83
|
+
jobs = [_job_to_response(j) for j in result.scalars()]
|
|
84
|
+
return JobListResponse(jobs=jobs)
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Learning/memory endpoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Depends, Query, Request
|
|
6
|
+
|
|
7
|
+
from attocode_intel.api.auth import verify_auth
|
|
8
|
+
from attocode_intel.api.knowledge_bridge import shared_operation
|
|
9
|
+
from attocode_intel.api.deps import BranchParam, ensure_branch_supported, get_service_or_404
|
|
10
|
+
from attocode_intel.api.models import (
|
|
11
|
+
LearningFeedbackRequest,
|
|
12
|
+
LearningItem,
|
|
13
|
+
LearningListResponse,
|
|
14
|
+
LearningRecallItem,
|
|
15
|
+
LearningRecallResponse,
|
|
16
|
+
RecordLearningRequest,
|
|
17
|
+
TextResult,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
router = APIRouter(
|
|
21
|
+
prefix="/api/v1",
|
|
22
|
+
tags=["learning"],
|
|
23
|
+
dependencies=[Depends(verify_auth)],
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
router_v2 = APIRouter(
|
|
27
|
+
prefix="/api/v2",
|
|
28
|
+
tags=["learning-v2"],
|
|
29
|
+
dependencies=[Depends(verify_auth)],
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@router.post("/projects/{project_id}/learnings", response_model=TextResult)
|
|
34
|
+
async def record_learning(
|
|
35
|
+
request: Request, project_id: str, req: RecordLearningRequest
|
|
36
|
+
) -> TextResult:
|
|
37
|
+
"""Record a new learning."""
|
|
38
|
+
shared = await shared_operation(request, project_id, "record_learning", req.model_dump())
|
|
39
|
+
if shared is not None:
|
|
40
|
+
return TextResult(result=shared["result"])
|
|
41
|
+
svc = await get_service_or_404(project_id)
|
|
42
|
+
return TextResult(
|
|
43
|
+
result=svc.record_learning(
|
|
44
|
+
type=req.type,
|
|
45
|
+
description=req.description,
|
|
46
|
+
details=req.details,
|
|
47
|
+
scope=req.scope,
|
|
48
|
+
confidence=req.confidence,
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@router.get("/projects/{project_id}/learnings/recall", response_model=TextResult)
|
|
54
|
+
async def recall(
|
|
55
|
+
request: Request,
|
|
56
|
+
project_id: str,
|
|
57
|
+
query: str = Query(...),
|
|
58
|
+
branch: BranchParam = "",
|
|
59
|
+
scope: str = "",
|
|
60
|
+
max_results: int = 10,
|
|
61
|
+
) -> TextResult:
|
|
62
|
+
"""Recall relevant learnings."""
|
|
63
|
+
shared = await shared_operation(
|
|
64
|
+
request, project_id, "recall", {"query": query, "scope": scope, "max_results": max_results}
|
|
65
|
+
)
|
|
66
|
+
if shared is not None:
|
|
67
|
+
return TextResult(result=shared["result"])
|
|
68
|
+
ensure_branch_supported(branch)
|
|
69
|
+
svc = await get_service_or_404(project_id)
|
|
70
|
+
return TextResult(result=svc.recall(query=query, scope=scope, max_results=max_results))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@router.post("/projects/{project_id}/learnings/{learning_id}/feedback", response_model=TextResult)
|
|
74
|
+
async def learning_feedback(
|
|
75
|
+
request: Request, project_id: str, learning_id: int, req: LearningFeedbackRequest
|
|
76
|
+
) -> TextResult:
|
|
77
|
+
"""Mark a learning as helpful/unhelpful."""
|
|
78
|
+
shared = await shared_operation(
|
|
79
|
+
request,
|
|
80
|
+
project_id,
|
|
81
|
+
"learning_feedback",
|
|
82
|
+
{"learning_id": learning_id, "helpful": req.helpful},
|
|
83
|
+
)
|
|
84
|
+
if shared is not None:
|
|
85
|
+
return TextResult(result=shared["result"])
|
|
86
|
+
svc = await get_service_or_404(project_id)
|
|
87
|
+
return TextResult(result=svc.learning_feedback(learning_id=learning_id, helpful=req.helpful))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@router.get("/projects/{project_id}/learnings", response_model=TextResult)
|
|
91
|
+
async def list_learnings(
|
|
92
|
+
request: Request,
|
|
93
|
+
project_id: str,
|
|
94
|
+
branch: BranchParam = "",
|
|
95
|
+
status: str = "active",
|
|
96
|
+
type: str = "", # noqa: A002
|
|
97
|
+
scope: str = "",
|
|
98
|
+
) -> TextResult:
|
|
99
|
+
"""List all learnings."""
|
|
100
|
+
shared = await shared_operation(
|
|
101
|
+
request, project_id, "list_learnings", {"status": status, "type": type, "scope": scope}
|
|
102
|
+
)
|
|
103
|
+
if shared is not None:
|
|
104
|
+
return TextResult(result=shared["result"])
|
|
105
|
+
ensure_branch_supported(branch)
|
|
106
|
+
svc = await get_service_or_404(project_id)
|
|
107
|
+
return TextResult(result=svc.list_learnings(status=status, type=type, scope=scope))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# ------------------------------------------------------------------
|
|
111
|
+
# v2 structured endpoints
|
|
112
|
+
# ------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _store_records_to_items(records: list[dict]) -> list[LearningItem]:
|
|
116
|
+
"""Convert raw store records to LearningItem models."""
|
|
117
|
+
return [
|
|
118
|
+
LearningItem(
|
|
119
|
+
id=r["id"],
|
|
120
|
+
type=r["type"],
|
|
121
|
+
description=r["description"],
|
|
122
|
+
details=r.get("details", ""),
|
|
123
|
+
scope=r.get("scope", ""),
|
|
124
|
+
confidence=r.get("confidence", 0.7),
|
|
125
|
+
status=r.get("status", "active"),
|
|
126
|
+
helpful_count=r.get("helpful_count", 0),
|
|
127
|
+
unhelpful_count=r.get("unhelpful_count", 0),
|
|
128
|
+
)
|
|
129
|
+
for r in records
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _store_records_to_recall_items(records: list[dict]) -> list[LearningRecallItem]:
|
|
134
|
+
"""Convert raw store recall records to LearningRecallItem models."""
|
|
135
|
+
return [
|
|
136
|
+
LearningRecallItem(
|
|
137
|
+
id=r["id"],
|
|
138
|
+
type=r["type"],
|
|
139
|
+
description=r["description"],
|
|
140
|
+
scope=r.get("scope", ""),
|
|
141
|
+
confidence=r.get("confidence", 0.7),
|
|
142
|
+
relevance_score=r.get("relevance_score", 0.0),
|
|
143
|
+
)
|
|
144
|
+
for r in records
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@router_v2.get("/projects/{project_id}/learnings", response_model=LearningListResponse)
|
|
149
|
+
async def list_learnings_v2(
|
|
150
|
+
request: Request,
|
|
151
|
+
project_id: str,
|
|
152
|
+
status: str = "active",
|
|
153
|
+
type: str = "", # noqa: A002
|
|
154
|
+
scope: str = "",
|
|
155
|
+
) -> LearningListResponse:
|
|
156
|
+
"""List all learnings (structured)."""
|
|
157
|
+
shared = await shared_operation(
|
|
158
|
+
request, project_id, "list_learnings", {"status": status, "type": type, "scope": scope}
|
|
159
|
+
)
|
|
160
|
+
if shared is not None:
|
|
161
|
+
return LearningListResponse(
|
|
162
|
+
learnings=_store_records_to_items(shared["data"]), total=len(shared["data"])
|
|
163
|
+
)
|
|
164
|
+
from fastapi import HTTPException
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
svc = await get_service_or_404(project_id)
|
|
168
|
+
store = svc._get_memory_store()
|
|
169
|
+
records = store.list_all(status=status, type=type or None)
|
|
170
|
+
if scope:
|
|
171
|
+
records = [r for r in records if r["scope"].startswith(scope) or r["scope"] == ""]
|
|
172
|
+
items = _store_records_to_items(records)
|
|
173
|
+
return LearningListResponse(learnings=items, total=len(items))
|
|
174
|
+
except HTTPException:
|
|
175
|
+
return LearningListResponse(learnings=[], total=0)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@router_v2.get("/projects/{project_id}/learnings/recall", response_model=LearningRecallResponse)
|
|
179
|
+
async def recall_v2(
|
|
180
|
+
request: Request,
|
|
181
|
+
project_id: str,
|
|
182
|
+
query: str = Query(...),
|
|
183
|
+
scope: str = "",
|
|
184
|
+
max_results: int = 10,
|
|
185
|
+
) -> LearningRecallResponse:
|
|
186
|
+
"""Recall relevant learnings (structured)."""
|
|
187
|
+
shared = await shared_operation(
|
|
188
|
+
request, project_id, "recall", {"query": query, "scope": scope, "max_results": max_results}
|
|
189
|
+
)
|
|
190
|
+
if shared is not None:
|
|
191
|
+
return LearningRecallResponse(
|
|
192
|
+
query=query,
|
|
193
|
+
results=_store_records_to_recall_items(shared["data"]),
|
|
194
|
+
total=len(shared["data"]),
|
|
195
|
+
)
|
|
196
|
+
from fastapi import HTTPException
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
svc = await get_service_or_404(project_id)
|
|
200
|
+
store = svc._get_memory_store()
|
|
201
|
+
records = store.recall(query=query, scope=scope, max_results=max_results)
|
|
202
|
+
items = _store_records_to_recall_items(records)
|
|
203
|
+
return LearningRecallResponse(query=query, results=items, total=len(items))
|
|
204
|
+
except HTTPException:
|
|
205
|
+
return LearningRecallResponse(query=query, results=[], total=0)
|