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,378 +0,0 @@
1
- """Trigger system (T7d) — workflows fire beyond 'manual'.
2
-
3
- Two real trigger types:
4
-
5
- * **interval** — a supervised scheduler loop fires the workflow every
6
- ``interval_seconds``. Firings missed while the server was down are
7
- SKIPPED with a recorded skip event (design-review amendment: no silent
8
- gaps, no thundering catch-up).
9
- * **brain_event** — the killer Digital Brain feature: "when new knowledge
10
- enters the brain, run this workflow". Wired through the existing hooks
11
- bus (the ingestion pipeline fires ``post_tool`` on ``kg_ingest.<source>``);
12
- an optional ``source_type`` filter narrows it.
13
-
14
- Trigger-fired runs carry provenance: their inputs include ``__trigger__``
15
- describing what fired them, persisted in the run record like any other
16
- input.
17
- """
18
-
19
- from __future__ import annotations
20
-
21
- import json
22
- import logging
23
- import os
24
- import threading
25
- import time
26
- from pathlib import Path
27
- from typing import Any, Callable, Dict, List, Optional
28
-
29
- try:
30
- from zoneinfo import ZoneInfo
31
- except Exception: # pragma: no cover
32
- ZoneInfo = None # type: ignore
33
-
34
- DEFAULT_TICK_SECONDS = 5.0
35
- MIN_INTERVAL_SECONDS = 60
36
-
37
- TRIGGER_HOOK_NAME = "brain-event-triggers"
38
-
39
-
40
- class TriggerService:
41
- """Scans workflow definitions for non-manual triggers and fires them."""
42
-
43
- def __init__(
44
- self,
45
- *,
46
- store: Any,
47
- run_workflow: Callable[[str, Dict[str, Any]], Dict[str, Any]],
48
- data_dir: Path,
49
- clock: Callable[[], float] = time.time,
50
- tick_seconds: float = DEFAULT_TICK_SECONDS,
51
- review_sink: Optional[Any] = None,
52
- tz_name: Optional[str] = None,
53
- ) -> None:
54
- self._store = store
55
- self._run_workflow = run_workflow
56
- # Optional review-queue seam (5.6.0). When wired, fired runs can drop a
57
- # reviewable suggestion into the queue. Default None keeps legacy behavior.
58
- self._review_sink = review_sink
59
- self._state_file = Path(data_dir) / "triggers_state.json"
60
- self._clock = clock
61
- self._tick = float(tick_seconds)
62
- self._stop_event = threading.Event()
63
- self._thread: Optional[threading.Thread] = None
64
- self._lock = threading.Lock()
65
- # LATTICE_TZ: wall-clock / display 용. interval 계산은 여전히 unix seconds (duration 기반, drift 방지).
66
- # describe()와 이벤트에 tz 정보 노출. calendar "daily at HH:MM" semantics 는 추후 cron 확장 시 사용.
67
- # Prefer explicit tz_name (from Config) for DI; fall back to env for legacy/compat.
68
- self._tz_name = (tz_name or os.environ.get("LATTICE_TZ") or "UTC").strip() or "UTC"
69
- self._tz = None
70
- if ZoneInfo is not None:
71
- try:
72
- self._tz = ZoneInfo(self._tz_name)
73
- except Exception:
74
- self._tz = ZoneInfo("UTC")
75
-
76
- # ── durable state ──────────────────────────────────────────────────────
77
- def _load_state(self) -> Dict[str, Any]:
78
- if not self._state_file.exists():
79
- return {}
80
- try:
81
- return json.loads(self._state_file.read_text(encoding="utf-8"))
82
- except Exception:
83
- return {}
84
-
85
- def _save_state(self, state: Dict[str, Any]) -> None:
86
- self._state_file.parent.mkdir(parents=True, exist_ok=True)
87
- tmp = self._state_file.with_suffix(".tmp")
88
- tmp.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")
89
- tmp.replace(self._state_file)
90
-
91
- def _record_event(self, state: Dict[str, Any], workflow_id: str, event: Dict[str, Any]) -> None:
92
- entry = state.setdefault(workflow_id, {})
93
- events = entry.setdefault("events", [])
94
- events.append({**event, "at": self._clock()})
95
- entry["events"] = events[-50:]
96
-
97
- # ── definition scanning ────────────────────────────────────────────────
98
- def _triggered_workflows(self) -> List[Dict[str, Any]]:
99
- found = []
100
- try:
101
- workflows = list(self._store.load_state().get("workflows") or [])
102
- except Exception:
103
- return []
104
- for wf in workflows:
105
- for node in wf.get("nodes") or []:
106
- if node.get("type") != "trigger":
107
- continue
108
- cfg = node.get("config") or {}
109
- kind = str(cfg.get("trigger") or "manual")
110
- if cfg.get("enabled") is False:
111
- continue
112
- if kind in ("interval", "brain_event"):
113
- found.append({"workflow": wf, "node": node, "kind": kind, "config": cfg})
114
- return found
115
-
116
- def describe(self) -> Dict[str, Any]:
117
- """Honest status surface: what is armed, when it last fired/skipped.
118
- Includes LATTICE_TZ, per-trigger status (armed|degraded), consecutive_failures.
119
- """
120
- state = self._load_state()
121
- armed = []
122
- for item in self._triggered_workflows():
123
- wf_id = item["workflow"].get("id")
124
- entry = state.get(wf_id) or {}
125
- fails = int(entry.get("consecutive_failures", 0))
126
- status = "degraded" if fails >= 3 else "armed"
127
- armed.append({
128
- "workflow_id": wf_id,
129
- "name": item["workflow"].get("name"),
130
- "kind": item["kind"],
131
- "config": {k: v for k, v in item["config"].items() if k != "trigger"},
132
- "last_fired_at": entry.get("last_fired_at"),
133
- "status": status,
134
- "consecutive_failures": fails,
135
- "recent_events": entry.get("events", [])[-5:],
136
- })
137
- return {
138
- "running": bool(self._thread and self._thread.is_alive()),
139
- "tick_seconds": self._tick,
140
- "tz": self._tz_name,
141
- "armed": armed,
142
- }
143
-
144
- # ── interval scheduling ────────────────────────────────────────────────
145
- def reconcile_missed(self) -> int:
146
- """Startup pass: record (never replay) firings missed while down."""
147
- now = self._clock()
148
- skipped = 0
149
- with self._lock:
150
- state = self._load_state()
151
- for item in self._triggered_workflows():
152
- if item["kind"] != "interval":
153
- continue
154
- wf_id = item["workflow"].get("id")
155
- interval = max(MIN_INTERVAL_SECONDS, int(item["config"].get("interval_seconds") or 0))
156
- entry = state.setdefault(wf_id, {})
157
- last = entry.get("last_fired_at")
158
- if last is not None and now - float(last) > interval:
159
- missed = int((now - float(last)) // interval)
160
- self._record_event(state, wf_id, {
161
- "type": "skipped",
162
- "reason": f"{missed} interval firing(s) missed while the server was down",
163
- })
164
- skipped += missed
165
- # Reset the cadence from now — no catch-up storm.
166
- entry["last_fired_at"] = now if last is not None else entry.get("last_fired_at")
167
- entry["last_attempt_at"] = now
168
- self._save_state(state)
169
- return skipped
170
-
171
- def tick_intervals(self) -> int:
172
- """One scheduler pass; returns how many workflows fired."""
173
- now = self._clock()
174
- fired = 0
175
- with self._lock:
176
- state = self._load_state()
177
- for item in self._triggered_workflows():
178
- if item["kind"] != "interval":
179
- continue
180
- wf_id = item["workflow"].get("id")
181
- interval = max(MIN_INTERVAL_SECONDS, int(item["config"].get("interval_seconds") or 0))
182
- entry = state.setdefault(wf_id, {})
183
- last = entry.get("last_fired_at")
184
- if last is None:
185
- # First sighting arms the schedule; it fires one interval later.
186
- entry["last_fired_at"] = now
187
- entry["last_attempt_at"] = now
188
- continue
189
- if now - float(last) < interval:
190
- continue
191
- # Dedup guard (edge case): short cooldown + last_attempt prevents rapid re-fire on
192
- # clock skew, tick jitter, or restart races. Interval 자체 + 이 가드로 중복 실행 방지.
193
- last_attempt = float(entry.get("last_attempt_at") or 0)
194
- if now - last_attempt < 10:
195
- continue
196
- entry["last_attempt_at"] = now
197
- entry["last_fired_at"] = now
198
- self._record_event(state, wf_id, {"type": "fired", "trigger": "interval"})
199
- fired += 1
200
- self._fire(wf_id, {
201
- "type": "interval",
202
- "interval_seconds": interval,
203
- "fired_at": now,
204
- })
205
- self._save_state(state)
206
- return fired
207
-
208
- # ── brain events ───────────────────────────────────────────────────────
209
- def on_brain_event(self, event: str, payload: Optional[Dict[str, Any]] = None) -> int:
210
- """Fire workflows whose brain_event trigger matches this ingestion."""
211
- payload = payload or {}
212
- source_type = str(payload.get("source_type") or _ingestion_source_type(event, payload) or "")
213
- event_workspace = str(payload.get("workspace_id") or "personal").strip()
214
- event_user = str(payload.get("user_email") or "").strip().lower()
215
- fired = 0
216
- with self._lock:
217
- state = self._load_state()
218
- for item in self._triggered_workflows():
219
- if item["kind"] != "brain_event":
220
- continue
221
- workflow_workspace = str(item["workflow"].get("workspace_id") or "personal").strip()
222
- workflow_user = str(item["workflow"].get("user_email") or "").strip().lower()
223
- if event_workspace != workflow_workspace:
224
- continue
225
- # Owned workflows only respond to their owner's ingestion.
226
- # Ownerless legacy workflows remain compatible in personal.
227
- if workflow_user and workflow_user != event_user:
228
- continue
229
- wanted = str(item["config"].get("source_type") or "").strip()
230
- if wanted and wanted != source_type:
231
- continue
232
- wf_id = item["workflow"].get("id")
233
- entry = state.setdefault(wf_id, {})
234
- now = self._clock()
235
- # Dedup guard for brain_event too (rapid ingest burst 등).
236
- last_attempt = float(entry.get("last_attempt_at") or 0)
237
- if now - last_attempt < 5:
238
- continue
239
- entry["last_attempt_at"] = now
240
- entry["last_fired_at"] = now
241
- self._record_event(state, wf_id, {
242
- "type": "fired", "trigger": "brain_event", "source_type": source_type,
243
- })
244
- fired += 1
245
- self._fire(wf_id, {
246
- "type": "brain_event",
247
- "event": event,
248
- "source_type": source_type,
249
- "node_id": payload.get("node_id"),
250
- "user_email": payload.get("user_email"),
251
- "workspace_id": payload.get("workspace_id"),
252
- })
253
- self._save_state(state)
254
- return fired
255
-
256
- def hook_runner(self):
257
- """A post_tool hook runner: ingestion events fan into triggers."""
258
- def runner(context):
259
- event = str(getattr(context, "event", "") or "")
260
- payload = context.payload if isinstance(context.payload, dict) else {}
261
- source_type = _ingestion_source_type(event, payload)
262
- if not source_type:
263
- return {"status": "ok", "output": "not an ingestion event"}
264
- if str(payload.get("status") or "ok") != "ok":
265
- return {"status": "ok", "output": "ignored failed ingestion event"}
266
- fired = self.on_brain_event(
267
- f"kg_ingest.{source_type}",
268
- {
269
- **payload,
270
- "source_type": source_type,
271
- "user_email": getattr(context, "user_email", None) or payload.get("user_email"),
272
- "workspace_id": getattr(context, "workspace_id", None) or payload.get("workspace_id"),
273
- },
274
- )
275
- return {"status": "ok", "output": f"fired {fired} workflow trigger(s)"}
276
- return runner
277
-
278
- # ── execution + lifecycle ──────────────────────────────────────────────
279
- def _fire(self, workflow_id: str, trigger_info: Dict[str, Any]) -> None:
280
- try:
281
- workflows = list(self._store.load_state().get("workflows") or [])
282
- workflow = next((wf for wf in workflows if wf.get("id") == workflow_id), None) or {}
283
- except Exception:
284
- workflow = {}
285
- trigger_info = dict(trigger_info)
286
- if not trigger_info.get("user_email"):
287
- trigger_info["user_email"] = workflow.get("user_email")
288
- if not trigger_info.get("workspace_id"):
289
- trigger_info["workspace_id"] = workflow.get("workspace_id")
290
-
291
- def _run():
292
- try:
293
- result = self._run_workflow(workflow_id, {"__trigger__": trigger_info})
294
- self._record_fire_outcome(workflow_id, ok=True)
295
- self._maybe_enqueue_review(workflow_id, trigger_info, result)
296
- except Exception as exc:
297
- logging.warning("trigger run failed for %s: %s", workflow_id, exc)
298
- self._record_fire_outcome(workflow_id, ok=False, detail=str(exc))
299
-
300
- threading.Thread(target=_run, name=f"trigger-{workflow_id}", daemon=True).start()
301
-
302
- def _maybe_enqueue_review(
303
- self, workflow_id: str, trigger_info: Dict[str, Any], result: Dict[str, Any],
304
- ) -> None:
305
- if self._review_sink is None:
306
- return
307
- try:
308
- from latticeai.services.review_queue import enqueue_from_automation
309
-
310
- workflows = list(self._store.load_state().get("workflows") or [])
311
- workflow = next((wf for wf in workflows if wf.get("id") == workflow_id), None)
312
- if workflow is None:
313
- return
314
- trigger_type = str(trigger_info.get("type") or "")
315
- source = "kg_change_digest" if trigger_type == "brain_event" else "trigger"
316
- enqueue_from_automation(
317
- self._review_sink,
318
- workflow=workflow,
319
- source=source,
320
- run_result=result,
321
- trigger_info=trigger_info,
322
- user_email=trigger_info.get("user_email") or workflow.get("user_email"),
323
- workspace_id=trigger_info.get("workspace_id") or workflow.get("workspace_id"),
324
- )
325
- except Exception as exc:
326
- logging.warning("review_sink enqueue failed for %s: %s", workflow_id, exc)
327
-
328
- def _record_fire_outcome(self, wf_id: str, *, ok: bool, detail: str = "") -> None:
329
- """Track consecutive launch failures for degraded status in describe().
330
- (Deep execution failures are visible via workflow run records; 여기서는 scheduler fire 자체 실패를 카운트.)
331
- """
332
- with self._lock:
333
- state = self._load_state()
334
- entry = state.setdefault(wf_id, {})
335
- if ok:
336
- entry["consecutive_failures"] = 0
337
- else:
338
- fails = int(entry.get("consecutive_failures", 0)) + 1
339
- entry["consecutive_failures"] = fails
340
- self._record_event(state, wf_id, {"type": "failed", "detail": detail[:200]})
341
- self._save_state(state)
342
-
343
- def start(self) -> None:
344
- if self._thread and self._thread.is_alive():
345
- return
346
- self.reconcile_missed()
347
- self._stop_event.clear()
348
-
349
- def _loop():
350
- while not self._stop_event.wait(self._tick):
351
- try:
352
- self.tick_intervals()
353
- except Exception as exc:
354
- logging.warning("trigger scheduler tick failed: %s", exc)
355
-
356
- self._thread = threading.Thread(target=_loop, name="trigger-scheduler", daemon=True)
357
- self._thread.start()
358
-
359
- def stop(self) -> None:
360
- self._stop_event.set()
361
- if self._thread:
362
- self._thread.join(timeout=2)
363
-
364
-
365
- def _ingestion_source_type(event: str, payload: Dict[str, Any]) -> str:
366
- """Normalize the real dispatch event (``tool.kg_ingest.*``) and legacy form."""
367
- tool = str(payload.get("tool") or "").strip()
368
- if tool.startswith("kg_ingest."):
369
- return tool.removeprefix("kg_ingest.").strip()
370
- normalized = str(event or "").strip()
371
- if normalized.startswith("tool.kg_ingest."):
372
- return normalized.removeprefix("tool.kg_ingest.").strip()
373
- if normalized.startswith("kg_ingest."):
374
- return normalized.removeprefix("kg_ingest.").strip()
375
- return ""
376
-
377
-
378
- __all__ = ["TriggerService", "TRIGGER_HOOK_NAME", "MIN_INTERVAL_SECONDS"]
@@ -1,185 +0,0 @@
1
- """Document upload parsing, safety checks, and knowledge-graph ingestion."""
2
-
3
- from __future__ import annotations
4
-
5
- import logging
6
- import tempfile
7
- from datetime import datetime
8
- from pathlib import Path
9
- from typing import Optional
10
-
11
- from fastapi import HTTPException, Request, UploadFile
12
-
13
- from lattice_brain.ingestion import IngestionItem
14
- from latticeai.core.quiet import quiet
15
- from latticeai.tools import ToolError, read_document
16
-
17
-
18
- def _workspace_scope_from_request(request: Request) -> Optional[str]:
19
- header = request.headers.get("X-Workspace-Id")
20
- if header and header.strip():
21
- return header.strip()
22
- query = request.query_params.get("workspace_id")
23
- return query.strip() if query and query.strip() else None
24
-
25
-
26
- async def process_uploaded_document(
27
- *,
28
- request: Request,
29
- file: UploadFile,
30
- current_user: str,
31
- enable_graph: bool,
32
- knowledge_graph,
33
- ingestion_pipeline=None,
34
- bytes_match_extension,
35
- classify_sensitive_message,
36
- append_audit_event,
37
- enforce_rate_limit,
38
- hooks=None,
39
- workspace_service=None,
40
- ) -> dict:
41
- enforce_rate_limit(current_user, "upload")
42
- requested_workspace = _workspace_scope_from_request(request)
43
- if workspace_service is not None:
44
- try:
45
- workspace_id = workspace_service.resolve_write_scope(
46
- requested_workspace,
47
- current_user or None,
48
- )
49
- except PermissionError as exc:
50
- raise HTTPException(status_code=403, detail=str(exc)) from exc
51
- else:
52
- workspace_id = requested_workspace
53
- suffix = Path(file.filename or "upload").suffix.lower()
54
- allowed = {".pdf", ".docx", ".xlsx", ".pptx", ".txt", ".md", ".csv"}
55
- if suffix not in allowed:
56
- raise HTTPException(status_code=400, detail=f"지원하지 않는 형식: {suffix}")
57
-
58
- contents = await file.read()
59
- if len(contents) > 10 * 1024 * 1024:
60
- raise HTTPException(status_code=400, detail="파일이 너무 큽니다. 최대 10MB.")
61
- if not bytes_match_extension(contents, suffix):
62
- raise HTTPException(status_code=400, detail=f"파일 내용이 확장자({suffix})와 일치하지 않습니다.")
63
-
64
- # ── pre_upload hook ── may gate the upload before any work happens.
65
- if hooks is not None:
66
- pre_up = hooks.fire_hook(
67
- "pre_upload", "document.upload",
68
- payload={"filename": file.filename, "ext": suffix, "bytes": len(contents)},
69
- user_email=current_user,
70
- )
71
- if pre_up.get("blocked"):
72
- raise HTTPException(status_code=403, detail=pre_up.get("block_reason") or "Upload blocked by a pre_upload hook.")
73
-
74
- with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
75
- tmp.write(contents)
76
- tmp_path = tmp.name
77
-
78
- try:
79
- result = read_document(tmp_path)
80
- sensitive = classify_sensitive_message(
81
- {
82
- "role": "document",
83
- "content": result.get("content") or result.get("preview") or "",
84
- "user_email": current_user,
85
- "timestamp": datetime.now().isoformat(),
86
- },
87
- -1,
88
- )
89
- # ── pre_index / post_index hooks bracket the KG ingest (chunk → embed →
90
- # graph-build), the actual indexing step.
91
- if hooks is not None:
92
- hooks.fire_hook("pre_index", "document.index",
93
- payload={"filename": file.filename, "chars": result.get("chars")},
94
- user_email=current_user)
95
- try:
96
- if not (enable_graph and knowledge_graph):
97
- raise RuntimeError("graph disabled")
98
- if ingestion_pipeline is not None:
99
- # v4: uploads enter the brain through the unified ingestion
100
- # pipeline (provenance + kg_ingest hook lifecycle).
101
- ingest = ingestion_pipeline.ingest(
102
- IngestionItem(
103
- source_type="upload",
104
- title=file.filename,
105
- path=tmp_path,
106
- mime_type=file.content_type,
107
- owner=current_user,
108
- workspace_id=workspace_id,
109
- conversation_id=request.query_params.get("conversation_id"),
110
- metadata={"extracted": result},
111
- ),
112
- user_email=current_user,
113
- )
114
- if ingest.status != "ok":
115
- raise RuntimeError(ingest.detail or f"ingestion {ingest.status}")
116
- result["knowledge_graph"] = {
117
- "node_id": ingest.node_id,
118
- "sha256": ingest.content_hash,
119
- "provenance_id": ingest.provenance_id,
120
- }
121
- else:
122
- graph_result = knowledge_graph.ingest_document(
123
- Path(tmp_path),
124
- original_filename=file.filename,
125
- mime_type=file.content_type,
126
- uploader=current_user,
127
- workspace_id=workspace_id,
128
- conversation_id=request.query_params.get("conversation_id"),
129
- extracted=result,
130
- )
131
- result["knowledge_graph"] = {
132
- "node_id": graph_result["node_id"],
133
- "sha256": graph_result["sha256"],
134
- }
135
- except Exception as graph_error:
136
- logging.warning("knowledge graph document ingest failed: %s", graph_error)
137
- result["knowledge_graph"] = {"error": str(graph_error)}
138
- if hooks is not None:
139
- _kg = result.get("knowledge_graph") or {}
140
- hooks.fire_hook("post_index", "document.index",
141
- payload={"filename": file.filename, "graph_node": _kg.get("node_id"),
142
- "indexed": bool(_kg.get("node_id")), "error": _kg.get("error")},
143
- user_email=current_user)
144
-
145
- append_audit_event(
146
- "document_upload",
147
- user_email=current_user,
148
- conversation_id=request.query_params.get("conversation_id"),
149
- filename=file.filename,
150
- mime_type=file.content_type,
151
- ext=suffix,
152
- bytes=len(contents),
153
- extracted_chars=result.get("chars"),
154
- graph_node=(result.get("knowledge_graph") or {}).get("node_id"),
155
- content_preview=sensitive.get("preview"),
156
- sensitivity=sensitive.get("sensitivity"),
157
- sensitive_labels=sensitive.get("labels") or [],
158
- )
159
- except ToolError as exc:
160
- raise HTTPException(status_code=400, detail=str(exc))
161
- finally:
162
- try:
163
- Path(tmp_path).unlink()
164
- except OSError:
165
- quiet()
166
-
167
- # ── post_upload hook ── the whole upload → parse → index pipeline finished.
168
- if hooks is not None:
169
- kg = result.get("knowledge_graph") or {}
170
- hooks.fire_hook(
171
- "post_upload", "document.uploaded",
172
- payload={
173
- "filename": file.filename,
174
- "chars": result.get("chars"),
175
- "graph_node": kg.get("node_id"),
176
- "indexed": bool(kg.get("node_id")),
177
- },
178
- user_email=current_user,
179
- )
180
-
181
- result["original_filename"] = file.filename
182
- return result
183
-
184
-
185
- __all__ = ["process_uploaded_document"]