ltcai 11.5.2 → 11.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -148
- package/bin/ltcai.js +234 -24
- package/docs/CHANGELOG.md +119 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +8 -6
- package/docs/ENTERPRISE.md +2 -1
- package/docs/MULTI_AGENT_RUNTIME.md +12 -5
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +6 -3
- package/docs/REALTIME_COLLABORATION.md +1 -1
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/WORKFLOW_DESIGNER.md +3 -2
- package/docs/kg-schema.md +1 -1
- package/docs/v11.6.0_ONE_DOOR_PLAN.md +170 -0
- package/lattice_brain/__init__.py +42 -79
- package/lattice_brain/graph/__init__.py +9 -24
- package/lattice_brain/graph/_kg_common/__init__.py +13 -24
- package/lattice_brain/ingestion/__init__.py +19 -58
- package/lattice_brain/ingestion/pipeline.py +20 -398
- package/lattice_brain/multimodal/__init__.py +7 -18
- package/lattice_brain/multimodal/images.py +6 -264
- package/lattice_brain/multimodal/video.py +8 -247
- package/lattice_brain/runtime/__init__.py +8 -79
- package/lattice_brain/runtime/hooks.py +22 -584
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/agent_worker_seam.py +7 -80
- package/latticeai/api/health.py +6 -20
- package/latticeai/api/local_files.py +16 -613
- package/latticeai/api/models.py +12 -87
- package/latticeai/api/search.py +17 -255
- package/latticeai/api/tools.py +69 -750
- package/latticeai/api/voice_capture.py +8 -70
- package/latticeai/api/worker_compute.py +842 -0
- package/latticeai/api/worker_seams.py +216 -0
- package/latticeai/app_factory.py +23 -232
- package/latticeai/cli/entrypoint.py +36 -249
- package/latticeai/core/agent_permission.py +16 -91
- package/latticeai/core/messages.py +89 -531
- package/latticeai/runtime/access_runtime.py +0 -14
- package/latticeai/runtime/bootstrap.py +10 -21
- package/latticeai/runtime/brain_runtime.py +35 -43
- package/latticeai/runtime/build_phases/__init__.py +21 -37
- package/latticeai/runtime/build_phases/features.py +99 -389
- package/latticeai/runtime/build_phases/foundation.py +82 -392
- package/latticeai/runtime/build_phases/web.py +91 -408
- package/latticeai/runtime/build_phases/worker_profile.py +244 -0
- package/latticeai/runtime/lifespan_runtime.py +7 -16
- package/latticeai/runtime/platform_services_runtime.py +1 -15
- package/latticeai/runtime/runtime_context.py +13 -130
- package/latticeai/runtime/security_runtime.py +10 -103
- package/latticeai/services/architecture_readiness.py +82 -43
- package/latticeai/services/model_runtime/__init__.py +2 -11
- package/latticeai/services/model_runtime/service.py +4 -28
- package/latticeai/services/p_reinforce.py +10 -261
- package/latticeai/services/product_readiness.py +31 -38
- package/latticeai/services/search_service.py +37 -795
- package/latticeai/services/tool_dispatch.py +30 -386
- package/latticeai/services/voice_capture.py +13 -107
- package/latticeai/tools/__init__.py +22 -49
- package/latticeai/tools/commands.py +0 -163
- package/latticeai/tools/computer.py +0 -39
- package/latticeai/tools/documents.py +1 -134
- package/latticeai/tools/filesystem.py +1 -247
- package/latticeai/tools/knowledge.py +1 -52
- package/latticeai/tools/local_files.py +0 -20
- package/latticeai/worker_app.py +75 -0
- package/package.json +2 -3
- package/requirements.txt +0 -5
- package/scripts/agent_eval.py +16 -28
- package/scripts/brain_quality_eval.py +20 -183
- package/scripts/bump_version.py +5 -5
- package/scripts/check_current_release_docs.mjs +7 -4
- package/scripts/check_openapi_drift.mjs +10 -0
- package/scripts/check_server_i18n.mjs +2 -18
- package/scripts/compose_openapi.py +377 -0
- package/scripts/export_openapi.py +32 -4
- package/scripts/gen_messages_catalog_fixture.py +302 -0
- package/scripts/gen_openapi_fragments.py +365 -0
- package/scripts/gen_redact_fixture.py +196 -0
- package/scripts/gen_worker_allowlist_fixture.py +115 -0
- package/scripts/generate_agent_parity_fixtures.py +33 -14
- package/scripts/openapi_route_families.json +2161 -0
- package/scripts/release_screen_claims.json +54 -0
- package/scripts/run_integration_tests.mjs +134 -29
- package/scripts/run_sidecar_e2e.mjs +104 -11
- package/scripts/wheel_smoke.py +32 -22
- package/src-tauri/Cargo.lock +375 -9
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/backend.rs +151 -45
- package/src-tauri/src/main.rs +18 -18
- package/src-tauri/src/topology.rs +58 -88
- package/src-tauri/tauri.conf.json +1 -2
- package/static/app/asset-manifest.json +41 -41
- package/static/app/assets/{Act-DcQizkl1.js → Act-BPcVAbOL.js} +1 -1
- package/static/app/assets/AdminConsole-Bw1ATQL0.js +1 -0
- package/static/app/assets/{Brain-3VCSHFcn.js → Brain-CT92Kos0.js} +2 -2
- package/static/app/assets/{BrainHome-Qm8eaztx.js → BrainHome-CFBkt1K_.js} +1 -1
- package/static/app/assets/{BrainSignals-DS9BtKOW.js → BrainSignals-ReLWF2H8.js} +1 -1
- package/static/app/assets/Capture-BsTokYkk.js +1 -0
- package/static/app/assets/{Chronicle-BGvuAchH.js → Chronicle-B6f0T9id.js} +1 -1
- package/static/app/assets/{CommandPalette-Bqhm0Urn.js → CommandPalette-CuvjTv1u.js} +1 -1
- package/static/app/assets/{Library-BV6NnF0a.js → Library-BGJbG9Hd.js} +1 -1
- package/static/app/assets/{LivingBrain-GzenJchP.js → LivingBrain-DGYK_Jsa.js} +1 -1
- package/static/app/assets/{ProductFlow-DEP6-vML.js → ProductFlow-DXBC6brE.js} +1 -1
- package/static/app/assets/{ReviewCard-CNZ7XjWG.js → ReviewCard-HXRle3qq.js} +2 -2
- package/static/app/assets/System-CMHSO9qM.js +1 -0
- package/static/app/assets/arrow-left-BfmkskWx.js +1 -0
- package/static/app/assets/{bot-B_K1Tdmw.js → bot-Cn8bWRuq.js} +1 -1
- package/static/app/assets/{brain-DWyaV1L1.js → brain-CQJberbE.js} +1 -1
- package/static/app/assets/{button-aTn4s84A.js → button-Ct9f2_oT.js} +1 -1
- package/static/app/assets/circle-check-DruOxB-4.js +1 -0
- package/static/app/assets/{circle-pause-xKgeGXkT.js → circle-pause-CmzC_apg.js} +1 -1
- package/static/app/assets/{circle-play-DkT6tYPX.js → circle-play-D8mW2aQ7.js} +1 -1
- package/static/app/assets/{cpu-85xYObUC.js → cpu-DZcdd0PZ.js} +1 -1
- package/static/app/assets/{download-B5Fm7YXo.js → download-bv1KEPGQ.js} +1 -1
- package/static/app/assets/{folder-open-kk2Xa52u.js → folder-open-d-Pip5gr.js} +1 -1
- package/static/app/assets/{hard-drive-DkA3zBW_.js → hard-drive-D20iavUb.js} +1 -1
- package/static/app/assets/index-D9x-kSNy.css +2 -0
- package/static/app/assets/{index-BMPdTmlY.js → index-Do83hDzJ.js} +3 -3
- package/static/app/assets/{input-B0nRf2jO.js → input-BLXVNmj1.js} +1 -1
- package/static/app/assets/{link-2-Dwb4gnTc.js → link-2-BPJOFlAy.js} +1 -1
- package/static/app/assets/{permissionCopy-CQDUBrOZ.js → permissionCopy-ChdJd493.js} +1 -1
- package/static/app/assets/primitives-Cv5tbZBY.js +1 -0
- package/static/app/assets/search-CT9aho2j.js +1 -0
- package/static/app/assets/{share-2-BsrxFglO.js → share-2-YNX_NtMU.js} +1 -1
- package/static/app/assets/{shield-alert-5BStfp2_.js → shield-alert-DuQ3zrVL.js} +1 -1
- package/static/app/assets/{textarea-Cg8IUA-k.js → textarea-DqwLnli4.js} +1 -1
- package/static/app/assets/{useFocusTrap-CYKvE46M.js → useFocusTrap-ZVI98jaW.js} +1 -1
- package/static/app/assets/{useMutation-CSn9t1op.js → useMutation-CVC4qv_D.js} +1 -1
- package/static/app/assets/{useQuery-CY2OI2uy.js → useQuery-C7BeG4HU.js} +1 -1
- package/static/app/assets/{utils-Ddol2RWD.js → utils-CiFtIdZq.js} +1 -1
- package/static/app/assets/{workspace-BqDwOz_p.js → workspace-DQz9vIId.js} +1 -1
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/lattice_brain/archive.py +0 -522
- package/lattice_brain/context.py +0 -325
- package/lattice_brain/conversations.py +0 -382
- package/lattice_brain/core.py +0 -82
- package/lattice_brain/graph/_kg_contract.py +0 -249
- package/lattice_brain/graph/curator.py +0 -676
- package/lattice_brain/graph/discovery.py +0 -595
- package/lattice_brain/graph/discovery_index/__init__.py +0 -35
- package/lattice_brain/graph/discovery_index/cleanup.py +0 -182
- package/lattice_brain/graph/discovery_index/extract.py +0 -137
- package/lattice_brain/graph/discovery_index/scan.py +0 -411
- package/lattice_brain/graph/discovery_index/upsert.py +0 -495
- package/lattice_brain/graph/documents.py +0 -380
- package/lattice_brain/graph/fusion.py +0 -395
- package/lattice_brain/graph/identity.py +0 -175
- package/lattice_brain/graph/image_vectors.py +0 -230
- package/lattice_brain/graph/ingest.py +0 -829
- package/lattice_brain/graph/network.py +0 -205
- package/lattice_brain/graph/proactive.py +0 -724
- package/lattice_brain/graph/projection/__init__.py +0 -42
- package/lattice_brain/graph/projection/curation.py +0 -500
- package/lattice_brain/graph/projection/v2_schema.py +0 -518
- package/lattice_brain/graph/provenance.py +0 -524
- package/lattice_brain/graph/rerank.py +0 -163
- package/lattice_brain/graph/retrieval/__init__.py +0 -54
- package/lattice_brain/graph/retrieval/context.py +0 -197
- package/lattice_brain/graph/retrieval/graph_view.py +0 -319
- package/lattice_brain/graph/retrieval/hybrid.py +0 -488
- package/lattice_brain/graph/retrieval/maintenance.py +0 -121
- package/lattice_brain/graph/retrieval/signals.py +0 -95
- package/lattice_brain/graph/retrieval_docgen.py +0 -253
- package/lattice_brain/graph/retrieval_policy.py +0 -180
- package/lattice_brain/graph/retrieval_reads.py +0 -769
- package/lattice_brain/graph/retrieval_vector/__init__.py +0 -42
- package/lattice_brain/graph/retrieval_vector/fingerprint.py +0 -97
- package/lattice_brain/graph/retrieval_vector/indexing.py +0 -347
- package/lattice_brain/graph/retrieval_vector/search.py +0 -560
- package/lattice_brain/graph/retrieval_vector/status.py +0 -374
- package/lattice_brain/graph/schema.py +0 -792
- package/lattice_brain/graph/store.py +0 -268
- package/lattice_brain/graph/vector_index/__init__.py +0 -85
- package/lattice_brain/graph/vector_index/base.py +0 -167
- package/lattice_brain/graph/vector_index/brute_force.py +0 -110
- package/lattice_brain/graph/vector_index/hnsw.py +0 -290
- package/lattice_brain/graph/vector_index/jobs.py +0 -287
- package/lattice_brain/graph/vector_index/quantized.py +0 -148
- package/lattice_brain/graph/vector_index/selector.py +0 -161
- package/lattice_brain/graph/write_master.py +0 -308
- package/lattice_brain/ingestion/_contract.py +0 -90
- package/lattice_brain/ingestion/folder_scan.py +0 -57
- package/lattice_brain/ingestion/folders.py +0 -258
- package/lattice_brain/ingestion/jobs_api.py +0 -107
- package/lattice_brain/ingestion/routing.py +0 -295
- package/lattice_brain/ingestion_jobs.py +0 -380
- package/lattice_brain/memory.py +0 -75
- package/lattice_brain/portability/__init__.py +0 -90
- package/lattice_brain/portability/_contract.py +0 -42
- package/lattice_brain/portability/backups.py +0 -338
- package/lattice_brain/portability/bundles.py +0 -136
- package/lattice_brain/portability/constants.py +0 -93
- package/lattice_brain/portability/fsops.py +0 -138
- package/lattice_brain/portability/service.py +0 -41
- package/lattice_brain/portability/sharing.py +0 -710
- package/lattice_brain/quality.py +0 -543
- package/lattice_brain/retrieval_benchmark_fixtures.py +0 -92
- package/lattice_brain/runtime/agent_runtime.py +0 -859
- package/lattice_brain/runtime/contracts.py +0 -460
- package/lattice_brain/runtime/multi_agent.py +0 -942
- package/lattice_brain/runtime/statuses.py +0 -10
- package/lattice_brain/sealed_box.py +0 -240
- package/lattice_brain/self_model.py +0 -675
- package/lattice_brain/sensitivity.py +0 -94
- package/lattice_brain/storage/__init__.py +0 -22
- package/lattice_brain/storage/base.py +0 -100
- package/lattice_brain/storage/docker.py +0 -105
- package/lattice_brain/storage/factory.py +0 -31
- package/lattice_brain/storage/migration.py +0 -191
- package/lattice_brain/storage/postgres.py +0 -123
- package/lattice_brain/storage/sqlite.py +0 -143
- package/lattice_brain/synthesis.py +0 -824
- package/lattice_brain/workflow.py +0 -497
- package/latticeai/api/admin.py +0 -471
- package/latticeai/api/agent_registry.py +0 -105
- package/latticeai/api/agents.py +0 -228
- package/latticeai/api/auth.py +0 -383
- package/latticeai/api/automation_intelligence.py +0 -401
- package/latticeai/api/brain_intelligence.py +0 -199
- package/latticeai/api/browser.py +0 -493
- package/latticeai/api/change_proposals.py +0 -89
- package/latticeai/api/chat.py +0 -572
- package/latticeai/api/chat_agent_http.py +0 -892
- package/latticeai/api/chat_contracts.py +0 -73
- package/latticeai/api/chat_documents.py +0 -276
- package/latticeai/api/chat_helpers.py +0 -460
- package/latticeai/api/chat_history.py +0 -101
- package/latticeai/api/chat_hybrid.py +0 -113
- package/latticeai/api/chat_intents.py +0 -646
- package/latticeai/api/chat_stream.py +0 -216
- package/latticeai/api/chronicle.py +0 -63
- package/latticeai/api/command_center.py +0 -51
- package/latticeai/api/computer_use.py +0 -474
- package/latticeai/api/evidence_actions.py +0 -48
- package/latticeai/api/features.py +0 -70
- package/latticeai/api/funnel_metrics.py +0 -31
- package/latticeai/api/garden.py +0 -34
- package/latticeai/api/hooks.py +0 -165
- package/latticeai/api/index_jobs.py +0 -145
- package/latticeai/api/invitations.py +0 -100
- package/latticeai/api/knowledge_graph.py +0 -536
- package/latticeai/api/marketplace.py +0 -105
- package/latticeai/api/mcp.py +0 -482
- package/latticeai/api/memory.py +0 -270
- package/latticeai/api/network.py +0 -81
- package/latticeai/api/network_boundary.py +0 -225
- package/latticeai/api/permission_mode.py +0 -61
- package/latticeai/api/permissions.py +0 -436
- package/latticeai/api/plugins.py +0 -126
- package/latticeai/api/portability.py +0 -391
- package/latticeai/api/project_sessions.py +0 -114
- package/latticeai/api/realtime.py +0 -118
- package/latticeai/api/review_queue.py +0 -364
- package/latticeai/api/security_dashboard.py +0 -604
- package/latticeai/api/setup.py +0 -319
- package/latticeai/api/static_routes.py +0 -354
- package/latticeai/api/ui_redirects.py +0 -26
- package/latticeai/api/workflow_designer.py +0 -394
- package/latticeai/api/workspace.py +0 -856
- package/latticeai/api/workspace_scope.py +0 -125
- package/latticeai/core/agent/__init__.py +0 -93
- package/latticeai/core/agent/_contract.py +0 -79
- package/latticeai/core/agent/context.py +0 -57
- package/latticeai/core/agent/deps.py +0 -125
- package/latticeai/core/agent/execution.py +0 -622
- package/latticeai/core/agent/planning.py +0 -145
- package/latticeai/core/agent/recovery.py +0 -157
- package/latticeai/core/agent/runtime.py +0 -210
- package/latticeai/core/agent/verification.py +0 -231
- package/latticeai/core/agent_eval.py +0 -739
- package/latticeai/core/agent_helpers.py +0 -493
- package/latticeai/core/agent_profiles.py +0 -110
- package/latticeai/core/agent_prompts.py +0 -171
- package/latticeai/core/agent_registry.py +0 -232
- package/latticeai/core/agent_state.py +0 -41
- package/latticeai/core/agent_trace.py +0 -104
- package/latticeai/core/artifact_ledger.py +0 -109
- package/latticeai/core/audit.py +0 -260
- package/latticeai/core/builtin_hooks.py +0 -105
- package/latticeai/core/context_builder.py +0 -394
- package/latticeai/core/document_generator.py +0 -103
- package/latticeai/core/enterprise.py +0 -154
- package/latticeai/core/enterprise_admin.py +0 -158
- package/latticeai/core/file_generation/__init__.py +0 -115
- package/latticeai/core/file_generation/bundles.py +0 -76
- package/latticeai/core/file_generation/extraction.py +0 -154
- package/latticeai/core/file_generation/inference.py +0 -235
- package/latticeai/core/file_generation/orchestration.py +0 -152
- package/latticeai/core/file_generation/prompting.py +0 -117
- package/latticeai/core/file_generation/repair.py +0 -114
- package/latticeai/core/file_generation/sanitize.py +0 -61
- package/latticeai/core/file_generation/validation.py +0 -201
- package/latticeai/core/invitations.py +0 -132
- package/latticeai/core/legacy_compatibility.py +0 -243
- package/latticeai/core/logging_safety.py +0 -46
- package/latticeai/core/marketplace.py +0 -293
- package/latticeai/core/mcp_catalog.py +0 -452
- package/latticeai/core/mcp_registry.py +0 -506
- package/latticeai/core/network_boundary.py +0 -168
- package/latticeai/core/oidc.py +0 -208
- package/latticeai/core/plugins.py +0 -432
- package/latticeai/core/product_hardening.py +0 -218
- package/latticeai/core/project_sessions.py +0 -337
- package/latticeai/core/realtime.py +0 -238
- package/latticeai/core/run_explain.py +0 -426
- package/latticeai/core/run_store.py +0 -252
- package/latticeai/core/timezones.py +0 -80
- package/latticeai/core/workspace_computer_memory.py +0 -84
- package/latticeai/core/workspace_graph_trace.py +0 -155
- package/latticeai/core/workspace_indexing.py +0 -102
- package/latticeai/core/workspace_memory.py +0 -77
- package/latticeai/core/workspace_onboarding.py +0 -104
- package/latticeai/core/workspace_os.py +0 -978
- package/latticeai/core/workspace_os_constants.py +0 -126
- package/latticeai/core/workspace_os_state.py +0 -180
- package/latticeai/core/workspace_os_utils.py +0 -103
- package/latticeai/core/workspace_permissions.py +0 -101
- package/latticeai/core/workspace_plugins.py +0 -97
- package/latticeai/core/workspace_relationships.py +0 -99
- package/latticeai/core/workspace_reorganization.py +0 -335
- package/latticeai/core/workspace_review_items.py +0 -112
- package/latticeai/core/workspace_runs.py +0 -726
- package/latticeai/core/workspace_skills.py +0 -109
- package/latticeai/core/workspace_snapshots.py +0 -198
- package/latticeai/core/workspace_timeline.py +0 -110
- package/latticeai/integrations/__init__.py +0 -0
- package/latticeai/integrations/telegram_bot/__init__.py +0 -123
- package/latticeai/integrations/telegram_bot/__main__.py +0 -17
- package/latticeai/integrations/telegram_bot/config.py +0 -86
- package/latticeai/integrations/telegram_bot/dispatch.py +0 -311
- package/latticeai/integrations/telegram_bot/flows.py +0 -478
- package/latticeai/integrations/telegram_bot/helpers.py +0 -322
- package/latticeai/integrations/telegram_bot/screens.py +0 -394
- package/latticeai/runtime/audit_runtime.py +0 -76
- package/latticeai/runtime/automation_runtime.py +0 -81
- package/latticeai/runtime/chat_wiring.py +0 -141
- package/latticeai/runtime/context_runtime.py +0 -66
- package/latticeai/runtime/feature_toggle_wiring.py +0 -157
- package/latticeai/runtime/history_runtime.py +0 -163
- package/latticeai/runtime/history_writer.py +0 -138
- package/latticeai/runtime/hooks_runtime.py +0 -77
- package/latticeai/runtime/model_wiring.py +0 -68
- package/latticeai/runtime/namespace_runtime.py +0 -163
- package/latticeai/runtime/network_boundary_wiring.py +0 -117
- package/latticeai/runtime/network_config_runtime.py +0 -56
- package/latticeai/runtime/permission_mode_wiring.py +0 -112
- package/latticeai/runtime/persistence_runtime.py +0 -159
- package/latticeai/runtime/platform_runtime_wiring.py +0 -89
- package/latticeai/runtime/review_wiring.py +0 -42
- package/latticeai/runtime/router_registration.py +0 -693
- package/latticeai/runtime/service_singletons.py +0 -55
- package/latticeai/runtime/sso_config_runtime.py +0 -128
- package/latticeai/runtime/user_key_runtime.py +0 -106
- package/latticeai/runtime/web_runtime.py +0 -92
- package/latticeai/server_app.py +0 -51
- package/latticeai/services/app_context.py +0 -130
- package/latticeai/services/automation_execution.py +0 -266
- package/latticeai/services/automation_intelligence.py +0 -614
- package/latticeai/services/brain_automation.py +0 -191
- package/latticeai/services/brain_intelligence/__init__.py +0 -58
- package/latticeai/services/brain_intelligence/_contract.py +0 -71
- package/latticeai/services/brain_intelligence/consistency.py +0 -193
- package/latticeai/services/brain_intelligence/constants.py +0 -47
- package/latticeai/services/brain_intelligence/digest.py +0 -258
- package/latticeai/services/brain_intelligence/health.py +0 -331
- package/latticeai/services/brain_intelligence/proposals.py +0 -259
- package/latticeai/services/brain_intelligence/sampling.py +0 -84
- package/latticeai/services/brain_intelligence/service.py +0 -48
- package/latticeai/services/change_proposals.py +0 -471
- package/latticeai/services/chat_service.py +0 -243
- package/latticeai/services/chronicle.py +0 -555
- package/latticeai/services/cloud_egress_audit.py +0 -85
- package/latticeai/services/cloud_extraction.py +0 -129
- package/latticeai/services/cloud_streaming.py +0 -268
- package/latticeai/services/cloud_token_guard.py +0 -84
- package/latticeai/services/command_center.py +0 -548
- package/latticeai/services/evidence_actions.py +0 -258
- package/latticeai/services/feature_toggles.py +0 -502
- package/latticeai/services/folder_watch.py +0 -520
- package/latticeai/services/funnel_metrics.py +0 -307
- package/latticeai/services/hybrid_chat.py +0 -316
- package/latticeai/services/hybrid_context.py +0 -228
- package/latticeai/services/hybrid_policy.py +0 -129
- package/latticeai/services/interop_bridges.py +0 -978
- package/latticeai/services/local_knowledge.py +0 -465
- package/latticeai/services/memory_service/__init__.py +0 -52
- package/latticeai/services/memory_service/_contract.py +0 -100
- package/latticeai/services/memory_service/brief.py +0 -431
- package/latticeai/services/memory_service/constants.py +0 -57
- package/latticeai/services/memory_service/maintenance.py +0 -138
- package/latticeai/services/memory_service/manager.py +0 -186
- package/latticeai/services/memory_service/proof.py +0 -136
- package/latticeai/services/memory_service/recall.py +0 -225
- package/latticeai/services/memory_service/service.py +0 -48
- package/latticeai/services/memory_service/stores.py +0 -110
- package/latticeai/services/mode_store.py +0 -132
- package/latticeai/services/model_recommendation.py +0 -224
- package/latticeai/services/model_runtime/cloud.py +0 -87
- package/latticeai/services/network_boundary_service.py +0 -117
- package/latticeai/services/obsidian_bridge.py +0 -609
- package/latticeai/services/openai_compatible_adapter.py +0 -101
- package/latticeai/services/permission_mode_service.py +0 -122
- package/latticeai/services/platform_runtime.py +0 -366
- package/latticeai/services/review_queue.py +0 -380
- package/latticeai/services/router_context.py +0 -59
- package/latticeai/services/run_executor.py +0 -387
- package/latticeai/services/self_model_service.py +0 -171
- package/latticeai/services/setup_detection.py +0 -147
- package/latticeai/services/triggers.py +0 -378
- package/latticeai/services/upload_service.py +0 -172
- package/latticeai/services/workspace_service.py +0 -165
- package/latticeai/setup/__init__.py +0 -25
- package/latticeai/setup/auto_setup.py +0 -846
- package/latticeai/setup/demo_corpus.py +0 -98
- package/latticeai/setup/wizard/__init__.py +0 -126
- package/latticeai/setup/wizard/catalog.py +0 -175
- package/latticeai/setup/wizard/detect.py +0 -302
- package/latticeai/setup/wizard/install.py +0 -348
- package/latticeai/setup/wizard/paths.py +0 -165
- package/latticeai/setup/wizard/plans.py +0 -74
- package/latticeai/setup/wizard/recommend.py +0 -320
- package/scripts/bench_agent_smoke.py +0 -409
- package/scripts/bench_models.py +0 -540
- package/scripts/bench_vector_index.py +0 -295
- package/scripts/funnel_soft_gate.py +0 -192
- package/scripts/generate_agent_loop_fixtures.py +0 -994
- package/scripts/generate_rust_parity_fixtures.py +0 -908
- package/scripts/migrate_brain_storage.py +0 -57
- package/scripts/parity_fixture_corpus_context.py +0 -162
- package/scripts/parity_fixture_corpus_docgen.py +0 -341
- package/scripts/profile_kg.py +0 -355
- package/server.py +0 -30
- package/static/app/assets/AdminConsole-cf4npybT.js +0 -1
- package/static/app/assets/Capture-DiQ219jW.js +0 -1
- package/static/app/assets/System-CieofHQa.js +0 -1
- package/static/app/assets/arrow-left-kfsrk0mv.js +0 -1
- package/static/app/assets/circle-check-qqLug9nU.js +0 -1
- package/static/app/assets/index-DxmOfNRi.css +0 -2
- package/static/app/assets/primitives-SNp0LRJz.js +0 -1
- package/static/app/assets/search-BcHqkjoy.js +0 -1
|
@@ -1,769 +0,0 @@
|
|
|
1
|
-
"""Non-search read surface of the knowledge graph (v9.9.5 decomposition).
|
|
2
|
-
|
|
3
|
-
Byte-identical move-only split out of :mod:`lattice_brain.graph.retrieval`,
|
|
4
|
-
which now keeps the search surface (``search`` / ``hybrid_search`` /
|
|
5
|
-
``context_for_query``). This mixin owns the document listing, workspace
|
|
6
|
-
scoping reads, node/neighbor/relationship lookups, graph traversal, and
|
|
7
|
-
store statistics. Mixins share ``self`` on
|
|
8
|
-
:class:`lattice_brain.graph.store.KnowledgeGraphStore`, so cross-mixin
|
|
9
|
-
calls (e.g. ``search`` → ``filter_scoped_nodes``) behave exactly as before.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
from __future__ import annotations
|
|
13
|
-
|
|
14
|
-
from typing import TYPE_CHECKING
|
|
15
|
-
|
|
16
|
-
from ..quiet import quiet
|
|
17
|
-
|
|
18
|
-
# ruff: noqa: F403,F405
|
|
19
|
-
from ._kg_common import * # noqa: F403,F401
|
|
20
|
-
from .schema import TEMPORAL_PREDICATE_SQL
|
|
21
|
-
|
|
22
|
-
# The cross-mixin surface (`_connect`, `_upsert_node`, …) is declared in
|
|
23
|
-
# `_kg_contract.KnowledgeGraphCore`. It is a typing-only base: at runtime this
|
|
24
|
-
# is `object`, so the MRO of `KnowledgeGraphStore` is unchanged.
|
|
25
|
-
if TYPE_CHECKING:
|
|
26
|
-
from ._kg_contract import KnowledgeGraphCore as _Core
|
|
27
|
-
else:
|
|
28
|
-
_Core = object
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def _as_of_stamp(value: Any) -> str:
|
|
32
|
-
"""Normalize an ``as_of`` argument into the store's own timestamp format.
|
|
33
|
-
|
|
34
|
-
The store writes naive local ISO-8601 seconds (``lattice_brain.utils.now_iso``),
|
|
35
|
-
so validity comparisons are plain lexicographic string comparisons against
|
|
36
|
-
that one format. An aware datetime is converted to local time first rather
|
|
37
|
-
than compared with an offset suffix the stored rows never carry.
|
|
38
|
-
"""
|
|
39
|
-
if isinstance(value, datetime):
|
|
40
|
-
moment = value.astimezone().replace(tzinfo=None) if value.tzinfo else value
|
|
41
|
-
return moment.isoformat(timespec="seconds")
|
|
42
|
-
text = str(value or "").strip()
|
|
43
|
-
if not text:
|
|
44
|
-
raise ValueError("as_of requires a timestamp")
|
|
45
|
-
return text
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def _record_access(db_path: Any, node_id: str) -> None:
|
|
49
|
-
"""Record one access on a node (the importance/decay input).
|
|
50
|
-
|
|
51
|
-
Runs on its own short transaction, deliberately *outside* the read that
|
|
52
|
-
triggered it: promoting every node read to a writer would let a concurrent
|
|
53
|
-
ingest's write lock fail an otherwise perfectly good read. The counter is a
|
|
54
|
-
nice-to-have, so every sqlite failure — a missing row, a missing column, a
|
|
55
|
-
busy database — is swallowed.
|
|
56
|
-
"""
|
|
57
|
-
if KGStoreV2 is None:
|
|
58
|
-
return
|
|
59
|
-
try:
|
|
60
|
-
KGStoreV2(db_path).touch_node(node_id)
|
|
61
|
-
except sqlite3.Error:
|
|
62
|
-
quiet()
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
class KnowledgeGraphReadsMixin(_Core):
|
|
66
|
-
def list_documents(self, limit: int = 200) -> Dict[str, Any]:
|
|
67
|
-
"""List ingested ``Document`` nodes with their ingest + index state.
|
|
68
|
-
|
|
69
|
-
Powers the Files view: every accepted upload and every indexed local
|
|
70
|
-
document becomes a ``Document`` node. A document is reported ``indexed``
|
|
71
|
-
once its retrieval chunks exist (searchable in Chat / Hybrid Search).
|
|
72
|
-
"""
|
|
73
|
-
limit = max(1, min(int(limit or 200), 1000))
|
|
74
|
-
nt, _ = self._read_tables()
|
|
75
|
-
documents: List[Dict[str, Any]] = []
|
|
76
|
-
with self._connect() as conn:
|
|
77
|
-
rows = conn.execute(
|
|
78
|
-
f"SELECT id, title, summary, metadata_json, created_at, updated_at "
|
|
79
|
-
f"FROM {nt} WHERE type='Document' ORDER BY updated_at DESC, id ASC LIMIT ?",
|
|
80
|
-
(limit,),
|
|
81
|
-
).fetchall()
|
|
82
|
-
for row in rows:
|
|
83
|
-
meta = _safe_loads(row["metadata_json"]) or {}
|
|
84
|
-
extracted = meta.get("extracted") or {}
|
|
85
|
-
node_id = row["id"]
|
|
86
|
-
chunk_count = conn.execute(
|
|
87
|
-
"SELECT COUNT(*) AS c FROM chunks WHERE source_node=?",
|
|
88
|
-
(node_id,),
|
|
89
|
-
).fetchone()["c"]
|
|
90
|
-
if not chunk_count:
|
|
91
|
-
# Legacy projections represented chunks as graph nodes and
|
|
92
|
-
# linked them only through metadata_json. Keep read
|
|
93
|
-
# compatibility without making the fragile LIKE path the
|
|
94
|
-
# primary query.
|
|
95
|
-
chunk_count = conn.execute(
|
|
96
|
-
f"SELECT COUNT(*) AS c FROM {nt} WHERE type='Chunk' AND metadata_json LIKE ?",
|
|
97
|
-
(f"%{node_id}%",),
|
|
98
|
-
).fetchone()["c"]
|
|
99
|
-
documents.append(
|
|
100
|
-
{
|
|
101
|
-
"id": node_id,
|
|
102
|
-
"filename": meta.get("filename") or row["title"],
|
|
103
|
-
"ext": meta.get("ext"),
|
|
104
|
-
"mime_type": meta.get("mime_type"),
|
|
105
|
-
"bytes": meta.get("bytes"),
|
|
106
|
-
"sha256": meta.get("sha256"),
|
|
107
|
-
"uploader": meta.get("uploader"),
|
|
108
|
-
"chars": extracted.get("chars"),
|
|
109
|
-
"chunks": int(chunk_count or 0),
|
|
110
|
-
"indexed": int(chunk_count or 0) > 0,
|
|
111
|
-
"ingest_state": "indexed"
|
|
112
|
-
if int(chunk_count or 0) > 0
|
|
113
|
-
else "ingested",
|
|
114
|
-
"created_at": row["created_at"],
|
|
115
|
-
"updated_at": row["updated_at"],
|
|
116
|
-
}
|
|
117
|
-
)
|
|
118
|
-
return {
|
|
119
|
-
"documents": documents,
|
|
120
|
-
"total": len(documents),
|
|
121
|
-
"generated_at": datetime.now().isoformat(timespec="seconds"),
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
def workspaces_of(self, node_ids) -> Dict[str, Optional[str]]:
|
|
125
|
-
"""Map known node ids to their workspace scope.
|
|
126
|
-
|
|
127
|
-
``None`` is returned only for a row that is explicitly present in the
|
|
128
|
-
authoritative v2 projection with a NULL workspace. Missing ids remain
|
|
129
|
-
missing, and projection/query failures propagate so callers can fail
|
|
130
|
-
closed instead of mistaking every candidate for legacy-global data.
|
|
131
|
-
"""
|
|
132
|
-
ids = [str(i) for i in node_ids if i]
|
|
133
|
-
if not ids:
|
|
134
|
-
return {}
|
|
135
|
-
placeholders = ",".join("?" for _ in ids)
|
|
136
|
-
with self._connect() as conn:
|
|
137
|
-
return {
|
|
138
|
-
row["id"]: row["workspace_id"]
|
|
139
|
-
for row in conn.execute(
|
|
140
|
-
f"SELECT id, workspace_id FROM nodes_v2 WHERE id IN ({placeholders})",
|
|
141
|
-
ids,
|
|
142
|
-
).fetchall()
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
def filter_scoped_nodes(
|
|
146
|
-
self,
|
|
147
|
-
items,
|
|
148
|
-
allowed_workspaces,
|
|
149
|
-
*,
|
|
150
|
-
id_key: str = "id",
|
|
151
|
-
include_legacy_global: bool = False,
|
|
152
|
-
):
|
|
153
|
-
"""Drop items scoped to a workspace the caller is not a member of.
|
|
154
|
-
|
|
155
|
-
``allowed_workspaces=None`` means no scoping (single-user / no-auth
|
|
156
|
-
mode). In scoped/multi-user mode, unknown ids are private and
|
|
157
|
-
legacy-global rows require the explicit ``include_legacy_global=True``
|
|
158
|
-
compatibility opt-in.
|
|
159
|
-
"""
|
|
160
|
-
candidates = list(items)
|
|
161
|
-
if allowed_workspaces is None:
|
|
162
|
-
return candidates
|
|
163
|
-
allowed = {str(workspace_id) for workspace_id in allowed_workspaces if workspace_id}
|
|
164
|
-
scopes = self.workspaces_of([item.get(id_key) for item in candidates])
|
|
165
|
-
visible = []
|
|
166
|
-
for item in candidates:
|
|
167
|
-
node_id = str(item.get(id_key) or "")
|
|
168
|
-
if not node_id or node_id not in scopes:
|
|
169
|
-
# Unknown/unprojected rows are never treated as public.
|
|
170
|
-
continue
|
|
171
|
-
workspace_id = scopes[node_id]
|
|
172
|
-
if workspace_id is None:
|
|
173
|
-
if include_legacy_global:
|
|
174
|
-
visible.append(item)
|
|
175
|
-
elif str(workspace_id) in allowed:
|
|
176
|
-
visible.append(item)
|
|
177
|
-
return visible
|
|
178
|
-
|
|
179
|
-
@staticmethod
|
|
180
|
-
def _workspace_scope_sql(
|
|
181
|
-
allowed_workspaces,
|
|
182
|
-
include_legacy_global: bool,
|
|
183
|
-
) -> Tuple[Optional[str], List[Any]]:
|
|
184
|
-
"""``nodes_v2`` predicate for a caller's scope, or ``(None, [])``.
|
|
185
|
-
|
|
186
|
-
``None`` means "no scoping" and is the unscoped single-user path.
|
|
187
|
-
An *empty* allowed set is not the same thing: it is a caller who may
|
|
188
|
-
read nothing, so it yields a predicate that matches nothing rather
|
|
189
|
-
than silently degrading into the unscoped query.
|
|
190
|
-
"""
|
|
191
|
-
if allowed_workspaces is None:
|
|
192
|
-
return None, []
|
|
193
|
-
allowed = sorted({str(item) for item in allowed_workspaces if item})
|
|
194
|
-
clauses: List[str] = []
|
|
195
|
-
params: List[Any] = []
|
|
196
|
-
if allowed:
|
|
197
|
-
placeholders = ",".join("?" for _ in allowed)
|
|
198
|
-
clauses.append(f"workspace_id IN ({placeholders})")
|
|
199
|
-
params.extend(allowed)
|
|
200
|
-
if include_legacy_global:
|
|
201
|
-
clauses.append("workspace_id IS NULL")
|
|
202
|
-
if not clauses:
|
|
203
|
-
return "0", []
|
|
204
|
-
return " OR ".join(clauses), params
|
|
205
|
-
|
|
206
|
-
def neighbors(
|
|
207
|
-
self,
|
|
208
|
-
node_id: str,
|
|
209
|
-
*,
|
|
210
|
-
allowed_workspaces=None,
|
|
211
|
-
include_legacy_global: bool = False,
|
|
212
|
-
as_of=None,
|
|
213
|
-
) -> Dict[str, Any]:
|
|
214
|
-
"""Return direct neighbors (1-hop) of a node.
|
|
215
|
-
|
|
216
|
-
``as_of`` is additive and defaults to ``None`` — the historical call
|
|
217
|
-
keeps its exact behaviour. With a timestamp, neighbors whose validity
|
|
218
|
-
window does not cover that instant are dropped (and with them the edges
|
|
219
|
-
that would dangle).
|
|
220
|
-
"""
|
|
221
|
-
if allowed_workspaces is not None and not self.filter_scoped_nodes(
|
|
222
|
-
[{"id": node_id}],
|
|
223
|
-
allowed_workspaces,
|
|
224
|
-
include_legacy_global=include_legacy_global,
|
|
225
|
-
):
|
|
226
|
-
raise ValueError(f"graph node not found: {node_id}")
|
|
227
|
-
stamp = None if as_of is None else _as_of_stamp(as_of)
|
|
228
|
-
nt, et = self._read_tables()
|
|
229
|
-
with self._connect() as conn:
|
|
230
|
-
edge_rows = conn.execute(
|
|
231
|
-
f"SELECT from_node, to_node, type, weight FROM {et} WHERE from_node=? OR to_node=? ORDER BY id ASC",
|
|
232
|
-
(node_id, node_id),
|
|
233
|
-
).fetchall()
|
|
234
|
-
neighbor_ids: set = set()
|
|
235
|
-
edges = []
|
|
236
|
-
for row in edge_rows:
|
|
237
|
-
neighbor_ids.add(row["from_node"])
|
|
238
|
-
neighbor_ids.add(row["to_node"])
|
|
239
|
-
edges.append(
|
|
240
|
-
{
|
|
241
|
-
"from": row["from_node"],
|
|
242
|
-
"to": row["to_node"],
|
|
243
|
-
"type": row["type"],
|
|
244
|
-
"weight": row["weight"],
|
|
245
|
-
}
|
|
246
|
-
)
|
|
247
|
-
neighbor_ids.discard(node_id)
|
|
248
|
-
nodes = []
|
|
249
|
-
if neighbor_ids:
|
|
250
|
-
placeholders = ",".join("?" * len(neighbor_ids))
|
|
251
|
-
nodes = [
|
|
252
|
-
{
|
|
253
|
-
"id": row["id"],
|
|
254
|
-
"type": row["type"],
|
|
255
|
-
"title": row["title"],
|
|
256
|
-
"summary": row["summary"],
|
|
257
|
-
"metadata": _safe_loads(row["metadata_json"]),
|
|
258
|
-
}
|
|
259
|
-
for row in conn.execute(
|
|
260
|
-
f"SELECT id, type, title, summary, metadata_json FROM {nt} WHERE id IN ({placeholders}) ORDER BY id ASC",
|
|
261
|
-
list(neighbor_ids),
|
|
262
|
-
)
|
|
263
|
-
]
|
|
264
|
-
if stamp is not None:
|
|
265
|
-
valid = self._valid_node_ids_at(conn, stamp)
|
|
266
|
-
nodes = [node for node in nodes if str(node.get("id")) in valid]
|
|
267
|
-
kept = {str(node.get("id")) for node in nodes} | {node_id}
|
|
268
|
-
edges = [
|
|
269
|
-
edge for edge in edges
|
|
270
|
-
if edge.get("from") in kept and edge.get("to") in kept
|
|
271
|
-
]
|
|
272
|
-
if allowed_workspaces is not None:
|
|
273
|
-
nodes = self.filter_scoped_nodes(
|
|
274
|
-
nodes,
|
|
275
|
-
allowed_workspaces,
|
|
276
|
-
include_legacy_global=include_legacy_global,
|
|
277
|
-
)
|
|
278
|
-
kept = {node.get("id") for node in nodes}
|
|
279
|
-
edges = [
|
|
280
|
-
edge for edge in edges
|
|
281
|
-
if (edge.get("from") == node_id or edge.get("from") in kept)
|
|
282
|
-
and (edge.get("to") == node_id or edge.get("to") in kept)
|
|
283
|
-
]
|
|
284
|
-
return {"node_id": node_id, "neighbors": nodes, "edges": edges}
|
|
285
|
-
|
|
286
|
-
def get_node(
|
|
287
|
-
self,
|
|
288
|
-
node_id: str,
|
|
289
|
-
*,
|
|
290
|
-
allowed_workspaces=None,
|
|
291
|
-
include_legacy_global: bool = False,
|
|
292
|
-
) -> Dict[str, Any]:
|
|
293
|
-
node_id = str(node_id or "").strip()
|
|
294
|
-
if not node_id:
|
|
295
|
-
raise ValueError("node_id required")
|
|
296
|
-
nt, et = self._read_tables()
|
|
297
|
-
with self._connect() as conn:
|
|
298
|
-
row = conn.execute(
|
|
299
|
-
f"""
|
|
300
|
-
SELECT id, type, title, summary, metadata_json, updated_at
|
|
301
|
-
FROM {nt}
|
|
302
|
-
WHERE id=?
|
|
303
|
-
""",
|
|
304
|
-
(node_id,),
|
|
305
|
-
).fetchone()
|
|
306
|
-
if not row:
|
|
307
|
-
raise ValueError(f"graph node not found: {node_id}")
|
|
308
|
-
degree = conn.execute(
|
|
309
|
-
f"SELECT COUNT(*) AS c FROM {et} WHERE from_node=? OR to_node=?",
|
|
310
|
-
(node_id, node_id),
|
|
311
|
-
).fetchone()["c"]
|
|
312
|
-
# Fetching a node by id is the one unambiguous "this memory was used"
|
|
313
|
-
# signal the read path has; it feeds importance_report(). Recorded
|
|
314
|
-
# after the read transaction closes so it can never fail the read.
|
|
315
|
-
_record_access(self.db_path, node_id)
|
|
316
|
-
node = {
|
|
317
|
-
"id": row["id"],
|
|
318
|
-
"type": row["type"],
|
|
319
|
-
"title": row["title"],
|
|
320
|
-
"summary": row["summary"],
|
|
321
|
-
"metadata": _safe_loads(row["metadata_json"]),
|
|
322
|
-
"updated_at": row["updated_at"],
|
|
323
|
-
"degree": degree,
|
|
324
|
-
}
|
|
325
|
-
if allowed_workspaces is not None and not self.filter_scoped_nodes(
|
|
326
|
-
[node],
|
|
327
|
-
allowed_workspaces,
|
|
328
|
-
include_legacy_global=include_legacy_global,
|
|
329
|
-
):
|
|
330
|
-
raise ValueError(f"graph node not found: {node_id}")
|
|
331
|
-
return node
|
|
332
|
-
|
|
333
|
-
# ── temporal reads (v11.1.0) ─────────────────────────────────────────
|
|
334
|
-
#
|
|
335
|
-
# Validity lives on ``nodes_v2``/``edges_v2``, the authoritative v2
|
|
336
|
-
# projection — the same table ``workspaces_of`` already reads for scoping,
|
|
337
|
-
# and the only one that carries the columns whichever read mode
|
|
338
|
-
# ``_read_tables()`` is in. The legacy compatibility tables have no
|
|
339
|
-
# temporal dimension, so slicing them would silently answer "everything".
|
|
340
|
-
|
|
341
|
-
def _valid_node_ids_at(self, conn: sqlite3.Connection, stamp: str) -> set:
|
|
342
|
-
"""Ids of nodes whose validity window covers ``stamp``."""
|
|
343
|
-
return {
|
|
344
|
-
row["id"]
|
|
345
|
-
for row in conn.execute(
|
|
346
|
-
f"SELECT id FROM nodes_v2 WHERE {TEMPORAL_PREDICATE_SQL}",
|
|
347
|
-
(stamp, stamp),
|
|
348
|
-
).fetchall()
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
def access_stats(self, node_ids=None) -> Dict[str, Dict[str, Any]]:
|
|
352
|
-
"""Per-node access bookkeeping read from the authoritative projection.
|
|
353
|
-
|
|
354
|
-
``{node_id: {"accesses": float, "last_used": str | None}}`` for the ids
|
|
355
|
-
requested (all nodes when ``node_ids`` is ``None``). Counts come from
|
|
356
|
-
``_touch_node``; a node never opened simply reports ``0.0``.
|
|
357
|
-
"""
|
|
358
|
-
query = "SELECT id, importance_score, last_used FROM nodes_v2"
|
|
359
|
-
params: List[Any] = []
|
|
360
|
-
if node_ids is not None:
|
|
361
|
-
ids = sorted({str(node_id) for node_id in node_ids if node_id})
|
|
362
|
-
if not ids:
|
|
363
|
-
return {}
|
|
364
|
-
query += f" WHERE id IN ({','.join('?' * len(ids))})"
|
|
365
|
-
params = list(ids)
|
|
366
|
-
with self._connect() as conn:
|
|
367
|
-
return {
|
|
368
|
-
row["id"]: {
|
|
369
|
-
"accesses": float(row["importance_score"] or 0.0),
|
|
370
|
-
"last_used": row["last_used"],
|
|
371
|
-
}
|
|
372
|
-
for row in conn.execute(query, params).fetchall()
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
def as_of(
|
|
376
|
-
self,
|
|
377
|
-
timestamp,
|
|
378
|
-
*,
|
|
379
|
-
limit: int = 300,
|
|
380
|
-
allowed_workspaces=None,
|
|
381
|
-
include_legacy_global: bool = False,
|
|
382
|
-
) -> Dict[str, Any]:
|
|
383
|
-
"""The graph slice that was valid at ``timestamp``.
|
|
384
|
-
|
|
385
|
-
Answers "what did I know in June 2025?": nodes and edges whose
|
|
386
|
-
``[valid_from, valid_to)`` window covers the instant, with
|
|
387
|
-
``valid_from`` falling back to ``created_at`` so a Brain that predates
|
|
388
|
-
the temporal columns reads as "always was true" rather than as empty.
|
|
389
|
-
Edges are returned only when *both* endpoints are in the slice.
|
|
390
|
-
"""
|
|
391
|
-
stamp = _as_of_stamp(timestamp)
|
|
392
|
-
# An explicit 0 clamps to 1 rather than re-expanding to the default:
|
|
393
|
-
# `limit or 300` would quietly return 300 rows to a caller who asked
|
|
394
|
-
# for none.
|
|
395
|
-
try:
|
|
396
|
-
limit = max(1, min(int(limit), 2000))
|
|
397
|
-
except (TypeError, ValueError):
|
|
398
|
-
limit = 300
|
|
399
|
-
scope_sql, scope_params = self._workspace_scope_sql(
|
|
400
|
-
allowed_workspaces, include_legacy_global
|
|
401
|
-
)
|
|
402
|
-
where = [TEMPORAL_PREDICATE_SQL]
|
|
403
|
-
params: List[Any] = [stamp, stamp]
|
|
404
|
-
if scope_sql is not None:
|
|
405
|
-
where.append(f"({scope_sql})")
|
|
406
|
-
params.extend(scope_params)
|
|
407
|
-
with self._connect() as conn:
|
|
408
|
-
node_rows = conn.execute(
|
|
409
|
-
"SELECT id, COALESCE(legacy_type, type) AS type, label AS title, "
|
|
410
|
-
"summary, attrs AS metadata_json, updated_at, "
|
|
411
|
-
"valid_from, valid_to, superseded_by "
|
|
412
|
-
f"FROM nodes_v2 WHERE {' AND '.join(where)} "
|
|
413
|
-
"ORDER BY updated_at DESC, id ASC LIMIT ?",
|
|
414
|
-
(*params, limit),
|
|
415
|
-
).fetchall()
|
|
416
|
-
nodes = [
|
|
417
|
-
{
|
|
418
|
-
"id": row["id"],
|
|
419
|
-
"type": row["type"],
|
|
420
|
-
"title": row["title"],
|
|
421
|
-
"summary": row["summary"],
|
|
422
|
-
"metadata": _safe_loads(row["metadata_json"]),
|
|
423
|
-
"updated_at": row["updated_at"],
|
|
424
|
-
"valid_from": row["valid_from"],
|
|
425
|
-
"valid_to": row["valid_to"],
|
|
426
|
-
"superseded_by": row["superseded_by"],
|
|
427
|
-
}
|
|
428
|
-
for row in node_rows
|
|
429
|
-
]
|
|
430
|
-
edges: List[Dict[str, Any]] = []
|
|
431
|
-
visible = sorted(str(node["id"]) for node in nodes)
|
|
432
|
-
if visible:
|
|
433
|
-
placeholders = ",".join("?" * len(visible))
|
|
434
|
-
edges = [
|
|
435
|
-
{
|
|
436
|
-
"id": row["id"],
|
|
437
|
-
"from": row["from_node"],
|
|
438
|
-
"to": row["to_node"],
|
|
439
|
-
"type": row["type"],
|
|
440
|
-
"weight": row["weight"],
|
|
441
|
-
"metadata": _safe_loads(row["metadata_json"]),
|
|
442
|
-
"valid_from": row["valid_from"],
|
|
443
|
-
"valid_to": row["valid_to"],
|
|
444
|
-
"superseded_by": row["superseded_by"],
|
|
445
|
-
}
|
|
446
|
-
for row in conn.execute(
|
|
447
|
-
"SELECT id, source AS from_node, target AS to_node, "
|
|
448
|
-
"COALESCE(legacy_type, type) AS type, weight, "
|
|
449
|
-
"metadata AS metadata_json, valid_from, valid_to, superseded_by "
|
|
450
|
-
f"FROM edges_v2 WHERE {TEMPORAL_PREDICATE_SQL} "
|
|
451
|
-
f"AND source IN ({placeholders}) AND target IN ({placeholders}) "
|
|
452
|
-
"ORDER BY weight DESC, created_at DESC, id ASC",
|
|
453
|
-
(stamp, stamp, *visible, *visible),
|
|
454
|
-
).fetchall()
|
|
455
|
-
]
|
|
456
|
-
return {
|
|
457
|
-
"as_of": stamp,
|
|
458
|
-
"nodes": nodes,
|
|
459
|
-
"edges": edges,
|
|
460
|
-
"node_count": len(nodes),
|
|
461
|
-
"edge_count": len(edges),
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
def relationship_search(
|
|
465
|
-
self,
|
|
466
|
-
*,
|
|
467
|
-
query: str = "",
|
|
468
|
-
node_id: str = "",
|
|
469
|
-
relationship_type: str = "",
|
|
470
|
-
limit: int = 30,
|
|
471
|
-
allowed_workspaces=None,
|
|
472
|
-
include_legacy_global: bool = False,
|
|
473
|
-
) -> Dict[str, Any]:
|
|
474
|
-
query = str(query or "").strip()
|
|
475
|
-
node_id = str(node_id or "").strip()
|
|
476
|
-
relationship_type = str(relationship_type or "").strip()
|
|
477
|
-
limit = max(1, min(int(limit or 30), 200))
|
|
478
|
-
nt, et = self._read_tables()
|
|
479
|
-
where = []
|
|
480
|
-
params: List[Any] = []
|
|
481
|
-
if node_id:
|
|
482
|
-
where.append("(e.from_node=? OR e.to_node=?)")
|
|
483
|
-
params.extend([node_id, node_id])
|
|
484
|
-
if relationship_type:
|
|
485
|
-
where.append("e.type LIKE ?")
|
|
486
|
-
params.append(f"%{relationship_type}%")
|
|
487
|
-
if query:
|
|
488
|
-
where.append(
|
|
489
|
-
"(e.type LIKE ? OR e.metadata_json LIKE ? OR src.title LIKE ? OR dst.title LIKE ? OR src.summary LIKE ? OR dst.summary LIKE ?)"
|
|
490
|
-
)
|
|
491
|
-
params.extend([f"%{query}%"] * 6)
|
|
492
|
-
where_sql = "WHERE " + " AND ".join(where) if where else ""
|
|
493
|
-
with self._connect() as conn:
|
|
494
|
-
rows = conn.execute(
|
|
495
|
-
f"""
|
|
496
|
-
SELECT
|
|
497
|
-
e.id, e.from_node, e.to_node, e.type, e.weight, e.metadata_json, e.created_at,
|
|
498
|
-
src.type AS source_type, src.title AS source_title, src.summary AS source_summary,
|
|
499
|
-
src.metadata_json AS source_metadata,
|
|
500
|
-
dst.type AS target_type, dst.title AS target_title, dst.summary AS target_summary,
|
|
501
|
-
dst.metadata_json AS target_metadata
|
|
502
|
-
FROM {et} e
|
|
503
|
-
JOIN {nt} src ON src.id=e.from_node
|
|
504
|
-
JOIN {nt} dst ON dst.id=e.to_node
|
|
505
|
-
{where_sql}
|
|
506
|
-
ORDER BY e.weight DESC, e.created_at DESC, e.id ASC
|
|
507
|
-
LIMIT ?
|
|
508
|
-
""",
|
|
509
|
-
(*params, limit),
|
|
510
|
-
).fetchall()
|
|
511
|
-
relationships = [
|
|
512
|
-
{
|
|
513
|
-
"id": row["id"],
|
|
514
|
-
"type": row["type"],
|
|
515
|
-
"weight": row["weight"],
|
|
516
|
-
"metadata": _safe_loads(row["metadata_json"]),
|
|
517
|
-
"created_at": row["created_at"],
|
|
518
|
-
"source": {
|
|
519
|
-
"id": row["from_node"],
|
|
520
|
-
"type": row["source_type"],
|
|
521
|
-
"title": row["source_title"],
|
|
522
|
-
"summary": row["source_summary"],
|
|
523
|
-
"metadata": _safe_loads(row["source_metadata"]),
|
|
524
|
-
},
|
|
525
|
-
"target": {
|
|
526
|
-
"id": row["to_node"],
|
|
527
|
-
"type": row["target_type"],
|
|
528
|
-
"title": row["target_title"],
|
|
529
|
-
"summary": row["target_summary"],
|
|
530
|
-
"metadata": _safe_loads(row["target_metadata"]),
|
|
531
|
-
},
|
|
532
|
-
}
|
|
533
|
-
for row in rows
|
|
534
|
-
]
|
|
535
|
-
if allowed_workspaces is not None:
|
|
536
|
-
kept = []
|
|
537
|
-
for rel in relationships:
|
|
538
|
-
endpoints = [
|
|
539
|
-
{"id": (rel.get("source") or {}).get("id")},
|
|
540
|
-
{"id": (rel.get("target") or {}).get("id")},
|
|
541
|
-
]
|
|
542
|
-
if len(
|
|
543
|
-
self.filter_scoped_nodes(
|
|
544
|
-
endpoints,
|
|
545
|
-
allowed_workspaces,
|
|
546
|
-
include_legacy_global=include_legacy_global,
|
|
547
|
-
)
|
|
548
|
-
) == 2:
|
|
549
|
-
kept.append(rel)
|
|
550
|
-
relationships = kept
|
|
551
|
-
return {
|
|
552
|
-
"query": query,
|
|
553
|
-
"node_id": node_id,
|
|
554
|
-
"relationship_type": relationship_type,
|
|
555
|
-
"relationships": relationships,
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
def traverse(
|
|
559
|
-
self,
|
|
560
|
-
node_id: str,
|
|
561
|
-
*,
|
|
562
|
-
depth: int = 1,
|
|
563
|
-
limit: int = 100,
|
|
564
|
-
allowed_workspaces=None,
|
|
565
|
-
include_legacy_global: bool = False,
|
|
566
|
-
) -> Dict[str, Any]:
|
|
567
|
-
node_id = str(node_id or "").strip()
|
|
568
|
-
if not node_id:
|
|
569
|
-
raise ValueError("node_id required")
|
|
570
|
-
if allowed_workspaces is not None and not self.filter_scoped_nodes(
|
|
571
|
-
[{"id": node_id}],
|
|
572
|
-
allowed_workspaces,
|
|
573
|
-
include_legacy_global=include_legacy_global,
|
|
574
|
-
):
|
|
575
|
-
raise ValueError(f"graph node not found: {node_id}")
|
|
576
|
-
depth = max(0, min(int(depth or 1), 4))
|
|
577
|
-
limit = max(1, min(int(limit or 100), 500))
|
|
578
|
-
nt, et = self._read_tables()
|
|
579
|
-
visited = {node_id}
|
|
580
|
-
frontier = {node_id}
|
|
581
|
-
edges_by_id: Dict[str, Dict[str, Any]] = {}
|
|
582
|
-
with self._connect() as conn:
|
|
583
|
-
for _ in range(depth):
|
|
584
|
-
if not frontier or len(visited) >= limit:
|
|
585
|
-
break
|
|
586
|
-
placeholders = ",".join("?" * len(frontier))
|
|
587
|
-
rows = conn.execute(
|
|
588
|
-
f"""
|
|
589
|
-
SELECT id, from_node, to_node, type, weight, metadata_json
|
|
590
|
-
FROM {et}
|
|
591
|
-
WHERE from_node IN ({placeholders}) OR to_node IN ({placeholders})
|
|
592
|
-
ORDER BY weight DESC, id ASC
|
|
593
|
-
LIMIT ?
|
|
594
|
-
""",
|
|
595
|
-
(*frontier, *frontier, limit * 3),
|
|
596
|
-
).fetchall()
|
|
597
|
-
next_frontier = set()
|
|
598
|
-
for row in rows:
|
|
599
|
-
edges_by_id[row["id"]] = {
|
|
600
|
-
"id": row["id"],
|
|
601
|
-
"from": row["from_node"],
|
|
602
|
-
"to": row["to_node"],
|
|
603
|
-
"type": row["type"],
|
|
604
|
-
"weight": row["weight"],
|
|
605
|
-
"metadata": _safe_loads(row["metadata_json"]),
|
|
606
|
-
}
|
|
607
|
-
for candidate in (row["from_node"], row["to_node"]):
|
|
608
|
-
if candidate not in visited and len(visited) < limit:
|
|
609
|
-
visited.add(candidate)
|
|
610
|
-
next_frontier.add(candidate)
|
|
611
|
-
frontier = next_frontier
|
|
612
|
-
placeholders = ",".join("?" * len(visited))
|
|
613
|
-
node_rows = conn.execute(
|
|
614
|
-
f"""
|
|
615
|
-
SELECT id, type, title, summary, metadata_json, updated_at
|
|
616
|
-
FROM {nt}
|
|
617
|
-
WHERE id IN ({placeholders})
|
|
618
|
-
ORDER BY updated_at DESC, id ASC
|
|
619
|
-
""",
|
|
620
|
-
list(visited),
|
|
621
|
-
).fetchall()
|
|
622
|
-
nodes = [
|
|
623
|
-
{
|
|
624
|
-
"id": row["id"],
|
|
625
|
-
"type": row["type"],
|
|
626
|
-
"title": row["title"],
|
|
627
|
-
"summary": row["summary"],
|
|
628
|
-
"metadata": _safe_loads(row["metadata_json"]),
|
|
629
|
-
"updated_at": row["updated_at"],
|
|
630
|
-
}
|
|
631
|
-
for row in node_rows
|
|
632
|
-
]
|
|
633
|
-
edges = list(edges_by_id.values())
|
|
634
|
-
if allowed_workspaces is not None:
|
|
635
|
-
nodes = self.filter_scoped_nodes(
|
|
636
|
-
nodes,
|
|
637
|
-
allowed_workspaces,
|
|
638
|
-
include_legacy_global=include_legacy_global,
|
|
639
|
-
)
|
|
640
|
-
kept = {node.get("id") for node in nodes}
|
|
641
|
-
edges = [edge for edge in edges if edge.get("from") in kept and edge.get("to") in kept]
|
|
642
|
-
return {"root": node_id, "depth": depth, "nodes": nodes, "edges": edges}
|
|
643
|
-
|
|
644
|
-
def stats(
|
|
645
|
-
self,
|
|
646
|
-
*,
|
|
647
|
-
allowed_workspaces=None,
|
|
648
|
-
include_legacy_global: bool = False,
|
|
649
|
-
) -> Dict[str, Any]:
|
|
650
|
-
"""Store statistics, optionally restricted to a caller's workspaces.
|
|
651
|
-
|
|
652
|
-
``allowed_workspaces=None`` keeps the historical whole-store counts
|
|
653
|
-
(single-user / no-auth mode). When a scope is given, the node and edge
|
|
654
|
-
histograms are counted through the authoritative ``nodes_v2``
|
|
655
|
-
projection, so a member of one organization workspace cannot read
|
|
656
|
-
another's volume off this endpoint. An edge counts only when *both*
|
|
657
|
-
endpoints are visible — the same rule ``graph()`` and ``neighbors()``
|
|
658
|
-
already apply to the rows they return.
|
|
659
|
-
"""
|
|
660
|
-
nt, et = self._read_tables()
|
|
661
|
-
scope_sql, scope_params = self._workspace_scope_sql(
|
|
662
|
-
allowed_workspaces, include_legacy_global
|
|
663
|
-
)
|
|
664
|
-
with self._connect() as conn:
|
|
665
|
-
if scope_sql is None:
|
|
666
|
-
node_counts = {
|
|
667
|
-
row["type"]: row["count"]
|
|
668
|
-
for row in conn.execute(
|
|
669
|
-
f"SELECT type, COUNT(*) AS count FROM {nt} GROUP BY type"
|
|
670
|
-
)
|
|
671
|
-
}
|
|
672
|
-
edge_counts = {
|
|
673
|
-
row["type"]: row["count"]
|
|
674
|
-
for row in conn.execute(
|
|
675
|
-
f"SELECT type, COUNT(*) AS count FROM {et} GROUP BY type"
|
|
676
|
-
)
|
|
677
|
-
}
|
|
678
|
-
local_sources = conn.execute(
|
|
679
|
-
"SELECT COUNT(*) AS c FROM knowledge_sources"
|
|
680
|
-
).fetchone()["c"]
|
|
681
|
-
local_file_status = {
|
|
682
|
-
row["status"]: row["count"]
|
|
683
|
-
for row in conn.execute(
|
|
684
|
-
"SELECT status, COUNT(*) AS count FROM local_file_index GROUP BY status"
|
|
685
|
-
)
|
|
686
|
-
}
|
|
687
|
-
else:
|
|
688
|
-
visible = f"SELECT id FROM nodes_v2 WHERE {scope_sql}"
|
|
689
|
-
node_counts = {
|
|
690
|
-
row["type"]: row["count"]
|
|
691
|
-
for row in conn.execute(
|
|
692
|
-
f"SELECT type, COUNT(*) AS count FROM {nt} "
|
|
693
|
-
f"WHERE id IN ({visible}) GROUP BY type",
|
|
694
|
-
scope_params,
|
|
695
|
-
)
|
|
696
|
-
}
|
|
697
|
-
edge_counts = {
|
|
698
|
-
row["type"]: row["count"]
|
|
699
|
-
for row in conn.execute(
|
|
700
|
-
f"SELECT type, COUNT(*) AS count FROM {et} "
|
|
701
|
-
f"WHERE from_node IN ({visible}) AND to_node IN ({visible}) "
|
|
702
|
-
f"GROUP BY type",
|
|
703
|
-
scope_params + scope_params,
|
|
704
|
-
)
|
|
705
|
-
}
|
|
706
|
-
# Local sources and the file index are machine-local ingestion
|
|
707
|
-
# bookkeeping with no workspace column. They are not another
|
|
708
|
-
# tenant's content, but they are also not this caller's scope,
|
|
709
|
-
# so a scoped read reports none rather than guessing.
|
|
710
|
-
local_sources = 0
|
|
711
|
-
local_file_status = {}
|
|
712
|
-
v2 = None
|
|
713
|
-
if KGStoreV2 is not None:
|
|
714
|
-
try:
|
|
715
|
-
v2 = (
|
|
716
|
-
KGStoreV2(self.db_path).stats()
|
|
717
|
-
if scope_sql is None
|
|
718
|
-
else self._scoped_v2_stats(scope_sql, scope_params)
|
|
719
|
-
)
|
|
720
|
-
except Exception as e:
|
|
721
|
-
v2 = {"available": False, "error": str(e)}
|
|
722
|
-
return {
|
|
723
|
-
"db_path": str(self.db_path),
|
|
724
|
-
"schema_version": GRAPH_SCHEMA_VERSION,
|
|
725
|
-
"v2_schema_available": KGStoreV2 is not None,
|
|
726
|
-
"nodes": node_counts,
|
|
727
|
-
"edges": edge_counts,
|
|
728
|
-
"local_sources": local_sources,
|
|
729
|
-
"local_file_status": local_file_status,
|
|
730
|
-
"v2": v2,
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
def _scoped_v2_stats(self, scope_sql: str, scope_params: List[Any]) -> Dict[str, Any]:
|
|
734
|
-
"""``KGStoreV2.stats()`` restricted to a caller's workspaces.
|
|
735
|
-
|
|
736
|
-
Starts from the real payload and overwrites only the counts, so the
|
|
737
|
-
key set stays whatever ``KGStoreV2`` defines — ``/knowledge-graph/schema``
|
|
738
|
-
returns this sub-object verbatim, making its shape part of the API
|
|
739
|
-
contract rather than something to re-derive here.
|
|
740
|
-
"""
|
|
741
|
-
payload: Dict[str, Any] = dict(KGStoreV2(self.db_path).stats())
|
|
742
|
-
visible = f"SELECT id FROM nodes_v2 WHERE {scope_sql}"
|
|
743
|
-
with self._connect() as conn:
|
|
744
|
-
by_node_type = {
|
|
745
|
-
row["type"]: row["c"]
|
|
746
|
-
for row in conn.execute(
|
|
747
|
-
f"SELECT type, COUNT(*) AS c FROM nodes_v2 "
|
|
748
|
-
f"WHERE {scope_sql} GROUP BY type",
|
|
749
|
-
scope_params,
|
|
750
|
-
).fetchall()
|
|
751
|
-
}
|
|
752
|
-
by_edge_type = {
|
|
753
|
-
row["type"]: row["c"]
|
|
754
|
-
for row in conn.execute(
|
|
755
|
-
f"SELECT type, COUNT(*) AS c FROM edges_v2 "
|
|
756
|
-
f"WHERE source IN ({visible}) AND target IN ({visible}) "
|
|
757
|
-
f"GROUP BY type",
|
|
758
|
-
scope_params + scope_params,
|
|
759
|
-
).fetchall()
|
|
760
|
-
}
|
|
761
|
-
payload.update(
|
|
762
|
-
{
|
|
763
|
-
"nodes": sum(by_node_type.values()),
|
|
764
|
-
"edges": sum(by_edge_type.values()),
|
|
765
|
-
"by_node_type": by_node_type,
|
|
766
|
-
"by_edge_type": by_edge_type,
|
|
767
|
-
}
|
|
768
|
-
)
|
|
769
|
-
return payload
|