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,258 +0,0 @@
|
|
|
1
|
-
"""What the Brain would tell you if you asked "what's going on in there?"
|
|
2
|
-
|
|
3
|
-
Three read-only views over the same graph slice, each answering a different
|
|
4
|
-
shape of that question:
|
|
5
|
-
|
|
6
|
-
* :meth:`insights` — the proactive digest: recent growth, trending types, stale
|
|
7
|
-
knowledge, orphans, and questions grounded in real node titles;
|
|
8
|
-
* :meth:`garden_overview` — the same knowledge as four beds a gardener tends
|
|
9
|
-
(new growth, weeds, neglect, load-bearing), because "how healthy is my
|
|
10
|
-
knowledge?" in aggregate is not a question anyone acts on;
|
|
11
|
-
* :meth:`graph_duplicates` / :meth:`quality_report` — the graph layer's own
|
|
12
|
-
proactive scans, surfaced without applying anything.
|
|
13
|
-
|
|
14
|
-
Honest when empty: an unavailable graph produces empty beds and
|
|
15
|
-
``available: False``, never invented plants.
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
from __future__ import annotations
|
|
19
|
-
|
|
20
|
-
from datetime import datetime, timedelta, timezone
|
|
21
|
-
from typing import Any, Dict, List, Optional
|
|
22
|
-
|
|
23
|
-
from latticeai.core.timeutil import now_iso as _now
|
|
24
|
-
|
|
25
|
-
from ._contract import BrainIntelligenceCore as _Core
|
|
26
|
-
from .constants import _RECENT_DAYS, _STALE_DAYS, LOGGER, _parse_ts
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class BrainDigestMixin(_Core):
|
|
30
|
-
"""Insights, garden, and the graph-layer quality reads."""
|
|
31
|
-
|
|
32
|
-
# ── insights digest ──────────────────────────────────────────────────
|
|
33
|
-
|
|
34
|
-
def insights(
|
|
35
|
-
self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None
|
|
36
|
-
) -> Dict[str, Any]:
|
|
37
|
-
sample = self._graph_sample(workspace_id=workspace_id)
|
|
38
|
-
nodes, edges = sample["nodes"], sample["edges"]
|
|
39
|
-
now = datetime.now(timezone.utc)
|
|
40
|
-
recent_cutoff = now - timedelta(days=_RECENT_DAYS)
|
|
41
|
-
stale_cutoff = now - timedelta(days=_STALE_DAYS)
|
|
42
|
-
|
|
43
|
-
recent_nodes: List[Dict[str, Any]] = []
|
|
44
|
-
stale_nodes: List[Dict[str, Any]] = []
|
|
45
|
-
type_counts: Dict[str, int] = {}
|
|
46
|
-
for node in nodes:
|
|
47
|
-
node_type = str(node.get("type") or "node")
|
|
48
|
-
ts = _parse_ts(node.get("updated_at"))
|
|
49
|
-
if ts is not None and ts >= recent_cutoff:
|
|
50
|
-
recent_nodes.append(node)
|
|
51
|
-
type_counts[node_type] = type_counts.get(node_type, 0) + 1
|
|
52
|
-
elif ts is not None and ts < stale_cutoff:
|
|
53
|
-
stale_nodes.append(node)
|
|
54
|
-
|
|
55
|
-
connected = set()
|
|
56
|
-
for edge in edges:
|
|
57
|
-
connected.add(str(edge.get("source") or edge.get("from_node") or ""))
|
|
58
|
-
connected.add(str(edge.get("target") or edge.get("to_node") or ""))
|
|
59
|
-
orphans = [n for n in nodes if str(n.get("id")) not in connected]
|
|
60
|
-
|
|
61
|
-
def _slim(node: Dict[str, Any]) -> Dict[str, Any]:
|
|
62
|
-
return {
|
|
63
|
-
"id": node.get("id"),
|
|
64
|
-
"type": node.get("type"),
|
|
65
|
-
"title": str(node.get("title") or "")[:120],
|
|
66
|
-
"updated_at": node.get("updated_at"),
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
trending = sorted(type_counts.items(), key=lambda kv: kv[1], reverse=True)[:5]
|
|
70
|
-
suggested_questions = [
|
|
71
|
-
f"{str(node.get('title') or '').strip()[:60]}에 대해 지금까지 알고 있는 것을 정리해줘"
|
|
72
|
-
for node in recent_nodes[:3]
|
|
73
|
-
if str(node.get("title") or "").strip()
|
|
74
|
-
]
|
|
75
|
-
|
|
76
|
-
return {
|
|
77
|
-
"window_days": _RECENT_DAYS,
|
|
78
|
-
"activity": {
|
|
79
|
-
"recent_nodes": len(recent_nodes),
|
|
80
|
-
"recent_samples": [_slim(n) for n in recent_nodes[:8]],
|
|
81
|
-
"trending_types": [{"type": t, "count": c} for t, c in trending],
|
|
82
|
-
},
|
|
83
|
-
"attention": {
|
|
84
|
-
"stale_nodes": len(stale_nodes),
|
|
85
|
-
"stale_samples": [_slim(n) for n in stale_nodes[:8]],
|
|
86
|
-
"orphan_nodes": len(orphans),
|
|
87
|
-
"orphan_samples": [_slim(n) for n in orphans[:8]],
|
|
88
|
-
},
|
|
89
|
-
"suggested_questions": suggested_questions,
|
|
90
|
-
"graph_available": sample["available"],
|
|
91
|
-
"generated_at": _now(),
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
def garden_overview(
|
|
95
|
-
self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None,
|
|
96
|
-
limit: int = 8,
|
|
97
|
-
) -> Dict[str, Any]:
|
|
98
|
-
"""The knowledge garden in four beds (v9.9.7).
|
|
99
|
-
|
|
100
|
-
Living Brain answers "how healthy is my knowledge?" in aggregate.
|
|
101
|
-
A gardener asks four concrete questions instead, and this answers all
|
|
102
|
-
four from one workspace-scoped graph sample plus the memory tier:
|
|
103
|
-
|
|
104
|
-
* **recent** — what came in lately (the garden's new growth);
|
|
105
|
-
* **contradictions** — what disagrees with itself (needs weeding);
|
|
106
|
-
* **stale** — what has not been touched in a long time;
|
|
107
|
-
* **frequent** — what the rest of the graph leans on most (by degree).
|
|
108
|
-
|
|
109
|
-
Read-only and honest: when the graph is unavailable every bed is empty
|
|
110
|
-
and ``available`` is false — the view never invents plants.
|
|
111
|
-
"""
|
|
112
|
-
# Explicit 0 clamps to 1 — `limit or 8` would silently re-expand it.
|
|
113
|
-
try:
|
|
114
|
-
limit = max(1, min(int(limit), 50))
|
|
115
|
-
except (TypeError, ValueError):
|
|
116
|
-
limit = 8
|
|
117
|
-
sample = self._graph_sample(workspace_id=workspace_id)
|
|
118
|
-
nodes, edges = sample["nodes"], sample["edges"]
|
|
119
|
-
now = datetime.now(timezone.utc)
|
|
120
|
-
recent_cutoff = now - timedelta(days=_RECENT_DAYS)
|
|
121
|
-
stale_cutoff = now - timedelta(days=_STALE_DAYS)
|
|
122
|
-
|
|
123
|
-
def _slim(node: Dict[str, Any], **extra: Any) -> Dict[str, Any]:
|
|
124
|
-
return {
|
|
125
|
-
"id": node.get("id"),
|
|
126
|
-
"type": node.get("type"),
|
|
127
|
-
"title": str(node.get("title") or "")[:120],
|
|
128
|
-
"updated_at": node.get("updated_at"),
|
|
129
|
-
**extra,
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
recent: List[Dict[str, Any]] = []
|
|
133
|
-
stale: List[Dict[str, Any]] = []
|
|
134
|
-
for node in nodes:
|
|
135
|
-
# Chunks are retrieval plumbing, not knowledge a gardener tends.
|
|
136
|
-
if str(node.get("type") or "") == "Chunk":
|
|
137
|
-
continue
|
|
138
|
-
ts = _parse_ts(node.get("updated_at"))
|
|
139
|
-
if ts is None:
|
|
140
|
-
continue
|
|
141
|
-
if ts >= recent_cutoff:
|
|
142
|
-
recent.append(node)
|
|
143
|
-
elif ts < stale_cutoff:
|
|
144
|
-
stale.append(node)
|
|
145
|
-
recent.sort(key=lambda n: str(n.get("updated_at") or ""), reverse=True)
|
|
146
|
-
stale.sort(key=lambda n: str(n.get("updated_at") or ""))
|
|
147
|
-
|
|
148
|
-
# "Frequent" is degree, not a guess: how many relations actually point
|
|
149
|
-
# at a node. Chunks are retrieval plumbing, never garden plants.
|
|
150
|
-
degree: Dict[str, int] = {}
|
|
151
|
-
for edge in edges:
|
|
152
|
-
for key in ("source", "target"):
|
|
153
|
-
node_id = str(edge.get(key) or "")
|
|
154
|
-
if node_id:
|
|
155
|
-
degree[node_id] = degree.get(node_id, 0) + 1
|
|
156
|
-
by_id = {str(node.get("id")): node for node in nodes}
|
|
157
|
-
frequent = [
|
|
158
|
-
_slim(by_id[node_id], degree=count)
|
|
159
|
-
for node_id, count in sorted(degree.items(), key=lambda kv: kv[1], reverse=True)
|
|
160
|
-
if node_id in by_id and str(by_id[node_id].get("type") or "") != "Chunk"
|
|
161
|
-
][:limit]
|
|
162
|
-
|
|
163
|
-
contradiction_items: List[Dict[str, Any]] = []
|
|
164
|
-
contradiction_count = 0
|
|
165
|
-
try:
|
|
166
|
-
found = self.contradictions(user_email=user_email, workspace_id=workspace_id)
|
|
167
|
-
items = found.get("items") if isinstance(found, dict) else None
|
|
168
|
-
if isinstance(items, list):
|
|
169
|
-
contradiction_count = len(items)
|
|
170
|
-
contradiction_items = items[:limit]
|
|
171
|
-
except Exception: # noqa: BLE001 — one empty bed, never a broken view
|
|
172
|
-
LOGGER.exception("garden overview contradictions failed")
|
|
173
|
-
|
|
174
|
-
return {
|
|
175
|
-
"available": sample["available"],
|
|
176
|
-
"window_days": _RECENT_DAYS,
|
|
177
|
-
"stale_threshold_days": _STALE_DAYS,
|
|
178
|
-
"beds": {
|
|
179
|
-
"recent": {
|
|
180
|
-
"count": len(recent),
|
|
181
|
-
"items": [_slim(node) for node in recent[:limit]],
|
|
182
|
-
},
|
|
183
|
-
"contradictions": {
|
|
184
|
-
"count": contradiction_count,
|
|
185
|
-
"items": contradiction_items,
|
|
186
|
-
},
|
|
187
|
-
"stale": {
|
|
188
|
-
"count": len(stale),
|
|
189
|
-
"items": [_slim(node) for node in stale[:limit]],
|
|
190
|
-
},
|
|
191
|
-
"frequent": {
|
|
192
|
-
"count": len(frequent),
|
|
193
|
-
"items": frequent,
|
|
194
|
-
},
|
|
195
|
-
},
|
|
196
|
-
"generated_at": _now(),
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
# ── graph-layer proactive quality (v9.6.x) ───────────────────────────
|
|
200
|
-
|
|
201
|
-
def graph_duplicates(
|
|
202
|
-
self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None
|
|
203
|
-
) -> Dict[str, Any]:
|
|
204
|
-
"""Duplicate graph nodes (exact groups + near pairs) — read only."""
|
|
205
|
-
proactive = self._proactive()
|
|
206
|
-
if proactive is None:
|
|
207
|
-
return {
|
|
208
|
-
"available": False,
|
|
209
|
-
"exact_groups": [],
|
|
210
|
-
"near_pairs": [],
|
|
211
|
-
"exact_duplicate_nodes": 0,
|
|
212
|
-
"nodes_scanned": 0,
|
|
213
|
-
"generated_at": _now(),
|
|
214
|
-
}
|
|
215
|
-
try:
|
|
216
|
-
result = dict(proactive.find_duplicates(workspace_id=workspace_id))
|
|
217
|
-
except Exception as exc:
|
|
218
|
-
LOGGER.exception("graph duplicates scan failed")
|
|
219
|
-
return {
|
|
220
|
-
"available": False,
|
|
221
|
-
"error": str(exc),
|
|
222
|
-
"exact_groups": [],
|
|
223
|
-
"near_pairs": [],
|
|
224
|
-
"exact_duplicate_nodes": 0,
|
|
225
|
-
"nodes_scanned": 0,
|
|
226
|
-
"generated_at": _now(),
|
|
227
|
-
}
|
|
228
|
-
result["available"] = True
|
|
229
|
-
result["generated_at"] = _now()
|
|
230
|
-
return result
|
|
231
|
-
|
|
232
|
-
def quality_report(
|
|
233
|
-
self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None
|
|
234
|
-
) -> Dict[str, Any]:
|
|
235
|
-
"""Combined graph quality report: duplicates, contradictions, stale
|
|
236
|
-
nodes, edge quality — one workspace-scoped graph sample."""
|
|
237
|
-
proactive = self._proactive()
|
|
238
|
-
if proactive is None:
|
|
239
|
-
return {"available": False, "generated_at": _now()}
|
|
240
|
-
try:
|
|
241
|
-
result = dict(proactive.quality_report(workspace_id=workspace_id))
|
|
242
|
-
except Exception as exc:
|
|
243
|
-
LOGGER.exception("graph quality report failed")
|
|
244
|
-
return {"available": False, "error": str(exc), "generated_at": _now()}
|
|
245
|
-
result["available"] = True
|
|
246
|
-
# v11.1.0: decay is part of quality, and "the Brain is tidying up" is a
|
|
247
|
-
# state the user is entitled to see rather than a background surprise.
|
|
248
|
-
importance = self.importance_report(
|
|
249
|
-
user_email=user_email, workspace_id=workspace_id
|
|
250
|
-
)
|
|
251
|
-
candidates = len(importance.get("candidates") or [])
|
|
252
|
-
result["importance"] = importance
|
|
253
|
-
result["tidying"] = bool(importance.get("available")) and candidates > 0
|
|
254
|
-
summary = dict(result.get("summary") or {})
|
|
255
|
-
summary["consolidation_candidates"] = candidates
|
|
256
|
-
result["summary"] = summary
|
|
257
|
-
result["generated_at"] = _now()
|
|
258
|
-
return result
|
|
@@ -1,331 +0,0 @@
|
|
|
1
|
-
"""The scored diagnosis, and the vector index's freshness.
|
|
2
|
-
|
|
3
|
-
:meth:`health_report` grades four dimensions — freshness, connectivity,
|
|
4
|
-
embedding coverage, consistency — from the live stores, and is deliberate about
|
|
5
|
-
what it refuses to grade. A dimension it cannot measure scores ``None`` with a
|
|
6
|
-
stated reason rather than a flattering number (an empty index covers 100% of
|
|
7
|
-
nothing; scoring that as a perfect 100 is how a brand-new Brain used to grade
|
|
8
|
-
itself "excellent"). The composite reports its own ``coverage``, and when
|
|
9
|
-
nothing could be measured it says why — the 9.9.7 rule that a "—" always states
|
|
10
|
-
its reason.
|
|
11
|
-
|
|
12
|
-
:meth:`vector_freshness` is a fixed four-key contract that never raises, plus
|
|
13
|
-
an additive ``breakdown`` when the store can split "never embedded" from
|
|
14
|
-
"embedded but stale".
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
from __future__ import annotations
|
|
18
|
-
|
|
19
|
-
from datetime import datetime, timedelta, timezone
|
|
20
|
-
from typing import Any, Dict, List, Optional
|
|
21
|
-
|
|
22
|
-
from latticeai.core.timeutil import now_iso as _now
|
|
23
|
-
|
|
24
|
-
from ._contract import BrainIntelligenceCore as _Core
|
|
25
|
-
from .constants import _STALE_DAYS, LOGGER, _no_graph_reason, _parse_ts
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class BrainHealthMixin(_Core):
|
|
29
|
-
"""Health report + vector freshness. Mixed into the service."""
|
|
30
|
-
|
|
31
|
-
# ── health report ─────────────────────────────────────────────────────
|
|
32
|
-
|
|
33
|
-
def health_report(
|
|
34
|
-
self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None
|
|
35
|
-
) -> Dict[str, Any]:
|
|
36
|
-
sample = self._graph_sample(workspace_id=workspace_id)
|
|
37
|
-
nodes, edges = sample["nodes"], sample["edges"]
|
|
38
|
-
now = datetime.now(timezone.utc)
|
|
39
|
-
dimensions: Dict[str, Dict[str, Any]] = {}
|
|
40
|
-
|
|
41
|
-
# Freshness — how much of the sampled knowledge saw recent updates.
|
|
42
|
-
if sample["available"] and nodes:
|
|
43
|
-
stale_cutoff = now - timedelta(days=_STALE_DAYS)
|
|
44
|
-
dated = [(_parse_ts(n.get("updated_at")), n) for n in nodes]
|
|
45
|
-
known = [pair for pair in dated if pair[0] is not None]
|
|
46
|
-
stale = [n for ts, n in known if ts is not None and ts < stale_cutoff]
|
|
47
|
-
fresh_ratio = 1.0 - (len(stale) / len(known)) if known else 0.0
|
|
48
|
-
dimensions["freshness"] = {
|
|
49
|
-
"status": "ok",
|
|
50
|
-
"score": round(fresh_ratio * 100),
|
|
51
|
-
"sampled": len(nodes),
|
|
52
|
-
"stale_nodes": len(stale),
|
|
53
|
-
"stale_threshold_days": _STALE_DAYS,
|
|
54
|
-
}
|
|
55
|
-
else:
|
|
56
|
-
dimensions["freshness"] = {
|
|
57
|
-
"status": "unavailable",
|
|
58
|
-
"score": None,
|
|
59
|
-
"reason": _no_graph_reason(sample["available"]),
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
# Connectivity — orphan nodes are knowledge the Brain cannot reason
|
|
63
|
-
# across; a well-tended graph keeps them rare.
|
|
64
|
-
if sample["available"] and nodes:
|
|
65
|
-
connected = set()
|
|
66
|
-
for edge in edges:
|
|
67
|
-
connected.add(str(edge.get("source") or edge.get("from_node") or ""))
|
|
68
|
-
connected.add(str(edge.get("target") or edge.get("to_node") or ""))
|
|
69
|
-
orphans = [n for n in nodes if str(n.get("id")) not in connected]
|
|
70
|
-
ratio = 1.0 - (len(orphans) / len(nodes))
|
|
71
|
-
dimensions["connectivity"] = {
|
|
72
|
-
"status": "ok",
|
|
73
|
-
"score": round(ratio * 100),
|
|
74
|
-
"sampled": len(nodes),
|
|
75
|
-
"orphan_nodes": len(orphans),
|
|
76
|
-
"edges": len(edges),
|
|
77
|
-
}
|
|
78
|
-
else:
|
|
79
|
-
dimensions["connectivity"] = {
|
|
80
|
-
"status": "unavailable",
|
|
81
|
-
"score": None,
|
|
82
|
-
"reason": _no_graph_reason(sample["available"]),
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
# Embedding coverage — semantic recall only works for indexed items.
|
|
86
|
-
index_status: Dict[str, Any] = {}
|
|
87
|
-
if self._enable_graph and hasattr(self._kg, "index_status"):
|
|
88
|
-
try:
|
|
89
|
-
index_status = self._kg.index_status()
|
|
90
|
-
except Exception as exc:
|
|
91
|
-
LOGGER.exception("brain intelligence index status failed")
|
|
92
|
-
index_status = {"error": str(exc)}
|
|
93
|
-
scale = index_status.get("scale") or {}
|
|
94
|
-
indexable = scale.get("source_items", index_status.get("source_items"))
|
|
95
|
-
if "coverage_ratio" not in scale:
|
|
96
|
-
dimensions["embedding_coverage"] = {
|
|
97
|
-
"status": "unavailable",
|
|
98
|
-
"score": None,
|
|
99
|
-
"reason": "this knowledge store does not report vector index coverage",
|
|
100
|
-
}
|
|
101
|
-
elif indexable == 0:
|
|
102
|
-
# An empty index covers 100% of nothing. Scoring that as a perfect
|
|
103
|
-
# 100 is how a brand-new Brain used to grade itself "excellent"
|
|
104
|
-
# off its only measurable dimension (audit v11.2.0, Finding 3).
|
|
105
|
-
dimensions["embedding_coverage"] = {
|
|
106
|
-
"status": "unavailable",
|
|
107
|
-
"score": None,
|
|
108
|
-
"reason": "no indexable items yet",
|
|
109
|
-
}
|
|
110
|
-
else:
|
|
111
|
-
dimensions["embedding_coverage"] = {
|
|
112
|
-
"status": "ok",
|
|
113
|
-
"score": round(float(scale["coverage_ratio"]) * 100),
|
|
114
|
-
"ready_items": scale.get("ready_items"),
|
|
115
|
-
"pending_items": scale.get("pending_items"),
|
|
116
|
-
"needs_reindex": index_status.get("status") == "needs_reindex",
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
# Edge quality + contradiction pressure — reuses the quality layer.
|
|
120
|
-
if sample["available"] and edges:
|
|
121
|
-
metrics = self._edge_quality.compute_quality_metrics(edges)
|
|
122
|
-
contradiction_edges = [
|
|
123
|
-
e for e in edges if "CONTRADICT" in str(e.get("type") or "").upper()
|
|
124
|
-
]
|
|
125
|
-
pressure = min(1.0, metrics.get("dup_rate", 0.0) + len(contradiction_edges) / max(len(edges), 1))
|
|
126
|
-
dimensions["consistency"] = {
|
|
127
|
-
"status": "ok",
|
|
128
|
-
"score": round((1.0 - pressure) * 100),
|
|
129
|
-
"edge_metrics": metrics,
|
|
130
|
-
"contradiction_edges": len(contradiction_edges),
|
|
131
|
-
}
|
|
132
|
-
else:
|
|
133
|
-
dimensions["consistency"] = {
|
|
134
|
-
"status": "unavailable",
|
|
135
|
-
"score": None,
|
|
136
|
-
"reason": (
|
|
137
|
-
_no_graph_reason(sample["available"])
|
|
138
|
-
if not (sample["available"] and nodes)
|
|
139
|
-
else "no relationships recorded yet"
|
|
140
|
-
),
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
scores = [d["score"] for d in dimensions.values() if d.get("score") is not None]
|
|
144
|
-
overall = round(sum(scores) / len(scores)) if scores else None
|
|
145
|
-
grade = (
|
|
146
|
-
None if overall is None
|
|
147
|
-
else "excellent" if overall >= 85
|
|
148
|
-
else "good" if overall >= 70
|
|
149
|
-
else "attention" if overall >= 50
|
|
150
|
-
else "critical"
|
|
151
|
-
)
|
|
152
|
-
# What the verdict rests on. A composite averages only what could be
|
|
153
|
-
# measured, so the count of measured dimensions is part of the answer
|
|
154
|
-
# rather than a footnote — and when nothing could be measured the
|
|
155
|
-
# report says so instead of leaving a bare null (the 9.9.7 rule: a
|
|
156
|
-
# "—" always states why).
|
|
157
|
-
unmeasured = sorted(
|
|
158
|
-
name for name, dim in dimensions.items() if dim.get("score") is None
|
|
159
|
-
)
|
|
160
|
-
coverage: Dict[str, Any] = {
|
|
161
|
-
"measured": len(scores),
|
|
162
|
-
"total": len(dimensions),
|
|
163
|
-
"unavailable": unmeasured,
|
|
164
|
-
"partial": bool(unmeasured),
|
|
165
|
-
}
|
|
166
|
-
reason: Optional[str] = None
|
|
167
|
-
if overall is None:
|
|
168
|
-
reason = (
|
|
169
|
-
"no health dimension could be measured yet — "
|
|
170
|
-
+ "; ".join(
|
|
171
|
-
f"{name}: {dimensions[name].get('reason') or 'unavailable'}"
|
|
172
|
-
for name in unmeasured
|
|
173
|
-
)
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
actions: List[Dict[str, str]] = []
|
|
177
|
-
emb = dimensions["embedding_coverage"]
|
|
178
|
-
if emb.get("needs_reindex"):
|
|
179
|
-
actions.append({
|
|
180
|
-
"id": "rebuild_vector_index",
|
|
181
|
-
"reason": f"{emb.get('pending_items', 0)} items are missing or stale in the vector index.",
|
|
182
|
-
})
|
|
183
|
-
conn_dim = dimensions["connectivity"]
|
|
184
|
-
conn_score = conn_dim.get("score")
|
|
185
|
-
if conn_score is not None and conn_score < 70:
|
|
186
|
-
actions.append({
|
|
187
|
-
"id": "review_orphans",
|
|
188
|
-
"reason": f"{conn_dim.get('orphan_nodes', 0)} nodes have no relationships.",
|
|
189
|
-
})
|
|
190
|
-
fresh_dim = dimensions["freshness"]
|
|
191
|
-
fresh_score = fresh_dim.get("score")
|
|
192
|
-
if fresh_score is not None and fresh_score < 60:
|
|
193
|
-
actions.append({
|
|
194
|
-
"id": "refresh_stale_knowledge",
|
|
195
|
-
"reason": f"{fresh_dim.get('stale_nodes', 0)} nodes untouched for over {_STALE_DAYS} days.",
|
|
196
|
-
})
|
|
197
|
-
cons_dim = dimensions["consistency"]
|
|
198
|
-
if cons_dim.get("contradiction_edges"):
|
|
199
|
-
actions.append({
|
|
200
|
-
"id": "resolve_contradictions",
|
|
201
|
-
"reason": f"{cons_dim['contradiction_edges']} contradiction edges recorded in the graph.",
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
report: Dict[str, Any] = {
|
|
205
|
-
"overall_score": overall,
|
|
206
|
-
"grade": grade,
|
|
207
|
-
"dimensions": dimensions,
|
|
208
|
-
"coverage": coverage,
|
|
209
|
-
"recommended_actions": actions,
|
|
210
|
-
"graph_available": sample["available"],
|
|
211
|
-
"generated_at": _now(),
|
|
212
|
-
}
|
|
213
|
-
if reason is not None:
|
|
214
|
-
report["reason"] = reason
|
|
215
|
-
return report
|
|
216
|
-
|
|
217
|
-
# ── vector freshness (v9.8.0) ────────────────────────────────────────
|
|
218
|
-
|
|
219
|
-
def vector_freshness(
|
|
220
|
-
self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None
|
|
221
|
-
) -> Dict[str, Any]:
|
|
222
|
-
"""Fixed-contract vector index freshness for ``/api/brain/vector-freshness``.
|
|
223
|
-
|
|
224
|
-
Always returns ``{"status": "ready"|"pending"|"unavailable",
|
|
225
|
-
"pending_items": int, "total_items": int, "detail": str}`` and never
|
|
226
|
-
raises. The vector index is store-global (not workspace-partitioned);
|
|
227
|
-
scope arguments are accepted for router symmetry but do not narrow
|
|
228
|
-
the report.
|
|
229
|
-
|
|
230
|
-
Since v11.2.0 a store that can split its backlog also gets a
|
|
231
|
-
``breakdown`` key (see :meth:`_freshness_breakdown`). It is additive:
|
|
232
|
-
the four keys above keep their meaning and their types, so the
|
|
233
|
-
freshness chip that reads ``pending_items`` is untouched, and a store
|
|
234
|
-
without the split simply has no ``breakdown``.
|
|
235
|
-
"""
|
|
236
|
-
payload = self._vector_freshness_contract()
|
|
237
|
-
breakdown = self._freshness_breakdown()
|
|
238
|
-
if breakdown is not None:
|
|
239
|
-
payload["breakdown"] = breakdown
|
|
240
|
-
return payload
|
|
241
|
-
|
|
242
|
-
def _freshness_breakdown(self) -> Optional[Dict[str, Any]]:
|
|
243
|
-
"""``vector_freshness_breakdown()`` from the store, or ``None``.
|
|
244
|
-
|
|
245
|
-
"12 pending" hides two different situations — twelve items never
|
|
246
|
-
embedded (a new import) and twelve whose text changed under an
|
|
247
|
-
existing embedding (edits, where current answers are quietly wrong).
|
|
248
|
-
The store has always known the difference; until now nothing asked it.
|
|
249
|
-
|
|
250
|
-
``None`` covers every reason the split is not available (graph off,
|
|
251
|
-
an older store, an unreadable index), because a caller can only act on
|
|
252
|
-
numbers that were really measured — never on zeros standing in for
|
|
253
|
-
them.
|
|
254
|
-
"""
|
|
255
|
-
if not self._enable_graph or self._kg is None:
|
|
256
|
-
return None
|
|
257
|
-
breakdown_fn = getattr(self._kg, "vector_freshness_breakdown", None)
|
|
258
|
-
if not callable(breakdown_fn):
|
|
259
|
-
return None
|
|
260
|
-
try:
|
|
261
|
-
raw = breakdown_fn()
|
|
262
|
-
except Exception:
|
|
263
|
-
LOGGER.exception("vector freshness breakdown read failed")
|
|
264
|
-
return None
|
|
265
|
-
# An empty or unrecognisable answer is "not measured", which is what
|
|
266
|
-
# ``None`` already means here — publishing an empty block would claim
|
|
267
|
-
# a split nobody computed.
|
|
268
|
-
if not isinstance(raw, dict) or not raw:
|
|
269
|
-
return None
|
|
270
|
-
return dict(raw)
|
|
271
|
-
|
|
272
|
-
def _vector_freshness_contract(self) -> Dict[str, Any]:
|
|
273
|
-
"""The four-key freshness payload, unchanged since v9.8.0."""
|
|
274
|
-
|
|
275
|
-
def _unavailable(detail: str) -> Dict[str, Any]:
|
|
276
|
-
return {
|
|
277
|
-
"status": "unavailable",
|
|
278
|
-
"pending_items": 0,
|
|
279
|
-
"total_items": 0,
|
|
280
|
-
"detail": detail,
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if not self._enable_graph or self._kg is None:
|
|
284
|
-
return _unavailable("knowledge graph is disabled; no vector index is configured")
|
|
285
|
-
|
|
286
|
-
freshness_fn = getattr(self._kg, "vector_freshness", None)
|
|
287
|
-
if callable(freshness_fn):
|
|
288
|
-
try:
|
|
289
|
-
raw = freshness_fn() or {}
|
|
290
|
-
except Exception as exc:
|
|
291
|
-
LOGGER.exception("vector freshness read failed")
|
|
292
|
-
return _unavailable(f"vector freshness read failed: {exc}")
|
|
293
|
-
status = str(raw.get("status") or "unavailable")
|
|
294
|
-
if status == "needs_reindex":
|
|
295
|
-
status = "pending"
|
|
296
|
-
if status not in {"ready", "pending", "unavailable"}:
|
|
297
|
-
status = "unavailable"
|
|
298
|
-
return {
|
|
299
|
-
"status": status,
|
|
300
|
-
"pending_items": int(raw.get("pending_items") or 0),
|
|
301
|
-
"total_items": int(raw.get("total_items") or 0),
|
|
302
|
-
"detail": str(raw.get("detail") or ""),
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
# Older/lighter stores: summarize index_status directly.
|
|
306
|
-
status_fn = getattr(self._kg, "index_status", None)
|
|
307
|
-
if callable(status_fn):
|
|
308
|
-
try:
|
|
309
|
-
raw = status_fn() or {}
|
|
310
|
-
except Exception as exc:
|
|
311
|
-
LOGGER.exception("vector index status read failed")
|
|
312
|
-
return _unavailable(f"vector index status unavailable: {exc}")
|
|
313
|
-
pending = int(raw.get("pending_items") or 0)
|
|
314
|
-
total = int(raw.get("source_items") or 0)
|
|
315
|
-
if pending > 0:
|
|
316
|
-
return {
|
|
317
|
-
"status": "pending",
|
|
318
|
-
"pending_items": pending,
|
|
319
|
-
"total_items": total,
|
|
320
|
-
"detail": (
|
|
321
|
-
f"{pending} of {total} items are missing or stale in the vector index"
|
|
322
|
-
),
|
|
323
|
-
}
|
|
324
|
-
return {
|
|
325
|
-
"status": "ready",
|
|
326
|
-
"pending_items": 0,
|
|
327
|
-
"total_items": total,
|
|
328
|
-
"detail": "vector index is up to date",
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
return _unavailable("this knowledge store does not expose a vector index")
|