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.
Files changed (345) hide show
  1. attocode_code_intel-0.1.0.dist-info/METADATA +221 -0
  2. attocode_code_intel-0.1.0.dist-info/RECORD +345 -0
  3. attocode_code_intel-0.1.0.dist-info/WHEEL +4 -0
  4. attocode_code_intel-0.1.0.dist-info/entry_points.txt +2 -0
  5. attocode_code_intel-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. attocode_intel/GUIDELINES.md +432 -0
  7. attocode_intel/__init__.py +52 -0
  8. attocode_intel/_internal/__init__.py +1 -0
  9. attocode_intel/_internal/errors/__init__.py +126 -0
  10. attocode_intel/_internal/integrations/__init__.py +1 -0
  11. attocode_intel/_internal/integrations/context/__init__.py +1 -0
  12. attocode_intel/_internal/integrations/context/architecture_drift.py +354 -0
  13. attocode_intel/_internal/integrations/context/ast_chunker.py +202 -0
  14. attocode_intel/_internal/integrations/context/ast_service.py +1483 -0
  15. attocode_intel/_internal/integrations/context/blob_oid.py +423 -0
  16. attocode_intel/_internal/integrations/context/cache_manifest.py +221 -0
  17. attocode_intel/_internal/integrations/context/cas.py +466 -0
  18. attocode_intel/_internal/integrations/context/code_analyzer.py +528 -0
  19. attocode_intel/_internal/integrations/context/codebase_ast.py +1461 -0
  20. attocode_intel/_internal/integrations/context/codebase_context.py +1736 -0
  21. attocode_intel/_internal/integrations/context/cross_mode.py +263 -0
  22. attocode_intel/_internal/integrations/context/cross_references.py +616 -0
  23. attocode_intel/_internal/integrations/context/embedding_rotation.py +597 -0
  24. attocode_intel/_internal/integrations/context/embeddings.py +282 -0
  25. attocode_intel/_internal/integrations/context/frecency.py +427 -0
  26. attocode_intel/_internal/integrations/context/fuzzy.py +320 -0
  27. attocode_intel/_internal/integrations/context/hierarchical_explorer.py +331 -0
  28. attocode_intel/_internal/integrations/context/hydration.py +90 -0
  29. attocode_intel/_internal/integrations/context/index_repair.py +87 -0
  30. attocode_intel/_internal/integrations/context/index_store.py +586 -0
  31. attocode_intel/_internal/integrations/context/memory_store.py +602 -0
  32. attocode_intel/_internal/integrations/context/module_resolution.py +52 -0
  33. attocode_intel/_internal/integrations/context/query_constraints.py +361 -0
  34. attocode_intel/_internal/integrations/context/query_history.py +417 -0
  35. attocode_intel/_internal/integrations/context/reranker.py +351 -0
  36. attocode_intel/_internal/integrations/context/semantic_search.py +2931 -0
  37. attocode_intel/_internal/integrations/context/syntax_evidence.py +94 -0
  38. attocode_intel/_internal/integrations/context/systemone_ranker.py +245 -0
  39. attocode_intel/_internal/integrations/context/temporal_coupling.py +411 -0
  40. attocode_intel/_internal/integrations/context/trigram_index.py +820 -0
  41. attocode_intel/_internal/integrations/context/trigram_regex.py +171 -0
  42. attocode_intel/_internal/integrations/context/ts_parser.py +2199 -0
  43. attocode_intel/_internal/integrations/context/vector_store.py +795 -0
  44. attocode_intel/_internal/integrations/feature_flags/__init__.py +357 -0
  45. attocode_intel/_internal/integrations/lsp/__init__.py +1 -0
  46. attocode_intel/_internal/integrations/lsp/client.py +1140 -0
  47. attocode_intel/_internal/integrations/mcp/__init__.py +1 -0
  48. attocode_intel/_internal/integrations/mcp/client.py +253 -0
  49. attocode_intel/_internal/integrations/security/__init__.py +1 -0
  50. attocode_intel/_internal/integrations/security/custom_rules.py +176 -0
  51. attocode_intel/_internal/integrations/security/dataflow.py +522 -0
  52. attocode_intel/_internal/integrations/security/dependency_audit.py +303 -0
  53. attocode_intel/_internal/integrations/security/matcher.py +44 -0
  54. attocode_intel/_internal/integrations/security/patterns.py +724 -0
  55. attocode_intel/_internal/integrations/security/scanner.py +351 -0
  56. attocode_intel/_internal/integrations/utilities/__init__.py +1 -0
  57. attocode_intel/_internal/integrations/utilities/ignore.py +64 -0
  58. attocode_intel/_internal/integrations/utilities/token_estimate.py +51 -0
  59. attocode_intel/_internal/tricks/__init__.py +1 -0
  60. attocode_intel/_internal/tricks/recursive_context.py +357 -0
  61. attocode_intel/_internal/types/__init__.py +1 -0
  62. attocode_intel/_internal/types/events.py +289 -0
  63. attocode_intel/_internal/types/severity.py +15 -0
  64. attocode_intel/_shared.py +341 -0
  65. attocode_intel/analysis_coverage.py +56 -0
  66. attocode_intel/api/__init__.py +1 -0
  67. attocode_intel/api/access.py +32 -0
  68. attocode_intel/api/app.py +247 -0
  69. attocode_intel/api/auth/__init__.py +128 -0
  70. attocode_intel/api/auth/api_keys.py +61 -0
  71. attocode_intel/api/auth/context.py +34 -0
  72. attocode_intel/api/auth/jwt.py +66 -0
  73. attocode_intel/api/auth/oauth.py +146 -0
  74. attocode_intel/api/auth/passwords.py +15 -0
  75. attocode_intel/api/deps.py +351 -0
  76. attocode_intel/api/knowledge_bridge.py +13 -0
  77. attocode_intel/api/mcp_transport.py +51 -0
  78. attocode_intel/api/middleware.py +411 -0
  79. attocode_intel/api/models.py +698 -0
  80. attocode_intel/api/providers/__init__.py +1 -0
  81. attocode_intel/api/providers/db_provider.py +1106 -0
  82. attocode_intel/api/providers/local_provider.py +476 -0
  83. attocode_intel/api/providers/remote_provider.py +740 -0
  84. attocode_intel/api/repository_source.py +21 -0
  85. attocode_intel/api/routes/__init__.py +1 -0
  86. attocode_intel/api/routes/_pin_helper.py +158 -0
  87. attocode_intel/api/routes/activity.py +148 -0
  88. attocode_intel/api/routes/adr.py +102 -0
  89. attocode_intel/api/routes/analysis.py +515 -0
  90. attocode_intel/api/routes/api_keys.py +170 -0
  91. attocode_intel/api/routes/auth.py +428 -0
  92. attocode_intel/api/routes/branches.py +262 -0
  93. attocode_intel/api/routes/cross_repo_search.py +198 -0
  94. attocode_intel/api/routes/embeddings.py +386 -0
  95. attocode_intel/api/routes/files.py +132 -0
  96. attocode_intel/api/routes/files_v2.py +388 -0
  97. attocode_intel/api/routes/gc.py +322 -0
  98. attocode_intel/api/routes/git_v2.py +433 -0
  99. attocode_intel/api/routes/graph.py +174 -0
  100. attocode_intel/api/routes/graph_viz.py +160 -0
  101. attocode_intel/api/routes/health.py +62 -0
  102. attocode_intel/api/routes/history.py +175 -0
  103. attocode_intel/api/routes/jobs.py +84 -0
  104. attocode_intel/api/routes/learning.py +205 -0
  105. attocode_intel/api/routes/lsp.py +272 -0
  106. attocode_intel/api/routes/notify.py +623 -0
  107. attocode_intel/api/routes/operations.py +27 -0
  108. attocode_intel/api/routes/orgs.py +924 -0
  109. attocode_intel/api/routes/preferences.py +100 -0
  110. attocode_intel/api/routes/presence.py +42 -0
  111. attocode_intel/api/routes/projects.py +116 -0
  112. attocode_intel/api/routes/repos.py +47 -0
  113. attocode_intel/api/routes/search.py +180 -0
  114. attocode_intel/api/routes/snapshots.py +614 -0
  115. attocode_intel/api/routes/webhooks.py +313 -0
  116. attocode_intel/api/routes/websocket.py +116 -0
  117. attocode_intel/api/static/graph.html +239 -0
  118. attocode_intel/api/utils.py +96 -0
  119. attocode_intel/artifacts/__init__.py +45 -0
  120. attocode_intel/artifacts/hashing.py +230 -0
  121. attocode_intel/artifacts/provenance.py +190 -0
  122. attocode_intel/artifacts/retrieval_pin.py +159 -0
  123. attocode_intel/audit.py +59 -0
  124. attocode_intel/bug_finder.py +206 -0
  125. attocode_intel/bundle.py +80 -0
  126. attocode_intel/catalog.py +242 -0
  127. attocode_intel/change_review.py +75 -0
  128. attocode_intel/cli.py +3062 -0
  129. attocode_intel/community.py +63 -0
  130. attocode_intel/confidence/__init__.py +144 -0
  131. attocode_intel/confidence/jev.py +234 -0
  132. attocode_intel/confidence/llm.py +330 -0
  133. attocode_intel/confidence/redact.py +44 -0
  134. attocode_intel/confidence/settings.py +74 -0
  135. attocode_intel/config.py +256 -0
  136. attocode_intel/crypto.py +31 -0
  137. attocode_intel/db/__init__.py +7 -0
  138. attocode_intel/db/base.py +33 -0
  139. attocode_intel/db/engine.py +61 -0
  140. attocode_intel/db/models.py +529 -0
  141. attocode_intel/entrypoint.py +185 -0
  142. attocode_intel/focused_evidence.py +130 -0
  143. attocode_intel/freshness.py +154 -0
  144. attocode_intel/gateway.py +734 -0
  145. attocode_intel/git/__init__.py +7 -0
  146. attocode_intel/git/credentials.py +75 -0
  147. attocode_intel/git/manager.py +493 -0
  148. attocode_intel/git/models.py +96 -0
  149. attocode_intel/git/storage.py +98 -0
  150. attocode_intel/graph_query_parser.py +761 -0
  151. attocode_intel/helpers.py +712 -0
  152. attocode_intel/indexing/__init__.py +29 -0
  153. attocode_intel/indexing/debouncer.py +136 -0
  154. attocode_intel/indexing/delta_indexer.py +151 -0
  155. attocode_intel/indexing/full_indexer.py +323 -0
  156. attocode_intel/indexing/incremental.py +240 -0
  157. attocode_intel/indexing/parser.py +403 -0
  158. attocode_intel/indexing/summarizer.py +264 -0
  159. attocode_intel/installer.py +1896 -0
  160. attocode_intel/knowledge.py +269 -0
  161. attocode_intel/local_knowledge.py +77 -0
  162. attocode_intel/migrations/alembic.ini +36 -0
  163. attocode_intel/migrations/env.py +71 -0
  164. attocode_intel/migrations/script.py.mako +27 -0
  165. attocode_intel/migrations/versions/001_initial_schema.py +219 -0
  166. attocode_intel/migrations/versions/002_branch_version_and_embeddings.py +61 -0
  167. attocode_intel/migrations/versions/003_embedding_uniqueness_and_cascade.py +51 -0
  168. attocode_intel/migrations/versions/004_audit_events.py +38 -0
  169. attocode_intel/migrations/versions/005_user_preferences.py +30 -0
  170. attocode_intel/migrations/versions/006_google_oauth.py +27 -0
  171. attocode_intel/migrations/versions/007_pgvector_embeddings.py +28 -0
  172. attocode_intel/migrations/versions/008_symbol_references.py +37 -0
  173. attocode_intel/migrations/versions/009_webhook_secret_encryption.py +25 -0
  174. attocode_intel/migrations/versions/010_add_commits_table.py +40 -0
  175. attocode_intel/migrations/versions/011_commit_changed_files.py +25 -0
  176. attocode_intel/migrations/versions/012_branch_merged_at.py +25 -0
  177. attocode_intel/migrations/versions/013_revoked_tokens.py +32 -0
  178. attocode_intel/migrations/versions/014_blame_hunks.py +40 -0
  179. attocode_intel/migrations/versions/015_commit_file_stats.py +40 -0
  180. attocode_intel/migrations/versions/016_embedding_provenance_columns.py +179 -0
  181. attocode_intel/migrations/versions/017_provenance_table.py +102 -0
  182. attocode_intel/migrations/versions/018_repo_snapshots.py +178 -0
  183. attocode_intel/migrations/versions/019_symref_caller.py +42 -0
  184. attocode_intel/migrations/versions/020_shared_knowledge.py +29 -0
  185. attocode_intel/onboarding.py +258 -0
  186. attocode_intel/output.py +178 -0
  187. attocode_intel/precision.py +133 -0
  188. attocode_intel/presence.py +160 -0
  189. attocode_intel/probe.py +110 -0
  190. attocode_intel/project_dir.py +97 -0
  191. attocode_intel/pubsub.py +113 -0
  192. attocode_intel/query_ranking.py +242 -0
  193. attocode_intel/readiness.py +972 -0
  194. attocode_intel/redis.py +51 -0
  195. attocode_intel/reference_pages.py +60 -0
  196. attocode_intel/remote.py +131 -0
  197. attocode_intel/repo_ranker.py +246 -0
  198. attocode_intel/request_context.py +82 -0
  199. attocode_intel/rules/__init__.py +28 -0
  200. attocode_intel/rules/ci.py +320 -0
  201. attocode_intel/rules/combinators.py +293 -0
  202. attocode_intel/rules/enricher.py +127 -0
  203. attocode_intel/rules/evolution.py +452 -0
  204. attocode_intel/rules/executor.py +531 -0
  205. attocode_intel/rules/filters/__init__.py +5 -0
  206. attocode_intel/rules/filters/pipeline.py +151 -0
  207. attocode_intel/rules/formatter.py +198 -0
  208. attocode_intel/rules/hygiene.py +242 -0
  209. attocode_intel/rules/importers/__init__.py +1 -0
  210. attocode_intel/rules/importers/semgrep.py +382 -0
  211. attocode_intel/rules/loader.py +292 -0
  212. attocode_intel/rules/marketplace.py +375 -0
  213. attocode_intel/rules/metavar.py +181 -0
  214. attocode_intel/rules/model.py +317 -0
  215. attocode_intel/rules/packs/__init__.py +1 -0
  216. attocode_intel/rules/packs/community/_license_templates/Apache-2.0.txt +176 -0
  217. attocode_intel/rules/packs/community/_license_templates/MIT.txt +21 -0
  218. attocode_intel/rules/packs/community/bandit-python/LICENSE +176 -0
  219. attocode_intel/rules/packs/community/bandit-python/NOTICE +13 -0
  220. attocode_intel/rules/packs/community/bandit-python/PORTING.md +23 -0
  221. attocode_intel/rules/packs/community/bandit-python/manifest.yaml +14 -0
  222. attocode_intel/rules/packs/community/bandit-python/rules/B105-hardcoded-password.yaml +23 -0
  223. attocode_intel/rules/packs/community/bandit-python/rules/B303-weak-md5.yaml +21 -0
  224. attocode_intel/rules/packs/community/bandit-python/rules/B304-weak-des.yaml +20 -0
  225. attocode_intel/rules/packs/community/bandit-python/rules/B307-use-of-eval.yaml +22 -0
  226. attocode_intel/rules/packs/community/bandit-python/rules/B324-weak-sha1.yaml +21 -0
  227. attocode_intel/rules/packs/community/bandit-python/rules/B501-no-cert-validation.yaml +23 -0
  228. attocode_intel/rules/packs/community/bandit-python/rules/B602-shell-true.yaml +22 -0
  229. attocode_intel/rules/packs/community/bandit-python/rules/B608-sqli.yaml +22 -0
  230. attocode_intel/rules/packs/community/eslint-typescript/LICENSE +21 -0
  231. attocode_intel/rules/packs/community/eslint-typescript/NOTICE +13 -0
  232. attocode_intel/rules/packs/community/eslint-typescript/PORTING.md +23 -0
  233. attocode_intel/rules/packs/community/eslint-typescript/manifest.yaml +14 -0
  234. attocode_intel/rules/packs/community/eslint-typescript/rules/eqeqeq.yaml +20 -0
  235. attocode_intel/rules/packs/community/eslint-typescript/rules/no-eval.yaml +22 -0
  236. attocode_intel/rules/packs/community/eslint-typescript/rules/no-implied-eval.yaml +20 -0
  237. attocode_intel/rules/packs/community/eslint-typescript/rules/no-new-func.yaml +21 -0
  238. attocode_intel/rules/packs/community/eslint-typescript/rules/no-throw-literal.yaml +19 -0
  239. attocode_intel/rules/packs/community/eslint-typescript/rules/no-var.yaml +20 -0
  240. attocode_intel/rules/packs/community/eslint-typescript/rules/no-with.yaml +18 -0
  241. attocode_intel/rules/packs/community/gosec-go/LICENSE +176 -0
  242. attocode_intel/rules/packs/community/gosec-go/NOTICE +13 -0
  243. attocode_intel/rules/packs/community/gosec-go/PORTING.md +23 -0
  244. attocode_intel/rules/packs/community/gosec-go/manifest.yaml +14 -0
  245. attocode_intel/rules/packs/community/gosec-go/rules/G101-hardcoded-credentials.yaml +22 -0
  246. attocode_intel/rules/packs/community/gosec-go/rules/G102-bind-all-interfaces.yaml +21 -0
  247. attocode_intel/rules/packs/community/gosec-go/rules/G103-unsafe-block.yaml +21 -0
  248. attocode_intel/rules/packs/community/gosec-go/rules/G201-sql-format.yaml +21 -0
  249. attocode_intel/rules/packs/community/gosec-go/rules/G304-file-inclusion.yaml +23 -0
  250. attocode_intel/rules/packs/community/gosec-go/rules/G401-weak-cipher.yaml +20 -0
  251. attocode_intel/rules/packs/community/gosec-go/rules/G402-tls-min-version.yaml +20 -0
  252. attocode_intel/rules/packs/community/gosec-go/rules/G501-md5-import.yaml +21 -0
  253. attocode_intel/rules/packs/examples/cpp/manifest.yaml +4 -0
  254. attocode_intel/rules/packs/examples/cpp/rules/security.yaml +90 -0
  255. attocode_intel/rules/packs/examples/cpp/rules/structural.yaml +42 -0
  256. attocode_intel/rules/packs/examples/csharp/manifest.yaml +4 -0
  257. attocode_intel/rules/packs/examples/csharp/rules/security.yaml +69 -0
  258. attocode_intel/rules/packs/examples/csharp/rules/structural.yaml +42 -0
  259. attocode_intel/rules/packs/examples/go/manifest.yaml +4 -0
  260. attocode_intel/rules/packs/examples/go/rules/performance.yaml +136 -0
  261. attocode_intel/rules/packs/examples/go/rules/security.yaml +48 -0
  262. attocode_intel/rules/packs/examples/go/rules/structural.yaml +58 -0
  263. attocode_intel/rules/packs/examples/go/taint/sanitizers.yaml +24 -0
  264. attocode_intel/rules/packs/examples/go/taint/sinks.yaml +51 -0
  265. attocode_intel/rules/packs/examples/go/taint/sources.yaml +26 -0
  266. attocode_intel/rules/packs/examples/java/manifest.yaml +4 -0
  267. attocode_intel/rules/packs/examples/java/rules/correctness.yaml +55 -0
  268. attocode_intel/rules/packs/examples/java/rules/structural.yaml +48 -0
  269. attocode_intel/rules/packs/examples/java/taint/sinks.yaml +37 -0
  270. attocode_intel/rules/packs/examples/java/taint/sources.yaml +21 -0
  271. attocode_intel/rules/packs/examples/kotlin/manifest.yaml +4 -0
  272. attocode_intel/rules/packs/examples/kotlin/rules/correctness.yaml +50 -0
  273. attocode_intel/rules/packs/examples/kotlin/rules/structural.yaml +38 -0
  274. attocode_intel/rules/packs/examples/php/manifest.yaml +4 -0
  275. attocode_intel/rules/packs/examples/php/rules/security.yaml +66 -0
  276. attocode_intel/rules/packs/examples/php/rules/structural.yaml +41 -0
  277. attocode_intel/rules/packs/examples/python/manifest.yaml +4 -0
  278. attocode_intel/rules/packs/examples/python/rules/performance.yaml +90 -0
  279. attocode_intel/rules/packs/examples/python/rules/structural.yaml +55 -0
  280. attocode_intel/rules/packs/examples/ruby/manifest.yaml +4 -0
  281. attocode_intel/rules/packs/examples/ruby/rules/security.yaml +73 -0
  282. attocode_intel/rules/packs/examples/ruby/rules/structural.yaml +41 -0
  283. attocode_intel/rules/packs/examples/rust/manifest.yaml +4 -0
  284. attocode_intel/rules/packs/examples/rust/rules/correctness.yaml +51 -0
  285. attocode_intel/rules/packs/examples/rust/rules/structural.yaml +49 -0
  286. attocode_intel/rules/packs/examples/typescript/manifest.yaml +4 -0
  287. attocode_intel/rules/packs/examples/typescript/rules/performance.yaml +70 -0
  288. attocode_intel/rules/packs/examples/typescript/rules/structural.yaml +48 -0
  289. attocode_intel/rules/packs/pack_loader.py +244 -0
  290. attocode_intel/rules/plugins/__init__.py +1 -0
  291. attocode_intel/rules/plugins/plugin_loader.py +132 -0
  292. attocode_intel/rules/profiling.py +332 -0
  293. attocode_intel/rules/registry.py +213 -0
  294. attocode_intel/rules/sarif.py +187 -0
  295. attocode_intel/rules/synthesis.py +420 -0
  296. attocode_intel/rules/testing.py +326 -0
  297. attocode_intel/server.py +734 -0
  298. attocode_intel/service.py +3416 -0
  299. attocode_intel/sharing.py +57 -0
  300. attocode_intel/storage/__init__.py +6 -0
  301. attocode_intel/storage/branch_overlay.py +352 -0
  302. attocode_intel/storage/content_store.py +220 -0
  303. attocode_intel/storage/dependency_store.py +143 -0
  304. attocode_intel/storage/diff_engine.py +187 -0
  305. attocode_intel/storage/embedding_store.py +594 -0
  306. attocode_intel/storage/provenance_store.py +137 -0
  307. attocode_intel/storage/security_scanner_db.py +164 -0
  308. attocode_intel/storage/symbol_store.py +160 -0
  309. attocode_intel/structural_evidence.py +144 -0
  310. attocode_intel/symbol_inspection.py +141 -0
  311. attocode_intel/symbol_links.py +167 -0
  312. attocode_intel/telemetry.py +42 -0
  313. attocode_intel/test_ranking.py +73 -0
  314. attocode_intel/testing/__init__.py +43 -0
  315. attocode_intel/testing/fixtures.py +122 -0
  316. attocode_intel/testing/helpers.py +75 -0
  317. attocode_intel/testing/mocks.py +99 -0
  318. attocode_intel/tools/__init__.py +2 -0
  319. attocode_intel/tools/adr_tools.py +628 -0
  320. attocode_intel/tools/analysis_tools.py +382 -0
  321. attocode_intel/tools/composite_tools.py +695 -0
  322. attocode_intel/tools/cross_mode_tools.py +173 -0
  323. attocode_intel/tools/dead_code_tools.py +582 -0
  324. attocode_intel/tools/distill_tools.py +353 -0
  325. attocode_intel/tools/frecency_tools.py +220 -0
  326. attocode_intel/tools/fuzzy_tools.py +255 -0
  327. attocode_intel/tools/history_tools.py +486 -0
  328. attocode_intel/tools/learning_tools.py +222 -0
  329. attocode_intel/tools/lsp_tools.py +218 -0
  330. attocode_intel/tools/maintenance_tools.py +1662 -0
  331. attocode_intel/tools/navigation_tools.py +220 -0
  332. attocode_intel/tools/overlay_tools.py +540 -0
  333. attocode_intel/tools/pin_store.py +432 -0
  334. attocode_intel/tools/pin_tools.py +266 -0
  335. attocode_intel/tools/query_constraints_tools.py +262 -0
  336. attocode_intel/tools/query_history_tools.py +158 -0
  337. attocode_intel/tools/readiness_tools.py +44 -0
  338. attocode_intel/tools/rule_tools.py +992 -0
  339. attocode_intel/tools/search_tools.py +770 -0
  340. attocode_intel/tools/snapshot_tools.py +624 -0
  341. attocode_intel/workers/__init__.py +1 -0
  342. attocode_intel/workers/job_utils.py +111 -0
  343. attocode_intel/workers/jobs.py +573 -0
  344. attocode_intel/workers/run.py +30 -0
  345. 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)