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
|
@@ -1,42 +1,31 @@
|
|
|
1
|
-
"""Tool registry, governance, and dispatch
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
"""Tool registry, governance, and the authorization half of tool dispatch.
|
|
2
|
+
|
|
3
|
+
``ToolRegistry`` owns the catalog and governance facts; this service owns the
|
|
4
|
+
request-time authorization callbacks that must be configured during app
|
|
5
|
+
construction.
|
|
6
|
+
|
|
7
|
+
v11.6.0 shrank it to the three questions ``POST /agent/tool`` asks before it
|
|
8
|
+
runs anything — what is this call's policy, may this role make it, and does the
|
|
9
|
+
target already exist — plus the ``configure`` seam that binds the user table.
|
|
10
|
+
The rest of the module was the Python agent loop's: ``build_agent_runtime``
|
|
11
|
+
(the edge that kept ``latticeai.core.agent`` alive), the artifact collectors,
|
|
12
|
+
the snapshot/rollback file surface behind the Review Center, and
|
|
13
|
+
``enforce_policy``'s approval flow. Orchestration lives in ``lattice-agent``
|
|
14
|
+
now, and the mode gating is the kernel's decision, made with the run's approval
|
|
15
|
+
state — which HTTP does not have.
|
|
5
16
|
"""
|
|
6
17
|
|
|
7
18
|
from __future__ import annotations
|
|
8
19
|
|
|
9
|
-
import subprocess
|
|
10
20
|
from dataclasses import dataclass, field
|
|
11
21
|
from pathlib import Path
|
|
12
22
|
from typing import Any, Callable, Dict, Mapping, Optional
|
|
13
23
|
|
|
14
24
|
from fastapi import HTTPException
|
|
15
25
|
|
|
16
|
-
from latticeai.core.agent import AgentDeps, SingleAgentRuntime
|
|
17
|
-
from latticeai.core.agent_permission import call_mode_source
|
|
18
|
-
from latticeai.core.agent_prompts import (
|
|
19
|
-
CRITIC_PROMPT,
|
|
20
|
-
EXECUTOR_PROMPT,
|
|
21
|
-
MEMORY_UPDATER_PROMPT,
|
|
22
|
-
PLANNER_PROMPT,
|
|
23
|
-
)
|
|
24
|
-
from latticeai.core.permission_mode import (
|
|
25
|
-
DEFAULT_MODE,
|
|
26
|
-
effective_auto_approve,
|
|
27
|
-
is_circuit_breaker,
|
|
28
|
-
normalize_mode,
|
|
29
|
-
)
|
|
30
26
|
from latticeai.core.policy import role_has_capability
|
|
31
|
-
from latticeai.core.tool_governor import classify_tool_call
|
|
32
27
|
from latticeai.core.tool_registry import ToolPermission, ToolPolicy
|
|
33
|
-
from latticeai.tools import
|
|
34
|
-
AGENT_ROOT,
|
|
35
|
-
DEFAULT_TOOL_REGISTRY,
|
|
36
|
-
ToolError,
|
|
37
|
-
document_output_target,
|
|
38
|
-
ensure_agent_root,
|
|
39
|
-
)
|
|
28
|
+
from latticeai.tools import AGENT_ROOT, DEFAULT_TOOL_REGISTRY, document_output_target
|
|
40
29
|
|
|
41
30
|
|
|
42
31
|
def _default_load_users() -> Dict[str, Any]:
|
|
@@ -47,21 +36,8 @@ def _default_get_user_role(_email, _users=None) -> str:
|
|
|
47
36
|
return "user"
|
|
48
37
|
|
|
49
38
|
|
|
50
|
-
def _default_permission_mode() -> Any:
|
|
51
|
-
return DEFAULT_MODE
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
FILE_CREATE_ACTIONS = set(DEFAULT_TOOL_REGISTRY.file_create_actions)
|
|
55
39
|
TOOL_GOVERNANCE: Dict[str, ToolPolicy] = dict(DEFAULT_TOOL_REGISTRY.governance)
|
|
56
40
|
TOOL_GOVERNANCE_DEFAULT: ToolPolicy = DEFAULT_TOOL_REGISTRY.default_policy
|
|
57
|
-
ADMIN_ONLY_TOOLS: frozenset[str] = DEFAULT_TOOL_REGISTRY.admin_only_tools
|
|
58
|
-
EXPLICIT_CONSENT_TOOLS: frozenset[str] = frozenset({
|
|
59
|
-
"local_list",
|
|
60
|
-
"local_read",
|
|
61
|
-
"local_write",
|
|
62
|
-
"read_document",
|
|
63
|
-
})
|
|
64
|
-
LOCAL_WRITE_BLOCKED_PREFIXES = DEFAULT_TOOL_REGISTRY.local_write_blocked_prefixes
|
|
65
41
|
RISK_LEVEL_MAP = DEFAULT_TOOL_REGISTRY.risk_level_map
|
|
66
42
|
|
|
67
43
|
|
|
@@ -69,24 +45,14 @@ RISK_LEVEL_MAP = DEFAULT_TOOL_REGISTRY.risk_level_map
|
|
|
69
45
|
class ToolDispatchService:
|
|
70
46
|
"""Runtime-facing tool policy and authorization boundary.
|
|
71
47
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
assembly code an injectable seam while the module-level functions below
|
|
76
|
-
preserve the historical ``server_app`` surface.
|
|
48
|
+
Keeping the callbacks on an instance rather than in module globals is what
|
|
49
|
+
lets a second application — or a test — bind its own user table without
|
|
50
|
+
reaching into this one's.
|
|
77
51
|
"""
|
|
78
52
|
|
|
79
53
|
registry: Any = field(default_factory=lambda: DEFAULT_TOOL_REGISTRY)
|
|
80
54
|
load_users: Callable[[], Dict[str, Any]] = field(default=_default_load_users)
|
|
81
55
|
get_user_role: Callable[..., str] = field(default=_default_get_user_role)
|
|
82
|
-
# Static mode, or a callable resolved per enforce_policy call. A callable
|
|
83
|
-
# may accept ``user_email``/``workspace_id`` scope kwargs (preferred) or no
|
|
84
|
-
# arguments at all — see ``call_mode_source``.
|
|
85
|
-
permission_mode: Any = field(default_factory=lambda: _default_permission_mode)
|
|
86
|
-
|
|
87
|
-
@property
|
|
88
|
-
def file_create_actions(self) -> frozenset[str]:
|
|
89
|
-
return self.registry.file_create_actions
|
|
90
56
|
|
|
91
57
|
@property
|
|
92
58
|
def tool_governance(self) -> Mapping[str, ToolPolicy]:
|
|
@@ -101,29 +67,9 @@ class ToolDispatchService:
|
|
|
101
67
|
*,
|
|
102
68
|
load_users: Callable[[], Dict[str, Any]],
|
|
103
69
|
get_user_role: Callable[..., str],
|
|
104
|
-
permission_mode: Any = None,
|
|
105
70
|
) -> None:
|
|
106
71
|
self.load_users = load_users
|
|
107
72
|
self.get_user_role = get_user_role
|
|
108
|
-
if permission_mode is not None:
|
|
109
|
-
self.permission_mode = permission_mode
|
|
110
|
-
|
|
111
|
-
def resolve_permission_mode(
|
|
112
|
-
self,
|
|
113
|
-
*,
|
|
114
|
-
user_email: Optional[str] = None,
|
|
115
|
-
workspace_id: Optional[str] = None,
|
|
116
|
-
) -> Any:
|
|
117
|
-
"""Active autonomy dial for this caller.
|
|
118
|
-
|
|
119
|
-
The scope matters: per-user and per-workspace overrides are stored by
|
|
120
|
-
``PermissionModeService``, so resolving without a scope would silently
|
|
121
|
-
collapse every caller onto the process-wide default.
|
|
122
|
-
"""
|
|
123
|
-
raw = call_mode_source(
|
|
124
|
-
self.permission_mode, user_email=user_email, workspace_id=workspace_id,
|
|
125
|
-
)
|
|
126
|
-
return normalize_mode(raw if raw is not None else DEFAULT_MODE)
|
|
127
73
|
|
|
128
74
|
def policy_for(self, action_name: str, args: dict) -> ToolPolicy:
|
|
129
75
|
return self.registry.policy_for(action_name, args)
|
|
@@ -131,9 +77,6 @@ class ToolDispatchService:
|
|
|
131
77
|
def risk_level(self, action_name: str, args: dict) -> str:
|
|
132
78
|
return self.registry.risk_level(action_name, args)
|
|
133
79
|
|
|
134
|
-
def risk_level_for_policy(self, policy: ToolPolicy) -> str:
|
|
135
|
-
return self.risk_level_map.get(policy["risk"], "medium")
|
|
136
|
-
|
|
137
80
|
def permission(self, name: str, args: Optional[dict] = None) -> ToolPermission:
|
|
138
81
|
return self.registry.permission(name, args or {})
|
|
139
82
|
|
|
@@ -191,160 +134,6 @@ class ToolDispatchService:
|
|
|
191
134
|
except Exception:
|
|
192
135
|
return "user"
|
|
193
136
|
|
|
194
|
-
def enforce_policy(
|
|
195
|
-
self,
|
|
196
|
-
tool_name: str,
|
|
197
|
-
args: Optional[dict],
|
|
198
|
-
*,
|
|
199
|
-
current_user: str,
|
|
200
|
-
source: str,
|
|
201
|
-
require_auto_approval: bool = True,
|
|
202
|
-
trusted_admin: bool = False,
|
|
203
|
-
permission_mode: Any = None,
|
|
204
|
-
workspace_id: Optional[str] = None,
|
|
205
|
-
) -> ToolPolicy:
|
|
206
|
-
"""Authorize a tool call before any hook or handler can execute.
|
|
207
|
-
|
|
208
|
-
This is the shared policy gate for direct HTTP/MCP/tool surfaces. Agent
|
|
209
|
-
runtime uses the same policy data and blocks non-auto-approved steps in
|
|
210
|
-
its state machine so Computer Use direct endpoints can remain unchanged
|
|
211
|
-
when explicitly excluded from a hardening pass.
|
|
212
|
-
|
|
213
|
-
The active :class:`PermissionMode` widens what may run without an extra
|
|
214
|
-
approval prompt; it never widens a circuit breaker.
|
|
215
|
-
"""
|
|
216
|
-
policy = self.policy_for(tool_name, args or {})
|
|
217
|
-
mode = normalize_mode(
|
|
218
|
-
permission_mode
|
|
219
|
-
if permission_mode is not None
|
|
220
|
-
else self.resolve_permission_mode(
|
|
221
|
-
user_email=current_user, workspace_id=workspace_id,
|
|
222
|
-
)
|
|
223
|
-
)
|
|
224
|
-
if source in {"mcp", "plugin"} and tool_name in EXPLICIT_CONSENT_TOOLS:
|
|
225
|
-
raise HTTPException(
|
|
226
|
-
status_code=403,
|
|
227
|
-
detail=(
|
|
228
|
-
f"'{tool_name}' requires the dedicated local-file approval flow "
|
|
229
|
-
"and cannot run through MCP or plugins."
|
|
230
|
-
),
|
|
231
|
-
)
|
|
232
|
-
if not trusted_admin:
|
|
233
|
-
self.check_role(tool_name, current_user)
|
|
234
|
-
|
|
235
|
-
breaker = is_circuit_breaker(tool_name, policy, args or {})
|
|
236
|
-
if breaker:
|
|
237
|
-
raise HTTPException(status_code=403, detail=f"'{tool_name}' 차단: {breaker}")
|
|
238
|
-
|
|
239
|
-
if policy["destructive"] or policy["risk"] == "destructive":
|
|
240
|
-
raise HTTPException(
|
|
241
|
-
status_code=403,
|
|
242
|
-
detail=f"'{tool_name}' 툴은 파괴적 작업으로 차단되었습니다.",
|
|
243
|
-
)
|
|
244
|
-
|
|
245
|
-
# Fail-closed governance: a call that would rewrite existing content but
|
|
246
|
-
# cannot be staged as a reviewable proposal is blocked, never applied
|
|
247
|
-
# silently. New-file (additive) calls are unaffected. This is
|
|
248
|
-
# mode-invariant — trusted/bypass skip *proposal staging*, but a binary
|
|
249
|
-
# overwrite has no safe apply path in any mode, so it stays blocked.
|
|
250
|
-
verdict = classify_tool_call(
|
|
251
|
-
tool_name, args or {},
|
|
252
|
-
policy=dict(policy),
|
|
253
|
-
path_exists=lambda candidate: self._governed_path_exists(tool_name, candidate),
|
|
254
|
-
)
|
|
255
|
-
if verdict.get("fail_closed"):
|
|
256
|
-
raise HTTPException(
|
|
257
|
-
status_code=409,
|
|
258
|
-
detail=(
|
|
259
|
-
f"'{tool_name}' 툴은 기존 콘텐츠를 변경하지만 검토 가능한 제안으로 "
|
|
260
|
-
"적용할 수 없어 차단되었습니다. 새 파일 이름으로 생성하거나 "
|
|
261
|
-
"지원되는 편집 도구(write_file/edit_file)를 사용하세요."
|
|
262
|
-
),
|
|
263
|
-
)
|
|
264
|
-
|
|
265
|
-
auto = policy["auto_approve"] or effective_auto_approve(
|
|
266
|
-
mode, tool_name, policy, change_class=verdict.get("change_class"), args=args or {},
|
|
267
|
-
)
|
|
268
|
-
if (
|
|
269
|
-
require_auto_approval
|
|
270
|
-
and not trusted_admin
|
|
271
|
-
and not auto
|
|
272
|
-
and self.user_role(current_user) not in {"admin", "owner"}
|
|
273
|
-
):
|
|
274
|
-
raise HTTPException(
|
|
275
|
-
status_code=403,
|
|
276
|
-
detail=(
|
|
277
|
-
f"'{tool_name}' 툴은 명시 승인이 필요합니다 "
|
|
278
|
-
f"(permission_mode={mode.value}). "
|
|
279
|
-
"승인 UI가 없는 직접 실행 경로에서 차단되었습니다."
|
|
280
|
-
),
|
|
281
|
-
)
|
|
282
|
-
return policy
|
|
283
|
-
|
|
284
|
-
def rollback_file(self, path: str) -> Dict[str, Any]:
|
|
285
|
-
r = subprocess.run(
|
|
286
|
-
["git", "checkout", "--", path],
|
|
287
|
-
cwd=str(AGENT_ROOT),
|
|
288
|
-
capture_output=True,
|
|
289
|
-
text=True,
|
|
290
|
-
timeout=10,
|
|
291
|
-
)
|
|
292
|
-
return {"path": path, "ok": r.returncode == 0, "stderr": r.stderr[:200]}
|
|
293
|
-
|
|
294
|
-
# ── snapshot rollback ports (review L7) ──────────────────────────────
|
|
295
|
-
# Pre-write capture + restore for workspaces where git rollback does not
|
|
296
|
-
# apply (untracked new files, no git). Strictly confined to AGENT_ROOT.
|
|
297
|
-
|
|
298
|
-
_SNAPSHOT_MAX_BYTES = 512 * 1024
|
|
299
|
-
|
|
300
|
-
def _workspace_path(self, path: str) -> Optional[Path]:
|
|
301
|
-
raw = Path(path)
|
|
302
|
-
candidate = raw if raw.is_absolute() else AGENT_ROOT / raw
|
|
303
|
-
try:
|
|
304
|
-
resolved = candidate.resolve()
|
|
305
|
-
root = AGENT_ROOT.resolve()
|
|
306
|
-
except OSError:
|
|
307
|
-
return None
|
|
308
|
-
if resolved != root and root not in resolved.parents:
|
|
309
|
-
return None
|
|
310
|
-
return resolved
|
|
311
|
-
|
|
312
|
-
def snapshot_file(self, path: str) -> Dict[str, Any]:
|
|
313
|
-
"""Pre-write state of a workspace file: existence + bounded content."""
|
|
314
|
-
target = self._workspace_path(path)
|
|
315
|
-
if target is None:
|
|
316
|
-
return {"existed": False, "content": None, "too_large": False,
|
|
317
|
-
"error": "path escapes the agent workspace"}
|
|
318
|
-
if not target.exists() or not target.is_file():
|
|
319
|
-
return {"existed": False, "content": None, "too_large": False}
|
|
320
|
-
try:
|
|
321
|
-
if target.stat().st_size > self._SNAPSHOT_MAX_BYTES:
|
|
322
|
-
return {"existed": True, "content": None, "too_large": True}
|
|
323
|
-
return {
|
|
324
|
-
"existed": True,
|
|
325
|
-
"content": target.read_text(encoding="utf-8", errors="replace"),
|
|
326
|
-
"too_large": False,
|
|
327
|
-
}
|
|
328
|
-
except OSError as exc:
|
|
329
|
-
return {"existed": True, "content": None, "too_large": True,
|
|
330
|
-
"error": str(exc)}
|
|
331
|
-
|
|
332
|
-
def restore_snapshot(self, path: str, content: Optional[str]) -> Dict[str, Any]:
|
|
333
|
-
"""Restore a pre-write snapshot: rewrite prior content, or delete a
|
|
334
|
-
file the run created (``content=None``)."""
|
|
335
|
-
target = self._workspace_path(path)
|
|
336
|
-
if target is None:
|
|
337
|
-
return {"path": path, "ok": False, "error": "path escapes the agent workspace"}
|
|
338
|
-
try:
|
|
339
|
-
if content is None:
|
|
340
|
-
target.unlink(missing_ok=True)
|
|
341
|
-
return {"path": path, "ok": True, "action": "deleted"}
|
|
342
|
-
target.parent.mkdir(parents=True, exist_ok=True)
|
|
343
|
-
target.write_text(content, encoding="utf-8")
|
|
344
|
-
return {"path": path, "ok": True, "action": "restored"}
|
|
345
|
-
except OSError as exc:
|
|
346
|
-
return {"path": path, "ok": False, "error": str(exc)}
|
|
347
|
-
|
|
348
137
|
|
|
349
138
|
DEFAULT_TOOL_DISPATCH_SERVICE = ToolDispatchService()
|
|
350
139
|
|
|
@@ -353,172 +142,27 @@ def configure_tool_dispatch(
|
|
|
353
142
|
*,
|
|
354
143
|
load_users: Callable[[], Dict[str, Any]],
|
|
355
144
|
get_user_role: Callable[..., str],
|
|
356
|
-
permission_mode: Any = None,
|
|
357
145
|
) -> None:
|
|
358
146
|
DEFAULT_TOOL_DISPATCH_SERVICE.configure(
|
|
359
|
-
load_users=load_users,
|
|
360
|
-
get_user_role=get_user_role,
|
|
361
|
-
permission_mode=permission_mode,
|
|
147
|
+
load_users=load_users, get_user_role=get_user_role
|
|
362
148
|
)
|
|
363
149
|
|
|
364
150
|
|
|
365
|
-
def agent_risk(action_name: str, args: dict) -> str:
|
|
366
|
-
return DEFAULT_TOOL_DISPATCH_SERVICE.risk_level(action_name, args)
|
|
367
|
-
|
|
368
|
-
|
|
369
151
|
def get_tool_permission(name: str, args: Optional[dict] = None) -> ToolPermission:
|
|
370
152
|
return DEFAULT_TOOL_DISPATCH_SERVICE.permission(name, args or {})
|
|
371
153
|
|
|
372
154
|
|
|
373
|
-
def list_tool_permissions() -> list:
|
|
374
|
-
return DEFAULT_TOOL_DISPATCH_SERVICE.permissions()
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
def tool_registry_diagnostics() -> Dict[str, Any]:
|
|
378
|
-
return DEFAULT_TOOL_DISPATCH_SERVICE.diagnostics()
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
def tool_registry_manifest() -> Dict[str, Any]:
|
|
382
|
-
return DEFAULT_TOOL_DISPATCH_SERVICE.manifest()
|
|
383
|
-
|
|
384
|
-
|
|
385
155
|
def check_tool_role(tool_name: str, current_user: str) -> None:
|
|
386
156
|
DEFAULT_TOOL_DISPATCH_SERVICE.check_role(tool_name, current_user)
|
|
387
157
|
|
|
388
158
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
) -> ToolPolicy:
|
|
400
|
-
return DEFAULT_TOOL_DISPATCH_SERVICE.enforce_policy(
|
|
401
|
-
tool_name,
|
|
402
|
-
args or {},
|
|
403
|
-
current_user=current_user,
|
|
404
|
-
source=source,
|
|
405
|
-
require_auto_approval=require_auto_approval,
|
|
406
|
-
trusted_admin=trusted_admin,
|
|
407
|
-
workspace_id=workspace_id,
|
|
408
|
-
permission_mode=permission_mode,
|
|
409
|
-
)
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
def collect_created_files(transcript: list) -> list:
|
|
413
|
-
files = []
|
|
414
|
-
for step in transcript:
|
|
415
|
-
if step.get("action") in FILE_CREATE_ACTIONS:
|
|
416
|
-
result = step.get("result", {})
|
|
417
|
-
if isinstance(result.get("created_files"), list):
|
|
418
|
-
for rel_path in result["created_files"]:
|
|
419
|
-
files.append({
|
|
420
|
-
"path": rel_path,
|
|
421
|
-
"filename": Path(rel_path).name,
|
|
422
|
-
"bytes": 0,
|
|
423
|
-
"action": step["action"],
|
|
424
|
-
})
|
|
425
|
-
continue
|
|
426
|
-
path = result.get("path")
|
|
427
|
-
if path:
|
|
428
|
-
files.append({
|
|
429
|
-
"path": path,
|
|
430
|
-
"filename": Path(path).name,
|
|
431
|
-
"bytes": result.get("bytes", 0),
|
|
432
|
-
"action": step["action"],
|
|
433
|
-
})
|
|
434
|
-
return files
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
def collect_artifacts(transcript: list) -> list:
|
|
438
|
-
"""Artifact-first view of a run: every produced file with honesty flags.
|
|
439
|
-
|
|
440
|
-
``repaired`` surfaces the ArtifactWritePipeline verdict recorded on the
|
|
441
|
-
transcript step (``content_sanitize``) so the UI can badge deterministic
|
|
442
|
-
scaffolds instead of presenting them as high-quality model output.
|
|
443
|
-
"""
|
|
444
|
-
from latticeai.core.file_generation import PREVIEWABLE_EXTENSIONS
|
|
445
|
-
|
|
446
|
-
artifacts = []
|
|
447
|
-
for step in transcript:
|
|
448
|
-
if step.get("action") not in FILE_CREATE_ACTIONS:
|
|
449
|
-
continue
|
|
450
|
-
result = step.get("result", {})
|
|
451
|
-
if not isinstance(result, dict):
|
|
452
|
-
continue
|
|
453
|
-
sanitize = step.get("content_sanitize") or {}
|
|
454
|
-
repaired = bool(sanitize.get("repaired"))
|
|
455
|
-
rel_paths = (
|
|
456
|
-
result["created_files"]
|
|
457
|
-
if isinstance(result.get("created_files"), list)
|
|
458
|
-
else ([result["path"]] if result.get("path") else [])
|
|
459
|
-
)
|
|
460
|
-
for rel_path in rel_paths:
|
|
461
|
-
artifacts.append({
|
|
462
|
-
"kind": "file",
|
|
463
|
-
"path": rel_path,
|
|
464
|
-
"filename": Path(rel_path).name,
|
|
465
|
-
"bytes": result.get("bytes", 0) if len(rel_paths) == 1 else 0,
|
|
466
|
-
"previewable": Path(rel_path).suffix.lower() in PREVIEWABLE_EXTENSIONS,
|
|
467
|
-
"valid": True,
|
|
468
|
-
"repaired": repaired,
|
|
469
|
-
})
|
|
470
|
-
return artifacts
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
def build_agent_runtime(
|
|
474
|
-
*,
|
|
475
|
-
model_router: Any,
|
|
476
|
-
execute_tool: Callable[..., Dict[str, Any]],
|
|
477
|
-
recent_chat_context: Callable[..., str],
|
|
478
|
-
clear_history: Callable[[int], Dict[str, Any]],
|
|
479
|
-
knowledge_save: Callable[..., Dict[str, Any]],
|
|
480
|
-
audit: Callable[..., None],
|
|
481
|
-
hooks: Any = None,
|
|
482
|
-
brain_memory: Any = None,
|
|
483
|
-
dispatch_service: ToolDispatchService = DEFAULT_TOOL_DISPATCH_SERVICE,
|
|
484
|
-
permission_mode: Any = None,
|
|
485
|
-
self_model_summary: Any = None,
|
|
486
|
-
) -> SingleAgentRuntime:
|
|
487
|
-
ensure_agent_root()
|
|
488
|
-
mode = permission_mode if permission_mode is not None else dispatch_service.permission_mode
|
|
489
|
-
deps = AgentDeps(
|
|
490
|
-
generate_as=model_router.generate_as,
|
|
491
|
-
generate=model_router.generate,
|
|
492
|
-
execute_tool=execute_tool,
|
|
493
|
-
policy_for=dispatch_service.policy_for,
|
|
494
|
-
risk_level=dispatch_service.risk_level_for_policy,
|
|
495
|
-
check_role=dispatch_service.check_role,
|
|
496
|
-
tool_governance=dispatch_service.tool_governance,
|
|
497
|
-
file_create_actions=dispatch_service.file_create_actions,
|
|
498
|
-
recent_chat_context=recent_chat_context,
|
|
499
|
-
clear_history=clear_history,
|
|
500
|
-
knowledge_save=knowledge_save,
|
|
501
|
-
audit=audit,
|
|
502
|
-
planner_prompt=PLANNER_PROMPT,
|
|
503
|
-
executor_prompt=EXECUTOR_PROMPT,
|
|
504
|
-
critic_prompt=CRITIC_PROMPT,
|
|
505
|
-
memory_updater_prompt=MEMORY_UPDATER_PROMPT,
|
|
506
|
-
agent_root=AGENT_ROOT,
|
|
507
|
-
rollback_file=dispatch_service.rollback_file,
|
|
508
|
-
snapshot_file=dispatch_service.snapshot_file,
|
|
509
|
-
restore_snapshot=dispatch_service.restore_snapshot,
|
|
510
|
-
hooks=hooks,
|
|
511
|
-
brain_memory=brain_memory,
|
|
512
|
-
permission_mode=mode,
|
|
513
|
-
# v11.2.0: what the Brain knows about its owner reaches the agent loop.
|
|
514
|
-
# ``None`` keeps the pre-11.2.0 prompt bytes exactly.
|
|
515
|
-
self_model_summary=self_model_summary,
|
|
516
|
-
)
|
|
517
|
-
return SingleAgentRuntime(deps)
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
def tool_response(fn, *args):
|
|
521
|
-
try:
|
|
522
|
-
return {"status": "ok", "workspace": str(AGENT_ROOT), "result": fn(*args)}
|
|
523
|
-
except ToolError as exc:
|
|
524
|
-
raise HTTPException(status_code=400, detail=str(exc))
|
|
159
|
+
__all__ = [
|
|
160
|
+
"DEFAULT_TOOL_DISPATCH_SERVICE",
|
|
161
|
+
"RISK_LEVEL_MAP",
|
|
162
|
+
"TOOL_GOVERNANCE",
|
|
163
|
+
"TOOL_GOVERNANCE_DEFAULT",
|
|
164
|
+
"ToolDispatchService",
|
|
165
|
+
"check_tool_role",
|
|
166
|
+
"configure_tool_dispatch",
|
|
167
|
+
"get_tool_permission",
|
|
168
|
+
]
|
|
@@ -25,6 +25,11 @@ scan is transcribed by whatever this service was given — see
|
|
|
25
25
|
:meth:`VoiceCaptureService.multimodal_ports`. One transcriber, one honesty
|
|
26
26
|
story: if a voice memo cannot become text here, a scanned recording cannot
|
|
27
27
|
either, and both say so the same way.
|
|
28
|
+
|
|
29
|
+
v11.6.0 took the *ingest* half away: ``POST /api/capture/voice`` is a native
|
|
30
|
+
route that stores the memo itself and asks ``POST /worker/asr`` for the words.
|
|
31
|
+
What is left is the transcriber and the two questions only this process can
|
|
32
|
+
answer about it — can it hear, and what will it accept.
|
|
28
33
|
"""
|
|
29
34
|
|
|
30
35
|
from __future__ import annotations
|
|
@@ -57,16 +62,14 @@ class TranscriptionUnavailable(RuntimeError):
|
|
|
57
62
|
|
|
58
63
|
|
|
59
64
|
class VoiceCaptureService:
|
|
60
|
-
"""
|
|
65
|
+
"""The local transcriber, and the honest report of whether there is one."""
|
|
61
66
|
|
|
62
67
|
def __init__(
|
|
63
68
|
self,
|
|
64
69
|
*,
|
|
65
|
-
pipeline: Any,
|
|
66
70
|
transcriber: Optional[Callable[[str], str]] = None,
|
|
67
71
|
max_bytes: int = MAX_AUDIO_BYTES,
|
|
68
72
|
) -> None:
|
|
69
|
-
self._pipeline = pipeline
|
|
70
73
|
self._transcriber = transcriber
|
|
71
74
|
self._max_bytes = max(1, int(max_bytes))
|
|
72
75
|
|
|
@@ -74,7 +77,11 @@ class VoiceCaptureService:
|
|
|
74
77
|
def status(self) -> Dict[str, Any]:
|
|
75
78
|
"""What this install can actually do with a voice memo, honestly."""
|
|
76
79
|
return {
|
|
77
|
-
|
|
80
|
+
# Storing the memo is native (``POST /api/capture/voice`` in
|
|
81
|
+
# lattice-platform), so capture is always available to a caller
|
|
82
|
+
# that reached this answer through the gateway. Only the
|
|
83
|
+
# transcriber is a fact about this process.
|
|
84
|
+
"capture": True,
|
|
78
85
|
"transcription": self._transcriber is not None,
|
|
79
86
|
"supported_extensions": sorted(SUPPORTED_AUDIO_EXTENSIONS),
|
|
80
87
|
"max_bytes": self._max_bytes,
|
|
@@ -110,106 +117,5 @@ class VoiceCaptureService:
|
|
|
110
117
|
raise TranscriptionUnavailable("the transcriber returned no text")
|
|
111
118
|
return cleaned
|
|
112
119
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
self,
|
|
116
|
-
audio_path: str,
|
|
117
|
-
*,
|
|
118
|
-
title: Optional[str] = None,
|
|
119
|
-
user_email: Optional[str] = None,
|
|
120
|
-
workspace_id: Optional[str] = None,
|
|
121
|
-
conversation_id: Optional[str] = None,
|
|
122
|
-
transcript: Optional[str] = None,
|
|
123
|
-
) -> Dict[str, Any]:
|
|
124
|
-
"""Ingest one voice memo.
|
|
125
|
-
|
|
126
|
-
``transcript`` lets a client that already has text (a phone's own
|
|
127
|
-
dictation, for example) skip local transcription entirely — the memo
|
|
128
|
-
still lands in the Brain through the same pipeline.
|
|
129
|
-
"""
|
|
130
|
-
from lattice_brain.ingestion import IngestionItem
|
|
131
|
-
|
|
132
|
-
path = Path(str(audio_path or "")).expanduser()
|
|
133
|
-
if not path.exists() or not path.is_file():
|
|
134
|
-
return self._failed("FILE_NOT_FOUND", f"audio file not found: {path}")
|
|
135
|
-
suffix = path.suffix.lower()
|
|
136
|
-
if suffix not in SUPPORTED_AUDIO_EXTENSIONS:
|
|
137
|
-
return self._failed(
|
|
138
|
-
"UNSUPPORTED_FORMAT",
|
|
139
|
-
f"{suffix or 'this file'} is not a supported audio container",
|
|
140
|
-
)
|
|
141
|
-
try:
|
|
142
|
-
size = path.stat().st_size
|
|
143
|
-
except OSError as exc:
|
|
144
|
-
return self._failed("FILE_NOT_FOUND", f"audio file unreadable: {exc}")
|
|
145
|
-
if size > self._max_bytes:
|
|
146
|
-
return self._failed(
|
|
147
|
-
"SIZE_LIMIT",
|
|
148
|
-
f"audio is {size} bytes; the memo limit is {self._max_bytes}",
|
|
149
|
-
)
|
|
150
|
-
if self._pipeline is None:
|
|
151
|
-
return self._failed("UNAVAILABLE", "the ingestion pipeline is not available")
|
|
152
|
-
|
|
153
|
-
supplied = str(transcript or "").strip()
|
|
154
|
-
transcription_state = "supplied" if supplied else "ok"
|
|
155
|
-
transcription_detail = ""
|
|
156
|
-
text = supplied
|
|
157
|
-
if not text:
|
|
158
|
-
try:
|
|
159
|
-
text = self._transcribe(path)
|
|
160
|
-
except TranscriptionUnavailable as exc:
|
|
161
|
-
transcription_state = "unavailable"
|
|
162
|
-
transcription_detail = str(exc)
|
|
163
|
-
text = ""
|
|
164
|
-
except Exception as exc: # noqa: BLE001 — a broken transcriber is a state
|
|
165
|
-
LOGGER.exception("voice transcription failed")
|
|
166
|
-
transcription_state = "failed"
|
|
167
|
-
transcription_detail = str(exc)
|
|
168
|
-
text = ""
|
|
169
|
-
|
|
170
|
-
display_title = str(title or "").strip() or path.stem
|
|
171
|
-
# With no transcript there is no text to index — the memo is recorded
|
|
172
|
-
# by what we honestly know (title + the audio file itself), and the
|
|
173
|
-
# response says so. It is never presented as a searchable note.
|
|
174
|
-
body = text or (
|
|
175
|
-
f"[음성 메모] {display_title}\n"
|
|
176
|
-
"이 메모는 아직 글로 바뀌지 않았습니다 — 음성 인식기가 없어 내용 검색은 되지 않습니다."
|
|
177
|
-
)
|
|
178
|
-
item = IngestionItem(
|
|
179
|
-
source_type="note",
|
|
180
|
-
title=display_title,
|
|
181
|
-
text=body,
|
|
182
|
-
source_uri=str(path),
|
|
183
|
-
mime_type=f"audio/{suffix.lstrip('.')}",
|
|
184
|
-
owner=user_email,
|
|
185
|
-
workspace_id=workspace_id,
|
|
186
|
-
conversation_id=conversation_id,
|
|
187
|
-
metadata={
|
|
188
|
-
"capture": "voice",
|
|
189
|
-
# Same key the multi-modal ingest door writes, so a memo and a
|
|
190
|
-
# scanned recording are one kind of thing in the graph.
|
|
191
|
-
"modality": "audio",
|
|
192
|
-
"audio_path": str(path),
|
|
193
|
-
"audio_bytes": size,
|
|
194
|
-
"transcription": transcription_state,
|
|
195
|
-
**({"transcription_detail": transcription_detail} if transcription_detail else {}),
|
|
196
|
-
},
|
|
197
|
-
)
|
|
198
|
-
result = self._pipeline.ingest(item, user_email=user_email)
|
|
199
|
-
payload = result.as_dict() if hasattr(result, "as_dict") else dict(result)
|
|
200
|
-
payload["transcription"] = transcription_state
|
|
201
|
-
payload["searchable"] = bool(text)
|
|
202
|
-
if transcription_detail:
|
|
203
|
-
payload["transcription_detail"] = transcription_detail
|
|
204
|
-
return payload
|
|
205
|
-
|
|
206
|
-
@staticmethod
|
|
207
|
-
def _failed(error: str, message: str) -> Dict[str, Any]:
|
|
208
|
-
return {
|
|
209
|
-
"status": "failed",
|
|
210
|
-
"source_type": "note",
|
|
211
|
-
"error": error,
|
|
212
|
-
"message": message,
|
|
213
|
-
"transcription": "skipped",
|
|
214
|
-
"searchable": False,
|
|
215
|
-
}
|
|
120
|
+
|
|
121
|
+
__all__ = ["VoiceCaptureService", "TranscriptionUnavailable", "SUPPORTED_AUDIO_EXTENSIONS"]
|