ltcai 11.5.2 → 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 +91 -148
- package/bin/ltcai.js +234 -24
- package/docs/CHANGELOG.md +75 -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 +21 -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-CToZnOHz.js} +1 -1
- package/static/app/assets/{AdminConsole-cf4npybT.js → AdminConsole-D5kbrBu8.js} +1 -1
- package/static/app/assets/{Brain-3VCSHFcn.js → Brain-C6zCdv4S.js} +1 -1
- package/static/app/assets/{BrainHome-Qm8eaztx.js → BrainHome-D4n3EDUt.js} +1 -1
- package/static/app/assets/{BrainSignals-DS9BtKOW.js → BrainSignals-CzeZ-vQ9.js} +1 -1
- package/static/app/assets/{Capture-DiQ219jW.js → Capture-DPcyTczq.js} +1 -1
- package/static/app/assets/{Chronicle-BGvuAchH.js → Chronicle-D3Nx-oWS.js} +1 -1
- package/static/app/assets/{CommandPalette-Bqhm0Urn.js → CommandPalette-DPPvpp28.js} +1 -1
- package/static/app/assets/{Library-BV6NnF0a.js → Library-Co2qTu3I.js} +1 -1
- package/static/app/assets/{LivingBrain-GzenJchP.js → LivingBrain-Bsmzwy8_.js} +1 -1
- package/static/app/assets/{ProductFlow-DEP6-vML.js → ProductFlow-CBLdmesT.js} +1 -1
- package/static/app/assets/{ReviewCard-CNZ7XjWG.js → ReviewCard-CfR6DLs7.js} +1 -1
- package/static/app/assets/{System-CieofHQa.js → System-Ce-P2YL6.js} +1 -1
- package/static/app/assets/arrow-left-DD5jFGHV.js +1 -0
- package/static/app/assets/{bot-B_K1Tdmw.js → bot-C5-XIww2.js} +1 -1
- package/static/app/assets/{brain-DWyaV1L1.js → brain-CFRJUvdX.js} +1 -1
- package/static/app/assets/{button-aTn4s84A.js → button-DI1KcQP-.js} +1 -1
- package/static/app/assets/circle-check-CY20XMCh.js +1 -0
- package/static/app/assets/{circle-pause-xKgeGXkT.js → circle-pause-C8HHyood.js} +1 -1
- package/static/app/assets/{circle-play-DkT6tYPX.js → circle-play-Cufc235E.js} +1 -1
- package/static/app/assets/{cpu-85xYObUC.js → cpu-DW2_JVsv.js} +1 -1
- package/static/app/assets/{download-B5Fm7YXo.js → download-gbdKTBb8.js} +1 -1
- package/static/app/assets/{folder-open-kk2Xa52u.js → folder-open-C9ydyX33.js} +1 -1
- package/static/app/assets/{hard-drive-DkA3zBW_.js → hard-drive-BxNMzBM3.js} +1 -1
- package/static/app/assets/index-BovSWRmQ.css +2 -0
- package/static/app/assets/{index-BMPdTmlY.js → index-q9XyDY7A.js} +3 -3
- package/static/app/assets/{input-B0nRf2jO.js → input-GLD94Y_K.js} +1 -1
- package/static/app/assets/{link-2-Dwb4gnTc.js → link-2-B4Y0gz1S.js} +1 -1
- package/static/app/assets/{permissionCopy-CQDUBrOZ.js → permissionCopy-CxrV0fvU.js} +1 -1
- package/static/app/assets/{primitives-SNp0LRJz.js → primitives-2ANdAsu2.js} +1 -1
- package/static/app/assets/search-DTxtjQ07.js +1 -0
- package/static/app/assets/{share-2-BsrxFglO.js → share-2-DxArfiuI.js} +1 -1
- package/static/app/assets/{shield-alert-5BStfp2_.js → shield-alert-CgftuFFs.js} +1 -1
- package/static/app/assets/{textarea-Cg8IUA-k.js → textarea-BHVBAoCr.js} +1 -1
- package/static/app/assets/{useFocusTrap-CYKvE46M.js → useFocusTrap-Dzq2j_OG.js} +1 -1
- package/static/app/assets/{useMutation-CSn9t1op.js → useMutation-BcqfMLNq.js} +1 -1
- package/static/app/assets/{useQuery-CY2OI2uy.js → useQuery-DBzLKCOv.js} +1 -1
- package/static/app/assets/{utils-Ddol2RWD.js → utils-B-Aah1bO.js} +1 -1
- package/static/app/assets/{workspace-BqDwOz_p.js → workspace-B6fFhoko.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/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/search-BcHqkjoy.js +0 -1
|
@@ -1,295 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Vector-index backend benchmark: latency *and* the recall it costs.
|
|
3
|
-
|
|
4
|
-
A speed number for an approximate index is only half a measurement. This
|
|
5
|
-
script reports both halves for every backend — p50/p95 query latency and
|
|
6
|
-
recall@k against the exact brute-force scan, which is the ground truth by
|
|
7
|
-
construction — so "hnsw is 20x faster" always arrives next to "and returns
|
|
8
|
-
0.97 of the exact top-10".
|
|
9
|
-
|
|
10
|
-
Deliberately **not** part of CI: it builds a 10 000-vector corpus, it depends
|
|
11
|
-
on an optional compiled extra for one of the three backends, and its numbers
|
|
12
|
-
are machine-specific. It is a local instrument, run by hand, whose output is
|
|
13
|
-
transcribed into docs/PERFORMANCE.md with the machine it came from.
|
|
14
|
-
|
|
15
|
-
Honesty notes:
|
|
16
|
-
|
|
17
|
-
* The corpus is synthetic — deterministic pseudo-text from a fixed vocabulary
|
|
18
|
-
embedded with the built-in hash embedder. Real embeddings cluster very
|
|
19
|
-
differently, so absolute recall here is an upper bound for a well-separated
|
|
20
|
-
corpus, not a promise about your notes.
|
|
21
|
-
* Rows are written straight into ``vector_embeddings`` rather than through the
|
|
22
|
-
ingest pipeline. This measures the *search* path; it is not an ingestion
|
|
23
|
-
benchmark, and PERFORMANCE.md keeps those numbers separate.
|
|
24
|
-
* A backend whose optional dependency is missing is reported as "not
|
|
25
|
-
measured", never as a blank or a zero.
|
|
26
|
-
* The candidate cap is lifted by default (``--max-candidates 0``). With the
|
|
27
|
-
product default of 10 000 the exact scan silently becomes "the newest 10 000
|
|
28
|
-
rows" on a larger index, and recall@k would then be measured against a
|
|
29
|
-
baseline that never looked at most of the corpus — which reads as "the ANN
|
|
30
|
-
backend is inaccurate" when the truth is that the two searched different
|
|
31
|
-
candidate sets. The ``trunc`` column reports whether any query still hit a
|
|
32
|
-
cap.
|
|
33
|
-
* Latency and memory are measured in separate passes: tracemalloc roughly
|
|
34
|
-
triples Python allocation cost, so timing under it would measure the
|
|
35
|
-
profiler. The ``peak MB`` column comes from one extra traced query.
|
|
36
|
-
|
|
37
|
-
Usage::
|
|
38
|
-
|
|
39
|
-
.venv/bin/python scripts/bench_vector_index.py # 10k vectors
|
|
40
|
-
.venv/bin/python scripts/bench_vector_index.py --nodes 1000 # quick
|
|
41
|
-
.venv/bin/python scripts/bench_vector_index.py --json # machine-readable
|
|
42
|
-
"""
|
|
43
|
-
|
|
44
|
-
from __future__ import annotations
|
|
45
|
-
|
|
46
|
-
import argparse
|
|
47
|
-
import json
|
|
48
|
-
import os
|
|
49
|
-
import random
|
|
50
|
-
import statistics
|
|
51
|
-
import sys
|
|
52
|
-
import tempfile
|
|
53
|
-
import time
|
|
54
|
-
import tracemalloc
|
|
55
|
-
from pathlib import Path
|
|
56
|
-
from typing import Any, Dict, List, Optional
|
|
57
|
-
|
|
58
|
-
ROOT = Path(__file__).resolve().parent.parent
|
|
59
|
-
if str(ROOT) not in sys.path:
|
|
60
|
-
sys.path.insert(0, str(ROOT))
|
|
61
|
-
|
|
62
|
-
from lattice_brain.graph.retrieval_vector import ( # noqa: E402
|
|
63
|
-
VECTOR_MAX_CANDIDATES_ENV,
|
|
64
|
-
)
|
|
65
|
-
from lattice_brain.graph.store import KnowledgeGraphStore # noqa: E402
|
|
66
|
-
from lattice_brain.graph.vector_index import ( # noqa: E402
|
|
67
|
-
VECTOR_INDEX_ENV,
|
|
68
|
-
hnswlib_available,
|
|
69
|
-
sidecar_paths,
|
|
70
|
-
)
|
|
71
|
-
from lattice_brain.utils import utc_now_iso # noqa: E402
|
|
72
|
-
|
|
73
|
-
VOCABULARY = [
|
|
74
|
-
"retrieval", "embedding", "vector", "index", "graph", "ingestion",
|
|
75
|
-
"provenance", "workspace", "chunk", "similarity", "cosine", "quantize",
|
|
76
|
-
"sqlite", "postgres", "backup", "restore", "agent", "proposal", "review",
|
|
77
|
-
"latency", "throughput", "recall", "precision", "fusion", "lexical",
|
|
78
|
-
"지식", "그래프", "검색", "임베딩", "인덱스", "문서", "요약", "회의",
|
|
79
|
-
]
|
|
80
|
-
BACKENDS = ("brute", "quantized", "hnsw")
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def _document(rng: random.Random, index: int) -> str:
|
|
84
|
-
words = [rng.choice(VOCABULARY) for _ in range(rng.randint(12, 60))]
|
|
85
|
-
return f"note {index} " + " ".join(words)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def build_corpus(store: KnowledgeGraphStore, count: int, seed: int) -> float:
|
|
89
|
-
"""Write ``count`` synthetic nodes + embeddings. Returns wall seconds."""
|
|
90
|
-
rng = random.Random(seed) # noqa: S311 — synthetic corpus, not a secret
|
|
91
|
-
model = store._embedding_model
|
|
92
|
-
started = time.perf_counter()
|
|
93
|
-
now = utc_now_iso()
|
|
94
|
-
with store._connect() as conn:
|
|
95
|
-
for index in range(count):
|
|
96
|
-
node_id = f"note:{index:06d}"
|
|
97
|
-
text = _document(rng, index)
|
|
98
|
-
conn.execute(
|
|
99
|
-
"INSERT OR REPLACE INTO nodes(id, type, title, summary, "
|
|
100
|
-
"metadata_json, raw_json, created_at, updated_at) "
|
|
101
|
-
"VALUES (?, 'Document', ?, ?, '{}', '{}', ?, ?)",
|
|
102
|
-
(node_id, f"Note {index}", text[:300], now, now),
|
|
103
|
-
)
|
|
104
|
-
conn.execute(
|
|
105
|
-
"INSERT OR REPLACE INTO vector_embeddings(item_id, item_type, "
|
|
106
|
-
"source_node, text_hash, embedding, embedding_dim, "
|
|
107
|
-
"embedding_model, metadata_json, indexed_at) "
|
|
108
|
-
"VALUES (?, 'node', ?, ?, ?, ?, ?, '{}', ?)",
|
|
109
|
-
(
|
|
110
|
-
node_id,
|
|
111
|
-
node_id,
|
|
112
|
-
f"hash-{index}",
|
|
113
|
-
model.encode(model.embed(text)),
|
|
114
|
-
int(model.dim),
|
|
115
|
-
model.model_id,
|
|
116
|
-
now,
|
|
117
|
-
),
|
|
118
|
-
)
|
|
119
|
-
return time.perf_counter() - started
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def _queries(rng: random.Random, count: int) -> List[str]:
|
|
123
|
-
return [
|
|
124
|
-
" ".join(rng.choice(VOCABULARY) for _ in range(rng.randint(2, 5)))
|
|
125
|
-
for _ in range(count)
|
|
126
|
-
]
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def _percentiles(samples: List[float]) -> Dict[str, float]:
|
|
130
|
-
ordered = sorted(samples)
|
|
131
|
-
return {
|
|
132
|
-
"p50_ms": round(statistics.median(ordered) * 1000, 2),
|
|
133
|
-
"p95_ms": round(ordered[max(0, int(len(ordered) * 0.95) - 1)] * 1000, 2),
|
|
134
|
-
"mean_ms": round(statistics.fmean(ordered) * 1000, 2),
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def measure(
|
|
139
|
-
store: KnowledgeGraphStore,
|
|
140
|
-
backend: str,
|
|
141
|
-
queries: List[str],
|
|
142
|
-
*,
|
|
143
|
-
top_k: int,
|
|
144
|
-
baseline: Optional[Dict[str, List[str]]],
|
|
145
|
-
) -> Dict[str, Any]:
|
|
146
|
-
"""Run every query against ``backend``; return latency + recall + hits."""
|
|
147
|
-
os.environ[VECTOR_INDEX_ENV] = backend
|
|
148
|
-
selection = store._vector_index_selection()
|
|
149
|
-
if not selection.honored:
|
|
150
|
-
return {"backend": backend, "measured": False, "detail": selection.detail}
|
|
151
|
-
|
|
152
|
-
# Warm the sidecar/graph so the reported percentiles are steady-state.
|
|
153
|
-
warm_started = time.perf_counter()
|
|
154
|
-
store.vector_search(queries[0], limit=top_k)
|
|
155
|
-
warm_ms = round((time.perf_counter() - warm_started) * 1000, 2)
|
|
156
|
-
|
|
157
|
-
# Latency first, *untraced*: tracemalloc roughly triples Python allocation
|
|
158
|
-
# cost, and a timing table where one column was measured under a profiler
|
|
159
|
-
# would compare backends against the profiler rather than each other.
|
|
160
|
-
hits: Dict[str, List[str]] = {}
|
|
161
|
-
samples: List[float] = []
|
|
162
|
-
truncated = False
|
|
163
|
-
for query in queries:
|
|
164
|
-
started = time.perf_counter()
|
|
165
|
-
payload = store.vector_search(query, limit=top_k)
|
|
166
|
-
samples.append(time.perf_counter() - started)
|
|
167
|
-
hits[query] = [match["id"] for match in payload["matches"]]
|
|
168
|
-
truncated = truncated or bool(payload["recall"]["truncated"])
|
|
169
|
-
|
|
170
|
-
# Memory in its own pass — one query is enough to see the peak.
|
|
171
|
-
tracemalloc.start()
|
|
172
|
-
store.vector_search(queries[0], limit=top_k)
|
|
173
|
-
peak = tracemalloc.get_traced_memory()[1]
|
|
174
|
-
tracemalloc.stop()
|
|
175
|
-
|
|
176
|
-
report: Dict[str, Any] = {
|
|
177
|
-
"backend": backend,
|
|
178
|
-
"measured": True,
|
|
179
|
-
"engine": selection.backend,
|
|
180
|
-
"approx": selection.approx,
|
|
181
|
-
"first_query_ms": warm_ms,
|
|
182
|
-
"candidates_truncated": truncated,
|
|
183
|
-
"peak_mb": round(peak / (1024 * 1024), 2),
|
|
184
|
-
**_percentiles(samples),
|
|
185
|
-
}
|
|
186
|
-
if baseline is None:
|
|
187
|
-
report[f"recall_at_{top_k}"] = 1.0 # this run *is* the ground truth
|
|
188
|
-
else:
|
|
189
|
-
scores = [
|
|
190
|
-
len(set(hits[query]) & set(baseline[query])) / max(1, len(baseline[query]))
|
|
191
|
-
for query in queries
|
|
192
|
-
]
|
|
193
|
-
report[f"recall_at_{top_k}"] = round(statistics.fmean(scores), 4)
|
|
194
|
-
# The product-level number: hybrid_search on the same backend, because
|
|
195
|
-
# that is what a user's question actually runs through.
|
|
196
|
-
hybrid: List[float] = []
|
|
197
|
-
for query in queries:
|
|
198
|
-
started = time.perf_counter()
|
|
199
|
-
store.hybrid_search(query, top_k=10)
|
|
200
|
-
hybrid.append(time.perf_counter() - started)
|
|
201
|
-
hybrid_stats = _percentiles(hybrid)
|
|
202
|
-
report["hybrid_p50_ms"] = hybrid_stats["p50_ms"]
|
|
203
|
-
report["hybrid_p95_ms"] = hybrid_stats["p95_ms"]
|
|
204
|
-
report["_hits"] = hits
|
|
205
|
-
return report
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
def render(report: Dict[str, Any]) -> str:
|
|
209
|
-
lines = [
|
|
210
|
-
f"vectors: {report['nodes']} queries: {report['queries']} "
|
|
211
|
-
f"top_k: {report['top_k']} max_candidates: {report['max_candidates']}"
|
|
212
|
-
f" corpus build: {report['build_seconds']}s",
|
|
213
|
-
"",
|
|
214
|
-
f"{'backend':<12}{'p50 ms':>10}{'p95 ms':>10}{'first ms':>10}"
|
|
215
|
-
f"{'peak MB':>10}{'recall@k':>10}{'trunc':>8}{'hybrid p50':>12}",
|
|
216
|
-
"-" * 82,
|
|
217
|
-
]
|
|
218
|
-
recall_key = "recall_at_%d" % report["top_k"]
|
|
219
|
-
for row in report["backends"]:
|
|
220
|
-
name = row["backend"]
|
|
221
|
-
if not row["measured"]:
|
|
222
|
-
lines.append(f"{name:<12} not measured — {row['detail']}")
|
|
223
|
-
continue
|
|
224
|
-
lines.append(
|
|
225
|
-
f"{name:<12}{row['p50_ms']:>10}{row['p95_ms']:>10}"
|
|
226
|
-
f"{row['first_query_ms']:>10}{row['peak_mb']:>10}"
|
|
227
|
-
f"{row[recall_key]:>10}"
|
|
228
|
-
f"{('yes' if row['candidates_truncated'] else 'no'):>8}"
|
|
229
|
-
f"{row['hybrid_p50_ms']:>12}"
|
|
230
|
-
)
|
|
231
|
-
return "\n".join(lines)
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
def main(argv: Optional[List[str]] = None) -> int:
|
|
235
|
-
parser = argparse.ArgumentParser(description=__doc__)
|
|
236
|
-
parser.add_argument("--nodes", type=int, default=10_000)
|
|
237
|
-
parser.add_argument("--queries", type=int, default=50)
|
|
238
|
-
parser.add_argument("--top-k", type=int, default=10)
|
|
239
|
-
parser.add_argument("--seed", type=int, default=42)
|
|
240
|
-
parser.add_argument(
|
|
241
|
-
"--max-candidates",
|
|
242
|
-
type=int,
|
|
243
|
-
default=0,
|
|
244
|
-
help="candidate cap for every backend (0 = uncapped, comparable recall)",
|
|
245
|
-
)
|
|
246
|
-
parser.add_argument("--db-path", default=None, help="keep the database around")
|
|
247
|
-
parser.add_argument("--json", action="store_true")
|
|
248
|
-
args = parser.parse_args(argv)
|
|
249
|
-
|
|
250
|
-
os.environ[VECTOR_MAX_CANDIDATES_ENV] = str(args.max_candidates)
|
|
251
|
-
workdir = Path(args.db_path) if args.db_path else Path(tempfile.mkdtemp())
|
|
252
|
-
workdir.mkdir(parents=True, exist_ok=True)
|
|
253
|
-
store = KnowledgeGraphStore(workdir / "bench.sqlite", workdir / "blobs")
|
|
254
|
-
build_seconds = build_corpus(store, args.nodes, args.seed)
|
|
255
|
-
queries = _queries(
|
|
256
|
-
random.Random(args.seed + 1), # noqa: S311 — synthetic queries
|
|
257
|
-
args.queries,
|
|
258
|
-
)
|
|
259
|
-
|
|
260
|
-
rows: List[Dict[str, Any]] = []
|
|
261
|
-
baseline: Optional[Dict[str, List[str]]] = None
|
|
262
|
-
for backend in BACKENDS:
|
|
263
|
-
if backend == "hnsw" and not hnswlib_available():
|
|
264
|
-
rows.append(
|
|
265
|
-
{
|
|
266
|
-
"backend": backend,
|
|
267
|
-
"measured": False,
|
|
268
|
-
"detail": "hnswlib is not installed (pip install 'ltcai[hnsw]')",
|
|
269
|
-
}
|
|
270
|
-
)
|
|
271
|
-
continue
|
|
272
|
-
row = measure(store, backend, queries, top_k=args.top_k, baseline=baseline)
|
|
273
|
-
if baseline is None and row["measured"]:
|
|
274
|
-
baseline = row["_hits"]
|
|
275
|
-
row.pop("_hits", None)
|
|
276
|
-
rows.append(row)
|
|
277
|
-
|
|
278
|
-
os.environ.pop(VECTOR_INDEX_ENV, None)
|
|
279
|
-
report = {
|
|
280
|
-
"nodes": args.nodes,
|
|
281
|
-
"queries": args.queries,
|
|
282
|
-
"top_k": args.top_k,
|
|
283
|
-
"max_candidates": args.max_candidates,
|
|
284
|
-
"seed": args.seed,
|
|
285
|
-
"build_seconds": round(build_seconds, 2),
|
|
286
|
-
"db_path": str(store.db_path),
|
|
287
|
-
"sidecar": str(sidecar_paths(store.db_path)[0]),
|
|
288
|
-
"backends": rows,
|
|
289
|
-
}
|
|
290
|
-
print(json.dumps(report, indent=2) if args.json else render(report))
|
|
291
|
-
return 0
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if __name__ == "__main__":
|
|
295
|
-
raise SystemExit(main())
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Funnel soft gate (review Wave 3.3) — funnel metrics as an advisory verdict.
|
|
3
|
-
|
|
4
|
-
Reads the same ``funnel_metrics.json`` production writes and grades it through
|
|
5
|
-
``FunnelMetricsService.snapshot()`` — no rate logic is duplicated here. Two
|
|
6
|
-
funnel-health rates carry WARN thresholds:
|
|
7
|
-
|
|
8
|
-
* ``code_only_rate`` — WARN above 0.05 (goal: >95% of recognized file
|
|
9
|
-
requests deliver a real artifact, not a code/prose-only answer);
|
|
10
|
-
* ``needs_review_rate`` — WARN above 0.30 (verifier fail-closed is healthy,
|
|
11
|
-
but a third of runs ending NEEDS_REVIEW means verification is starving).
|
|
12
|
-
|
|
13
|
-
Rates whose denominator is still zero come back ``None`` from the snapshot;
|
|
14
|
-
they are reported as "no data" and never warn — a fresh install stays green.
|
|
15
|
-
``real_file_rate``, ``approval_resume_rate`` and TTFV are shown as
|
|
16
|
-
informational context only.
|
|
17
|
-
|
|
18
|
-
Exit codes
|
|
19
|
-
==========
|
|
20
|
-
* default — advisory: ALWAYS ``0``; warnings are printed, not enforced;
|
|
21
|
-
* ``--strict`` — CI opt-in: ``1`` when any WARN fired ("no data" never
|
|
22
|
-
fails, even under --strict);
|
|
23
|
-
* bad flags — argparse's usual ``2`` (the only real script error).
|
|
24
|
-
|
|
25
|
-
Usage
|
|
26
|
-
=====
|
|
27
|
-
.venv/bin/python scripts/funnel_soft_gate.py # advisory report
|
|
28
|
-
.venv/bin/python scripts/funnel_soft_gate.py --strict # CI gate opt-in
|
|
29
|
-
.venv/bin/python scripts/funnel_soft_gate.py --json # machine output
|
|
30
|
-
.venv/bin/python scripts/funnel_soft_gate.py --path /tmp/m.json
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
-
from __future__ import annotations
|
|
34
|
-
|
|
35
|
-
import argparse
|
|
36
|
-
import json
|
|
37
|
-
import os
|
|
38
|
-
import sys
|
|
39
|
-
from pathlib import Path
|
|
40
|
-
from typing import Any, Dict, List, Optional
|
|
41
|
-
|
|
42
|
-
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
43
|
-
|
|
44
|
-
from latticeai.services.funnel_metrics import FunnelMetricsService # noqa: E402
|
|
45
|
-
|
|
46
|
-
# Advisory thresholds (overridable per run via flags). WARN fires strictly
|
|
47
|
-
# ABOVE the threshold, so a rate sitting exactly on the goal boundary passes.
|
|
48
|
-
CODE_ONLY_WARN_THRESHOLD = 0.05
|
|
49
|
-
NEEDS_REVIEW_WARN_THRESHOLD = 0.30
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def default_metrics_path() -> Path:
|
|
53
|
-
"""The exact file production writes: ``Config.from_env`` data-dir pattern
|
|
54
|
-
(``LATTICEAI_DATA_DIR`` env or ``~/.ltcai``) + the ``funnel_metrics.json``
|
|
55
|
-
filename from ``latticeai/runtime/persistence_runtime.py``."""
|
|
56
|
-
data_dir = Path(os.environ.get("LATTICEAI_DATA_DIR") or (Path.home() / ".ltcai"))
|
|
57
|
-
return data_dir / "funnel_metrics.json"
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def evaluate_snapshot(
|
|
61
|
-
snapshot: Dict[str, Any],
|
|
62
|
-
*,
|
|
63
|
-
code_only_warn: float = CODE_ONLY_WARN_THRESHOLD,
|
|
64
|
-
needs_review_warn: float = NEEDS_REVIEW_WARN_THRESHOLD,
|
|
65
|
-
) -> Dict[str, Any]:
|
|
66
|
-
"""Grade a ``FunnelMetricsService.snapshot()`` payload into verdicts."""
|
|
67
|
-
rates = snapshot.get("rates") or {}
|
|
68
|
-
checks: List[Dict[str, Any]] = []
|
|
69
|
-
|
|
70
|
-
def check(name: str, threshold: float, goal: str) -> None:
|
|
71
|
-
rate: Optional[float] = rates.get(name)
|
|
72
|
-
if rate is None:
|
|
73
|
-
verdict, detail = "no_data", "no denominator recorded yet"
|
|
74
|
-
elif rate > threshold:
|
|
75
|
-
verdict, detail = "warn", f"{rate} > {threshold} — {goal}"
|
|
76
|
-
else:
|
|
77
|
-
verdict, detail = "ok", f"{rate} <= {threshold}"
|
|
78
|
-
checks.append({
|
|
79
|
-
"name": name, "rate": rate, "threshold": threshold,
|
|
80
|
-
"verdict": verdict, "detail": detail,
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
check("code_only_rate", code_only_warn,
|
|
84
|
-
"goal: >95% of file requests deliver real files")
|
|
85
|
-
check("needs_review_rate", needs_review_warn,
|
|
86
|
-
"goal: <30% of agent runs end NEEDS_REVIEW")
|
|
87
|
-
|
|
88
|
-
warnings = [c["name"] for c in checks if c["verdict"] == "warn"]
|
|
89
|
-
if warnings:
|
|
90
|
-
status = "warn"
|
|
91
|
-
elif all(c["verdict"] == "no_data" for c in checks):
|
|
92
|
-
status = "no_data"
|
|
93
|
-
else:
|
|
94
|
-
status = "ok"
|
|
95
|
-
return {
|
|
96
|
-
"status": status,
|
|
97
|
-
"warnings": warnings,
|
|
98
|
-
"checks": checks,
|
|
99
|
-
# Informational context — never gated.
|
|
100
|
-
"info": {
|
|
101
|
-
"real_file_rate": rates.get("real_file_rate"),
|
|
102
|
-
"approval_resume_rate": rates.get("approval_resume_rate"),
|
|
103
|
-
"ttfv_seconds": snapshot.get("ttfv_seconds"),
|
|
104
|
-
"counters": snapshot.get("counters") or {},
|
|
105
|
-
},
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def format_report(payload: Dict[str, Any]) -> str:
|
|
110
|
-
lines = [
|
|
111
|
-
f"Funnel soft gate (advisory) — {payload['path']}",
|
|
112
|
-
"=" * 72,
|
|
113
|
-
]
|
|
114
|
-
for check in payload["checks"]:
|
|
115
|
-
rate = "no data" if check["rate"] is None else str(check["rate"])
|
|
116
|
-
verdict = {"warn": "WARN", "ok": "ok", "no_data": "-"}[check["verdict"]]
|
|
117
|
-
lines.append(
|
|
118
|
-
f" {check['name']:<20} {rate:<8} {verdict:<5} {check['detail']}"
|
|
119
|
-
)
|
|
120
|
-
info = payload["info"]
|
|
121
|
-
lines.append("-" * 72)
|
|
122
|
-
lines.append(
|
|
123
|
-
f" info: real_file_rate={info['real_file_rate']} "
|
|
124
|
-
f"approval_resume_rate={info['approval_resume_rate']} "
|
|
125
|
-
f"ttfv_seconds={info['ttfv_seconds']}"
|
|
126
|
-
)
|
|
127
|
-
lines.append("=" * 72)
|
|
128
|
-
if payload["status"] == "warn":
|
|
129
|
-
mode = (
|
|
130
|
-
"STRICT — exiting 1" if payload["strict"]
|
|
131
|
-
else "advisory — exit 0; opt into enforcement with --strict"
|
|
132
|
-
)
|
|
133
|
-
lines.append(f"verdict: WARN ({', '.join(payload['warnings'])}) [{mode}]")
|
|
134
|
-
elif payload["status"] == "no_data":
|
|
135
|
-
lines.append("verdict: no data yet — nothing to grade (exit 0)")
|
|
136
|
-
else:
|
|
137
|
-
lines.append("verdict: OK — funnel rates within goals")
|
|
138
|
-
return "\n".join(lines)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def main(argv: List[str] | None = None) -> int:
|
|
142
|
-
parser = argparse.ArgumentParser(
|
|
143
|
-
description="Advisory funnel-metrics gate (warn on funnel-health regressions)"
|
|
144
|
-
)
|
|
145
|
-
parser.add_argument(
|
|
146
|
-
"--path",
|
|
147
|
-
help="metrics JSON path (default: the production data-dir funnel_metrics.json)",
|
|
148
|
-
)
|
|
149
|
-
parser.add_argument(
|
|
150
|
-
"--strict", action="store_true",
|
|
151
|
-
help="exit 1 when any WARN fired (CI opt-in; 'no data' still exits 0)",
|
|
152
|
-
)
|
|
153
|
-
parser.add_argument(
|
|
154
|
-
"--json", dest="json_out", action="store_true",
|
|
155
|
-
help="print the machine-readable verdict JSON to stdout",
|
|
156
|
-
)
|
|
157
|
-
parser.add_argument(
|
|
158
|
-
"--code-only-warn", type=float, default=CODE_ONLY_WARN_THRESHOLD,
|
|
159
|
-
help=f"code_only_rate WARN threshold (default {CODE_ONLY_WARN_THRESHOLD})",
|
|
160
|
-
)
|
|
161
|
-
parser.add_argument(
|
|
162
|
-
"--needs-review-warn", type=float, default=NEEDS_REVIEW_WARN_THRESHOLD,
|
|
163
|
-
help=f"needs_review_rate WARN threshold (default {NEEDS_REVIEW_WARN_THRESHOLD})",
|
|
164
|
-
)
|
|
165
|
-
args = parser.parse_args(argv)
|
|
166
|
-
|
|
167
|
-
path = Path(args.path) if args.path else default_metrics_path()
|
|
168
|
-
# A missing or corrupt file yields the service's zeroed state → all rates
|
|
169
|
-
# None → "no data" (never a crash, never a failure).
|
|
170
|
-
snapshot = FunnelMetricsService(path).snapshot()
|
|
171
|
-
result = evaluate_snapshot(
|
|
172
|
-
snapshot,
|
|
173
|
-
code_only_warn=args.code_only_warn,
|
|
174
|
-
needs_review_warn=args.needs_review_warn,
|
|
175
|
-
)
|
|
176
|
-
exit_code = 1 if (args.strict and result["status"] == "warn") else 0
|
|
177
|
-
payload = {
|
|
178
|
-
"mode": "funnel-soft-gate",
|
|
179
|
-
"path": str(path),
|
|
180
|
-
"strict": bool(args.strict),
|
|
181
|
-
"exit_code": exit_code,
|
|
182
|
-
**result,
|
|
183
|
-
}
|
|
184
|
-
if args.json_out:
|
|
185
|
-
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
186
|
-
else:
|
|
187
|
-
print(format_report(payload))
|
|
188
|
-
return exit_code
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if __name__ == "__main__":
|
|
192
|
-
raise SystemExit(main())
|