ltcai 11.5.1 → 11.6.0

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 (473) hide show
  1. package/README.md +92 -117
  2. package/bin/ltcai.js +234 -24
  3. package/docs/CHANGELOG.md +129 -0
  4. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  5. package/docs/DEVELOPMENT.md +8 -6
  6. package/docs/ENTERPRISE.md +2 -1
  7. package/docs/HYBRID_CLOUD_KG_STREAMING.md +8 -4
  8. package/docs/MULTI_AGENT_RUNTIME.md +12 -5
  9. package/docs/ONBOARDING.md +1 -1
  10. package/docs/OPERATIONS.md +6 -3
  11. package/docs/REALTIME_COLLABORATION.md +1 -1
  12. package/docs/TRUST_MODEL.md +1 -1
  13. package/docs/WHY_LATTICE.md +1 -1
  14. package/docs/WORKFLOW_DESIGNER.md +3 -2
  15. package/docs/architecture.md +4 -4
  16. package/docs/kg-schema.md +1 -1
  17. package/docs/v11.5.2_TIGHT_SHIP_PLAN.md +144 -0
  18. package/docs/v11.6.0_ONE_DOOR_PLAN.md +170 -0
  19. package/lattice_brain/__init__.py +42 -79
  20. package/lattice_brain/graph/__init__.py +9 -24
  21. package/lattice_brain/graph/_kg_common/__init__.py +13 -24
  22. package/lattice_brain/ingestion/__init__.py +19 -58
  23. package/lattice_brain/ingestion/pipeline.py +20 -398
  24. package/lattice_brain/multimodal/__init__.py +7 -18
  25. package/lattice_brain/multimodal/images.py +6 -264
  26. package/lattice_brain/multimodal/video.py +8 -247
  27. package/lattice_brain/runtime/__init__.py +8 -79
  28. package/lattice_brain/runtime/hooks.py +22 -597
  29. package/latticeai/__init__.py +1 -1
  30. package/latticeai/api/agent_worker_seam.py +7 -80
  31. package/latticeai/api/health.py +26 -20
  32. package/latticeai/api/local_files.py +25 -608
  33. package/latticeai/api/models.py +12 -87
  34. package/latticeai/api/search.py +17 -231
  35. package/latticeai/api/tools.py +69 -750
  36. package/latticeai/api/voice_capture.py +8 -70
  37. package/latticeai/api/worker_compute.py +842 -0
  38. package/latticeai/api/worker_seams.py +216 -0
  39. package/latticeai/app_factory.py +23 -232
  40. package/latticeai/cli/entrypoint.py +36 -249
  41. package/latticeai/cli/runtime.py +7 -3
  42. package/latticeai/core/agent_permission.py +16 -91
  43. package/latticeai/core/config.py +47 -3
  44. package/latticeai/core/csrf.py +28 -2
  45. package/latticeai/core/embedding_providers/__init__.py +3 -5
  46. package/latticeai/core/embedding_providers/base.py +1 -1
  47. package/latticeai/core/embedding_providers/text.py +1 -1
  48. package/latticeai/core/http_origin.py +146 -0
  49. package/latticeai/core/io_utils.py +2 -11
  50. package/latticeai/core/messages.py +94 -531
  51. package/latticeai/core/model_compat.py +2 -8
  52. package/latticeai/core/module_probe.py +37 -0
  53. package/latticeai/core/security.py +13 -0
  54. package/latticeai/core/sessions.py +3 -2
  55. package/latticeai/core/sse.py +25 -0
  56. package/latticeai/core/users.py +0 -9
  57. package/latticeai/models/router/documents.py +0 -35
  58. package/latticeai/models/router/registry.py +0 -4
  59. package/latticeai/runtime/access_runtime.py +16 -18
  60. package/latticeai/runtime/bootstrap.py +10 -21
  61. package/latticeai/runtime/brain_runtime.py +35 -43
  62. package/latticeai/runtime/build_phases/__init__.py +21 -37
  63. package/latticeai/runtime/build_phases/features.py +101 -389
  64. package/latticeai/runtime/build_phases/foundation.py +82 -392
  65. package/latticeai/runtime/build_phases/web.py +91 -406
  66. package/latticeai/runtime/build_phases/worker_profile.py +244 -0
  67. package/latticeai/runtime/lifespan_runtime.py +7 -16
  68. package/latticeai/runtime/platform_services_runtime.py +1 -15
  69. package/latticeai/runtime/runtime_context.py +14 -143
  70. package/latticeai/runtime/security_runtime.py +10 -103
  71. package/latticeai/services/architecture_readiness.py +82 -43
  72. package/latticeai/services/model_capability_registry.py +0 -15
  73. package/latticeai/services/model_catalog.py +0 -7
  74. package/latticeai/services/model_loading.py +3 -2
  75. package/latticeai/services/model_runtime/__init__.py +2 -68
  76. package/latticeai/services/model_runtime/engines.py +27 -128
  77. package/latticeai/services/model_runtime/loading.py +2 -2
  78. package/latticeai/services/model_runtime/service.py +4 -28
  79. package/latticeai/services/p_reinforce.py +10 -261
  80. package/latticeai/services/product_readiness.py +31 -38
  81. package/latticeai/services/search_service.py +37 -795
  82. package/latticeai/services/tool_dispatch.py +30 -390
  83. package/latticeai/services/voice_capture.py +13 -107
  84. package/latticeai/tools/__init__.py +22 -49
  85. package/latticeai/tools/commands.py +0 -163
  86. package/latticeai/tools/computer.py +0 -39
  87. package/latticeai/tools/documents.py +1 -134
  88. package/latticeai/tools/filesystem.py +1 -247
  89. package/latticeai/tools/knowledge.py +1 -52
  90. package/latticeai/tools/local_files.py +0 -20
  91. package/latticeai/worker_app.py +75 -0
  92. package/package.json +3 -6
  93. package/requirements.txt +0 -5
  94. package/scripts/agent_eval.py +16 -28
  95. package/scripts/brain_quality_eval.py +20 -183
  96. package/scripts/bump_version.py +19 -6
  97. package/scripts/check_current_release_docs.mjs +7 -4
  98. package/scripts/check_legacy_debt.mjs +1 -1
  99. package/scripts/check_openapi_drift.mjs +10 -0
  100. package/scripts/check_server_i18n.mjs +2 -18
  101. package/scripts/compose_openapi.py +377 -0
  102. package/scripts/export_openapi.py +32 -4
  103. package/scripts/gen_messages_catalog_fixture.py +302 -0
  104. package/scripts/gen_openapi_fragments.py +365 -0
  105. package/scripts/gen_redact_fixture.py +196 -0
  106. package/scripts/gen_worker_allowlist_fixture.py +115 -0
  107. package/scripts/generate_agent_parity_fixtures.py +33 -14
  108. package/scripts/openapi_route_families.json +2161 -0
  109. package/scripts/release_screen_claims.json +31 -0
  110. package/scripts/run_integration_tests.mjs +134 -29
  111. package/scripts/run_sidecar_e2e.mjs +104 -11
  112. package/scripts/wheel_smoke.py +32 -22
  113. package/src-tauri/Cargo.lock +375 -9
  114. package/src-tauri/Cargo.toml +1 -1
  115. package/src-tauri/src/backend.rs +223 -43
  116. package/src-tauri/src/main.rs +27 -17
  117. package/src-tauri/src/topology.rs +58 -88
  118. package/src-tauri/tauri.conf.json +2 -3
  119. package/static/app/asset-manifest.json +41 -41
  120. package/static/app/assets/{Act-Drs_jd-O.js → Act-CToZnOHz.js} +1 -1
  121. package/static/app/assets/{AdminConsole-BqNx5oF6.js → AdminConsole-D5kbrBu8.js} +1 -1
  122. package/static/app/assets/{Brain-C7bdNmz-.js → Brain-C6zCdv4S.js} +1 -1
  123. package/static/app/assets/{BrainHome-DcTnINcI.js → BrainHome-D4n3EDUt.js} +1 -1
  124. package/static/app/assets/{BrainSignals-Df1MRIPD.js → BrainSignals-CzeZ-vQ9.js} +1 -1
  125. package/static/app/assets/{Capture-Dgsxvnbx.js → Capture-DPcyTczq.js} +1 -1
  126. package/static/app/assets/{Chronicle-BrkTxJK1.js → Chronicle-D3Nx-oWS.js} +1 -1
  127. package/static/app/assets/{CommandPalette-DDziw7Oj.js → CommandPalette-DPPvpp28.js} +1 -1
  128. package/static/app/assets/{Library-CYxeuraA.js → Library-Co2qTu3I.js} +1 -1
  129. package/static/app/assets/{LivingBrain-CNp6mvCm.js → LivingBrain-Bsmzwy8_.js} +1 -1
  130. package/static/app/assets/{ProductFlow-BVsowu3Z.js → ProductFlow-CBLdmesT.js} +1 -1
  131. package/static/app/assets/{ReviewCard-D1LbOfJS.js → ReviewCard-CfR6DLs7.js} +1 -1
  132. package/static/app/assets/{System-DcRAq7wj.js → System-Ce-P2YL6.js} +1 -1
  133. package/static/app/assets/arrow-left-DD5jFGHV.js +1 -0
  134. package/static/app/assets/{bot-BK2xQ9mN.js → bot-C5-XIww2.js} +1 -1
  135. package/static/app/assets/{brain-BZNztFBb.js → brain-CFRJUvdX.js} +1 -1
  136. package/static/app/assets/{button-CjueubVZ.js → button-DI1KcQP-.js} +1 -1
  137. package/static/app/assets/circle-check-CY20XMCh.js +1 -0
  138. package/static/app/assets/{circle-pause-HRjSmpyY.js → circle-pause-C8HHyood.js} +1 -1
  139. package/static/app/assets/{circle-play-CMcKDVwu.js → circle-play-Cufc235E.js} +1 -1
  140. package/static/app/assets/{cpu-B_MyTfYr.js → cpu-DW2_JVsv.js} +1 -1
  141. package/static/app/assets/{download-CTy0EByV.js → download-gbdKTBb8.js} +1 -1
  142. package/static/app/assets/{folder-open-DF6bLhTg.js → folder-open-C9ydyX33.js} +1 -1
  143. package/static/app/assets/{hard-drive-CpWHG77C.js → hard-drive-BxNMzBM3.js} +1 -1
  144. package/static/app/assets/index-BovSWRmQ.css +2 -0
  145. package/static/app/assets/{index-Dd6abJHX.js → index-q9XyDY7A.js} +3 -3
  146. package/static/app/assets/{input-bVgIRN3s.js → input-GLD94Y_K.js} +1 -1
  147. package/static/app/assets/{link-2-bg6CG5Vk.js → link-2-B4Y0gz1S.js} +1 -1
  148. package/static/app/assets/{permissionCopy-DFTf1HDZ.js → permissionCopy-CxrV0fvU.js} +1 -1
  149. package/static/app/assets/{primitives-D7D-sQ3T.js → primitives-2ANdAsu2.js} +1 -1
  150. package/static/app/assets/search-DTxtjQ07.js +1 -0
  151. package/static/app/assets/{share-2-BLzo7U4L.js → share-2-DxArfiuI.js} +1 -1
  152. package/static/app/assets/{shield-alert-BmngqnsJ.js → shield-alert-CgftuFFs.js} +1 -1
  153. package/static/app/assets/{textarea-DOSlAQQ3.js → textarea-BHVBAoCr.js} +1 -1
  154. package/static/app/assets/{useFocusTrap-pZhgeee4.js → useFocusTrap-Dzq2j_OG.js} +1 -1
  155. package/static/app/assets/{useMutation-BMDwNk4I.js → useMutation-BcqfMLNq.js} +1 -1
  156. package/static/app/assets/{useQuery-CSjKttRo.js → useQuery-DBzLKCOv.js} +1 -1
  157. package/static/app/assets/{utils-D3u_yv7B.js → utils-B-Aah1bO.js} +1 -1
  158. package/static/app/assets/{workspace-H3bMjYXC.js → workspace-B6fFhoko.js} +1 -1
  159. package/static/app/index.html +4 -4
  160. package/static/sw.js +1 -1
  161. package/desktop/electron/README.md +0 -9
  162. package/desktop/electron/main.cjs +0 -58
  163. package/desktop/electron/preload.cjs +0 -5
  164. package/lattice_brain/archive.py +0 -522
  165. package/lattice_brain/context.py +0 -325
  166. package/lattice_brain/conversations.py +0 -382
  167. package/lattice_brain/core.py +0 -82
  168. package/lattice_brain/graph/_kg_contract.py +0 -249
  169. package/lattice_brain/graph/curator.py +0 -676
  170. package/lattice_brain/graph/discovery.py +0 -595
  171. package/lattice_brain/graph/discovery_index/__init__.py +0 -35
  172. package/lattice_brain/graph/discovery_index/cleanup.py +0 -182
  173. package/lattice_brain/graph/discovery_index/extract.py +0 -137
  174. package/lattice_brain/graph/discovery_index/scan.py +0 -411
  175. package/lattice_brain/graph/discovery_index/upsert.py +0 -495
  176. package/lattice_brain/graph/documents.py +0 -380
  177. package/lattice_brain/graph/fusion.py +0 -395
  178. package/lattice_brain/graph/identity.py +0 -175
  179. package/lattice_brain/graph/image_vectors.py +0 -230
  180. package/lattice_brain/graph/ingest.py +0 -829
  181. package/lattice_brain/graph/network.py +0 -205
  182. package/lattice_brain/graph/proactive.py +0 -724
  183. package/lattice_brain/graph/projection/__init__.py +0 -42
  184. package/lattice_brain/graph/projection/curation.py +0 -500
  185. package/lattice_brain/graph/projection/v2_schema.py +0 -546
  186. package/lattice_brain/graph/provenance.py +0 -524
  187. package/lattice_brain/graph/rerank.py +0 -163
  188. package/lattice_brain/graph/retrieval/__init__.py +0 -54
  189. package/lattice_brain/graph/retrieval/context.py +0 -197
  190. package/lattice_brain/graph/retrieval/graph_view.py +0 -319
  191. package/lattice_brain/graph/retrieval/hybrid.py +0 -488
  192. package/lattice_brain/graph/retrieval/maintenance.py +0 -121
  193. package/lattice_brain/graph/retrieval/signals.py +0 -95
  194. package/lattice_brain/graph/retrieval_docgen.py +0 -253
  195. package/lattice_brain/graph/retrieval_policy.py +0 -180
  196. package/lattice_brain/graph/retrieval_reads.py +0 -769
  197. package/lattice_brain/graph/retrieval_vector/__init__.py +0 -42
  198. package/lattice_brain/graph/retrieval_vector/fingerprint.py +0 -97
  199. package/lattice_brain/graph/retrieval_vector/indexing.py +0 -347
  200. package/lattice_brain/graph/retrieval_vector/search.py +0 -560
  201. package/lattice_brain/graph/retrieval_vector/status.py +0 -374
  202. package/lattice_brain/graph/schema.py +0 -792
  203. package/lattice_brain/graph/store.py +0 -268
  204. package/lattice_brain/graph/vector_index/__init__.py +0 -85
  205. package/lattice_brain/graph/vector_index/base.py +0 -170
  206. package/lattice_brain/graph/vector_index/brute_force.py +0 -114
  207. package/lattice_brain/graph/vector_index/hnsw.py +0 -293
  208. package/lattice_brain/graph/vector_index/jobs.py +0 -287
  209. package/lattice_brain/graph/vector_index/quantized.py +0 -151
  210. package/lattice_brain/graph/vector_index/selector.py +0 -161
  211. package/lattice_brain/graph/write_master.py +0 -308
  212. package/lattice_brain/ingestion/_contract.py +0 -90
  213. package/lattice_brain/ingestion/folder_scan.py +0 -57
  214. package/lattice_brain/ingestion/folders.py +0 -258
  215. package/lattice_brain/ingestion/jobs_api.py +0 -107
  216. package/lattice_brain/ingestion/routing.py +0 -295
  217. package/lattice_brain/ingestion_jobs.py +0 -383
  218. package/lattice_brain/memory.py +0 -102
  219. package/lattice_brain/portability/__init__.py +0 -90
  220. package/lattice_brain/portability/_contract.py +0 -42
  221. package/lattice_brain/portability/backups.py +0 -338
  222. package/lattice_brain/portability/bundles.py +0 -136
  223. package/lattice_brain/portability/constants.py +0 -93
  224. package/lattice_brain/portability/fsops.py +0 -138
  225. package/lattice_brain/portability/service.py +0 -41
  226. package/lattice_brain/portability/sharing.py +0 -714
  227. package/lattice_brain/quality.py +0 -592
  228. package/lattice_brain/retrieval_benchmark_fixtures.py +0 -92
  229. package/lattice_brain/runtime/agent_runtime.py +0 -859
  230. package/lattice_brain/runtime/contracts.py +0 -460
  231. package/lattice_brain/runtime/multi_agent.py +0 -942
  232. package/lattice_brain/runtime/statuses.py +0 -10
  233. package/lattice_brain/sealed_box.py +0 -244
  234. package/lattice_brain/self_model.py +0 -675
  235. package/lattice_brain/sensitivity.py +0 -94
  236. package/lattice_brain/storage/__init__.py +0 -22
  237. package/lattice_brain/storage/base.py +0 -100
  238. package/lattice_brain/storage/docker.py +0 -105
  239. package/lattice_brain/storage/factory.py +0 -31
  240. package/lattice_brain/storage/migration.py +0 -191
  241. package/lattice_brain/storage/postgres.py +0 -123
  242. package/lattice_brain/storage/sqlite.py +0 -143
  243. package/lattice_brain/synthesis.py +0 -824
  244. package/lattice_brain/workflow.py +0 -497
  245. package/latticeai/api/admin.py +0 -471
  246. package/latticeai/api/agent_registry.py +0 -105
  247. package/latticeai/api/agents.py +0 -228
  248. package/latticeai/api/auth.py +0 -360
  249. package/latticeai/api/automation_intelligence.py +0 -401
  250. package/latticeai/api/brain_intelligence.py +0 -199
  251. package/latticeai/api/browser.py +0 -493
  252. package/latticeai/api/change_proposals.py +0 -89
  253. package/latticeai/api/chat.py +0 -572
  254. package/latticeai/api/chat_agent_http.py +0 -901
  255. package/latticeai/api/chat_contracts.py +0 -73
  256. package/latticeai/api/chat_documents.py +0 -276
  257. package/latticeai/api/chat_helpers.py +0 -463
  258. package/latticeai/api/chat_history.py +0 -101
  259. package/latticeai/api/chat_hybrid.py +0 -113
  260. package/latticeai/api/chat_intents.py +0 -646
  261. package/latticeai/api/chat_stream.py +0 -216
  262. package/latticeai/api/chronicle.py +0 -63
  263. package/latticeai/api/command_center.py +0 -51
  264. package/latticeai/api/computer_use.py +0 -485
  265. package/latticeai/api/evidence_actions.py +0 -48
  266. package/latticeai/api/features.py +0 -70
  267. package/latticeai/api/funnel_metrics.py +0 -31
  268. package/latticeai/api/garden.py +0 -34
  269. package/latticeai/api/hooks.py +0 -165
  270. package/latticeai/api/index_jobs.py +0 -145
  271. package/latticeai/api/invitations.py +0 -100
  272. package/latticeai/api/knowledge_graph.py +0 -536
  273. package/latticeai/api/marketplace.py +0 -105
  274. package/latticeai/api/mcp.py +0 -482
  275. package/latticeai/api/memory.py +0 -270
  276. package/latticeai/api/network.py +0 -81
  277. package/latticeai/api/network_boundary.py +0 -225
  278. package/latticeai/api/permission_mode.py +0 -61
  279. package/latticeai/api/permissions.py +0 -410
  280. package/latticeai/api/plugins.py +0 -126
  281. package/latticeai/api/portability.py +0 -391
  282. package/latticeai/api/project_sessions.py +0 -114
  283. package/latticeai/api/realtime.py +0 -118
  284. package/latticeai/api/review_queue.py +0 -364
  285. package/latticeai/api/security_dashboard.py +0 -604
  286. package/latticeai/api/setup.py +0 -319
  287. package/latticeai/api/static_routes.py +0 -351
  288. package/latticeai/api/ui_redirects.py +0 -26
  289. package/latticeai/api/workflow_designer.py +0 -394
  290. package/latticeai/api/workspace.py +0 -856
  291. package/latticeai/api/workspace_scope.py +0 -125
  292. package/latticeai/core/agent/__init__.py +0 -93
  293. package/latticeai/core/agent/_contract.py +0 -79
  294. package/latticeai/core/agent/context.py +0 -57
  295. package/latticeai/core/agent/deps.py +0 -125
  296. package/latticeai/core/agent/execution.py +0 -622
  297. package/latticeai/core/agent/planning.py +0 -145
  298. package/latticeai/core/agent/recovery.py +0 -157
  299. package/latticeai/core/agent/runtime.py +0 -210
  300. package/latticeai/core/agent/verification.py +0 -231
  301. package/latticeai/core/agent_eval.py +0 -739
  302. package/latticeai/core/agent_helpers.py +0 -493
  303. package/latticeai/core/agent_profiles.py +0 -110
  304. package/latticeai/core/agent_prompts.py +0 -171
  305. package/latticeai/core/agent_registry.py +0 -232
  306. package/latticeai/core/agent_state.py +0 -41
  307. package/latticeai/core/agent_trace.py +0 -104
  308. package/latticeai/core/artifact_ledger.py +0 -109
  309. package/latticeai/core/audit.py +0 -260
  310. package/latticeai/core/builtin_hooks.py +0 -105
  311. package/latticeai/core/context_builder.py +0 -394
  312. package/latticeai/core/document_generator.py +0 -103
  313. package/latticeai/core/enterprise.py +0 -157
  314. package/latticeai/core/enterprise_admin.py +0 -158
  315. package/latticeai/core/file_generation/__init__.py +0 -115
  316. package/latticeai/core/file_generation/bundles.py +0 -76
  317. package/latticeai/core/file_generation/extraction.py +0 -154
  318. package/latticeai/core/file_generation/inference.py +0 -235
  319. package/latticeai/core/file_generation/orchestration.py +0 -152
  320. package/latticeai/core/file_generation/prompting.py +0 -117
  321. package/latticeai/core/file_generation/repair.py +0 -114
  322. package/latticeai/core/file_generation/sanitize.py +0 -61
  323. package/latticeai/core/file_generation/validation.py +0 -201
  324. package/latticeai/core/graph_curator.py +0 -11
  325. package/latticeai/core/hooks.py +0 -11
  326. package/latticeai/core/invitations.py +0 -131
  327. package/latticeai/core/legacy_compatibility.py +0 -243
  328. package/latticeai/core/local_embeddings.py +0 -104
  329. package/latticeai/core/logging_safety.py +0 -46
  330. package/latticeai/core/marketplace.py +0 -293
  331. package/latticeai/core/mcp_catalog.py +0 -452
  332. package/latticeai/core/mcp_registry.py +0 -506
  333. package/latticeai/core/multi_agent.py +0 -11
  334. package/latticeai/core/network_boundary.py +0 -168
  335. package/latticeai/core/oidc.py +0 -208
  336. package/latticeai/core/plugins.py +0 -432
  337. package/latticeai/core/product_hardening.py +0 -218
  338. package/latticeai/core/project_sessions.py +0 -337
  339. package/latticeai/core/realtime.py +0 -238
  340. package/latticeai/core/run_explain.py +0 -423
  341. package/latticeai/core/run_store.py +0 -252
  342. package/latticeai/core/timezones.py +0 -80
  343. package/latticeai/core/workflow_engine.py +0 -11
  344. package/latticeai/core/workspace_computer_memory.py +0 -84
  345. package/latticeai/core/workspace_graph_trace.py +0 -161
  346. package/latticeai/core/workspace_indexing.py +0 -102
  347. package/latticeai/core/workspace_memory.py +0 -77
  348. package/latticeai/core/workspace_onboarding.py +0 -104
  349. package/latticeai/core/workspace_os.py +0 -988
  350. package/latticeai/core/workspace_os_constants.py +0 -126
  351. package/latticeai/core/workspace_os_state.py +0 -180
  352. package/latticeai/core/workspace_os_utils.py +0 -69
  353. package/latticeai/core/workspace_permissions.py +0 -101
  354. package/latticeai/core/workspace_plugins.py +0 -97
  355. package/latticeai/core/workspace_relationships.py +0 -99
  356. package/latticeai/core/workspace_reorganization.py +0 -335
  357. package/latticeai/core/workspace_review_items.py +0 -118
  358. package/latticeai/core/workspace_runs.py +0 -732
  359. package/latticeai/core/workspace_skills.py +0 -114
  360. package/latticeai/core/workspace_snapshots.py +0 -198
  361. package/latticeai/core/workspace_timeline.py +0 -110
  362. package/latticeai/integrations/__init__.py +0 -0
  363. package/latticeai/integrations/telegram_bot/__init__.py +0 -123
  364. package/latticeai/integrations/telegram_bot/__main__.py +0 -17
  365. package/latticeai/integrations/telegram_bot/config.py +0 -86
  366. package/latticeai/integrations/telegram_bot/dispatch.py +0 -311
  367. package/latticeai/integrations/telegram_bot/flows.py +0 -478
  368. package/latticeai/integrations/telegram_bot/helpers.py +0 -322
  369. package/latticeai/integrations/telegram_bot/screens.py +0 -394
  370. package/latticeai/runtime/audit_runtime.py +0 -76
  371. package/latticeai/runtime/automation_runtime.py +0 -81
  372. package/latticeai/runtime/chat_wiring.py +0 -141
  373. package/latticeai/runtime/context_runtime.py +0 -66
  374. package/latticeai/runtime/feature_toggle_wiring.py +0 -163
  375. package/latticeai/runtime/history_runtime.py +0 -163
  376. package/latticeai/runtime/history_writer.py +0 -138
  377. package/latticeai/runtime/hooks_runtime.py +0 -77
  378. package/latticeai/runtime/model_wiring.py +0 -68
  379. package/latticeai/runtime/namespace_runtime.py +0 -163
  380. package/latticeai/runtime/network_boundary_wiring.py +0 -128
  381. package/latticeai/runtime/network_config_runtime.py +0 -56
  382. package/latticeai/runtime/permission_mode_wiring.py +0 -119
  383. package/latticeai/runtime/persistence_runtime.py +0 -159
  384. package/latticeai/runtime/platform_runtime_wiring.py +0 -89
  385. package/latticeai/runtime/review_wiring.py +0 -42
  386. package/latticeai/runtime/router_registration.py +0 -689
  387. package/latticeai/runtime/sso_config_runtime.py +0 -128
  388. package/latticeai/runtime/user_key_runtime.py +0 -106
  389. package/latticeai/runtime/web_runtime.py +0 -92
  390. package/latticeai/server_app.py +0 -51
  391. package/latticeai/services/app_context.py +0 -130
  392. package/latticeai/services/automation_execution.py +0 -266
  393. package/latticeai/services/automation_intelligence.py +0 -614
  394. package/latticeai/services/brain_automation.py +0 -191
  395. package/latticeai/services/brain_intelligence/__init__.py +0 -58
  396. package/latticeai/services/brain_intelligence/_contract.py +0 -71
  397. package/latticeai/services/brain_intelligence/consistency.py +0 -193
  398. package/latticeai/services/brain_intelligence/constants.py +0 -47
  399. package/latticeai/services/brain_intelligence/digest.py +0 -258
  400. package/latticeai/services/brain_intelligence/health.py +0 -331
  401. package/latticeai/services/brain_intelligence/proposals.py +0 -264
  402. package/latticeai/services/brain_intelligence/sampling.py +0 -84
  403. package/latticeai/services/brain_intelligence/service.py +0 -48
  404. package/latticeai/services/change_proposals.py +0 -505
  405. package/latticeai/services/chat_service.py +0 -243
  406. package/latticeai/services/chronicle.py +0 -557
  407. package/latticeai/services/cloud_egress_audit.py +0 -85
  408. package/latticeai/services/cloud_extraction.py +0 -129
  409. package/latticeai/services/cloud_streaming.py +0 -268
  410. package/latticeai/services/cloud_token_guard.py +0 -84
  411. package/latticeai/services/command_center.py +0 -548
  412. package/latticeai/services/evidence_actions.py +0 -255
  413. package/latticeai/services/feature_toggles.py +0 -502
  414. package/latticeai/services/folder_watch.py +0 -520
  415. package/latticeai/services/funnel_metrics.py +0 -307
  416. package/latticeai/services/hybrid_chat.py +0 -316
  417. package/latticeai/services/hybrid_context.py +0 -228
  418. package/latticeai/services/hybrid_policy.py +0 -178
  419. package/latticeai/services/ingestion.py +0 -11
  420. package/latticeai/services/interop_bridges.py +0 -978
  421. package/latticeai/services/kg_portability.py +0 -11
  422. package/latticeai/services/local_knowledge.py +0 -465
  423. package/latticeai/services/memory_service/__init__.py +0 -52
  424. package/latticeai/services/memory_service/_contract.py +0 -100
  425. package/latticeai/services/memory_service/brief.py +0 -431
  426. package/latticeai/services/memory_service/constants.py +0 -57
  427. package/latticeai/services/memory_service/maintenance.py +0 -138
  428. package/latticeai/services/memory_service/manager.py +0 -186
  429. package/latticeai/services/memory_service/proof.py +0 -136
  430. package/latticeai/services/memory_service/recall.py +0 -225
  431. package/latticeai/services/memory_service/service.py +0 -48
  432. package/latticeai/services/memory_service/stores.py +0 -110
  433. package/latticeai/services/model_recommendation.py +0 -224
  434. package/latticeai/services/model_runtime/cloud.py +0 -87
  435. package/latticeai/services/multimodal_streaming.py +0 -129
  436. package/latticeai/services/network_boundary_service.py +0 -154
  437. package/latticeai/services/obsidian_bridge.py +0 -609
  438. package/latticeai/services/openai_compatible_adapter.py +0 -101
  439. package/latticeai/services/permission_mode_service.py +0 -170
  440. package/latticeai/services/platform_runtime.py +0 -366
  441. package/latticeai/services/review_queue.py +0 -380
  442. package/latticeai/services/router_context.py +0 -59
  443. package/latticeai/services/run_executor.py +0 -387
  444. package/latticeai/services/self_model_service.py +0 -171
  445. package/latticeai/services/setup_detection.py +0 -81
  446. package/latticeai/services/triggers.py +0 -378
  447. package/latticeai/services/upload_service.py +0 -185
  448. package/latticeai/services/workspace_service.py +0 -171
  449. package/latticeai/setup/__init__.py +0 -25
  450. package/latticeai/setup/auto_setup.py +0 -857
  451. package/latticeai/setup/demo_corpus.py +0 -98
  452. package/latticeai/setup/wizard/__init__.py +0 -126
  453. package/latticeai/setup/wizard/catalog.py +0 -172
  454. package/latticeai/setup/wizard/detect.py +0 -323
  455. package/latticeai/setup/wizard/install.py +0 -348
  456. package/latticeai/setup/wizard/paths.py +0 -168
  457. package/latticeai/setup/wizard/plans.py +0 -74
  458. package/latticeai/setup/wizard/recommend.py +0 -320
  459. package/scripts/bench_agent_smoke.py +0 -409
  460. package/scripts/bench_models.py +0 -540
  461. package/scripts/bench_vector_index.py +0 -295
  462. package/scripts/funnel_soft_gate.py +0 -192
  463. package/scripts/generate_agent_loop_fixtures.py +0 -907
  464. package/scripts/generate_rust_parity_fixtures.py +0 -1000
  465. package/scripts/measure_brain_home_fill.mjs +0 -141
  466. package/scripts/migrate_brain_storage.py +0 -57
  467. package/scripts/parity_fixture_corpus_docgen.py +0 -341
  468. package/scripts/profile_kg.py +0 -355
  469. package/server.py +0 -30
  470. package/static/app/assets/arrow-left-BHYmOTWU.js +0 -1
  471. package/static/app/assets/circle-check-CLeWJr67.js +0 -1
  472. package/static/app/assets/index-DxmOfNRi.css +0 -2
  473. package/static/app/assets/search-BgSVX6NG.js +0 -1
@@ -1,493 +0,0 @@
1
- """Browser & web ingestion — Knowledge Graph inputs, not standalone features.
2
-
3
- v3.6.0 Knowledge Graph First: a public URL or an open browser tab is just another
4
- source that converges into the Knowledge Graph through the unified ingestion
5
- pipeline. Everything runs on the **local runtime** — the server fetches/reads
6
- locally, stores into local SQLite, and never uploads to a cloud service.
7
-
8
- Two layers, both feeding ``IngestionPipeline.ingest``:
9
-
10
- * ``POST /api/browser/read-url`` — the runtime fetches a public URL locally,
11
- extracts readable text, stores it as ``source_type=web_url``. Fails gracefully
12
- on blocked / login-required pages.
13
- * ``POST /api/browser/ingest-current-tab`` — accepts a payload from the local
14
- browser extension (url/title/text/selected_text/html), sanitizes + size-limits
15
- it, stores it as ``source_type=browser_tab``.
16
- """
17
-
18
- from __future__ import annotations
19
-
20
- import ipaddress
21
- import socket
22
- from html.parser import HTMLParser
23
- from typing import Any, Callable, Optional, Tuple
24
- from urllib.parse import SplitResult, urljoin, urlsplit, urlunsplit
25
-
26
- from fastapi import APIRouter, HTTPException, Request
27
- from pydantic import BaseModel
28
-
29
- from lattice_brain.ingestion import IngestionItem, capture_quality_verdict
30
- from latticeai import __version__
31
- from latticeai.api.workspace_scope import resolve_workspace_scope
32
- from latticeai.core.messages import DEFAULT_LANGUAGE, http_error, resolve_language
33
- from latticeai.core.quiet import quiet
34
-
35
- MAX_TAB_BYTES = 4 * 1024 * 1024 # 4 MB per captured tab payload
36
- MAX_URL_FETCH_BYTES = 4 * 1024 * 1024 # 4 MB cap on a fetched page
37
- URL_FETCH_TIMEOUT = 12.0 # seconds
38
- MAX_URL_LENGTH = 8192
39
- MAX_URL_REDIRECTS = 5
40
-
41
- _REDIRECT_STATUSES = frozenset({301, 302, 303, 307, 308})
42
- _TEXTUAL_APPLICATION_TYPES = frozenset({
43
- "application/json",
44
- "application/ld+json",
45
- "application/xhtml+xml",
46
- "application/xml",
47
- })
48
-
49
-
50
- class BrowserFetchError(Exception):
51
- """A URL could not be fetched (blocked, login-required, timeout, too big)."""
52
-
53
-
54
- # ── readable-text extraction ─────────────────────────────────────────────────
55
- _SKIP_TAGS = {"script", "style", "noscript", "template", "svg", "head"}
56
-
57
-
58
- class _TextExtractor(HTMLParser):
59
- def __init__(self) -> None:
60
- super().__init__(convert_charrefs=True)
61
- self._skip_depth = 0
62
- self._chunks: list[str] = []
63
- self.title: str = ""
64
- self._in_title = False
65
-
66
- def handle_starttag(self, tag, attrs):
67
- if tag in _SKIP_TAGS:
68
- self._skip_depth += 1
69
- if tag == "title":
70
- self._in_title = True
71
-
72
- def handle_endtag(self, tag):
73
- if tag in _SKIP_TAGS and self._skip_depth > 0:
74
- self._skip_depth -= 1
75
- if tag == "title":
76
- self._in_title = False
77
- if tag in {"p", "div", "br", "li", "h1", "h2", "h3", "h4", "section", "article"}:
78
- self._chunks.append("\n")
79
-
80
- def handle_data(self, data):
81
- if self._in_title:
82
- self.title += data
83
- if self._skip_depth == 0:
84
- text = data.strip()
85
- if text:
86
- self._chunks.append(text)
87
-
88
- def text(self) -> str:
89
- raw = " ".join(self._chunks)
90
- # collapse runs of whitespace while keeping paragraph breaks
91
- lines = [ln.strip() for ln in raw.replace("\r", "").split("\n")]
92
- return "\n".join([ln for ln in lines if ln]).strip()
93
-
94
-
95
- def extract_readable_text(html: str) -> Tuple[str, str]:
96
- """Return (title, readable_text) from an HTML string. Never raises."""
97
- parser = _TextExtractor()
98
- try:
99
- parser.feed(html or "")
100
- except Exception: # noqa: BLE001 — malformed HTML must still yield best-effort text
101
- quiet()
102
- return parser.title.strip(), parser.text()
103
-
104
-
105
- def _parse_http_url(url: str) -> tuple[str, SplitResult, str, int]:
106
- """Parse a URL into safe, unambiguous HTTP connection components."""
107
- cleaned = (url or "").strip()
108
- if not cleaned:
109
- raise ValueError("url is required.")
110
- if len(cleaned) > MAX_URL_LENGTH:
111
- raise ValueError("URL is too long.")
112
- if "\\" in cleaned or any(ord(char) < 32 or ord(char) == 127 for char in cleaned):
113
- raise ValueError("Malformed URL.")
114
-
115
- try:
116
- parsed = urlsplit(cleaned)
117
- hostname = parsed.hostname
118
- port = parsed.port
119
- except ValueError as exc:
120
- raise ValueError("Malformed URL.") from exc
121
- if parsed.scheme.lower() not in ("http", "https"):
122
- raise ValueError("Only http(s) URLs are supported.")
123
- if not parsed.netloc or not hostname:
124
- raise ValueError("Malformed URL.")
125
- if parsed.username is not None or parsed.password is not None:
126
- raise ValueError("URLs containing credentials are not supported.")
127
- if "%" in hostname:
128
- # Scoped IPv6 literals are interface-local by definition and also have
129
- # inconsistent URL parser semantics across HTTP clients.
130
- raise ValueError("Scoped IP addresses are not supported.")
131
-
132
- hostname = hostname.rstrip(".")
133
- if not hostname:
134
- raise ValueError("Malformed URL.")
135
- try:
136
- ascii_hostname = hostname.encode("idna").decode("ascii").lower()
137
- except UnicodeError as exc:
138
- raise ValueError("Malformed URL hostname.") from exc
139
- if len(ascii_hostname) > 253:
140
- raise ValueError("Malformed URL hostname.")
141
-
142
- port = port if port is not None else (443 if parsed.scheme.lower() == "https" else 80)
143
- if port < 1 or port > 65535:
144
- raise ValueError("Malformed URL port.")
145
- return cleaned, parsed, ascii_hostname, port
146
-
147
-
148
- def _is_public_ip(value: str) -> bool:
149
- """Return whether *value* is globally routable (IPv4 or IPv6)."""
150
- try:
151
- address = ipaddress.ip_address(value)
152
- except ValueError:
153
- return False
154
- # Some Python releases report multicast addresses as ``is_global``. Keep
155
- # the security classes explicit instead of relying on that single flag.
156
- return address.is_global and not any((
157
- address.is_loopback,
158
- address.is_private,
159
- address.is_link_local,
160
- address.is_multicast,
161
- address.is_unspecified,
162
- address.is_reserved,
163
- getattr(address, "is_site_local", False),
164
- ))
165
-
166
-
167
- def _resolve_public_target(
168
- url: str,
169
- *,
170
- resolver: Optional[Callable[..., Any]] = None,
171
- ) -> tuple[str, SplitResult, str, int, tuple[str, ...]]:
172
- """Resolve a URL and reject every non-public address before connecting."""
173
- try:
174
- cleaned, parsed, hostname, port = _parse_http_url(url)
175
- except ValueError as exc:
176
- raise BrowserFetchError(str(exc)) from exc
177
-
178
- if hostname == "localhost" or hostname.endswith(".localhost"):
179
- raise BrowserFetchError("Local and private network URLs are not allowed.")
180
-
181
- resolve = resolver or socket.getaddrinfo
182
- try:
183
- records = resolve(
184
- hostname,
185
- port,
186
- family=socket.AF_UNSPEC,
187
- type=socket.SOCK_STREAM,
188
- )
189
- except (OSError, socket.gaierror) as exc:
190
- raise BrowserFetchError(f"Could not resolve the page host: {hostname}.") from exc
191
-
192
- addresses: list[str] = []
193
- for record in records or ():
194
- try:
195
- address = str(record[4][0])
196
- except (IndexError, TypeError):
197
- raise BrowserFetchError("The page host returned an invalid DNS record.")
198
- if not _is_public_ip(address):
199
- raise BrowserFetchError("Local and private network URLs are not allowed.")
200
- if address not in addresses:
201
- addresses.append(address)
202
- if not addresses:
203
- raise BrowserFetchError(f"Could not resolve the page host: {hostname}.")
204
- return cleaned, parsed, hostname, port, tuple(addresses)
205
-
206
-
207
- def _origin_host_header(hostname: str, port: int, scheme: str) -> str:
208
- try:
209
- is_ipv6 = ipaddress.ip_address(hostname).version == 6
210
- except ValueError:
211
- is_ipv6 = False
212
- host = f"[{hostname}]" if is_ipv6 else hostname
213
- default_port = 443 if scheme == "https" else 80
214
- return host if port == default_port else f"{host}:{port}"
215
-
216
-
217
- def _pinned_request_url(parsed: SplitResult, address: str, port: int) -> str:
218
- """Build a URL that connects to a pre-validated DNS result.
219
-
220
- The request retains the original Host header and TLS SNI separately. This
221
- closes the DNS-rebinding window between validation and socket connection.
222
- """
223
- ip = ipaddress.ip_address(address)
224
- host = f"[{ip.compressed}]" if ip.version == 6 else ip.compressed
225
- default_port = 443 if parsed.scheme.lower() == "https" else 80
226
- netloc = host if port == default_port else f"{host}:{port}"
227
- return urlunsplit((parsed.scheme.lower(), netloc, parsed.path or "/", parsed.query, ""))
228
-
229
-
230
- def _read_limited_response(response: Any, max_bytes: int) -> bytes:
231
- content_length = response.headers.get("content-length")
232
- if content_length:
233
- try:
234
- declared_size = int(content_length)
235
- except ValueError:
236
- declared_size = -1
237
- if declared_size > max_bytes:
238
- raise BrowserFetchError("The page is too large to ingest.")
239
-
240
- body = bytearray()
241
- for chunk in response.iter_bytes():
242
- if len(body) + len(chunk) > max_bytes:
243
- raise BrowserFetchError("The page is too large to ingest.")
244
- body.extend(chunk)
245
- return bytes(body)
246
-
247
-
248
- def _default_fetch_url(
249
- url: str,
250
- *,
251
- resolver: Optional[Callable[..., Any]] = None,
252
- transport: Any = None,
253
- max_bytes: int = MAX_URL_FETCH_BYTES,
254
- max_redirects: int = MAX_URL_REDIRECTS,
255
- ) -> Tuple[str, str]:
256
- """Fetch a public URL on the local runtime and extract readable text.
257
-
258
- Raises :class:`BrowserFetchError` on any non-success (blocked, login wall,
259
- timeout, oversized, non-HTML) so the route can fail gracefully.
260
- """
261
- import httpx
262
-
263
- if max_bytes < 1 or max_redirects < 0:
264
- raise BrowserFetchError("Invalid URL fetch limits.")
265
-
266
- client_options: dict[str, Any] = {
267
- "follow_redirects": False,
268
- "timeout": URL_FETCH_TIMEOUT,
269
- "trust_env": False,
270
- "limits": httpx.Limits(max_keepalive_connections=0),
271
- }
272
- if transport is not None:
273
- client_options["transport"] = transport
274
-
275
- current_url = url
276
- try:
277
- with httpx.Client(**client_options) as client:
278
- for redirect_count in range(max_redirects + 1):
279
- current_url, parsed, hostname, port, addresses = _resolve_public_target(
280
- current_url,
281
- resolver=resolver,
282
- )
283
- next_url: Optional[str] = None
284
- connect_errors: list[Exception] = []
285
-
286
- for address in addresses:
287
- request = client.build_request(
288
- "GET",
289
- _pinned_request_url(parsed, address, port),
290
- headers={
291
- "Accept": "text/html, text/plain;q=0.9, application/xhtml+xml;q=0.8",
292
- "Accept-Encoding": "identity",
293
- "Connection": "close",
294
- "Host": _origin_host_header(hostname, port, parsed.scheme.lower()),
295
- "User-Agent": f"LatticeAI-local/{__version__} (+local-first knowledge graph)",
296
- },
297
- extensions={"sni_hostname": hostname},
298
- )
299
- response = None
300
- try:
301
- response = client.send(request, stream=True)
302
- except (httpx.ConnectError, httpx.ConnectTimeout) as exc:
303
- connect_errors.append(exc)
304
- continue
305
-
306
- try:
307
- if response.status_code in _REDIRECT_STATUSES:
308
- location = (response.headers.get("location") or "").strip()
309
- if not location:
310
- raise BrowserFetchError("The page returned a redirect without a location.")
311
- if redirect_count >= max_redirects:
312
- raise BrowserFetchError("The page redirected too many times.")
313
- # Join against the original public URL, never the pinned
314
- # IP URL exposed only to the transport.
315
- next_url = urljoin(current_url, location)
316
- break
317
- if response.status_code in (401, 403):
318
- raise BrowserFetchError(
319
- "The page is login-required or blocked "
320
- f"(HTTP {response.status_code})."
321
- )
322
- if response.status_code >= 400:
323
- raise BrowserFetchError(
324
- f"The page returned HTTP {response.status_code}."
325
- )
326
-
327
- content_type = response.headers.get("content-type", "")
328
- media_type = content_type.split(";", 1)[0].strip().lower()
329
- if not (
330
- media_type.startswith("text/")
331
- or media_type in _TEXTUAL_APPLICATION_TYPES
332
- ):
333
- raise BrowserFetchError(
334
- f"Unsupported content type: {content_type or 'unknown'}."
335
- )
336
-
337
- raw_body = _read_limited_response(response, max_bytes)
338
- try:
339
- encoding = response.encoding or "utf-8"
340
- body = raw_body.decode(encoding, "replace")
341
- except (LookupError, UnicodeError):
342
- body = raw_body.decode("utf-8", "replace")
343
- title, text = extract_readable_text(body)
344
- return (title or current_url, text)
345
- finally:
346
- response.close()
347
-
348
- if next_url is not None:
349
- current_url = next_url
350
- continue
351
- if connect_errors:
352
- raise BrowserFetchError(
353
- f"Could not reach the page: {connect_errors[-1]}"
354
- ) from connect_errors[-1]
355
- raise BrowserFetchError("Could not reach the page.")
356
- except BrowserFetchError:
357
- raise
358
- except httpx.HTTPError as exc:
359
- raise BrowserFetchError(f"Could not reach the page: {exc}") from exc
360
-
361
- raise BrowserFetchError("The page redirected too many times.")
362
-
363
-
364
- def _validate_http_url(url: str) -> str:
365
- try:
366
- cleaned, _parsed, _hostname, _port = _parse_http_url(url)
367
- except ValueError as exc:
368
- raise HTTPException(status_code=400, detail=str(exc)) from exc
369
- return cleaned
370
-
371
-
372
- # ── request models ───────────────────────────────────────────────────────────
373
- class ReadUrlRequest(BaseModel):
374
- url: str
375
- workspace_id: Optional[str] = None
376
-
377
-
378
- class IngestTabRequest(BaseModel):
379
- url: str
380
- title: Optional[str] = None
381
- text: Optional[str] = None
382
- selected_text: Optional[str] = None
383
- html: Optional[str] = None
384
- captured_at: Optional[str] = None
385
- workspace_id: Optional[str] = None
386
-
387
-
388
- def create_browser_router(
389
- *,
390
- pipeline: Any,
391
- require_user: Callable[[Request], str],
392
- workspace_service: Any = None,
393
- fetch_url: Optional[Callable[[str], Tuple[str, str]]] = None,
394
- max_tab_bytes: int = MAX_TAB_BYTES,
395
- ) -> APIRouter:
396
- router = APIRouter()
397
- _fetch = fetch_url or _default_fetch_url
398
-
399
- def _require_pipeline(language: str = DEFAULT_LANGUAGE):
400
- if pipeline is None or not pipeline.available():
401
- raise http_error(503, "capture.ingestion_disabled", language)
402
-
403
- def _write_workspace(request: Request, body_workspace: Optional[str], user: str) -> Optional[str]:
404
- return resolve_workspace_scope(
405
- request,
406
- user=user,
407
- workspace_service=workspace_service,
408
- write=True,
409
- body_workspace=body_workspace,
410
- )
411
-
412
- @router.post("/api/browser/read-url")
413
- async def read_url(req: ReadUrlRequest, request: Request):
414
- """Fetch a public URL locally and ingest it as a web_url source."""
415
- user = require_user(request)
416
- _require_pipeline(resolve_language(request))
417
- workspace_id = _write_workspace(request, req.workspace_id, user)
418
- url = _validate_http_url(req.url)
419
- try:
420
- title, text = _fetch(url)
421
- except BrowserFetchError as exc:
422
- # Graceful failure — not a 5xx; the page was simply unreadable.
423
- raise HTTPException(status_code=422, detail=str(exc))
424
- if not (text or "").strip():
425
- return {"status": "empty", "source_type": "web_url", "url": url,
426
- "detail": "No readable text was extracted from the page.",
427
- # Structured CTA (backlog #9): the extraction produced
428
- # nothing, tell the UI what the user can do about it.
429
- "capture_quality": capture_quality_verdict(None, source_type="web_url")}
430
- res = pipeline.ingest(
431
- IngestionItem(
432
- source_type="web_url", title=title, text=text, source_uri=url,
433
- owner=user, workspace_id=workspace_id,
434
- ),
435
- user_email=user,
436
- )
437
- payload = res.as_dict()
438
- payload["capture_quality"] = capture_quality_verdict(
439
- res.extraction_quality, source_type="web_url",
440
- )
441
- return payload
442
-
443
- @router.post("/api/browser/ingest-current-tab")
444
- async def ingest_current_tab(req: IngestTabRequest, request: Request):
445
- """Ingest a payload captured from the local browser extension."""
446
- user = require_user(request)
447
- _require_pipeline(resolve_language(request))
448
- workspace_id = _write_workspace(request, req.workspace_id, user)
449
- url = _validate_http_url(req.url)
450
- # Bound the entire capture, not merely each field independently. The
451
- # extension commonly supplies full text, selection, and HTML together.
452
- captured_bytes = sum(
453
- len(value.encode("utf-8", "ignore"))
454
- for value in (
455
- req.url,
456
- req.title,
457
- req.text,
458
- req.selected_text,
459
- req.html,
460
- req.captured_at,
461
- workspace_id,
462
- )
463
- if value
464
- )
465
- if captured_bytes > max_tab_bytes:
466
- raise http_error(413, "capture.payload_too_large", resolve_language(request))
467
- text = (req.text or "").strip()
468
- if not text and req.html:
469
- _title, text = extract_readable_text(req.html)
470
- if not text:
471
- text = (req.selected_text or "").strip()
472
- if not text:
473
- raise http_error(400, "capture.nothing_to_capture", resolve_language(request))
474
- res = pipeline.ingest(
475
- IngestionItem(
476
- source_type="browser_tab",
477
- title=req.title or url,
478
- text=text,
479
- source_uri=url,
480
- captured_at=req.captured_at,
481
- owner=user,
482
- workspace_id=workspace_id,
483
- metadata={"has_selection": bool(req.selected_text)},
484
- ),
485
- user_email=user,
486
- )
487
- payload = res.as_dict()
488
- payload["capture_quality"] = capture_quality_verdict(
489
- res.extraction_quality, source_type="browser_tab",
490
- )
491
- return payload
492
-
493
- return router
@@ -1,89 +0,0 @@
1
- """Change proposal API router (v9.6.0).
2
-
3
- Read/approve/reject surface over
4
- :class:`~latticeai.services.change_proposals.ChangeProposalService`. The
5
- items live in the shared review queue (source ``change_proposal``), so the
6
- Act review center shows them too; this router adds the proposal-specific
7
- semantics: **approve applies the staged content exactly as reviewed**,
8
- reject discards it, and nothing touches disk while a proposal is pending.
9
- """
10
-
11
- from __future__ import annotations
12
-
13
- from typing import Callable, Optional
14
-
15
- from fastapi import APIRouter, HTTPException, Request
16
- from pydantic import BaseModel
17
-
18
- from latticeai.services.change_proposals import ChangeProposalService
19
-
20
-
21
- class RejectProposalRequest(BaseModel):
22
- """Optional rejection context — kept in the item's provenance."""
23
-
24
- reason: str = ""
25
-
26
-
27
- def create_change_proposals_router(
28
- *,
29
- service: ChangeProposalService,
30
- require_user: Callable[[Request], str],
31
- gate_read: Callable[[Request], Optional[str]],
32
- gate_write: Callable[[Request], Optional[str]],
33
- ) -> APIRouter:
34
- router = APIRouter()
35
-
36
- @router.get("/api/proposals")
37
- async def list_proposals(request: Request):
38
- user = require_user(request)
39
- scope = gate_read(request)
40
- return service.pending(user_email=user, workspace_id=scope)
41
-
42
- # NOTE: declared before /api/proposals/{item_id} so "counts" never
43
- # resolves as an item id.
44
- @router.get("/api/proposals/counts")
45
- async def proposal_counts(request: Request):
46
- user = require_user(request)
47
- scope = gate_read(request)
48
- return service.counts(user_email=user, workspace_id=scope)
49
-
50
- @router.get("/api/proposals/{item_id}")
51
- async def get_proposal(item_id: str, request: Request):
52
- require_user(request)
53
- scope = gate_read(request)
54
- try:
55
- return service.get_proposal(item_id, workspace_id=scope)
56
- except (KeyError, FileNotFoundError) as exc:
57
- raise HTTPException(status_code=404, detail=str(exc))
58
-
59
- @router.post("/api/proposals/{item_id}/approve")
60
- async def approve_proposal(item_id: str, request: Request):
61
- user = require_user(request)
62
- scope = gate_write(request)
63
- try:
64
- return service.approve_and_apply(
65
- item_id, user_email=user, workspace_id=scope
66
- )
67
- except (KeyError, FileNotFoundError) as exc:
68
- raise HTTPException(status_code=404, detail=str(exc))
69
- except ValueError as exc:
70
- raise HTTPException(status_code=400, detail=str(exc))
71
-
72
- @router.post("/api/proposals/{item_id}/reject")
73
- async def reject_proposal(
74
- item_id: str, request: Request, req: Optional[RejectProposalRequest] = None
75
- ):
76
- user = require_user(request)
77
- scope = gate_write(request)
78
- try:
79
- return service.reject(
80
- item_id, user_email=user, workspace_id=scope,
81
- reason=(req.reason if req else ""),
82
- )
83
- except (KeyError, FileNotFoundError) as exc:
84
- raise HTTPException(status_code=404, detail=str(exc))
85
-
86
- return router
87
-
88
-
89
- __all__ = ["create_change_proposals_router", "RejectProposalRequest"]