ltcai 11.5.1 → 11.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -117
- package/bin/ltcai.js +234 -24
- package/docs/CHANGELOG.md +129 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +8 -6
- package/docs/ENTERPRISE.md +2 -1
- package/docs/HYBRID_CLOUD_KG_STREAMING.md +8 -4
- 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/architecture.md +4 -4
- package/docs/kg-schema.md +1 -1
- package/docs/v11.5.2_TIGHT_SHIP_PLAN.md +144 -0
- 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 -597
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/agent_worker_seam.py +7 -80
- package/latticeai/api/health.py +26 -20
- package/latticeai/api/local_files.py +25 -608
- package/latticeai/api/models.py +12 -87
- package/latticeai/api/search.py +17 -231
- 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/cli/runtime.py +7 -3
- package/latticeai/core/agent_permission.py +16 -91
- package/latticeai/core/config.py +47 -3
- package/latticeai/core/csrf.py +28 -2
- package/latticeai/core/embedding_providers/__init__.py +3 -5
- package/latticeai/core/embedding_providers/base.py +1 -1
- package/latticeai/core/embedding_providers/text.py +1 -1
- package/latticeai/core/http_origin.py +146 -0
- package/latticeai/core/io_utils.py +2 -11
- package/latticeai/core/messages.py +94 -531
- package/latticeai/core/model_compat.py +2 -8
- package/latticeai/core/module_probe.py +37 -0
- package/latticeai/core/security.py +13 -0
- package/latticeai/core/sessions.py +3 -2
- package/latticeai/core/sse.py +25 -0
- package/latticeai/core/users.py +0 -9
- package/latticeai/models/router/documents.py +0 -35
- package/latticeai/models/router/registry.py +0 -4
- package/latticeai/runtime/access_runtime.py +16 -18
- 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 +101 -389
- package/latticeai/runtime/build_phases/foundation.py +82 -392
- package/latticeai/runtime/build_phases/web.py +91 -406
- 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 +14 -143
- package/latticeai/runtime/security_runtime.py +10 -103
- package/latticeai/services/architecture_readiness.py +82 -43
- package/latticeai/services/model_capability_registry.py +0 -15
- package/latticeai/services/model_catalog.py +0 -7
- package/latticeai/services/model_loading.py +3 -2
- package/latticeai/services/model_runtime/__init__.py +2 -68
- package/latticeai/services/model_runtime/engines.py +27 -128
- package/latticeai/services/model_runtime/loading.py +2 -2
- 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 -390
- 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 +3 -6
- 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 +19 -6
- package/scripts/check_current_release_docs.mjs +7 -4
- package/scripts/check_legacy_debt.mjs +1 -1
- 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 +31 -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 +223 -43
- package/src-tauri/src/main.rs +27 -17
- package/src-tauri/src/topology.rs +58 -88
- package/src-tauri/tauri.conf.json +2 -3
- package/static/app/asset-manifest.json +41 -41
- package/static/app/assets/{Act-Drs_jd-O.js → Act-CToZnOHz.js} +1 -1
- package/static/app/assets/{AdminConsole-BqNx5oF6.js → AdminConsole-D5kbrBu8.js} +1 -1
- package/static/app/assets/{Brain-C7bdNmz-.js → Brain-C6zCdv4S.js} +1 -1
- package/static/app/assets/{BrainHome-DcTnINcI.js → BrainHome-D4n3EDUt.js} +1 -1
- package/static/app/assets/{BrainSignals-Df1MRIPD.js → BrainSignals-CzeZ-vQ9.js} +1 -1
- package/static/app/assets/{Capture-Dgsxvnbx.js → Capture-DPcyTczq.js} +1 -1
- package/static/app/assets/{Chronicle-BrkTxJK1.js → Chronicle-D3Nx-oWS.js} +1 -1
- package/static/app/assets/{CommandPalette-DDziw7Oj.js → CommandPalette-DPPvpp28.js} +1 -1
- package/static/app/assets/{Library-CYxeuraA.js → Library-Co2qTu3I.js} +1 -1
- package/static/app/assets/{LivingBrain-CNp6mvCm.js → LivingBrain-Bsmzwy8_.js} +1 -1
- package/static/app/assets/{ProductFlow-BVsowu3Z.js → ProductFlow-CBLdmesT.js} +1 -1
- package/static/app/assets/{ReviewCard-D1LbOfJS.js → ReviewCard-CfR6DLs7.js} +1 -1
- package/static/app/assets/{System-DcRAq7wj.js → System-Ce-P2YL6.js} +1 -1
- package/static/app/assets/arrow-left-DD5jFGHV.js +1 -0
- package/static/app/assets/{bot-BK2xQ9mN.js → bot-C5-XIww2.js} +1 -1
- package/static/app/assets/{brain-BZNztFBb.js → brain-CFRJUvdX.js} +1 -1
- package/static/app/assets/{button-CjueubVZ.js → button-DI1KcQP-.js} +1 -1
- package/static/app/assets/circle-check-CY20XMCh.js +1 -0
- package/static/app/assets/{circle-pause-HRjSmpyY.js → circle-pause-C8HHyood.js} +1 -1
- package/static/app/assets/{circle-play-CMcKDVwu.js → circle-play-Cufc235E.js} +1 -1
- package/static/app/assets/{cpu-B_MyTfYr.js → cpu-DW2_JVsv.js} +1 -1
- package/static/app/assets/{download-CTy0EByV.js → download-gbdKTBb8.js} +1 -1
- package/static/app/assets/{folder-open-DF6bLhTg.js → folder-open-C9ydyX33.js} +1 -1
- package/static/app/assets/{hard-drive-CpWHG77C.js → hard-drive-BxNMzBM3.js} +1 -1
- package/static/app/assets/index-BovSWRmQ.css +2 -0
- package/static/app/assets/{index-Dd6abJHX.js → index-q9XyDY7A.js} +3 -3
- package/static/app/assets/{input-bVgIRN3s.js → input-GLD94Y_K.js} +1 -1
- package/static/app/assets/{link-2-bg6CG5Vk.js → link-2-B4Y0gz1S.js} +1 -1
- package/static/app/assets/{permissionCopy-DFTf1HDZ.js → permissionCopy-CxrV0fvU.js} +1 -1
- package/static/app/assets/{primitives-D7D-sQ3T.js → primitives-2ANdAsu2.js} +1 -1
- package/static/app/assets/search-DTxtjQ07.js +1 -0
- package/static/app/assets/{share-2-BLzo7U4L.js → share-2-DxArfiuI.js} +1 -1
- package/static/app/assets/{shield-alert-BmngqnsJ.js → shield-alert-CgftuFFs.js} +1 -1
- package/static/app/assets/{textarea-DOSlAQQ3.js → textarea-BHVBAoCr.js} +1 -1
- package/static/app/assets/{useFocusTrap-pZhgeee4.js → useFocusTrap-Dzq2j_OG.js} +1 -1
- package/static/app/assets/{useMutation-BMDwNk4I.js → useMutation-BcqfMLNq.js} +1 -1
- package/static/app/assets/{useQuery-CSjKttRo.js → useQuery-DBzLKCOv.js} +1 -1
- package/static/app/assets/{utils-D3u_yv7B.js → utils-B-Aah1bO.js} +1 -1
- package/static/app/assets/{workspace-H3bMjYXC.js → workspace-B6fFhoko.js} +1 -1
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/desktop/electron/README.md +0 -9
- package/desktop/electron/main.cjs +0 -58
- package/desktop/electron/preload.cjs +0 -5
- 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 -546
- 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 -170
- package/lattice_brain/graph/vector_index/brute_force.py +0 -114
- package/lattice_brain/graph/vector_index/hnsw.py +0 -293
- package/lattice_brain/graph/vector_index/jobs.py +0 -287
- package/lattice_brain/graph/vector_index/quantized.py +0 -151
- 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 -383
- package/lattice_brain/memory.py +0 -102
- 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 -714
- package/lattice_brain/quality.py +0 -592
- 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 -244
- 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 -360
- 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 -901
- package/latticeai/api/chat_contracts.py +0 -73
- package/latticeai/api/chat_documents.py +0 -276
- package/latticeai/api/chat_helpers.py +0 -463
- 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 -485
- 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 -410
- 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 -351
- 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 -157
- 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/graph_curator.py +0 -11
- package/latticeai/core/hooks.py +0 -11
- package/latticeai/core/invitations.py +0 -131
- package/latticeai/core/legacy_compatibility.py +0 -243
- package/latticeai/core/local_embeddings.py +0 -104
- 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/multi_agent.py +0 -11
- 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 -423
- package/latticeai/core/run_store.py +0 -252
- package/latticeai/core/timezones.py +0 -80
- package/latticeai/core/workflow_engine.py +0 -11
- package/latticeai/core/workspace_computer_memory.py +0 -84
- package/latticeai/core/workspace_graph_trace.py +0 -161
- 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 -988
- 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 -69
- 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 -118
- package/latticeai/core/workspace_runs.py +0 -732
- package/latticeai/core/workspace_skills.py +0 -114
- 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 -163
- 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 -128
- package/latticeai/runtime/network_config_runtime.py +0 -56
- package/latticeai/runtime/permission_mode_wiring.py +0 -119
- 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 -689
- 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 -264
- 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 -505
- package/latticeai/services/chat_service.py +0 -243
- package/latticeai/services/chronicle.py +0 -557
- 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 -255
- 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 -178
- package/latticeai/services/ingestion.py +0 -11
- package/latticeai/services/interop_bridges.py +0 -978
- package/latticeai/services/kg_portability.py +0 -11
- 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/model_recommendation.py +0 -224
- package/latticeai/services/model_runtime/cloud.py +0 -87
- package/latticeai/services/multimodal_streaming.py +0 -129
- package/latticeai/services/network_boundary_service.py +0 -154
- 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 -170
- 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 -81
- package/latticeai/services/triggers.py +0 -378
- package/latticeai/services/upload_service.py +0 -185
- package/latticeai/services/workspace_service.py +0 -171
- package/latticeai/setup/__init__.py +0 -25
- package/latticeai/setup/auto_setup.py +0 -857
- package/latticeai/setup/demo_corpus.py +0 -98
- package/latticeai/setup/wizard/__init__.py +0 -126
- package/latticeai/setup/wizard/catalog.py +0 -172
- package/latticeai/setup/wizard/detect.py +0 -323
- package/latticeai/setup/wizard/install.py +0 -348
- package/latticeai/setup/wizard/paths.py +0 -168
- 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 -907
- package/scripts/generate_rust_parity_fixtures.py +0 -1000
- package/scripts/measure_brain_home_fill.mjs +0 -141
- package/scripts/migrate_brain_storage.py +0 -57
- 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/arrow-left-BHYmOTWU.js +0 -1
- package/static/app/assets/circle-check-CLeWJr67.js +0 -1
- package/static/app/assets/index-DxmOfNRi.css +0 -2
- package/static/app/assets/search-BgSVX6NG.js +0 -1
|
@@ -1,378 +0,0 @@
|
|
|
1
|
-
"""Trigger system (T7d) — workflows fire beyond 'manual'.
|
|
2
|
-
|
|
3
|
-
Two real trigger types:
|
|
4
|
-
|
|
5
|
-
* **interval** — a supervised scheduler loop fires the workflow every
|
|
6
|
-
``interval_seconds``. Firings missed while the server was down are
|
|
7
|
-
SKIPPED with a recorded skip event (design-review amendment: no silent
|
|
8
|
-
gaps, no thundering catch-up).
|
|
9
|
-
* **brain_event** — the killer Digital Brain feature: "when new knowledge
|
|
10
|
-
enters the brain, run this workflow". Wired through the existing hooks
|
|
11
|
-
bus (the ingestion pipeline fires ``post_tool`` on ``kg_ingest.<source>``);
|
|
12
|
-
an optional ``source_type`` filter narrows it.
|
|
13
|
-
|
|
14
|
-
Trigger-fired runs carry provenance: their inputs include ``__trigger__``
|
|
15
|
-
describing what fired them, persisted in the run record like any other
|
|
16
|
-
input.
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
from __future__ import annotations
|
|
20
|
-
|
|
21
|
-
import json
|
|
22
|
-
import logging
|
|
23
|
-
import os
|
|
24
|
-
import threading
|
|
25
|
-
import time
|
|
26
|
-
from pathlib import Path
|
|
27
|
-
from typing import Any, Callable, Dict, List, Optional
|
|
28
|
-
|
|
29
|
-
try:
|
|
30
|
-
from zoneinfo import ZoneInfo
|
|
31
|
-
except Exception: # pragma: no cover
|
|
32
|
-
ZoneInfo = None # type: ignore
|
|
33
|
-
|
|
34
|
-
DEFAULT_TICK_SECONDS = 5.0
|
|
35
|
-
MIN_INTERVAL_SECONDS = 60
|
|
36
|
-
|
|
37
|
-
TRIGGER_HOOK_NAME = "brain-event-triggers"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
class TriggerService:
|
|
41
|
-
"""Scans workflow definitions for non-manual triggers and fires them."""
|
|
42
|
-
|
|
43
|
-
def __init__(
|
|
44
|
-
self,
|
|
45
|
-
*,
|
|
46
|
-
store: Any,
|
|
47
|
-
run_workflow: Callable[[str, Dict[str, Any]], Dict[str, Any]],
|
|
48
|
-
data_dir: Path,
|
|
49
|
-
clock: Callable[[], float] = time.time,
|
|
50
|
-
tick_seconds: float = DEFAULT_TICK_SECONDS,
|
|
51
|
-
review_sink: Optional[Any] = None,
|
|
52
|
-
tz_name: Optional[str] = None,
|
|
53
|
-
) -> None:
|
|
54
|
-
self._store = store
|
|
55
|
-
self._run_workflow = run_workflow
|
|
56
|
-
# Optional review-queue seam (5.6.0). When wired, fired runs can drop a
|
|
57
|
-
# reviewable suggestion into the queue. Default None keeps legacy behavior.
|
|
58
|
-
self._review_sink = review_sink
|
|
59
|
-
self._state_file = Path(data_dir) / "triggers_state.json"
|
|
60
|
-
self._clock = clock
|
|
61
|
-
self._tick = float(tick_seconds)
|
|
62
|
-
self._stop_event = threading.Event()
|
|
63
|
-
self._thread: Optional[threading.Thread] = None
|
|
64
|
-
self._lock = threading.Lock()
|
|
65
|
-
# LATTICE_TZ: wall-clock / display 용. interval 계산은 여전히 unix seconds (duration 기반, drift 방지).
|
|
66
|
-
# describe()와 이벤트에 tz 정보 노출. calendar "daily at HH:MM" semantics 는 추후 cron 확장 시 사용.
|
|
67
|
-
# Prefer explicit tz_name (from Config) for DI; fall back to env for legacy/compat.
|
|
68
|
-
self._tz_name = (tz_name or os.environ.get("LATTICE_TZ") or "UTC").strip() or "UTC"
|
|
69
|
-
self._tz = None
|
|
70
|
-
if ZoneInfo is not None:
|
|
71
|
-
try:
|
|
72
|
-
self._tz = ZoneInfo(self._tz_name)
|
|
73
|
-
except Exception:
|
|
74
|
-
self._tz = ZoneInfo("UTC")
|
|
75
|
-
|
|
76
|
-
# ── durable state ──────────────────────────────────────────────────────
|
|
77
|
-
def _load_state(self) -> Dict[str, Any]:
|
|
78
|
-
if not self._state_file.exists():
|
|
79
|
-
return {}
|
|
80
|
-
try:
|
|
81
|
-
return json.loads(self._state_file.read_text(encoding="utf-8"))
|
|
82
|
-
except Exception:
|
|
83
|
-
return {}
|
|
84
|
-
|
|
85
|
-
def _save_state(self, state: Dict[str, Any]) -> None:
|
|
86
|
-
self._state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
87
|
-
tmp = self._state_file.with_suffix(".tmp")
|
|
88
|
-
tmp.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
89
|
-
tmp.replace(self._state_file)
|
|
90
|
-
|
|
91
|
-
def _record_event(self, state: Dict[str, Any], workflow_id: str, event: Dict[str, Any]) -> None:
|
|
92
|
-
entry = state.setdefault(workflow_id, {})
|
|
93
|
-
events = entry.setdefault("events", [])
|
|
94
|
-
events.append({**event, "at": self._clock()})
|
|
95
|
-
entry["events"] = events[-50:]
|
|
96
|
-
|
|
97
|
-
# ── definition scanning ────────────────────────────────────────────────
|
|
98
|
-
def _triggered_workflows(self) -> List[Dict[str, Any]]:
|
|
99
|
-
found = []
|
|
100
|
-
try:
|
|
101
|
-
workflows = list(self._store.load_state().get("workflows") or [])
|
|
102
|
-
except Exception:
|
|
103
|
-
return []
|
|
104
|
-
for wf in workflows:
|
|
105
|
-
for node in wf.get("nodes") or []:
|
|
106
|
-
if node.get("type") != "trigger":
|
|
107
|
-
continue
|
|
108
|
-
cfg = node.get("config") or {}
|
|
109
|
-
kind = str(cfg.get("trigger") or "manual")
|
|
110
|
-
if cfg.get("enabled") is False:
|
|
111
|
-
continue
|
|
112
|
-
if kind in ("interval", "brain_event"):
|
|
113
|
-
found.append({"workflow": wf, "node": node, "kind": kind, "config": cfg})
|
|
114
|
-
return found
|
|
115
|
-
|
|
116
|
-
def describe(self) -> Dict[str, Any]:
|
|
117
|
-
"""Honest status surface: what is armed, when it last fired/skipped.
|
|
118
|
-
Includes LATTICE_TZ, per-trigger status (armed|degraded), consecutive_failures.
|
|
119
|
-
"""
|
|
120
|
-
state = self._load_state()
|
|
121
|
-
armed = []
|
|
122
|
-
for item in self._triggered_workflows():
|
|
123
|
-
wf_id = item["workflow"].get("id")
|
|
124
|
-
entry = state.get(wf_id) or {}
|
|
125
|
-
fails = int(entry.get("consecutive_failures", 0))
|
|
126
|
-
status = "degraded" if fails >= 3 else "armed"
|
|
127
|
-
armed.append({
|
|
128
|
-
"workflow_id": wf_id,
|
|
129
|
-
"name": item["workflow"].get("name"),
|
|
130
|
-
"kind": item["kind"],
|
|
131
|
-
"config": {k: v for k, v in item["config"].items() if k != "trigger"},
|
|
132
|
-
"last_fired_at": entry.get("last_fired_at"),
|
|
133
|
-
"status": status,
|
|
134
|
-
"consecutive_failures": fails,
|
|
135
|
-
"recent_events": entry.get("events", [])[-5:],
|
|
136
|
-
})
|
|
137
|
-
return {
|
|
138
|
-
"running": bool(self._thread and self._thread.is_alive()),
|
|
139
|
-
"tick_seconds": self._tick,
|
|
140
|
-
"tz": self._tz_name,
|
|
141
|
-
"armed": armed,
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
# ── interval scheduling ────────────────────────────────────────────────
|
|
145
|
-
def reconcile_missed(self) -> int:
|
|
146
|
-
"""Startup pass: record (never replay) firings missed while down."""
|
|
147
|
-
now = self._clock()
|
|
148
|
-
skipped = 0
|
|
149
|
-
with self._lock:
|
|
150
|
-
state = self._load_state()
|
|
151
|
-
for item in self._triggered_workflows():
|
|
152
|
-
if item["kind"] != "interval":
|
|
153
|
-
continue
|
|
154
|
-
wf_id = item["workflow"].get("id")
|
|
155
|
-
interval = max(MIN_INTERVAL_SECONDS, int(item["config"].get("interval_seconds") or 0))
|
|
156
|
-
entry = state.setdefault(wf_id, {})
|
|
157
|
-
last = entry.get("last_fired_at")
|
|
158
|
-
if last is not None and now - float(last) > interval:
|
|
159
|
-
missed = int((now - float(last)) // interval)
|
|
160
|
-
self._record_event(state, wf_id, {
|
|
161
|
-
"type": "skipped",
|
|
162
|
-
"reason": f"{missed} interval firing(s) missed while the server was down",
|
|
163
|
-
})
|
|
164
|
-
skipped += missed
|
|
165
|
-
# Reset the cadence from now — no catch-up storm.
|
|
166
|
-
entry["last_fired_at"] = now if last is not None else entry.get("last_fired_at")
|
|
167
|
-
entry["last_attempt_at"] = now
|
|
168
|
-
self._save_state(state)
|
|
169
|
-
return skipped
|
|
170
|
-
|
|
171
|
-
def tick_intervals(self) -> int:
|
|
172
|
-
"""One scheduler pass; returns how many workflows fired."""
|
|
173
|
-
now = self._clock()
|
|
174
|
-
fired = 0
|
|
175
|
-
with self._lock:
|
|
176
|
-
state = self._load_state()
|
|
177
|
-
for item in self._triggered_workflows():
|
|
178
|
-
if item["kind"] != "interval":
|
|
179
|
-
continue
|
|
180
|
-
wf_id = item["workflow"].get("id")
|
|
181
|
-
interval = max(MIN_INTERVAL_SECONDS, int(item["config"].get("interval_seconds") or 0))
|
|
182
|
-
entry = state.setdefault(wf_id, {})
|
|
183
|
-
last = entry.get("last_fired_at")
|
|
184
|
-
if last is None:
|
|
185
|
-
# First sighting arms the schedule; it fires one interval later.
|
|
186
|
-
entry["last_fired_at"] = now
|
|
187
|
-
entry["last_attempt_at"] = now
|
|
188
|
-
continue
|
|
189
|
-
if now - float(last) < interval:
|
|
190
|
-
continue
|
|
191
|
-
# Dedup guard (edge case): short cooldown + last_attempt prevents rapid re-fire on
|
|
192
|
-
# clock skew, tick jitter, or restart races. Interval 자체 + 이 가드로 중복 실행 방지.
|
|
193
|
-
last_attempt = float(entry.get("last_attempt_at") or 0)
|
|
194
|
-
if now - last_attempt < 10:
|
|
195
|
-
continue
|
|
196
|
-
entry["last_attempt_at"] = now
|
|
197
|
-
entry["last_fired_at"] = now
|
|
198
|
-
self._record_event(state, wf_id, {"type": "fired", "trigger": "interval"})
|
|
199
|
-
fired += 1
|
|
200
|
-
self._fire(wf_id, {
|
|
201
|
-
"type": "interval",
|
|
202
|
-
"interval_seconds": interval,
|
|
203
|
-
"fired_at": now,
|
|
204
|
-
})
|
|
205
|
-
self._save_state(state)
|
|
206
|
-
return fired
|
|
207
|
-
|
|
208
|
-
# ── brain events ───────────────────────────────────────────────────────
|
|
209
|
-
def on_brain_event(self, event: str, payload: Optional[Dict[str, Any]] = None) -> int:
|
|
210
|
-
"""Fire workflows whose brain_event trigger matches this ingestion."""
|
|
211
|
-
payload = payload or {}
|
|
212
|
-
source_type = str(payload.get("source_type") or _ingestion_source_type(event, payload) or "")
|
|
213
|
-
event_workspace = str(payload.get("workspace_id") or "personal").strip()
|
|
214
|
-
event_user = str(payload.get("user_email") or "").strip().lower()
|
|
215
|
-
fired = 0
|
|
216
|
-
with self._lock:
|
|
217
|
-
state = self._load_state()
|
|
218
|
-
for item in self._triggered_workflows():
|
|
219
|
-
if item["kind"] != "brain_event":
|
|
220
|
-
continue
|
|
221
|
-
workflow_workspace = str(item["workflow"].get("workspace_id") or "personal").strip()
|
|
222
|
-
workflow_user = str(item["workflow"].get("user_email") or "").strip().lower()
|
|
223
|
-
if event_workspace != workflow_workspace:
|
|
224
|
-
continue
|
|
225
|
-
# Owned workflows only respond to their owner's ingestion.
|
|
226
|
-
# Ownerless legacy workflows remain compatible in personal.
|
|
227
|
-
if workflow_user and workflow_user != event_user:
|
|
228
|
-
continue
|
|
229
|
-
wanted = str(item["config"].get("source_type") or "").strip()
|
|
230
|
-
if wanted and wanted != source_type:
|
|
231
|
-
continue
|
|
232
|
-
wf_id = item["workflow"].get("id")
|
|
233
|
-
entry = state.setdefault(wf_id, {})
|
|
234
|
-
now = self._clock()
|
|
235
|
-
# Dedup guard for brain_event too (rapid ingest burst 등).
|
|
236
|
-
last_attempt = float(entry.get("last_attempt_at") or 0)
|
|
237
|
-
if now - last_attempt < 5:
|
|
238
|
-
continue
|
|
239
|
-
entry["last_attempt_at"] = now
|
|
240
|
-
entry["last_fired_at"] = now
|
|
241
|
-
self._record_event(state, wf_id, {
|
|
242
|
-
"type": "fired", "trigger": "brain_event", "source_type": source_type,
|
|
243
|
-
})
|
|
244
|
-
fired += 1
|
|
245
|
-
self._fire(wf_id, {
|
|
246
|
-
"type": "brain_event",
|
|
247
|
-
"event": event,
|
|
248
|
-
"source_type": source_type,
|
|
249
|
-
"node_id": payload.get("node_id"),
|
|
250
|
-
"user_email": payload.get("user_email"),
|
|
251
|
-
"workspace_id": payload.get("workspace_id"),
|
|
252
|
-
})
|
|
253
|
-
self._save_state(state)
|
|
254
|
-
return fired
|
|
255
|
-
|
|
256
|
-
def hook_runner(self):
|
|
257
|
-
"""A post_tool hook runner: ingestion events fan into triggers."""
|
|
258
|
-
def runner(context):
|
|
259
|
-
event = str(getattr(context, "event", "") or "")
|
|
260
|
-
payload = context.payload if isinstance(context.payload, dict) else {}
|
|
261
|
-
source_type = _ingestion_source_type(event, payload)
|
|
262
|
-
if not source_type:
|
|
263
|
-
return {"status": "ok", "output": "not an ingestion event"}
|
|
264
|
-
if str(payload.get("status") or "ok") != "ok":
|
|
265
|
-
return {"status": "ok", "output": "ignored failed ingestion event"}
|
|
266
|
-
fired = self.on_brain_event(
|
|
267
|
-
f"kg_ingest.{source_type}",
|
|
268
|
-
{
|
|
269
|
-
**payload,
|
|
270
|
-
"source_type": source_type,
|
|
271
|
-
"user_email": getattr(context, "user_email", None) or payload.get("user_email"),
|
|
272
|
-
"workspace_id": getattr(context, "workspace_id", None) or payload.get("workspace_id"),
|
|
273
|
-
},
|
|
274
|
-
)
|
|
275
|
-
return {"status": "ok", "output": f"fired {fired} workflow trigger(s)"}
|
|
276
|
-
return runner
|
|
277
|
-
|
|
278
|
-
# ── execution + lifecycle ──────────────────────────────────────────────
|
|
279
|
-
def _fire(self, workflow_id: str, trigger_info: Dict[str, Any]) -> None:
|
|
280
|
-
try:
|
|
281
|
-
workflows = list(self._store.load_state().get("workflows") or [])
|
|
282
|
-
workflow = next((wf for wf in workflows if wf.get("id") == workflow_id), None) or {}
|
|
283
|
-
except Exception:
|
|
284
|
-
workflow = {}
|
|
285
|
-
trigger_info = dict(trigger_info)
|
|
286
|
-
if not trigger_info.get("user_email"):
|
|
287
|
-
trigger_info["user_email"] = workflow.get("user_email")
|
|
288
|
-
if not trigger_info.get("workspace_id"):
|
|
289
|
-
trigger_info["workspace_id"] = workflow.get("workspace_id")
|
|
290
|
-
|
|
291
|
-
def _run():
|
|
292
|
-
try:
|
|
293
|
-
result = self._run_workflow(workflow_id, {"__trigger__": trigger_info})
|
|
294
|
-
self._record_fire_outcome(workflow_id, ok=True)
|
|
295
|
-
self._maybe_enqueue_review(workflow_id, trigger_info, result)
|
|
296
|
-
except Exception as exc:
|
|
297
|
-
logging.warning("trigger run failed for %s: %s", workflow_id, exc)
|
|
298
|
-
self._record_fire_outcome(workflow_id, ok=False, detail=str(exc))
|
|
299
|
-
|
|
300
|
-
threading.Thread(target=_run, name=f"trigger-{workflow_id}", daemon=True).start()
|
|
301
|
-
|
|
302
|
-
def _maybe_enqueue_review(
|
|
303
|
-
self, workflow_id: str, trigger_info: Dict[str, Any], result: Dict[str, Any],
|
|
304
|
-
) -> None:
|
|
305
|
-
if self._review_sink is None:
|
|
306
|
-
return
|
|
307
|
-
try:
|
|
308
|
-
from latticeai.services.review_queue import enqueue_from_automation
|
|
309
|
-
|
|
310
|
-
workflows = list(self._store.load_state().get("workflows") or [])
|
|
311
|
-
workflow = next((wf for wf in workflows if wf.get("id") == workflow_id), None)
|
|
312
|
-
if workflow is None:
|
|
313
|
-
return
|
|
314
|
-
trigger_type = str(trigger_info.get("type") or "")
|
|
315
|
-
source = "kg_change_digest" if trigger_type == "brain_event" else "trigger"
|
|
316
|
-
enqueue_from_automation(
|
|
317
|
-
self._review_sink,
|
|
318
|
-
workflow=workflow,
|
|
319
|
-
source=source,
|
|
320
|
-
run_result=result,
|
|
321
|
-
trigger_info=trigger_info,
|
|
322
|
-
user_email=trigger_info.get("user_email") or workflow.get("user_email"),
|
|
323
|
-
workspace_id=trigger_info.get("workspace_id") or workflow.get("workspace_id"),
|
|
324
|
-
)
|
|
325
|
-
except Exception as exc:
|
|
326
|
-
logging.warning("review_sink enqueue failed for %s: %s", workflow_id, exc)
|
|
327
|
-
|
|
328
|
-
def _record_fire_outcome(self, wf_id: str, *, ok: bool, detail: str = "") -> None:
|
|
329
|
-
"""Track consecutive launch failures for degraded status in describe().
|
|
330
|
-
(Deep execution failures are visible via workflow run records; 여기서는 scheduler fire 자체 실패를 카운트.)
|
|
331
|
-
"""
|
|
332
|
-
with self._lock:
|
|
333
|
-
state = self._load_state()
|
|
334
|
-
entry = state.setdefault(wf_id, {})
|
|
335
|
-
if ok:
|
|
336
|
-
entry["consecutive_failures"] = 0
|
|
337
|
-
else:
|
|
338
|
-
fails = int(entry.get("consecutive_failures", 0)) + 1
|
|
339
|
-
entry["consecutive_failures"] = fails
|
|
340
|
-
self._record_event(state, wf_id, {"type": "failed", "detail": detail[:200]})
|
|
341
|
-
self._save_state(state)
|
|
342
|
-
|
|
343
|
-
def start(self) -> None:
|
|
344
|
-
if self._thread and self._thread.is_alive():
|
|
345
|
-
return
|
|
346
|
-
self.reconcile_missed()
|
|
347
|
-
self._stop_event.clear()
|
|
348
|
-
|
|
349
|
-
def _loop():
|
|
350
|
-
while not self._stop_event.wait(self._tick):
|
|
351
|
-
try:
|
|
352
|
-
self.tick_intervals()
|
|
353
|
-
except Exception as exc:
|
|
354
|
-
logging.warning("trigger scheduler tick failed: %s", exc)
|
|
355
|
-
|
|
356
|
-
self._thread = threading.Thread(target=_loop, name="trigger-scheduler", daemon=True)
|
|
357
|
-
self._thread.start()
|
|
358
|
-
|
|
359
|
-
def stop(self) -> None:
|
|
360
|
-
self._stop_event.set()
|
|
361
|
-
if self._thread:
|
|
362
|
-
self._thread.join(timeout=2)
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
def _ingestion_source_type(event: str, payload: Dict[str, Any]) -> str:
|
|
366
|
-
"""Normalize the real dispatch event (``tool.kg_ingest.*``) and legacy form."""
|
|
367
|
-
tool = str(payload.get("tool") or "").strip()
|
|
368
|
-
if tool.startswith("kg_ingest."):
|
|
369
|
-
return tool.removeprefix("kg_ingest.").strip()
|
|
370
|
-
normalized = str(event or "").strip()
|
|
371
|
-
if normalized.startswith("tool.kg_ingest."):
|
|
372
|
-
return normalized.removeprefix("tool.kg_ingest.").strip()
|
|
373
|
-
if normalized.startswith("kg_ingest."):
|
|
374
|
-
return normalized.removeprefix("kg_ingest.").strip()
|
|
375
|
-
return ""
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
__all__ = ["TriggerService", "TRIGGER_HOOK_NAME", "MIN_INTERVAL_SECONDS"]
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
"""Document upload parsing, safety checks, and knowledge-graph ingestion."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import logging
|
|
6
|
-
import tempfile
|
|
7
|
-
from datetime import datetime
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from typing import Optional
|
|
10
|
-
|
|
11
|
-
from fastapi import HTTPException, Request, UploadFile
|
|
12
|
-
|
|
13
|
-
from lattice_brain.ingestion import IngestionItem
|
|
14
|
-
from latticeai.core.quiet import quiet
|
|
15
|
-
from latticeai.tools import ToolError, read_document
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _workspace_scope_from_request(request: Request) -> Optional[str]:
|
|
19
|
-
header = request.headers.get("X-Workspace-Id")
|
|
20
|
-
if header and header.strip():
|
|
21
|
-
return header.strip()
|
|
22
|
-
query = request.query_params.get("workspace_id")
|
|
23
|
-
return query.strip() if query and query.strip() else None
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
async def process_uploaded_document(
|
|
27
|
-
*,
|
|
28
|
-
request: Request,
|
|
29
|
-
file: UploadFile,
|
|
30
|
-
current_user: str,
|
|
31
|
-
enable_graph: bool,
|
|
32
|
-
knowledge_graph,
|
|
33
|
-
ingestion_pipeline=None,
|
|
34
|
-
bytes_match_extension,
|
|
35
|
-
classify_sensitive_message,
|
|
36
|
-
append_audit_event,
|
|
37
|
-
enforce_rate_limit,
|
|
38
|
-
hooks=None,
|
|
39
|
-
workspace_service=None,
|
|
40
|
-
) -> dict:
|
|
41
|
-
enforce_rate_limit(current_user, "upload")
|
|
42
|
-
requested_workspace = _workspace_scope_from_request(request)
|
|
43
|
-
if workspace_service is not None:
|
|
44
|
-
try:
|
|
45
|
-
workspace_id = workspace_service.resolve_write_scope(
|
|
46
|
-
requested_workspace,
|
|
47
|
-
current_user or None,
|
|
48
|
-
)
|
|
49
|
-
except PermissionError as exc:
|
|
50
|
-
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
|
51
|
-
else:
|
|
52
|
-
workspace_id = requested_workspace
|
|
53
|
-
suffix = Path(file.filename or "upload").suffix.lower()
|
|
54
|
-
allowed = {".pdf", ".docx", ".xlsx", ".pptx", ".txt", ".md", ".csv"}
|
|
55
|
-
if suffix not in allowed:
|
|
56
|
-
raise HTTPException(status_code=400, detail=f"지원하지 않는 형식: {suffix}")
|
|
57
|
-
|
|
58
|
-
contents = await file.read()
|
|
59
|
-
if len(contents) > 10 * 1024 * 1024:
|
|
60
|
-
raise HTTPException(status_code=400, detail="파일이 너무 큽니다. 최대 10MB.")
|
|
61
|
-
if not bytes_match_extension(contents, suffix):
|
|
62
|
-
raise HTTPException(status_code=400, detail=f"파일 내용이 확장자({suffix})와 일치하지 않습니다.")
|
|
63
|
-
|
|
64
|
-
# ── pre_upload hook ── may gate the upload before any work happens.
|
|
65
|
-
if hooks is not None:
|
|
66
|
-
pre_up = hooks.fire_hook(
|
|
67
|
-
"pre_upload", "document.upload",
|
|
68
|
-
payload={"filename": file.filename, "ext": suffix, "bytes": len(contents)},
|
|
69
|
-
user_email=current_user,
|
|
70
|
-
)
|
|
71
|
-
if pre_up.get("blocked"):
|
|
72
|
-
raise HTTPException(status_code=403, detail=pre_up.get("block_reason") or "Upload blocked by a pre_upload hook.")
|
|
73
|
-
|
|
74
|
-
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
|
|
75
|
-
tmp.write(contents)
|
|
76
|
-
tmp_path = tmp.name
|
|
77
|
-
|
|
78
|
-
try:
|
|
79
|
-
result = read_document(tmp_path)
|
|
80
|
-
sensitive = classify_sensitive_message(
|
|
81
|
-
{
|
|
82
|
-
"role": "document",
|
|
83
|
-
"content": result.get("content") or result.get("preview") or "",
|
|
84
|
-
"user_email": current_user,
|
|
85
|
-
"timestamp": datetime.now().isoformat(),
|
|
86
|
-
},
|
|
87
|
-
-1,
|
|
88
|
-
)
|
|
89
|
-
# ── pre_index / post_index hooks bracket the KG ingest (chunk → embed →
|
|
90
|
-
# graph-build), the actual indexing step.
|
|
91
|
-
if hooks is not None:
|
|
92
|
-
hooks.fire_hook("pre_index", "document.index",
|
|
93
|
-
payload={"filename": file.filename, "chars": result.get("chars")},
|
|
94
|
-
user_email=current_user)
|
|
95
|
-
try:
|
|
96
|
-
if not (enable_graph and knowledge_graph):
|
|
97
|
-
raise RuntimeError("graph disabled")
|
|
98
|
-
if ingestion_pipeline is not None:
|
|
99
|
-
# v4: uploads enter the brain through the unified ingestion
|
|
100
|
-
# pipeline (provenance + kg_ingest hook lifecycle).
|
|
101
|
-
ingest = ingestion_pipeline.ingest(
|
|
102
|
-
IngestionItem(
|
|
103
|
-
source_type="upload",
|
|
104
|
-
title=file.filename,
|
|
105
|
-
path=tmp_path,
|
|
106
|
-
mime_type=file.content_type,
|
|
107
|
-
owner=current_user,
|
|
108
|
-
workspace_id=workspace_id,
|
|
109
|
-
conversation_id=request.query_params.get("conversation_id"),
|
|
110
|
-
metadata={"extracted": result},
|
|
111
|
-
),
|
|
112
|
-
user_email=current_user,
|
|
113
|
-
)
|
|
114
|
-
if ingest.status != "ok":
|
|
115
|
-
raise RuntimeError(ingest.detail or f"ingestion {ingest.status}")
|
|
116
|
-
result["knowledge_graph"] = {
|
|
117
|
-
"node_id": ingest.node_id,
|
|
118
|
-
"sha256": ingest.content_hash,
|
|
119
|
-
"provenance_id": ingest.provenance_id,
|
|
120
|
-
}
|
|
121
|
-
else:
|
|
122
|
-
graph_result = knowledge_graph.ingest_document(
|
|
123
|
-
Path(tmp_path),
|
|
124
|
-
original_filename=file.filename,
|
|
125
|
-
mime_type=file.content_type,
|
|
126
|
-
uploader=current_user,
|
|
127
|
-
workspace_id=workspace_id,
|
|
128
|
-
conversation_id=request.query_params.get("conversation_id"),
|
|
129
|
-
extracted=result,
|
|
130
|
-
)
|
|
131
|
-
result["knowledge_graph"] = {
|
|
132
|
-
"node_id": graph_result["node_id"],
|
|
133
|
-
"sha256": graph_result["sha256"],
|
|
134
|
-
}
|
|
135
|
-
except Exception as graph_error:
|
|
136
|
-
logging.warning("knowledge graph document ingest failed: %s", graph_error)
|
|
137
|
-
result["knowledge_graph"] = {"error": str(graph_error)}
|
|
138
|
-
if hooks is not None:
|
|
139
|
-
_kg = result.get("knowledge_graph") or {}
|
|
140
|
-
hooks.fire_hook("post_index", "document.index",
|
|
141
|
-
payload={"filename": file.filename, "graph_node": _kg.get("node_id"),
|
|
142
|
-
"indexed": bool(_kg.get("node_id")), "error": _kg.get("error")},
|
|
143
|
-
user_email=current_user)
|
|
144
|
-
|
|
145
|
-
append_audit_event(
|
|
146
|
-
"document_upload",
|
|
147
|
-
user_email=current_user,
|
|
148
|
-
conversation_id=request.query_params.get("conversation_id"),
|
|
149
|
-
filename=file.filename,
|
|
150
|
-
mime_type=file.content_type,
|
|
151
|
-
ext=suffix,
|
|
152
|
-
bytes=len(contents),
|
|
153
|
-
extracted_chars=result.get("chars"),
|
|
154
|
-
graph_node=(result.get("knowledge_graph") or {}).get("node_id"),
|
|
155
|
-
content_preview=sensitive.get("preview"),
|
|
156
|
-
sensitivity=sensitive.get("sensitivity"),
|
|
157
|
-
sensitive_labels=sensitive.get("labels") or [],
|
|
158
|
-
)
|
|
159
|
-
except ToolError as exc:
|
|
160
|
-
raise HTTPException(status_code=400, detail=str(exc))
|
|
161
|
-
finally:
|
|
162
|
-
try:
|
|
163
|
-
Path(tmp_path).unlink()
|
|
164
|
-
except OSError:
|
|
165
|
-
quiet()
|
|
166
|
-
|
|
167
|
-
# ── post_upload hook ── the whole upload → parse → index pipeline finished.
|
|
168
|
-
if hooks is not None:
|
|
169
|
-
kg = result.get("knowledge_graph") or {}
|
|
170
|
-
hooks.fire_hook(
|
|
171
|
-
"post_upload", "document.uploaded",
|
|
172
|
-
payload={
|
|
173
|
-
"filename": file.filename,
|
|
174
|
-
"chars": result.get("chars"),
|
|
175
|
-
"graph_node": kg.get("node_id"),
|
|
176
|
-
"indexed": bool(kg.get("node_id")),
|
|
177
|
-
},
|
|
178
|
-
user_email=current_user,
|
|
179
|
-
)
|
|
180
|
-
|
|
181
|
-
result["original_filename"] = file.filename
|
|
182
|
-
return result
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
__all__ = ["process_uploaded_document"]
|