ltcai 11.5.2 → 11.7.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 (443) hide show
  1. package/README.md +93 -148
  2. package/bin/ltcai.js +234 -24
  3. package/docs/CHANGELOG.md +119 -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/MULTI_AGENT_RUNTIME.md +12 -5
  8. package/docs/ONBOARDING.md +1 -1
  9. package/docs/OPERATIONS.md +6 -3
  10. package/docs/REALTIME_COLLABORATION.md +1 -1
  11. package/docs/TRUST_MODEL.md +1 -1
  12. package/docs/WHY_LATTICE.md +1 -1
  13. package/docs/WORKFLOW_DESIGNER.md +3 -2
  14. package/docs/kg-schema.md +1 -1
  15. package/docs/v11.6.0_ONE_DOOR_PLAN.md +170 -0
  16. package/lattice_brain/__init__.py +42 -79
  17. package/lattice_brain/graph/__init__.py +9 -24
  18. package/lattice_brain/graph/_kg_common/__init__.py +13 -24
  19. package/lattice_brain/ingestion/__init__.py +19 -58
  20. package/lattice_brain/ingestion/pipeline.py +20 -398
  21. package/lattice_brain/multimodal/__init__.py +7 -18
  22. package/lattice_brain/multimodal/images.py +6 -264
  23. package/lattice_brain/multimodal/video.py +8 -247
  24. package/lattice_brain/runtime/__init__.py +8 -79
  25. package/lattice_brain/runtime/hooks.py +22 -584
  26. package/latticeai/__init__.py +1 -1
  27. package/latticeai/api/agent_worker_seam.py +7 -80
  28. package/latticeai/api/health.py +6 -20
  29. package/latticeai/api/local_files.py +16 -613
  30. package/latticeai/api/models.py +12 -87
  31. package/latticeai/api/search.py +17 -255
  32. package/latticeai/api/tools.py +69 -750
  33. package/latticeai/api/voice_capture.py +8 -70
  34. package/latticeai/api/worker_compute.py +842 -0
  35. package/latticeai/api/worker_seams.py +216 -0
  36. package/latticeai/app_factory.py +23 -232
  37. package/latticeai/cli/entrypoint.py +36 -249
  38. package/latticeai/core/agent_permission.py +16 -91
  39. package/latticeai/core/messages.py +89 -531
  40. package/latticeai/runtime/access_runtime.py +0 -14
  41. package/latticeai/runtime/bootstrap.py +10 -21
  42. package/latticeai/runtime/brain_runtime.py +35 -43
  43. package/latticeai/runtime/build_phases/__init__.py +21 -37
  44. package/latticeai/runtime/build_phases/features.py +99 -389
  45. package/latticeai/runtime/build_phases/foundation.py +82 -392
  46. package/latticeai/runtime/build_phases/web.py +91 -408
  47. package/latticeai/runtime/build_phases/worker_profile.py +244 -0
  48. package/latticeai/runtime/lifespan_runtime.py +7 -16
  49. package/latticeai/runtime/platform_services_runtime.py +1 -15
  50. package/latticeai/runtime/runtime_context.py +13 -130
  51. package/latticeai/runtime/security_runtime.py +10 -103
  52. package/latticeai/services/architecture_readiness.py +82 -43
  53. package/latticeai/services/model_runtime/__init__.py +2 -11
  54. package/latticeai/services/model_runtime/service.py +4 -28
  55. package/latticeai/services/p_reinforce.py +10 -261
  56. package/latticeai/services/product_readiness.py +31 -38
  57. package/latticeai/services/search_service.py +37 -795
  58. package/latticeai/services/tool_dispatch.py +30 -386
  59. package/latticeai/services/voice_capture.py +13 -107
  60. package/latticeai/tools/__init__.py +22 -49
  61. package/latticeai/tools/commands.py +0 -163
  62. package/latticeai/tools/computer.py +0 -39
  63. package/latticeai/tools/documents.py +1 -134
  64. package/latticeai/tools/filesystem.py +1 -247
  65. package/latticeai/tools/knowledge.py +1 -52
  66. package/latticeai/tools/local_files.py +0 -20
  67. package/latticeai/worker_app.py +75 -0
  68. package/package.json +2 -3
  69. package/requirements.txt +0 -5
  70. package/scripts/agent_eval.py +16 -28
  71. package/scripts/brain_quality_eval.py +20 -183
  72. package/scripts/bump_version.py +5 -5
  73. package/scripts/check_current_release_docs.mjs +7 -4
  74. package/scripts/check_openapi_drift.mjs +10 -0
  75. package/scripts/check_server_i18n.mjs +2 -18
  76. package/scripts/compose_openapi.py +377 -0
  77. package/scripts/export_openapi.py +32 -4
  78. package/scripts/gen_messages_catalog_fixture.py +302 -0
  79. package/scripts/gen_openapi_fragments.py +365 -0
  80. package/scripts/gen_redact_fixture.py +196 -0
  81. package/scripts/gen_worker_allowlist_fixture.py +115 -0
  82. package/scripts/generate_agent_parity_fixtures.py +33 -14
  83. package/scripts/openapi_route_families.json +2161 -0
  84. package/scripts/release_screen_claims.json +54 -0
  85. package/scripts/run_integration_tests.mjs +134 -29
  86. package/scripts/run_sidecar_e2e.mjs +104 -11
  87. package/scripts/wheel_smoke.py +32 -22
  88. package/src-tauri/Cargo.lock +375 -9
  89. package/src-tauri/Cargo.toml +1 -1
  90. package/src-tauri/src/backend.rs +151 -45
  91. package/src-tauri/src/main.rs +18 -18
  92. package/src-tauri/src/topology.rs +58 -88
  93. package/src-tauri/tauri.conf.json +1 -2
  94. package/static/app/asset-manifest.json +41 -41
  95. package/static/app/assets/{Act-DcQizkl1.js → Act-BPcVAbOL.js} +1 -1
  96. package/static/app/assets/AdminConsole-Bw1ATQL0.js +1 -0
  97. package/static/app/assets/{Brain-3VCSHFcn.js → Brain-CT92Kos0.js} +2 -2
  98. package/static/app/assets/{BrainHome-Qm8eaztx.js → BrainHome-CFBkt1K_.js} +1 -1
  99. package/static/app/assets/{BrainSignals-DS9BtKOW.js → BrainSignals-ReLWF2H8.js} +1 -1
  100. package/static/app/assets/Capture-BsTokYkk.js +1 -0
  101. package/static/app/assets/{Chronicle-BGvuAchH.js → Chronicle-B6f0T9id.js} +1 -1
  102. package/static/app/assets/{CommandPalette-Bqhm0Urn.js → CommandPalette-CuvjTv1u.js} +1 -1
  103. package/static/app/assets/{Library-BV6NnF0a.js → Library-BGJbG9Hd.js} +1 -1
  104. package/static/app/assets/{LivingBrain-GzenJchP.js → LivingBrain-DGYK_Jsa.js} +1 -1
  105. package/static/app/assets/{ProductFlow-DEP6-vML.js → ProductFlow-DXBC6brE.js} +1 -1
  106. package/static/app/assets/{ReviewCard-CNZ7XjWG.js → ReviewCard-HXRle3qq.js} +2 -2
  107. package/static/app/assets/System-CMHSO9qM.js +1 -0
  108. package/static/app/assets/arrow-left-BfmkskWx.js +1 -0
  109. package/static/app/assets/{bot-B_K1Tdmw.js → bot-Cn8bWRuq.js} +1 -1
  110. package/static/app/assets/{brain-DWyaV1L1.js → brain-CQJberbE.js} +1 -1
  111. package/static/app/assets/{button-aTn4s84A.js → button-Ct9f2_oT.js} +1 -1
  112. package/static/app/assets/circle-check-DruOxB-4.js +1 -0
  113. package/static/app/assets/{circle-pause-xKgeGXkT.js → circle-pause-CmzC_apg.js} +1 -1
  114. package/static/app/assets/{circle-play-DkT6tYPX.js → circle-play-D8mW2aQ7.js} +1 -1
  115. package/static/app/assets/{cpu-85xYObUC.js → cpu-DZcdd0PZ.js} +1 -1
  116. package/static/app/assets/{download-B5Fm7YXo.js → download-bv1KEPGQ.js} +1 -1
  117. package/static/app/assets/{folder-open-kk2Xa52u.js → folder-open-d-Pip5gr.js} +1 -1
  118. package/static/app/assets/{hard-drive-DkA3zBW_.js → hard-drive-D20iavUb.js} +1 -1
  119. package/static/app/assets/index-D9x-kSNy.css +2 -0
  120. package/static/app/assets/{index-BMPdTmlY.js → index-Do83hDzJ.js} +3 -3
  121. package/static/app/assets/{input-B0nRf2jO.js → input-BLXVNmj1.js} +1 -1
  122. package/static/app/assets/{link-2-Dwb4gnTc.js → link-2-BPJOFlAy.js} +1 -1
  123. package/static/app/assets/{permissionCopy-CQDUBrOZ.js → permissionCopy-ChdJd493.js} +1 -1
  124. package/static/app/assets/primitives-Cv5tbZBY.js +1 -0
  125. package/static/app/assets/search-CT9aho2j.js +1 -0
  126. package/static/app/assets/{share-2-BsrxFglO.js → share-2-YNX_NtMU.js} +1 -1
  127. package/static/app/assets/{shield-alert-5BStfp2_.js → shield-alert-DuQ3zrVL.js} +1 -1
  128. package/static/app/assets/{textarea-Cg8IUA-k.js → textarea-DqwLnli4.js} +1 -1
  129. package/static/app/assets/{useFocusTrap-CYKvE46M.js → useFocusTrap-ZVI98jaW.js} +1 -1
  130. package/static/app/assets/{useMutation-CSn9t1op.js → useMutation-CVC4qv_D.js} +1 -1
  131. package/static/app/assets/{useQuery-CY2OI2uy.js → useQuery-C7BeG4HU.js} +1 -1
  132. package/static/app/assets/{utils-Ddol2RWD.js → utils-CiFtIdZq.js} +1 -1
  133. package/static/app/assets/{workspace-BqDwOz_p.js → workspace-DQz9vIId.js} +1 -1
  134. package/static/app/index.html +4 -4
  135. package/static/sw.js +1 -1
  136. package/lattice_brain/archive.py +0 -522
  137. package/lattice_brain/context.py +0 -325
  138. package/lattice_brain/conversations.py +0 -382
  139. package/lattice_brain/core.py +0 -82
  140. package/lattice_brain/graph/_kg_contract.py +0 -249
  141. package/lattice_brain/graph/curator.py +0 -676
  142. package/lattice_brain/graph/discovery.py +0 -595
  143. package/lattice_brain/graph/discovery_index/__init__.py +0 -35
  144. package/lattice_brain/graph/discovery_index/cleanup.py +0 -182
  145. package/lattice_brain/graph/discovery_index/extract.py +0 -137
  146. package/lattice_brain/graph/discovery_index/scan.py +0 -411
  147. package/lattice_brain/graph/discovery_index/upsert.py +0 -495
  148. package/lattice_brain/graph/documents.py +0 -380
  149. package/lattice_brain/graph/fusion.py +0 -395
  150. package/lattice_brain/graph/identity.py +0 -175
  151. package/lattice_brain/graph/image_vectors.py +0 -230
  152. package/lattice_brain/graph/ingest.py +0 -829
  153. package/lattice_brain/graph/network.py +0 -205
  154. package/lattice_brain/graph/proactive.py +0 -724
  155. package/lattice_brain/graph/projection/__init__.py +0 -42
  156. package/lattice_brain/graph/projection/curation.py +0 -500
  157. package/lattice_brain/graph/projection/v2_schema.py +0 -518
  158. package/lattice_brain/graph/provenance.py +0 -524
  159. package/lattice_brain/graph/rerank.py +0 -163
  160. package/lattice_brain/graph/retrieval/__init__.py +0 -54
  161. package/lattice_brain/graph/retrieval/context.py +0 -197
  162. package/lattice_brain/graph/retrieval/graph_view.py +0 -319
  163. package/lattice_brain/graph/retrieval/hybrid.py +0 -488
  164. package/lattice_brain/graph/retrieval/maintenance.py +0 -121
  165. package/lattice_brain/graph/retrieval/signals.py +0 -95
  166. package/lattice_brain/graph/retrieval_docgen.py +0 -253
  167. package/lattice_brain/graph/retrieval_policy.py +0 -180
  168. package/lattice_brain/graph/retrieval_reads.py +0 -769
  169. package/lattice_brain/graph/retrieval_vector/__init__.py +0 -42
  170. package/lattice_brain/graph/retrieval_vector/fingerprint.py +0 -97
  171. package/lattice_brain/graph/retrieval_vector/indexing.py +0 -347
  172. package/lattice_brain/graph/retrieval_vector/search.py +0 -560
  173. package/lattice_brain/graph/retrieval_vector/status.py +0 -374
  174. package/lattice_brain/graph/schema.py +0 -792
  175. package/lattice_brain/graph/store.py +0 -268
  176. package/lattice_brain/graph/vector_index/__init__.py +0 -85
  177. package/lattice_brain/graph/vector_index/base.py +0 -167
  178. package/lattice_brain/graph/vector_index/brute_force.py +0 -110
  179. package/lattice_brain/graph/vector_index/hnsw.py +0 -290
  180. package/lattice_brain/graph/vector_index/jobs.py +0 -287
  181. package/lattice_brain/graph/vector_index/quantized.py +0 -148
  182. package/lattice_brain/graph/vector_index/selector.py +0 -161
  183. package/lattice_brain/graph/write_master.py +0 -308
  184. package/lattice_brain/ingestion/_contract.py +0 -90
  185. package/lattice_brain/ingestion/folder_scan.py +0 -57
  186. package/lattice_brain/ingestion/folders.py +0 -258
  187. package/lattice_brain/ingestion/jobs_api.py +0 -107
  188. package/lattice_brain/ingestion/routing.py +0 -295
  189. package/lattice_brain/ingestion_jobs.py +0 -380
  190. package/lattice_brain/memory.py +0 -75
  191. package/lattice_brain/portability/__init__.py +0 -90
  192. package/lattice_brain/portability/_contract.py +0 -42
  193. package/lattice_brain/portability/backups.py +0 -338
  194. package/lattice_brain/portability/bundles.py +0 -136
  195. package/lattice_brain/portability/constants.py +0 -93
  196. package/lattice_brain/portability/fsops.py +0 -138
  197. package/lattice_brain/portability/service.py +0 -41
  198. package/lattice_brain/portability/sharing.py +0 -710
  199. package/lattice_brain/quality.py +0 -543
  200. package/lattice_brain/retrieval_benchmark_fixtures.py +0 -92
  201. package/lattice_brain/runtime/agent_runtime.py +0 -859
  202. package/lattice_brain/runtime/contracts.py +0 -460
  203. package/lattice_brain/runtime/multi_agent.py +0 -942
  204. package/lattice_brain/runtime/statuses.py +0 -10
  205. package/lattice_brain/sealed_box.py +0 -240
  206. package/lattice_brain/self_model.py +0 -675
  207. package/lattice_brain/sensitivity.py +0 -94
  208. package/lattice_brain/storage/__init__.py +0 -22
  209. package/lattice_brain/storage/base.py +0 -100
  210. package/lattice_brain/storage/docker.py +0 -105
  211. package/lattice_brain/storage/factory.py +0 -31
  212. package/lattice_brain/storage/migration.py +0 -191
  213. package/lattice_brain/storage/postgres.py +0 -123
  214. package/lattice_brain/storage/sqlite.py +0 -143
  215. package/lattice_brain/synthesis.py +0 -824
  216. package/lattice_brain/workflow.py +0 -497
  217. package/latticeai/api/admin.py +0 -471
  218. package/latticeai/api/agent_registry.py +0 -105
  219. package/latticeai/api/agents.py +0 -228
  220. package/latticeai/api/auth.py +0 -383
  221. package/latticeai/api/automation_intelligence.py +0 -401
  222. package/latticeai/api/brain_intelligence.py +0 -199
  223. package/latticeai/api/browser.py +0 -493
  224. package/latticeai/api/change_proposals.py +0 -89
  225. package/latticeai/api/chat.py +0 -572
  226. package/latticeai/api/chat_agent_http.py +0 -892
  227. package/latticeai/api/chat_contracts.py +0 -73
  228. package/latticeai/api/chat_documents.py +0 -276
  229. package/latticeai/api/chat_helpers.py +0 -460
  230. package/latticeai/api/chat_history.py +0 -101
  231. package/latticeai/api/chat_hybrid.py +0 -113
  232. package/latticeai/api/chat_intents.py +0 -646
  233. package/latticeai/api/chat_stream.py +0 -216
  234. package/latticeai/api/chronicle.py +0 -63
  235. package/latticeai/api/command_center.py +0 -51
  236. package/latticeai/api/computer_use.py +0 -474
  237. package/latticeai/api/evidence_actions.py +0 -48
  238. package/latticeai/api/features.py +0 -70
  239. package/latticeai/api/funnel_metrics.py +0 -31
  240. package/latticeai/api/garden.py +0 -34
  241. package/latticeai/api/hooks.py +0 -165
  242. package/latticeai/api/index_jobs.py +0 -145
  243. package/latticeai/api/invitations.py +0 -100
  244. package/latticeai/api/knowledge_graph.py +0 -536
  245. package/latticeai/api/marketplace.py +0 -105
  246. package/latticeai/api/mcp.py +0 -482
  247. package/latticeai/api/memory.py +0 -270
  248. package/latticeai/api/network.py +0 -81
  249. package/latticeai/api/network_boundary.py +0 -225
  250. package/latticeai/api/permission_mode.py +0 -61
  251. package/latticeai/api/permissions.py +0 -436
  252. package/latticeai/api/plugins.py +0 -126
  253. package/latticeai/api/portability.py +0 -391
  254. package/latticeai/api/project_sessions.py +0 -114
  255. package/latticeai/api/realtime.py +0 -118
  256. package/latticeai/api/review_queue.py +0 -364
  257. package/latticeai/api/security_dashboard.py +0 -604
  258. package/latticeai/api/setup.py +0 -319
  259. package/latticeai/api/static_routes.py +0 -354
  260. package/latticeai/api/ui_redirects.py +0 -26
  261. package/latticeai/api/workflow_designer.py +0 -394
  262. package/latticeai/api/workspace.py +0 -856
  263. package/latticeai/api/workspace_scope.py +0 -125
  264. package/latticeai/core/agent/__init__.py +0 -93
  265. package/latticeai/core/agent/_contract.py +0 -79
  266. package/latticeai/core/agent/context.py +0 -57
  267. package/latticeai/core/agent/deps.py +0 -125
  268. package/latticeai/core/agent/execution.py +0 -622
  269. package/latticeai/core/agent/planning.py +0 -145
  270. package/latticeai/core/agent/recovery.py +0 -157
  271. package/latticeai/core/agent/runtime.py +0 -210
  272. package/latticeai/core/agent/verification.py +0 -231
  273. package/latticeai/core/agent_eval.py +0 -739
  274. package/latticeai/core/agent_helpers.py +0 -493
  275. package/latticeai/core/agent_profiles.py +0 -110
  276. package/latticeai/core/agent_prompts.py +0 -171
  277. package/latticeai/core/agent_registry.py +0 -232
  278. package/latticeai/core/agent_state.py +0 -41
  279. package/latticeai/core/agent_trace.py +0 -104
  280. package/latticeai/core/artifact_ledger.py +0 -109
  281. package/latticeai/core/audit.py +0 -260
  282. package/latticeai/core/builtin_hooks.py +0 -105
  283. package/latticeai/core/context_builder.py +0 -394
  284. package/latticeai/core/document_generator.py +0 -103
  285. package/latticeai/core/enterprise.py +0 -154
  286. package/latticeai/core/enterprise_admin.py +0 -158
  287. package/latticeai/core/file_generation/__init__.py +0 -115
  288. package/latticeai/core/file_generation/bundles.py +0 -76
  289. package/latticeai/core/file_generation/extraction.py +0 -154
  290. package/latticeai/core/file_generation/inference.py +0 -235
  291. package/latticeai/core/file_generation/orchestration.py +0 -152
  292. package/latticeai/core/file_generation/prompting.py +0 -117
  293. package/latticeai/core/file_generation/repair.py +0 -114
  294. package/latticeai/core/file_generation/sanitize.py +0 -61
  295. package/latticeai/core/file_generation/validation.py +0 -201
  296. package/latticeai/core/invitations.py +0 -132
  297. package/latticeai/core/legacy_compatibility.py +0 -243
  298. package/latticeai/core/logging_safety.py +0 -46
  299. package/latticeai/core/marketplace.py +0 -293
  300. package/latticeai/core/mcp_catalog.py +0 -452
  301. package/latticeai/core/mcp_registry.py +0 -506
  302. package/latticeai/core/network_boundary.py +0 -168
  303. package/latticeai/core/oidc.py +0 -208
  304. package/latticeai/core/plugins.py +0 -432
  305. package/latticeai/core/product_hardening.py +0 -218
  306. package/latticeai/core/project_sessions.py +0 -337
  307. package/latticeai/core/realtime.py +0 -238
  308. package/latticeai/core/run_explain.py +0 -426
  309. package/latticeai/core/run_store.py +0 -252
  310. package/latticeai/core/timezones.py +0 -80
  311. package/latticeai/core/workspace_computer_memory.py +0 -84
  312. package/latticeai/core/workspace_graph_trace.py +0 -155
  313. package/latticeai/core/workspace_indexing.py +0 -102
  314. package/latticeai/core/workspace_memory.py +0 -77
  315. package/latticeai/core/workspace_onboarding.py +0 -104
  316. package/latticeai/core/workspace_os.py +0 -978
  317. package/latticeai/core/workspace_os_constants.py +0 -126
  318. package/latticeai/core/workspace_os_state.py +0 -180
  319. package/latticeai/core/workspace_os_utils.py +0 -103
  320. package/latticeai/core/workspace_permissions.py +0 -101
  321. package/latticeai/core/workspace_plugins.py +0 -97
  322. package/latticeai/core/workspace_relationships.py +0 -99
  323. package/latticeai/core/workspace_reorganization.py +0 -335
  324. package/latticeai/core/workspace_review_items.py +0 -112
  325. package/latticeai/core/workspace_runs.py +0 -726
  326. package/latticeai/core/workspace_skills.py +0 -109
  327. package/latticeai/core/workspace_snapshots.py +0 -198
  328. package/latticeai/core/workspace_timeline.py +0 -110
  329. package/latticeai/integrations/__init__.py +0 -0
  330. package/latticeai/integrations/telegram_bot/__init__.py +0 -123
  331. package/latticeai/integrations/telegram_bot/__main__.py +0 -17
  332. package/latticeai/integrations/telegram_bot/config.py +0 -86
  333. package/latticeai/integrations/telegram_bot/dispatch.py +0 -311
  334. package/latticeai/integrations/telegram_bot/flows.py +0 -478
  335. package/latticeai/integrations/telegram_bot/helpers.py +0 -322
  336. package/latticeai/integrations/telegram_bot/screens.py +0 -394
  337. package/latticeai/runtime/audit_runtime.py +0 -76
  338. package/latticeai/runtime/automation_runtime.py +0 -81
  339. package/latticeai/runtime/chat_wiring.py +0 -141
  340. package/latticeai/runtime/context_runtime.py +0 -66
  341. package/latticeai/runtime/feature_toggle_wiring.py +0 -157
  342. package/latticeai/runtime/history_runtime.py +0 -163
  343. package/latticeai/runtime/history_writer.py +0 -138
  344. package/latticeai/runtime/hooks_runtime.py +0 -77
  345. package/latticeai/runtime/model_wiring.py +0 -68
  346. package/latticeai/runtime/namespace_runtime.py +0 -163
  347. package/latticeai/runtime/network_boundary_wiring.py +0 -117
  348. package/latticeai/runtime/network_config_runtime.py +0 -56
  349. package/latticeai/runtime/permission_mode_wiring.py +0 -112
  350. package/latticeai/runtime/persistence_runtime.py +0 -159
  351. package/latticeai/runtime/platform_runtime_wiring.py +0 -89
  352. package/latticeai/runtime/review_wiring.py +0 -42
  353. package/latticeai/runtime/router_registration.py +0 -693
  354. package/latticeai/runtime/service_singletons.py +0 -55
  355. package/latticeai/runtime/sso_config_runtime.py +0 -128
  356. package/latticeai/runtime/user_key_runtime.py +0 -106
  357. package/latticeai/runtime/web_runtime.py +0 -92
  358. package/latticeai/server_app.py +0 -51
  359. package/latticeai/services/app_context.py +0 -130
  360. package/latticeai/services/automation_execution.py +0 -266
  361. package/latticeai/services/automation_intelligence.py +0 -614
  362. package/latticeai/services/brain_automation.py +0 -191
  363. package/latticeai/services/brain_intelligence/__init__.py +0 -58
  364. package/latticeai/services/brain_intelligence/_contract.py +0 -71
  365. package/latticeai/services/brain_intelligence/consistency.py +0 -193
  366. package/latticeai/services/brain_intelligence/constants.py +0 -47
  367. package/latticeai/services/brain_intelligence/digest.py +0 -258
  368. package/latticeai/services/brain_intelligence/health.py +0 -331
  369. package/latticeai/services/brain_intelligence/proposals.py +0 -259
  370. package/latticeai/services/brain_intelligence/sampling.py +0 -84
  371. package/latticeai/services/brain_intelligence/service.py +0 -48
  372. package/latticeai/services/change_proposals.py +0 -471
  373. package/latticeai/services/chat_service.py +0 -243
  374. package/latticeai/services/chronicle.py +0 -555
  375. package/latticeai/services/cloud_egress_audit.py +0 -85
  376. package/latticeai/services/cloud_extraction.py +0 -129
  377. package/latticeai/services/cloud_streaming.py +0 -268
  378. package/latticeai/services/cloud_token_guard.py +0 -84
  379. package/latticeai/services/command_center.py +0 -548
  380. package/latticeai/services/evidence_actions.py +0 -258
  381. package/latticeai/services/feature_toggles.py +0 -502
  382. package/latticeai/services/folder_watch.py +0 -520
  383. package/latticeai/services/funnel_metrics.py +0 -307
  384. package/latticeai/services/hybrid_chat.py +0 -316
  385. package/latticeai/services/hybrid_context.py +0 -228
  386. package/latticeai/services/hybrid_policy.py +0 -129
  387. package/latticeai/services/interop_bridges.py +0 -978
  388. package/latticeai/services/local_knowledge.py +0 -465
  389. package/latticeai/services/memory_service/__init__.py +0 -52
  390. package/latticeai/services/memory_service/_contract.py +0 -100
  391. package/latticeai/services/memory_service/brief.py +0 -431
  392. package/latticeai/services/memory_service/constants.py +0 -57
  393. package/latticeai/services/memory_service/maintenance.py +0 -138
  394. package/latticeai/services/memory_service/manager.py +0 -186
  395. package/latticeai/services/memory_service/proof.py +0 -136
  396. package/latticeai/services/memory_service/recall.py +0 -225
  397. package/latticeai/services/memory_service/service.py +0 -48
  398. package/latticeai/services/memory_service/stores.py +0 -110
  399. package/latticeai/services/mode_store.py +0 -132
  400. package/latticeai/services/model_recommendation.py +0 -224
  401. package/latticeai/services/model_runtime/cloud.py +0 -87
  402. package/latticeai/services/network_boundary_service.py +0 -117
  403. package/latticeai/services/obsidian_bridge.py +0 -609
  404. package/latticeai/services/openai_compatible_adapter.py +0 -101
  405. package/latticeai/services/permission_mode_service.py +0 -122
  406. package/latticeai/services/platform_runtime.py +0 -366
  407. package/latticeai/services/review_queue.py +0 -380
  408. package/latticeai/services/router_context.py +0 -59
  409. package/latticeai/services/run_executor.py +0 -387
  410. package/latticeai/services/self_model_service.py +0 -171
  411. package/latticeai/services/setup_detection.py +0 -147
  412. package/latticeai/services/triggers.py +0 -378
  413. package/latticeai/services/upload_service.py +0 -172
  414. package/latticeai/services/workspace_service.py +0 -165
  415. package/latticeai/setup/__init__.py +0 -25
  416. package/latticeai/setup/auto_setup.py +0 -846
  417. package/latticeai/setup/demo_corpus.py +0 -98
  418. package/latticeai/setup/wizard/__init__.py +0 -126
  419. package/latticeai/setup/wizard/catalog.py +0 -175
  420. package/latticeai/setup/wizard/detect.py +0 -302
  421. package/latticeai/setup/wizard/install.py +0 -348
  422. package/latticeai/setup/wizard/paths.py +0 -165
  423. package/latticeai/setup/wizard/plans.py +0 -74
  424. package/latticeai/setup/wizard/recommend.py +0 -320
  425. package/scripts/bench_agent_smoke.py +0 -409
  426. package/scripts/bench_models.py +0 -540
  427. package/scripts/bench_vector_index.py +0 -295
  428. package/scripts/funnel_soft_gate.py +0 -192
  429. package/scripts/generate_agent_loop_fixtures.py +0 -994
  430. package/scripts/generate_rust_parity_fixtures.py +0 -908
  431. package/scripts/migrate_brain_storage.py +0 -57
  432. package/scripts/parity_fixture_corpus_context.py +0 -162
  433. package/scripts/parity_fixture_corpus_docgen.py +0 -341
  434. package/scripts/profile_kg.py +0 -355
  435. package/server.py +0 -30
  436. package/static/app/assets/AdminConsole-cf4npybT.js +0 -1
  437. package/static/app/assets/Capture-DiQ219jW.js +0 -1
  438. package/static/app/assets/System-CieofHQa.js +0 -1
  439. package/static/app/assets/arrow-left-kfsrk0mv.js +0 -1
  440. package/static/app/assets/circle-check-qqLug9nU.js +0 -1
  441. package/static/app/assets/index-DxmOfNRi.css +0 -2
  442. package/static/app/assets/primitives-SNp0LRJz.js +0 -1
  443. package/static/app/assets/search-BcHqkjoy.js +0 -1
@@ -0,0 +1,842 @@
1
+ """The v11.6.0 pure-compute seams: data in, data out, nothing stored.
2
+
3
+ Plan §설계 결정 2 (revised) hands **every write** to Rust — platform state and
4
+ the knowledge graph both. What is left for Python is the part Rust cannot do
5
+ without shipping a model runtime and half of PyPI: turning text into vectors,
6
+ turning a document into text, turning a spec into document bytes, turning audio
7
+ into a transcript, and turning a picture into facts about it.
8
+
9
+ ``worker_seams.py`` holds the *state* seams (``/worker/chat/record-turn``,
10
+ ``/worker/graph/mutate``) that Wave 2 codes against and Wave 2.5 §W3 retires.
11
+ This module is the opposite half and is deliberately a separate file: nothing
12
+ here opens a database, writes a file, or reaches a store. Each handler is a
13
+ function of its request body, so a caller can retry it, cache it, or run two of
14
+ them at once without asking who else is writing.
15
+
16
+ The five seams, and what each was extracted from:
17
+
18
+ ``POST /worker/embed``
19
+ ``EmbeddingProvider.embed_batch`` on the *resolved* provider — the same
20
+ object ``phase_brain`` hands the graph store, so a vector produced here is
21
+ the vector Python would have written. ``kind="passage"`` applies the
22
+ 50,000-character clamp ``write_master._upsert_vector_item`` applies before
23
+ embedding; ``kind="query"`` does not, matching ``retrieval_vector.search``.
24
+
25
+ ``POST /worker/parse``
26
+ :func:`latticeai.tools.documents.read_document` over the posted bytes. The
27
+ parser matrix (pdfplumber / python-docx / openpyxl / python-pptx / plain
28
+ text) is unchanged; only the file it reads is a temporary one this seam
29
+ writes and deletes.
30
+
31
+ ``POST /worker/render/{docx,xlsx,pptx,pdf}``
32
+ The *building* half of ``tools.documents.create_*`` — same libraries, same
33
+ layout decisions, same filename sanitising — with ``save(path)`` replaced by
34
+ ``save(BytesIO)``. Rust places the file; this seam never learns where.
35
+
36
+ ``POST /worker/asr``
37
+ ``VoiceCaptureService._transcribe``'s contract without the ingest: the same
38
+ injected transcriber port, the same container whitelist and size ceiling,
39
+ and the same refusal to call an empty transcript "text".
40
+
41
+ ``POST /worker/multimodal/describe``
42
+ :func:`lattice_brain.multimodal.extract_image_facts` plus
43
+ :func:`~lattice_brain.multimodal.image_quality_score` — exactly what
44
+ ``IngestionRoutingMixin._ingest_image`` computes before it writes a node.
45
+ The describe step *is* separable here because Brain Core already split it:
46
+ observing an image and writing it are two functions, and only the first one
47
+ is compute.
48
+
49
+ ``POST /worker/extract``
50
+ :func:`~lattice_brain.graph._kg_common.extraction._extract_concepts` (LLM-first),
51
+ :func:`~lattice_brain.graph._kg_common.extraction._extract_triples` and
52
+ :func:`~lattice_brain.graph._kg_common.extraction._semantic_items` — the
53
+ structures ``ingest_message`` / ``ingest_document`` / ``ingest_source``
54
+ consume, already classified. Rust writes the Concept/Task/Decision subgraph;
55
+ this seam never opens a store.
56
+
57
+ Gating is the seam gate the rest of the worker uses — ``LATTICEAI_AGENT_TOOL_SEAM``
58
+ read per request through :func:`latticeai.api.agent_worker_seam._seam_open`,
59
+ then ``require_user`` and the ``agent_seam`` rate bucket. Off ⇒ 404. Mounted
60
+ only by :func:`latticeai.runtime.build_phases.worker_profile.phase_worker_routes`,
61
+ so ``create_app`` and the committed OpenAPI contract are untouched.
62
+ """
63
+
64
+ from __future__ import annotations
65
+
66
+ import asyncio
67
+ import base64
68
+ import binascii
69
+ import contextlib
70
+ import io
71
+ import logging
72
+ import tempfile
73
+ from pathlib import Path
74
+ from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple
75
+
76
+ from fastapi import APIRouter, Request
77
+ from pydantic import BaseModel, Field
78
+
79
+ from latticeai.api.agent_worker_seam import SEAM_RATE_BUCKET
80
+ from latticeai.api.agent_worker_seam import _seam_open as seam_open
81
+ from latticeai.core.messages import MESSAGES, http_error, resolve_language
82
+ from latticeai.core.quiet import quiet
83
+ from latticeai.services.voice_capture import (
84
+ MAX_AUDIO_BYTES,
85
+ SUPPORTED_AUDIO_EXTENSIONS,
86
+ )
87
+ from latticeai.tools import _CJK_FONT_CANDIDATES, ToolError
88
+ from latticeai.tools.documents import _body_to_str, _safe_filename, read_document
89
+
90
+ logger = logging.getLogger(__name__)
91
+
92
+ #: What an embedding is *for*. The two differ in one thing only, and that thing
93
+ #: is real: the write path clamps to 50,000 characters before embedding
94
+ #: (``write_master._upsert_vector_item``) while the query path does not
95
+ #: (``retrieval_vector.search``). Naming it keeps Rust from having to know.
96
+ EMBED_KINDS: Tuple[str, ...] = ("query", "passage")
97
+
98
+ #: Which ingest door the caller is standing in. ``message`` is
99
+ #: ``ingest_message`` (concept limit 12); ``document`` is ``ingest_document``
100
+ #: / ``ingest_source`` (limit 15). The text itself is whatever that door
101
+ #: composed — the message body, or ``filename\\n{text}`` / ``title\\n{text}``.
102
+ EXTRACT_KINDS: Tuple[str, ...] = ("message", "document")
103
+ EXTRACT_LIMITS: Dict[str, int] = {"message": 12, "document": 15}
104
+
105
+ #: The clamp ``write_master`` applies. Copied as a constant rather than
106
+ #: imported: it is a literal at that call site, and importing a literal out of a
107
+ #: write-side module would tie this compute seam to a module W1 is replacing.
108
+ PASSAGE_MAX_CHARS = 50_000
109
+
110
+ #: Render kind → the suffix ``_safe_filename`` enforces. Same table as
111
+ #: ``documents._DOCUMENT_TOOL_TARGETS``, minus the output directory: where the
112
+ #: file lands is Rust's decision now, so this seam does not carry it.
113
+ RENDER_SUFFIXES: Dict[str, str] = {
114
+ "docx": ".docx",
115
+ "xlsx": ".xlsx",
116
+ "pptx": ".pptx",
117
+ "pdf": ".pdf",
118
+ }
119
+
120
+ #: The CJK-capable fonts ``create_pdf`` looks for, as a module attribute so a
121
+ #: test can point the probe at a font that exists (and at one that does not)
122
+ #: instead of asserting whatever the host machine happens to have installed.
123
+ CJK_FONT_CANDIDATES: List[str] = list(_CJK_FONT_CANDIDATES)
124
+
125
+ #: Messages this module owns. Registered into the one shared catalog with
126
+ #: ``setdefault`` — the same pattern ``worker_seams.py`` uses — so the entries
127
+ #: can be lifted into ``latticeai/core/messages.py`` verbatim later and this
128
+ #: registration becomes a no-op rather than an overwrite.
129
+ WORKER_COMPUTE_MESSAGES: Dict[str, Dict[str, str]] = {
130
+ "worker_compute.embedder_unavailable": {
131
+ "ko": "임베딩 공급자가 연결되어 있지 않습니다.",
132
+ "en": "No embedding provider is connected to this worker.",
133
+ },
134
+ "worker_compute.kind_invalid": {
135
+ "ko": "'{kind}' 은(는) 임베딩 종류가 아닙니다. {allowed} 중 하나여야 합니다.",
136
+ "en": "'{kind}' is not an embedding kind. Use one of {allowed}.",
137
+ },
138
+ "worker_compute.content_invalid": {
139
+ "ko": "본문을 base64로 읽지 못했습니다: {reason}",
140
+ "en": "The payload is not valid base64: {reason}",
141
+ },
142
+ "worker_compute.parse_failed": {
143
+ "ko": "문서를 읽지 못했습니다: {reason}",
144
+ "en": "The document could not be parsed: {reason}",
145
+ },
146
+ "worker_compute.render_unavailable": {
147
+ "ko": "이 워커는 '{kind}' 문서를 만들 수 없습니다: {reason}",
148
+ "en": "This worker cannot render '{kind}' documents: {reason}",
149
+ },
150
+ "worker_compute.render_failed": {
151
+ "ko": "'{kind}' 문서 생성이 실패했습니다: {reason}",
152
+ "en": "Rendering the '{kind}' document failed: {reason}",
153
+ },
154
+ "worker_compute.audio_unsupported": {
155
+ "ko": "'{suffix}' 은(는) 지원하는 오디오 형식이 아닙니다. {allowed} 만 가능합니다.",
156
+ "en": "'{suffix}' is not a supported audio container. Supported: {allowed}.",
157
+ },
158
+ "worker_compute.audio_too_large": {
159
+ "ko": "오디오가 {size} bytes 입니다. 한도는 {limit} bytes 입니다.",
160
+ "en": "The audio is {size} bytes; the limit is {limit} bytes.",
161
+ },
162
+ "worker_compute.extract_kind_invalid": {
163
+ "ko": "'{kind}' 은(는) 추출 종류가 아닙니다. {allowed} 중 하나여야 합니다.",
164
+ "en": "'{kind}' is not an extraction kind. Use one of {allowed}.",
165
+ },
166
+ }
167
+
168
+
169
+ def register_worker_compute_messages() -> None:
170
+ """Publish this module's messages into the one shared catalog."""
171
+ for key, entry in WORKER_COMPUTE_MESSAGES.items():
172
+ MESSAGES.setdefault(key, entry)
173
+
174
+
175
+ register_worker_compute_messages()
176
+
177
+
178
+ # ── shared helpers ──────────────────────────────────────────────────────────
179
+
180
+
181
+ @contextlib.contextmanager
182
+ def _temp_payload(data: bytes, suffix: str) -> Iterator[str]:
183
+ """The posted bytes as a real file, deleted whatever happens next.
184
+
185
+ Every parser and model port in this file takes a *path*: pdfplumber, PIL and
186
+ a local transcriber all open files, and rewriting them to take buffers would
187
+ be a much larger change than this seam is. So the bytes become a file for
188
+ the duration of one call and nothing survives it — which is what "owns zero
189
+ state" has to mean in practice.
190
+ """
191
+ handle = tempfile.NamedTemporaryFile( # noqa: SIM115 — closed below, path reused
192
+ prefix="ltcai-worker-", suffix=suffix, delete=False
193
+ )
194
+ try:
195
+ handle.write(data)
196
+ handle.close()
197
+ yield handle.name
198
+ finally:
199
+ try:
200
+ Path(handle.name).unlink(missing_ok=True)
201
+ except OSError:
202
+ quiet("worker compute temp cleanup")
203
+
204
+
205
+ def _suffix_for(filename: Optional[str], mime: Optional[str], default: str) -> str:
206
+ """The extension to give the temp file: filename first, then MIME, then a default.
207
+
208
+ The filename wins because it is what the user actually named the thing; a
209
+ MIME type is what some client claimed about it. Neither is trusted for
210
+ *content* — the parsers and PIL sniff that themselves — so this only has to
211
+ be good enough to route.
212
+ """
213
+ suffix = Path(str(filename or "")).suffix.lower()
214
+ if not suffix:
215
+ subtype = str(mime or "").split("/")[-1].split(";")[0].strip().lower()
216
+ suffix = f".{subtype}" if subtype else ""
217
+ return suffix or default
218
+
219
+
220
+ def _callable_identity(fn: Any) -> str:
221
+ """A name for an injected port, so the receipt says *which* model answered."""
222
+ module = str(getattr(fn, "__module__", "") or "")
223
+ name = str(getattr(fn, "__qualname__", "") or type(fn).__name__)
224
+ return f"{module}.{name}" if module else name
225
+
226
+
227
+ # ── document builders: create_* minus the filesystem ────────────────────────
228
+
229
+
230
+ def build_docx_bytes(title: str, body: Any) -> bytes:
231
+ """``tools.documents.create_docx``'s document, returned instead of saved."""
232
+ try:
233
+ from docx import Document
234
+ except Exception as exc: # noqa: BLE001 — an absent library is a configuration
235
+ raise ToolError(
236
+ "python-docx is not installed. Run `pip install -r requirements.txt`."
237
+ ) from exc
238
+
239
+ document = Document()
240
+ if title:
241
+ document.add_heading(str(title), level=1)
242
+ for block in _body_to_str(body).split("\n\n"):
243
+ text = block.strip()
244
+ if text:
245
+ document.add_paragraph(text)
246
+ buffer = io.BytesIO()
247
+ document.save(buffer)
248
+ return buffer.getvalue()
249
+
250
+
251
+ def build_xlsx_bytes(rows: Any, sheet_name: str = "Sheet1") -> bytes:
252
+ """``tools.documents.create_xlsx``'s workbook, returned instead of saved."""
253
+ try:
254
+ from openpyxl import Workbook
255
+ except Exception as exc: # noqa: BLE001 — an absent library is a configuration
256
+ raise ToolError(
257
+ "openpyxl is not installed. Run `pip install -r requirements.txt`."
258
+ ) from exc
259
+
260
+ if not isinstance(rows, list) or not all(isinstance(row, list) for row in rows):
261
+ raise ToolError("Rows must be a list of lists.")
262
+
263
+ workbook = Workbook()
264
+ sheet = workbook.active
265
+ sheet.title = (sheet_name or "Sheet1")[:31]
266
+ for row in rows:
267
+ sheet.append(row)
268
+ buffer = io.BytesIO()
269
+ workbook.save(buffer)
270
+ return buffer.getvalue()
271
+
272
+
273
+ def build_pptx_bytes(title: str, slides: Any) -> bytes:
274
+ """``tools.documents.create_pptx``'s deck, returned instead of saved."""
275
+ try:
276
+ from pptx import Presentation
277
+ except Exception as exc: # noqa: BLE001 — an absent library is a configuration
278
+ raise ToolError(
279
+ "python-pptx is not installed. Run `pip install -r requirements.txt`."
280
+ ) from exc
281
+
282
+ presentation = Presentation()
283
+ first_layout = presentation.slide_layouts[0]
284
+ first = presentation.slides.add_slide(first_layout)
285
+ first.shapes.title.text = title or "Presentation"
286
+ first.placeholders[1].text = ""
287
+
288
+ content_layout = presentation.slide_layouts[1]
289
+ for slide_data in slides or []:
290
+ slide = presentation.slides.add_slide(content_layout)
291
+ slide.shapes.title.text = str(slide_data.get("title") or "Slide")
292
+ body = slide.placeholders[1].text_frame
293
+ body.clear()
294
+ bullets = slide_data.get("bullets") or []
295
+ if isinstance(bullets, str):
296
+ bullets = [bullets]
297
+ for index, bullet in enumerate(bullets):
298
+ paragraph = body.paragraphs[0] if index == 0 else body.add_paragraph()
299
+ paragraph.text = str(bullet)
300
+ paragraph.level = 0
301
+
302
+ buffer = io.BytesIO()
303
+ presentation.save(buffer)
304
+ return buffer.getvalue()
305
+
306
+
307
+ def build_pdf_bytes(title: str, body: Any) -> bytes:
308
+ """``tools.documents.create_pdf``'s PDF, returned instead of saved.
309
+
310
+ The CJK font hunt is kept verbatim: without it a Korean document renders as
311
+ a page of empty boxes, and that is not a cosmetic difference.
312
+ """
313
+ try:
314
+ from reportlab.lib.pagesizes import A4
315
+ from reportlab.lib.styles import ParagraphStyle
316
+ from reportlab.lib.units import mm
317
+ from reportlab.pdfbase import pdfmetrics
318
+ from reportlab.pdfbase.ttfonts import TTFont
319
+ from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer
320
+ except Exception as exc: # noqa: BLE001 — an absent library is a configuration
321
+ raise ToolError("reportlab is not installed. Run `pip install reportlab`.") from exc
322
+
323
+ font_name = "Helvetica"
324
+ for font_path in CJK_FONT_CANDIDATES:
325
+ if Path(font_path).exists():
326
+ try:
327
+ pdfmetrics.registerFont(TTFont("KoreanFont", font_path))
328
+ font_name = "KoreanFont"
329
+ except Exception: # noqa: BLE001 — a broken font file is not a failed render
330
+ quiet("CJK font registration")
331
+ break
332
+
333
+ title_style = ParagraphStyle("Title", fontName=font_name, fontSize=18, spaceAfter=8, leading=24)
334
+ body_style = ParagraphStyle("Body", fontName=font_name, fontSize=11, spaceAfter=6, leading=16)
335
+
336
+ story: List[Any] = []
337
+ if title:
338
+ story.append(Paragraph(str(title), title_style))
339
+ story.append(Spacer(1, 4 * mm))
340
+
341
+ for block in _body_to_str(body).split("\n\n"):
342
+ text = block.strip()
343
+ if text:
344
+ safe_text = text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
345
+ story.append(Paragraph(safe_text, body_style))
346
+ story.append(Spacer(1, 2 * mm))
347
+
348
+ buffer = io.BytesIO()
349
+ doc = SimpleDocTemplate(
350
+ buffer,
351
+ pagesize=A4,
352
+ leftMargin=20 * mm,
353
+ rightMargin=20 * mm,
354
+ topMargin=20 * mm,
355
+ bottomMargin=20 * mm,
356
+ )
357
+ doc.build(story)
358
+ return buffer.getvalue()
359
+
360
+
361
+ # ── request bodies ──────────────────────────────────────────────────────────
362
+
363
+
364
+ class EmbedRequest(BaseModel):
365
+ """Texts in, vectors out. ``kind`` selects the write-path clamp."""
366
+
367
+ texts: List[str] = Field(default_factory=list)
368
+ kind: str = "passage"
369
+
370
+
371
+ class ParseRequest(BaseModel):
372
+ """One document, by name and by bytes. ``filename`` picks the parser."""
373
+
374
+ filename: str
375
+ content_b64: str
376
+
377
+
378
+ class RenderDocxRequest(BaseModel):
379
+ """``ToolDocxRequest``, verbatim — W3 forwards its body unchanged."""
380
+
381
+ title: str = ""
382
+ body: str = ""
383
+ filename: str = "document.docx"
384
+
385
+
386
+ class RenderXlsxRequest(BaseModel):
387
+ """``ToolXlsxRequest``, verbatim."""
388
+
389
+ rows: List[List] = Field(default_factory=list)
390
+ filename: str = "spreadsheet.xlsx"
391
+ sheet_name: str = "Sheet1"
392
+
393
+
394
+ class RenderPptxRequest(BaseModel):
395
+ """``ToolPptxRequest``, verbatim."""
396
+
397
+ title: str = ""
398
+ slides: List[Dict] = Field(default_factory=list)
399
+ filename: str = "presentation.pptx"
400
+
401
+
402
+ class RenderPdfRequest(BaseModel):
403
+ """``ToolPdfRequest``, verbatim."""
404
+
405
+ title: str = ""
406
+ body: str = ""
407
+ filename: str = "document.pdf"
408
+
409
+
410
+ class AsrRequest(BaseModel):
411
+ """One recording. ``filename``/``mime`` only choose the temp suffix."""
412
+
413
+ audio_b64: str
414
+ mime: Optional[str] = None
415
+ filename: Optional[str] = None
416
+
417
+
418
+ class DescribeRequest(BaseModel):
419
+ """One picture, with the two observation switches ``extract_image_facts`` takes."""
420
+
421
+ content_b64: str
422
+ mime: Optional[str] = None
423
+ filename: Optional[str] = None
424
+ ocr: bool = True
425
+ thumbnail: bool = True
426
+
427
+
428
+ class ExtractRequest(BaseModel):
429
+ """The text one ingest door would hand ``_extract_concepts``."""
430
+
431
+ text: str = ""
432
+ kind: str = "message"
433
+
434
+
435
+ def build_extract_reply(text: str, kind: str) -> Dict[str, Any]:
436
+ """The structures ``ingest_message`` / ``ingest_document`` / ``ingest_source`` consume.
437
+
438
+ Concepts are already classified (``_classify_node_type``), triples carry the
439
+ evidence class the edge metadata keeps, and semantic items keep the raw
440
+ dict the node stores. This is the golden generator's ``_concepts`` /
441
+ ``_triples`` / ``_semantic`` over the same functions the Python write path
442
+ calls — a test asserts equality against those call sites, not against a
443
+ second implementation.
444
+ """
445
+ from lattice_brain.graph._kg_common import (
446
+ _classify_node_type,
447
+ _extract_concepts,
448
+ _extract_triples,
449
+ _semantic_items,
450
+ )
451
+
452
+ limit = EXTRACT_LIMITS[kind]
453
+ raw_concepts = _extract_concepts(text, limit=limit)
454
+ concepts = [
455
+ {"text": concept, "node_type": _classify_node_type(concept, text)}
456
+ for concept in raw_concepts
457
+ ]
458
+ triples: List[Dict[str, Any]] = []
459
+ for triple in _extract_triples(text, raw_concepts):
460
+ triples.append(
461
+ {
462
+ "subject": triple["subject"],
463
+ "object": triple["object"],
464
+ "relation": triple["relation"],
465
+ "weight": float(triple.get("weight") or 1.0),
466
+ "context": str(triple.get("context") or ""),
467
+ "evidence": str(triple.get("evidence") or ""),
468
+ "confidence": triple.get("confidence"),
469
+ }
470
+ )
471
+ semantic = [
472
+ {
473
+ "item_type": item["type"],
474
+ "title": item["title"],
475
+ "summary": item["summary"],
476
+ "raw": dict(item),
477
+ }
478
+ for item in _semantic_items(text)
479
+ ]
480
+ return {"concepts": concepts, "triples": triples, "semantic": semantic}
481
+
482
+
483
+ def create_worker_compute_router(
484
+ *,
485
+ embedder: Any,
486
+ transcriber: Optional[Callable[[str], str]] = None,
487
+ multimodal_ports: Any = None,
488
+ require_user: Callable[[Request], Any],
489
+ enforce_rate_limit: Callable[[str, str], None],
490
+ ) -> APIRouter:
491
+ """The nine compute seams, wired to what this worker actually resolved.
492
+
493
+ ``embedder`` is the :class:`~latticeai.core.embedding_providers.text.ResolvedEmbedder`
494
+ ``phase_brain`` built (``None`` ⇒ 503, because a worker with no embedder is
495
+ a configuration rather than a crash). ``transcriber`` and
496
+ ``multimodal_ports`` are the same injected ports the voice and ingestion
497
+ paths hold; absent, the relevant seam reports the absence instead of
498
+ inventing a transcript or a caption.
499
+ """
500
+ router = APIRouter()
501
+
502
+ def _require_seam(request: Request) -> None:
503
+ """404 unless the host opened the seam for this worker."""
504
+ if not seam_open():
505
+ raise http_error(404, "agent_seam.disabled", resolve_language(request))
506
+
507
+ def _admit(request: Request) -> str:
508
+ """Authenticate and charge this call against the per-step budget."""
509
+ current_user = require_user(request)
510
+ enforce_rate_limit(current_user, SEAM_RATE_BUCKET)
511
+ return str(current_user or "")
512
+
513
+ def _decode(payload: str, language: str) -> bytes:
514
+ """Base64 in, bytes out — a malformed body is 422, never a 500."""
515
+ try:
516
+ return base64.b64decode(str(payload or ""), validate=True)
517
+ except (binascii.Error, ValueError) as exc:
518
+ raise http_error(
519
+ 422, "worker_compute.content_invalid", language, reason=str(exc)
520
+ ) from exc
521
+
522
+ async def _render(
523
+ kind: str,
524
+ builder: Callable[[], bytes],
525
+ filename: str,
526
+ language: str,
527
+ **extra: Any,
528
+ ) -> Dict[str, Any]:
529
+ """Build one document off the event loop and hand back its bytes."""
530
+ try:
531
+ payload = await asyncio.to_thread(builder)
532
+ except ToolError as exc:
533
+ # The library is not installed here. That is a property of the
534
+ # install, not of the request, so it is 503 and not 500.
535
+ raise http_error(
536
+ 503, "worker_compute.render_unavailable", language, kind=kind, reason=str(exc)
537
+ ) from exc
538
+ except Exception as exc: # noqa: BLE001 — reported with the kind, not swallowed
539
+ logger.warning("worker render %s failed: %s", kind, exc)
540
+ raise http_error(
541
+ 500, "worker_compute.render_failed", language, kind=kind, reason=str(exc)
542
+ ) from exc
543
+ return {
544
+ "filename": _safe_filename(filename, RENDER_SUFFIXES[kind]),
545
+ "content_b64": base64.b64encode(payload).decode("ascii"),
546
+ "bytes": len(payload),
547
+ **extra,
548
+ }
549
+
550
+ @router.post("/worker/embed")
551
+ async def worker_embed(req: EmbedRequest, request: Request):
552
+ """Vectors for these texts, from the provider this worker resolved.
553
+
554
+ ``dim`` and ``model_id`` are read *after* the call because a network
555
+ provider locks its index identity to the width the model actually
556
+ returned — reading them first would report the guess, and Rust files
557
+ every vector under whatever this says.
558
+ """
559
+ _require_seam(request)
560
+ _admit(request)
561
+ language = resolve_language(request)
562
+ if embedder is None:
563
+ raise http_error(503, "worker_compute.embedder_unavailable", language)
564
+ kind = req.kind.strip().lower()
565
+ if kind not in EMBED_KINDS:
566
+ raise http_error(
567
+ 422,
568
+ "worker_compute.kind_invalid",
569
+ language,
570
+ kind=req.kind,
571
+ allowed=", ".join(EMBED_KINDS),
572
+ )
573
+ provider = embedder.provider
574
+ texts = list(req.texts)
575
+ if kind == "passage":
576
+ texts = [text[:PASSAGE_MAX_CHARS] for text in texts]
577
+ vectors = await asyncio.to_thread(provider.embed_batch, texts)
578
+ return {
579
+ "vectors": vectors,
580
+ "dim": provider.dim,
581
+ "provider": embedder.active,
582
+ "model_id": provider.model_id,
583
+ "kind": kind,
584
+ }
585
+
586
+ @router.post("/worker/parse")
587
+ async def worker_parse(req: ParseRequest, request: Request):
588
+ """The parser matrix over posted bytes: text, counts, preview.
589
+
590
+ The ``path`` key ``read_document`` reports is dropped: it named a
591
+ temporary file that no longer exists by the time this answers, and
592
+ echoing it would hand Rust a path it must not act on. ``filename`` — the
593
+ one the caller named — takes its place.
594
+ """
595
+ _require_seam(request)
596
+ _admit(request)
597
+ language = resolve_language(request)
598
+ data = _decode(req.content_b64, language)
599
+ suffix = Path(req.filename or "").suffix.lower()
600
+ with _temp_payload(data, suffix) as tmp_path:
601
+ try:
602
+ parsed = await asyncio.to_thread(read_document, tmp_path)
603
+ except ToolError as exc:
604
+ raise http_error(
605
+ 400, "worker_compute.parse_failed", language, reason=str(exc)
606
+ ) from exc
607
+ parsed.pop("path", None)
608
+ parsed["filename"] = req.filename
609
+ return parsed
610
+
611
+ @router.post("/worker/render/docx")
612
+ async def worker_render_docx(req: RenderDocxRequest, request: Request):
613
+ """A Word document's bytes. Rust decides where it lands."""
614
+ _require_seam(request)
615
+ _admit(request)
616
+ return await _render(
617
+ "docx",
618
+ lambda: build_docx_bytes(req.title, req.body),
619
+ req.filename,
620
+ resolve_language(request),
621
+ )
622
+
623
+ @router.post("/worker/render/xlsx")
624
+ async def worker_render_xlsx(req: RenderXlsxRequest, request: Request):
625
+ """A workbook's bytes, with the row count ``create_xlsx`` reported."""
626
+ _require_seam(request)
627
+ _admit(request)
628
+ return await _render(
629
+ "xlsx",
630
+ lambda: build_xlsx_bytes(req.rows, req.sheet_name),
631
+ req.filename,
632
+ resolve_language(request),
633
+ rows=len(req.rows),
634
+ )
635
+
636
+ @router.post("/worker/render/pptx")
637
+ async def worker_render_pptx(req: RenderPptxRequest, request: Request):
638
+ """A deck's bytes. ``slides`` counts the title slide, as ``create_pptx`` did."""
639
+ _require_seam(request)
640
+ _admit(request)
641
+ return await _render(
642
+ "pptx",
643
+ lambda: build_pptx_bytes(req.title, req.slides),
644
+ req.filename,
645
+ resolve_language(request),
646
+ slides=len(req.slides) + 1,
647
+ )
648
+
649
+ @router.post("/worker/render/pdf")
650
+ async def worker_render_pdf(req: RenderPdfRequest, request: Request):
651
+ """A PDF's bytes, CJK font and all."""
652
+ _require_seam(request)
653
+ _admit(request)
654
+ return await _render(
655
+ "pdf",
656
+ lambda: build_pdf_bytes(req.title, req.body),
657
+ req.filename,
658
+ resolve_language(request),
659
+ )
660
+
661
+ @router.post("/worker/asr")
662
+ async def worker_asr(req: AsrRequest, request: Request):
663
+ """Transcribe one recording — and never claim a transcript it did not get.
664
+
665
+ ``status`` is the state machine ``VoiceCaptureService.capture`` records
666
+ on the node: ``ok`` when there are words, ``unavailable`` when this
667
+ machine has no transcriber or the transcriber returned nothing, and
668
+ ``failed`` when it broke. All three are 200: "we could not hear it" is
669
+ an answer about the audio, not an error about the request.
670
+ """
671
+ _require_seam(request)
672
+ _admit(request)
673
+ language = resolve_language(request)
674
+ data = _decode(req.audio_b64, language)
675
+ suffix = _suffix_for(req.filename, req.mime, ".m4a")
676
+ if suffix not in SUPPORTED_AUDIO_EXTENSIONS:
677
+ raise http_error(
678
+ 400,
679
+ "worker_compute.audio_unsupported",
680
+ language,
681
+ suffix=suffix,
682
+ allowed=", ".join(sorted(SUPPORTED_AUDIO_EXTENSIONS)),
683
+ )
684
+ if len(data) > MAX_AUDIO_BYTES:
685
+ raise http_error(
686
+ 413,
687
+ "worker_compute.audio_too_large",
688
+ language,
689
+ size=len(data),
690
+ limit=MAX_AUDIO_BYTES,
691
+ )
692
+ if transcriber is None:
693
+ return {
694
+ "text": "",
695
+ "segments": None,
696
+ "provider": "",
697
+ "status": "unavailable",
698
+ "detail": "no local transcriber is configured",
699
+ }
700
+ provider = _callable_identity(transcriber)
701
+ with _temp_payload(data, suffix) as tmp_path:
702
+ try:
703
+ raw = await asyncio.to_thread(transcriber, tmp_path)
704
+ except Exception as exc: # noqa: BLE001 — a broken transcriber is a state
705
+ logger.warning("worker asr failed: %s", exc)
706
+ return {
707
+ "text": "",
708
+ "segments": None,
709
+ "provider": provider,
710
+ "status": "failed",
711
+ "detail": str(exc),
712
+ }
713
+ text = str(raw or "").strip()
714
+ if not text:
715
+ # An empty transcript is not text: reporting it as one would put an
716
+ # empty note in the Brain and call it a memory.
717
+ return {
718
+ "text": "",
719
+ "segments": None,
720
+ "provider": provider,
721
+ "status": "unavailable",
722
+ "detail": "the transcriber returned no text",
723
+ }
724
+ return {
725
+ "text": text,
726
+ # The port's contract is ``(path) -> str``. There are no timings to
727
+ # report, and inventing an empty list would read as "no speech".
728
+ "segments": None,
729
+ "provider": provider,
730
+ "status": "ok",
731
+ "detail": "",
732
+ }
733
+
734
+ @router.post("/worker/multimodal/describe")
735
+ async def worker_multimodal_describe(req: DescribeRequest, request: Request):
736
+ """Everything ``_ingest_image`` knows before it writes a node.
737
+
738
+ ``metadata`` is ``ImageFacts.as_metadata()`` — the exact dict
739
+ ``write_image_memory`` merges onto the node — and ``index_text`` is the
740
+ exact text it chunks. ``embedding`` is present only when a vision model
741
+ produced one, which is the one property an image vector must have.
742
+ """
743
+ _require_seam(request)
744
+ _admit(request)
745
+ language = resolve_language(request)
746
+ data = _decode(req.content_b64, language)
747
+ suffix = _suffix_for(req.filename, req.mime, ".png")
748
+
749
+ from lattice_brain.ingestion import _quality_level
750
+ from lattice_brain.multimodal import (
751
+ MODALITY_IMAGE,
752
+ MultimodalPorts,
753
+ extract_image_facts,
754
+ image_quality_score,
755
+ )
756
+
757
+ ports = multimodal_ports or MultimodalPorts()
758
+ with _temp_payload(data, suffix) as tmp_path:
759
+ facts = await asyncio.to_thread(
760
+ lambda: extract_image_facts(
761
+ tmp_path, ports=ports, ocr=req.ocr, thumbnail=req.thumbnail
762
+ )
763
+ )
764
+ quality = image_quality_score(facts)
765
+ return {
766
+ "modality": MODALITY_IMAGE,
767
+ "readable": facts.readable,
768
+ "error": facts.error,
769
+ "width": facts.width,
770
+ "height": facts.height,
771
+ "format": facts.image_format,
772
+ "mode": facts.mode,
773
+ "ocr_status": facts.ocr_status,
774
+ "ocr_text": facts.ocr_text,
775
+ "ocr_detail": facts.ocr_detail,
776
+ "caption_status": facts.caption_status,
777
+ "caption": facts.caption,
778
+ "embedding_status": facts.embedding_status,
779
+ "embedding": facts.embedding,
780
+ "embedding_detail": facts.embedding_detail,
781
+ "thumbnail": facts.thumbnail,
782
+ "index_text": facts.index_text(),
783
+ "metadata": facts.as_metadata(),
784
+ "quality": {
785
+ "score": quality["score"],
786
+ "level": _quality_level(quality["score"]),
787
+ "reasons": quality["reasons"],
788
+ },
789
+ "ports": ports.describe(),
790
+ }
791
+
792
+ @router.post("/worker/extract")
793
+ async def worker_extract(req: ExtractRequest, request: Request):
794
+ """Concepts, triples and Task/Decision items for this text.
795
+
796
+ The reply is field-for-field what ``GraphWriter``'s ingest doors take
797
+ as ``concepts`` / ``triples`` / ``semantic``. Classification and the
798
+ evidence class happen here because they are compute over the text, not
799
+ a write.
800
+ """
801
+ _require_seam(request)
802
+ _admit(request)
803
+ language = resolve_language(request)
804
+ kind = req.kind.strip().lower()
805
+ if kind not in EXTRACT_KINDS:
806
+ raise http_error(
807
+ 422,
808
+ "worker_compute.extract_kind_invalid",
809
+ language,
810
+ kind=req.kind,
811
+ allowed=", ".join(EXTRACT_KINDS),
812
+ )
813
+ return await asyncio.to_thread(build_extract_reply, req.text, kind)
814
+
815
+ return router
816
+
817
+
818
+ __all__ = [
819
+ "CJK_FONT_CANDIDATES",
820
+ "EMBED_KINDS",
821
+ "EXTRACT_KINDS",
822
+ "EXTRACT_LIMITS",
823
+ "PASSAGE_MAX_CHARS",
824
+ "RENDER_SUFFIXES",
825
+ "WORKER_COMPUTE_MESSAGES",
826
+ "AsrRequest",
827
+ "DescribeRequest",
828
+ "EmbedRequest",
829
+ "ExtractRequest",
830
+ "ParseRequest",
831
+ "RenderDocxRequest",
832
+ "RenderPdfRequest",
833
+ "RenderPptxRequest",
834
+ "RenderXlsxRequest",
835
+ "build_docx_bytes",
836
+ "build_extract_reply",
837
+ "build_pdf_bytes",
838
+ "build_pptx_bytes",
839
+ "build_xlsx_bytes",
840
+ "create_worker_compute_router",
841
+ "register_worker_compute_messages",
842
+ ]