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.
- package/README.md +93 -148
- package/bin/ltcai.js +234 -24
- package/docs/CHANGELOG.md +119 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +8 -6
- package/docs/ENTERPRISE.md +2 -1
- package/docs/MULTI_AGENT_RUNTIME.md +12 -5
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +6 -3
- package/docs/REALTIME_COLLABORATION.md +1 -1
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/WORKFLOW_DESIGNER.md +3 -2
- package/docs/kg-schema.md +1 -1
- package/docs/v11.6.0_ONE_DOOR_PLAN.md +170 -0
- package/lattice_brain/__init__.py +42 -79
- package/lattice_brain/graph/__init__.py +9 -24
- package/lattice_brain/graph/_kg_common/__init__.py +13 -24
- package/lattice_brain/ingestion/__init__.py +19 -58
- package/lattice_brain/ingestion/pipeline.py +20 -398
- package/lattice_brain/multimodal/__init__.py +7 -18
- package/lattice_brain/multimodal/images.py +6 -264
- package/lattice_brain/multimodal/video.py +8 -247
- package/lattice_brain/runtime/__init__.py +8 -79
- package/lattice_brain/runtime/hooks.py +22 -584
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/agent_worker_seam.py +7 -80
- package/latticeai/api/health.py +6 -20
- package/latticeai/api/local_files.py +16 -613
- package/latticeai/api/models.py +12 -87
- package/latticeai/api/search.py +17 -255
- package/latticeai/api/tools.py +69 -750
- package/latticeai/api/voice_capture.py +8 -70
- package/latticeai/api/worker_compute.py +842 -0
- package/latticeai/api/worker_seams.py +216 -0
- package/latticeai/app_factory.py +23 -232
- package/latticeai/cli/entrypoint.py +36 -249
- package/latticeai/core/agent_permission.py +16 -91
- package/latticeai/core/messages.py +89 -531
- package/latticeai/runtime/access_runtime.py +0 -14
- package/latticeai/runtime/bootstrap.py +10 -21
- package/latticeai/runtime/brain_runtime.py +35 -43
- package/latticeai/runtime/build_phases/__init__.py +21 -37
- package/latticeai/runtime/build_phases/features.py +99 -389
- package/latticeai/runtime/build_phases/foundation.py +82 -392
- package/latticeai/runtime/build_phases/web.py +91 -408
- package/latticeai/runtime/build_phases/worker_profile.py +244 -0
- package/latticeai/runtime/lifespan_runtime.py +7 -16
- package/latticeai/runtime/platform_services_runtime.py +1 -15
- package/latticeai/runtime/runtime_context.py +13 -130
- package/latticeai/runtime/security_runtime.py +10 -103
- package/latticeai/services/architecture_readiness.py +82 -43
- package/latticeai/services/model_runtime/__init__.py +2 -11
- package/latticeai/services/model_runtime/service.py +4 -28
- package/latticeai/services/p_reinforce.py +10 -261
- package/latticeai/services/product_readiness.py +31 -38
- package/latticeai/services/search_service.py +37 -795
- package/latticeai/services/tool_dispatch.py +30 -386
- package/latticeai/services/voice_capture.py +13 -107
- package/latticeai/tools/__init__.py +22 -49
- package/latticeai/tools/commands.py +0 -163
- package/latticeai/tools/computer.py +0 -39
- package/latticeai/tools/documents.py +1 -134
- package/latticeai/tools/filesystem.py +1 -247
- package/latticeai/tools/knowledge.py +1 -52
- package/latticeai/tools/local_files.py +0 -20
- package/latticeai/worker_app.py +75 -0
- package/package.json +2 -3
- package/requirements.txt +0 -5
- package/scripts/agent_eval.py +16 -28
- package/scripts/brain_quality_eval.py +20 -183
- package/scripts/bump_version.py +5 -5
- package/scripts/check_current_release_docs.mjs +7 -4
- package/scripts/check_openapi_drift.mjs +10 -0
- package/scripts/check_server_i18n.mjs +2 -18
- package/scripts/compose_openapi.py +377 -0
- package/scripts/export_openapi.py +32 -4
- package/scripts/gen_messages_catalog_fixture.py +302 -0
- package/scripts/gen_openapi_fragments.py +365 -0
- package/scripts/gen_redact_fixture.py +196 -0
- package/scripts/gen_worker_allowlist_fixture.py +115 -0
- package/scripts/generate_agent_parity_fixtures.py +33 -14
- package/scripts/openapi_route_families.json +2161 -0
- package/scripts/release_screen_claims.json +54 -0
- package/scripts/run_integration_tests.mjs +134 -29
- package/scripts/run_sidecar_e2e.mjs +104 -11
- package/scripts/wheel_smoke.py +32 -22
- package/src-tauri/Cargo.lock +375 -9
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/backend.rs +151 -45
- package/src-tauri/src/main.rs +18 -18
- package/src-tauri/src/topology.rs +58 -88
- package/src-tauri/tauri.conf.json +1 -2
- package/static/app/asset-manifest.json +41 -41
- package/static/app/assets/{Act-DcQizkl1.js → Act-BPcVAbOL.js} +1 -1
- package/static/app/assets/AdminConsole-Bw1ATQL0.js +1 -0
- package/static/app/assets/{Brain-3VCSHFcn.js → Brain-CT92Kos0.js} +2 -2
- package/static/app/assets/{BrainHome-Qm8eaztx.js → BrainHome-CFBkt1K_.js} +1 -1
- package/static/app/assets/{BrainSignals-DS9BtKOW.js → BrainSignals-ReLWF2H8.js} +1 -1
- package/static/app/assets/Capture-BsTokYkk.js +1 -0
- package/static/app/assets/{Chronicle-BGvuAchH.js → Chronicle-B6f0T9id.js} +1 -1
- package/static/app/assets/{CommandPalette-Bqhm0Urn.js → CommandPalette-CuvjTv1u.js} +1 -1
- package/static/app/assets/{Library-BV6NnF0a.js → Library-BGJbG9Hd.js} +1 -1
- package/static/app/assets/{LivingBrain-GzenJchP.js → LivingBrain-DGYK_Jsa.js} +1 -1
- package/static/app/assets/{ProductFlow-DEP6-vML.js → ProductFlow-DXBC6brE.js} +1 -1
- package/static/app/assets/{ReviewCard-CNZ7XjWG.js → ReviewCard-HXRle3qq.js} +2 -2
- package/static/app/assets/System-CMHSO9qM.js +1 -0
- package/static/app/assets/arrow-left-BfmkskWx.js +1 -0
- package/static/app/assets/{bot-B_K1Tdmw.js → bot-Cn8bWRuq.js} +1 -1
- package/static/app/assets/{brain-DWyaV1L1.js → brain-CQJberbE.js} +1 -1
- package/static/app/assets/{button-aTn4s84A.js → button-Ct9f2_oT.js} +1 -1
- package/static/app/assets/circle-check-DruOxB-4.js +1 -0
- package/static/app/assets/{circle-pause-xKgeGXkT.js → circle-pause-CmzC_apg.js} +1 -1
- package/static/app/assets/{circle-play-DkT6tYPX.js → circle-play-D8mW2aQ7.js} +1 -1
- package/static/app/assets/{cpu-85xYObUC.js → cpu-DZcdd0PZ.js} +1 -1
- package/static/app/assets/{download-B5Fm7YXo.js → download-bv1KEPGQ.js} +1 -1
- package/static/app/assets/{folder-open-kk2Xa52u.js → folder-open-d-Pip5gr.js} +1 -1
- package/static/app/assets/{hard-drive-DkA3zBW_.js → hard-drive-D20iavUb.js} +1 -1
- package/static/app/assets/index-D9x-kSNy.css +2 -0
- package/static/app/assets/{index-BMPdTmlY.js → index-Do83hDzJ.js} +3 -3
- package/static/app/assets/{input-B0nRf2jO.js → input-BLXVNmj1.js} +1 -1
- package/static/app/assets/{link-2-Dwb4gnTc.js → link-2-BPJOFlAy.js} +1 -1
- package/static/app/assets/{permissionCopy-CQDUBrOZ.js → permissionCopy-ChdJd493.js} +1 -1
- package/static/app/assets/primitives-Cv5tbZBY.js +1 -0
- package/static/app/assets/search-CT9aho2j.js +1 -0
- package/static/app/assets/{share-2-BsrxFglO.js → share-2-YNX_NtMU.js} +1 -1
- package/static/app/assets/{shield-alert-5BStfp2_.js → shield-alert-DuQ3zrVL.js} +1 -1
- package/static/app/assets/{textarea-Cg8IUA-k.js → textarea-DqwLnli4.js} +1 -1
- package/static/app/assets/{useFocusTrap-CYKvE46M.js → useFocusTrap-ZVI98jaW.js} +1 -1
- package/static/app/assets/{useMutation-CSn9t1op.js → useMutation-CVC4qv_D.js} +1 -1
- package/static/app/assets/{useQuery-CY2OI2uy.js → useQuery-C7BeG4HU.js} +1 -1
- package/static/app/assets/{utils-Ddol2RWD.js → utils-CiFtIdZq.js} +1 -1
- package/static/app/assets/{workspace-BqDwOz_p.js → workspace-DQz9vIId.js} +1 -1
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/lattice_brain/archive.py +0 -522
- package/lattice_brain/context.py +0 -325
- package/lattice_brain/conversations.py +0 -382
- package/lattice_brain/core.py +0 -82
- package/lattice_brain/graph/_kg_contract.py +0 -249
- package/lattice_brain/graph/curator.py +0 -676
- package/lattice_brain/graph/discovery.py +0 -595
- package/lattice_brain/graph/discovery_index/__init__.py +0 -35
- package/lattice_brain/graph/discovery_index/cleanup.py +0 -182
- package/lattice_brain/graph/discovery_index/extract.py +0 -137
- package/lattice_brain/graph/discovery_index/scan.py +0 -411
- package/lattice_brain/graph/discovery_index/upsert.py +0 -495
- package/lattice_brain/graph/documents.py +0 -380
- package/lattice_brain/graph/fusion.py +0 -395
- package/lattice_brain/graph/identity.py +0 -175
- package/lattice_brain/graph/image_vectors.py +0 -230
- package/lattice_brain/graph/ingest.py +0 -829
- package/lattice_brain/graph/network.py +0 -205
- package/lattice_brain/graph/proactive.py +0 -724
- package/lattice_brain/graph/projection/__init__.py +0 -42
- package/lattice_brain/graph/projection/curation.py +0 -500
- package/lattice_brain/graph/projection/v2_schema.py +0 -518
- package/lattice_brain/graph/provenance.py +0 -524
- package/lattice_brain/graph/rerank.py +0 -163
- package/lattice_brain/graph/retrieval/__init__.py +0 -54
- package/lattice_brain/graph/retrieval/context.py +0 -197
- package/lattice_brain/graph/retrieval/graph_view.py +0 -319
- package/lattice_brain/graph/retrieval/hybrid.py +0 -488
- package/lattice_brain/graph/retrieval/maintenance.py +0 -121
- package/lattice_brain/graph/retrieval/signals.py +0 -95
- package/lattice_brain/graph/retrieval_docgen.py +0 -253
- package/lattice_brain/graph/retrieval_policy.py +0 -180
- package/lattice_brain/graph/retrieval_reads.py +0 -769
- package/lattice_brain/graph/retrieval_vector/__init__.py +0 -42
- package/lattice_brain/graph/retrieval_vector/fingerprint.py +0 -97
- package/lattice_brain/graph/retrieval_vector/indexing.py +0 -347
- package/lattice_brain/graph/retrieval_vector/search.py +0 -560
- package/lattice_brain/graph/retrieval_vector/status.py +0 -374
- package/lattice_brain/graph/schema.py +0 -792
- package/lattice_brain/graph/store.py +0 -268
- package/lattice_brain/graph/vector_index/__init__.py +0 -85
- package/lattice_brain/graph/vector_index/base.py +0 -167
- package/lattice_brain/graph/vector_index/brute_force.py +0 -110
- package/lattice_brain/graph/vector_index/hnsw.py +0 -290
- package/lattice_brain/graph/vector_index/jobs.py +0 -287
- package/lattice_brain/graph/vector_index/quantized.py +0 -148
- package/lattice_brain/graph/vector_index/selector.py +0 -161
- package/lattice_brain/graph/write_master.py +0 -308
- package/lattice_brain/ingestion/_contract.py +0 -90
- package/lattice_brain/ingestion/folder_scan.py +0 -57
- package/lattice_brain/ingestion/folders.py +0 -258
- package/lattice_brain/ingestion/jobs_api.py +0 -107
- package/lattice_brain/ingestion/routing.py +0 -295
- package/lattice_brain/ingestion_jobs.py +0 -380
- package/lattice_brain/memory.py +0 -75
- package/lattice_brain/portability/__init__.py +0 -90
- package/lattice_brain/portability/_contract.py +0 -42
- package/lattice_brain/portability/backups.py +0 -338
- package/lattice_brain/portability/bundles.py +0 -136
- package/lattice_brain/portability/constants.py +0 -93
- package/lattice_brain/portability/fsops.py +0 -138
- package/lattice_brain/portability/service.py +0 -41
- package/lattice_brain/portability/sharing.py +0 -710
- package/lattice_brain/quality.py +0 -543
- package/lattice_brain/retrieval_benchmark_fixtures.py +0 -92
- package/lattice_brain/runtime/agent_runtime.py +0 -859
- package/lattice_brain/runtime/contracts.py +0 -460
- package/lattice_brain/runtime/multi_agent.py +0 -942
- package/lattice_brain/runtime/statuses.py +0 -10
- package/lattice_brain/sealed_box.py +0 -240
- package/lattice_brain/self_model.py +0 -675
- package/lattice_brain/sensitivity.py +0 -94
- package/lattice_brain/storage/__init__.py +0 -22
- package/lattice_brain/storage/base.py +0 -100
- package/lattice_brain/storage/docker.py +0 -105
- package/lattice_brain/storage/factory.py +0 -31
- package/lattice_brain/storage/migration.py +0 -191
- package/lattice_brain/storage/postgres.py +0 -123
- package/lattice_brain/storage/sqlite.py +0 -143
- package/lattice_brain/synthesis.py +0 -824
- package/lattice_brain/workflow.py +0 -497
- package/latticeai/api/admin.py +0 -471
- package/latticeai/api/agent_registry.py +0 -105
- package/latticeai/api/agents.py +0 -228
- package/latticeai/api/auth.py +0 -383
- package/latticeai/api/automation_intelligence.py +0 -401
- package/latticeai/api/brain_intelligence.py +0 -199
- package/latticeai/api/browser.py +0 -493
- package/latticeai/api/change_proposals.py +0 -89
- package/latticeai/api/chat.py +0 -572
- package/latticeai/api/chat_agent_http.py +0 -892
- package/latticeai/api/chat_contracts.py +0 -73
- package/latticeai/api/chat_documents.py +0 -276
- package/latticeai/api/chat_helpers.py +0 -460
- package/latticeai/api/chat_history.py +0 -101
- package/latticeai/api/chat_hybrid.py +0 -113
- package/latticeai/api/chat_intents.py +0 -646
- package/latticeai/api/chat_stream.py +0 -216
- package/latticeai/api/chronicle.py +0 -63
- package/latticeai/api/command_center.py +0 -51
- package/latticeai/api/computer_use.py +0 -474
- package/latticeai/api/evidence_actions.py +0 -48
- package/latticeai/api/features.py +0 -70
- package/latticeai/api/funnel_metrics.py +0 -31
- package/latticeai/api/garden.py +0 -34
- package/latticeai/api/hooks.py +0 -165
- package/latticeai/api/index_jobs.py +0 -145
- package/latticeai/api/invitations.py +0 -100
- package/latticeai/api/knowledge_graph.py +0 -536
- package/latticeai/api/marketplace.py +0 -105
- package/latticeai/api/mcp.py +0 -482
- package/latticeai/api/memory.py +0 -270
- package/latticeai/api/network.py +0 -81
- package/latticeai/api/network_boundary.py +0 -225
- package/latticeai/api/permission_mode.py +0 -61
- package/latticeai/api/permissions.py +0 -436
- package/latticeai/api/plugins.py +0 -126
- package/latticeai/api/portability.py +0 -391
- package/latticeai/api/project_sessions.py +0 -114
- package/latticeai/api/realtime.py +0 -118
- package/latticeai/api/review_queue.py +0 -364
- package/latticeai/api/security_dashboard.py +0 -604
- package/latticeai/api/setup.py +0 -319
- package/latticeai/api/static_routes.py +0 -354
- package/latticeai/api/ui_redirects.py +0 -26
- package/latticeai/api/workflow_designer.py +0 -394
- package/latticeai/api/workspace.py +0 -856
- package/latticeai/api/workspace_scope.py +0 -125
- package/latticeai/core/agent/__init__.py +0 -93
- package/latticeai/core/agent/_contract.py +0 -79
- package/latticeai/core/agent/context.py +0 -57
- package/latticeai/core/agent/deps.py +0 -125
- package/latticeai/core/agent/execution.py +0 -622
- package/latticeai/core/agent/planning.py +0 -145
- package/latticeai/core/agent/recovery.py +0 -157
- package/latticeai/core/agent/runtime.py +0 -210
- package/latticeai/core/agent/verification.py +0 -231
- package/latticeai/core/agent_eval.py +0 -739
- package/latticeai/core/agent_helpers.py +0 -493
- package/latticeai/core/agent_profiles.py +0 -110
- package/latticeai/core/agent_prompts.py +0 -171
- package/latticeai/core/agent_registry.py +0 -232
- package/latticeai/core/agent_state.py +0 -41
- package/latticeai/core/agent_trace.py +0 -104
- package/latticeai/core/artifact_ledger.py +0 -109
- package/latticeai/core/audit.py +0 -260
- package/latticeai/core/builtin_hooks.py +0 -105
- package/latticeai/core/context_builder.py +0 -394
- package/latticeai/core/document_generator.py +0 -103
- package/latticeai/core/enterprise.py +0 -154
- package/latticeai/core/enterprise_admin.py +0 -158
- package/latticeai/core/file_generation/__init__.py +0 -115
- package/latticeai/core/file_generation/bundles.py +0 -76
- package/latticeai/core/file_generation/extraction.py +0 -154
- package/latticeai/core/file_generation/inference.py +0 -235
- package/latticeai/core/file_generation/orchestration.py +0 -152
- package/latticeai/core/file_generation/prompting.py +0 -117
- package/latticeai/core/file_generation/repair.py +0 -114
- package/latticeai/core/file_generation/sanitize.py +0 -61
- package/latticeai/core/file_generation/validation.py +0 -201
- package/latticeai/core/invitations.py +0 -132
- package/latticeai/core/legacy_compatibility.py +0 -243
- package/latticeai/core/logging_safety.py +0 -46
- package/latticeai/core/marketplace.py +0 -293
- package/latticeai/core/mcp_catalog.py +0 -452
- package/latticeai/core/mcp_registry.py +0 -506
- package/latticeai/core/network_boundary.py +0 -168
- package/latticeai/core/oidc.py +0 -208
- package/latticeai/core/plugins.py +0 -432
- package/latticeai/core/product_hardening.py +0 -218
- package/latticeai/core/project_sessions.py +0 -337
- package/latticeai/core/realtime.py +0 -238
- package/latticeai/core/run_explain.py +0 -426
- package/latticeai/core/run_store.py +0 -252
- package/latticeai/core/timezones.py +0 -80
- package/latticeai/core/workspace_computer_memory.py +0 -84
- package/latticeai/core/workspace_graph_trace.py +0 -155
- package/latticeai/core/workspace_indexing.py +0 -102
- package/latticeai/core/workspace_memory.py +0 -77
- package/latticeai/core/workspace_onboarding.py +0 -104
- package/latticeai/core/workspace_os.py +0 -978
- package/latticeai/core/workspace_os_constants.py +0 -126
- package/latticeai/core/workspace_os_state.py +0 -180
- package/latticeai/core/workspace_os_utils.py +0 -103
- package/latticeai/core/workspace_permissions.py +0 -101
- package/latticeai/core/workspace_plugins.py +0 -97
- package/latticeai/core/workspace_relationships.py +0 -99
- package/latticeai/core/workspace_reorganization.py +0 -335
- package/latticeai/core/workspace_review_items.py +0 -112
- package/latticeai/core/workspace_runs.py +0 -726
- package/latticeai/core/workspace_skills.py +0 -109
- package/latticeai/core/workspace_snapshots.py +0 -198
- package/latticeai/core/workspace_timeline.py +0 -110
- package/latticeai/integrations/__init__.py +0 -0
- package/latticeai/integrations/telegram_bot/__init__.py +0 -123
- package/latticeai/integrations/telegram_bot/__main__.py +0 -17
- package/latticeai/integrations/telegram_bot/config.py +0 -86
- package/latticeai/integrations/telegram_bot/dispatch.py +0 -311
- package/latticeai/integrations/telegram_bot/flows.py +0 -478
- package/latticeai/integrations/telegram_bot/helpers.py +0 -322
- package/latticeai/integrations/telegram_bot/screens.py +0 -394
- package/latticeai/runtime/audit_runtime.py +0 -76
- package/latticeai/runtime/automation_runtime.py +0 -81
- package/latticeai/runtime/chat_wiring.py +0 -141
- package/latticeai/runtime/context_runtime.py +0 -66
- package/latticeai/runtime/feature_toggle_wiring.py +0 -157
- package/latticeai/runtime/history_runtime.py +0 -163
- package/latticeai/runtime/history_writer.py +0 -138
- package/latticeai/runtime/hooks_runtime.py +0 -77
- package/latticeai/runtime/model_wiring.py +0 -68
- package/latticeai/runtime/namespace_runtime.py +0 -163
- package/latticeai/runtime/network_boundary_wiring.py +0 -117
- package/latticeai/runtime/network_config_runtime.py +0 -56
- package/latticeai/runtime/permission_mode_wiring.py +0 -112
- package/latticeai/runtime/persistence_runtime.py +0 -159
- package/latticeai/runtime/platform_runtime_wiring.py +0 -89
- package/latticeai/runtime/review_wiring.py +0 -42
- package/latticeai/runtime/router_registration.py +0 -693
- package/latticeai/runtime/service_singletons.py +0 -55
- package/latticeai/runtime/sso_config_runtime.py +0 -128
- package/latticeai/runtime/user_key_runtime.py +0 -106
- package/latticeai/runtime/web_runtime.py +0 -92
- package/latticeai/server_app.py +0 -51
- package/latticeai/services/app_context.py +0 -130
- package/latticeai/services/automation_execution.py +0 -266
- package/latticeai/services/automation_intelligence.py +0 -614
- package/latticeai/services/brain_automation.py +0 -191
- package/latticeai/services/brain_intelligence/__init__.py +0 -58
- package/latticeai/services/brain_intelligence/_contract.py +0 -71
- package/latticeai/services/brain_intelligence/consistency.py +0 -193
- package/latticeai/services/brain_intelligence/constants.py +0 -47
- package/latticeai/services/brain_intelligence/digest.py +0 -258
- package/latticeai/services/brain_intelligence/health.py +0 -331
- package/latticeai/services/brain_intelligence/proposals.py +0 -259
- package/latticeai/services/brain_intelligence/sampling.py +0 -84
- package/latticeai/services/brain_intelligence/service.py +0 -48
- package/latticeai/services/change_proposals.py +0 -471
- package/latticeai/services/chat_service.py +0 -243
- package/latticeai/services/chronicle.py +0 -555
- package/latticeai/services/cloud_egress_audit.py +0 -85
- package/latticeai/services/cloud_extraction.py +0 -129
- package/latticeai/services/cloud_streaming.py +0 -268
- package/latticeai/services/cloud_token_guard.py +0 -84
- package/latticeai/services/command_center.py +0 -548
- package/latticeai/services/evidence_actions.py +0 -258
- package/latticeai/services/feature_toggles.py +0 -502
- package/latticeai/services/folder_watch.py +0 -520
- package/latticeai/services/funnel_metrics.py +0 -307
- package/latticeai/services/hybrid_chat.py +0 -316
- package/latticeai/services/hybrid_context.py +0 -228
- package/latticeai/services/hybrid_policy.py +0 -129
- package/latticeai/services/interop_bridges.py +0 -978
- package/latticeai/services/local_knowledge.py +0 -465
- package/latticeai/services/memory_service/__init__.py +0 -52
- package/latticeai/services/memory_service/_contract.py +0 -100
- package/latticeai/services/memory_service/brief.py +0 -431
- package/latticeai/services/memory_service/constants.py +0 -57
- package/latticeai/services/memory_service/maintenance.py +0 -138
- package/latticeai/services/memory_service/manager.py +0 -186
- package/latticeai/services/memory_service/proof.py +0 -136
- package/latticeai/services/memory_service/recall.py +0 -225
- package/latticeai/services/memory_service/service.py +0 -48
- package/latticeai/services/memory_service/stores.py +0 -110
- package/latticeai/services/mode_store.py +0 -132
- package/latticeai/services/model_recommendation.py +0 -224
- package/latticeai/services/model_runtime/cloud.py +0 -87
- package/latticeai/services/network_boundary_service.py +0 -117
- package/latticeai/services/obsidian_bridge.py +0 -609
- package/latticeai/services/openai_compatible_adapter.py +0 -101
- package/latticeai/services/permission_mode_service.py +0 -122
- package/latticeai/services/platform_runtime.py +0 -366
- package/latticeai/services/review_queue.py +0 -380
- package/latticeai/services/router_context.py +0 -59
- package/latticeai/services/run_executor.py +0 -387
- package/latticeai/services/self_model_service.py +0 -171
- package/latticeai/services/setup_detection.py +0 -147
- package/latticeai/services/triggers.py +0 -378
- package/latticeai/services/upload_service.py +0 -172
- package/latticeai/services/workspace_service.py +0 -165
- package/latticeai/setup/__init__.py +0 -25
- package/latticeai/setup/auto_setup.py +0 -846
- package/latticeai/setup/demo_corpus.py +0 -98
- package/latticeai/setup/wizard/__init__.py +0 -126
- package/latticeai/setup/wizard/catalog.py +0 -175
- package/latticeai/setup/wizard/detect.py +0 -302
- package/latticeai/setup/wizard/install.py +0 -348
- package/latticeai/setup/wizard/paths.py +0 -165
- package/latticeai/setup/wizard/plans.py +0 -74
- package/latticeai/setup/wizard/recommend.py +0 -320
- package/scripts/bench_agent_smoke.py +0 -409
- package/scripts/bench_models.py +0 -540
- package/scripts/bench_vector_index.py +0 -295
- package/scripts/funnel_soft_gate.py +0 -192
- package/scripts/generate_agent_loop_fixtures.py +0 -994
- package/scripts/generate_rust_parity_fixtures.py +0 -908
- package/scripts/migrate_brain_storage.py +0 -57
- package/scripts/parity_fixture_corpus_context.py +0 -162
- package/scripts/parity_fixture_corpus_docgen.py +0 -341
- package/scripts/profile_kg.py +0 -355
- package/server.py +0 -30
- package/static/app/assets/AdminConsole-cf4npybT.js +0 -1
- package/static/app/assets/Capture-DiQ219jW.js +0 -1
- package/static/app/assets/System-CieofHQa.js +0 -1
- package/static/app/assets/arrow-left-kfsrk0mv.js +0 -1
- package/static/app/assets/circle-check-qqLug9nU.js +0 -1
- package/static/app/assets/index-DxmOfNRi.css +0 -2
- package/static/app/assets/primitives-SNp0LRJz.js +0 -1
- 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("&", "&").replace("<", "<").replace(">", ">")
|
|
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
|
+
]
|