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,609 +0,0 @@
|
|
|
1
|
-
"""Obsidian vault bridge — an *external* vault through the one ingestion gate.
|
|
2
|
-
|
|
3
|
-
Not to be confused with ``latticeai/tools/knowledge.py`` (``obsidian_save`` /
|
|
4
|
-
``obsidian_search``), which writes Lattice's *own* mirror vault under the brain
|
|
5
|
-
directory. This module reads a vault the user already owns — a folder full of
|
|
6
|
-
``.md`` notes written by Obsidian — and pushes every note through the same
|
|
7
|
-
:class:`~lattice_brain.ingestion.IngestionPipeline` door as files, folders,
|
|
8
|
-
web captures, and chat: content hashing, hooks, provenance, extraction quality.
|
|
9
|
-
No second write path into the graph, no silo.
|
|
10
|
-
|
|
11
|
-
What the bridge adds on top of a plain folder ingest is the vault's *structure*:
|
|
12
|
-
|
|
13
|
-
* ``[[wikilinks]]`` (including ``[[target|alias]]``, ``[[target#heading]]``,
|
|
14
|
-
and ``![[embeds]]``) plus relative markdown links between notes become real
|
|
15
|
-
``REFERENCES`` edges between the ingested note nodes, so a backlink in
|
|
16
|
-
Obsidian is a traversable relation in the Brain.
|
|
17
|
-
* Frontmatter ``tags`` become ``Topic`` nodes joined by ``TAGGED_AS``, sharing
|
|
18
|
-
the store's own scoped-slug identity so a vault tag and an extracted topic of
|
|
19
|
-
the same name are one node, not two.
|
|
20
|
-
* A link whose target does not exist in the vault (or is ambiguous between two
|
|
21
|
-
notes of the same basename) is **reported**, never invented. The pending list
|
|
22
|
-
is part of the summary.
|
|
23
|
-
|
|
24
|
-
Direct edge writes are legitimate here because a vault sync is user-initiated
|
|
25
|
-
ingestion, not an agent proposal — the same rule that lets ``/api/ingestion/
|
|
26
|
-
folder`` write. Re-running is idempotent: content-hash dedup is the pipeline's
|
|
27
|
-
job, and edge/topic ids are deterministic, so a second sync updates rather than
|
|
28
|
-
duplicates.
|
|
29
|
-
|
|
30
|
-
Scope (v11.1.0): manual one-shot sync only. There is no watch mode and no
|
|
31
|
-
background scheduling for external vaults — link edges need the node ids that
|
|
32
|
-
only a completed inline ingest has, and a deferred edge that never lands would
|
|
33
|
-
be a lie in the summary.
|
|
34
|
-
"""
|
|
35
|
-
|
|
36
|
-
from __future__ import annotations
|
|
37
|
-
|
|
38
|
-
import os
|
|
39
|
-
import re
|
|
40
|
-
from dataclasses import dataclass, field
|
|
41
|
-
from pathlib import Path
|
|
42
|
-
from typing import Any, Dict, List, Optional, Tuple
|
|
43
|
-
from urllib.parse import unquote
|
|
44
|
-
|
|
45
|
-
# ``_scoped_slug_id`` is the store's own scoped-id helper. Importing it rather
|
|
46
|
-
# than re-deriving the slug rules is what makes a vault tag and a
|
|
47
|
-
# store-extracted topic the same node instead of two nodes with one label.
|
|
48
|
-
from lattice_brain.graph.ingest import _scoped_slug_id
|
|
49
|
-
from lattice_brain.ingestion import IngestionItem
|
|
50
|
-
from latticeai.services.interop_bridges import edge_row, topic_row
|
|
51
|
-
|
|
52
|
-
SOURCE_TYPE = "obsidian"
|
|
53
|
-
NOTE_EXTENSIONS = frozenset({".md", ".markdown"})
|
|
54
|
-
#: Directories never walked. Hidden entries (``.obsidian``, ``.trash``,
|
|
55
|
-
#: ``.git``) are skipped by the leading-dot rule; these are the visible ones.
|
|
56
|
-
SKIP_DIRS = frozenset({"node_modules", "__pycache__"})
|
|
57
|
-
DEFAULT_MAX_FILES = 2000
|
|
58
|
-
DEFAULT_MAX_FILE_BYTES = 2_000_000
|
|
59
|
-
#: Vault link → graph relation. ``REFERENCES`` is the store's canonical
|
|
60
|
-
#: FILE → FILE / URL edge, so no new taxonomy is minted here.
|
|
61
|
-
LINK_RELATION = "REFERENCES"
|
|
62
|
-
TAG_RELATION = "TAGGED_AS"
|
|
63
|
-
TOPIC_NODE_TYPE = "Topic"
|
|
64
|
-
UNRESOLVED_REPORT_CAP = 50
|
|
65
|
-
ERROR_REPORT_CAP = 25
|
|
66
|
-
|
|
67
|
-
_WIKILINK_RE = re.compile(r"!?\[\[([^\[\]]{1,300}?)\]\]")
|
|
68
|
-
_MD_LINK_RE = re.compile(r"(?<!!)\[[^\]\n]{0,200}\]\(([^()\s]{1,300})\)")
|
|
69
|
-
_EXTERNAL_PREFIXES = ("http://", "https://", "mailto:", "obsidian://", "//")
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
@dataclass
|
|
73
|
-
class VaultNote:
|
|
74
|
-
"""One parsed note, before it reaches the pipeline."""
|
|
75
|
-
|
|
76
|
-
path: Path
|
|
77
|
-
relative_path: str
|
|
78
|
-
title: str
|
|
79
|
-
body: str
|
|
80
|
-
frontmatter: Dict[str, Any] = field(default_factory=dict)
|
|
81
|
-
tags: List[str] = field(default_factory=list)
|
|
82
|
-
#: Raw link targets exactly as written in the note.
|
|
83
|
-
link_targets: List[str] = field(default_factory=list)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def parse_frontmatter(text: str) -> Tuple[Dict[str, Any], str]:
|
|
87
|
-
"""Split a leading ``---`` YAML block off a note.
|
|
88
|
-
|
|
89
|
-
Deliberately a *minimal* YAML subset — ``key: value``, ``key: [a, b]``,
|
|
90
|
-
and ``- item`` continuation lines — because the only thing this bridge
|
|
91
|
-
needs from frontmatter is tags, and pulling a YAML dependency in for that
|
|
92
|
-
would put a parser in the ingest path for no recall gain. Anything the
|
|
93
|
-
subset does not understand is left in the block and ignored, never
|
|
94
|
-
guessed at.
|
|
95
|
-
"""
|
|
96
|
-
raw = str(text or "")
|
|
97
|
-
lines = raw.splitlines()
|
|
98
|
-
if not lines or lines[0].strip() != "---":
|
|
99
|
-
return {}, raw
|
|
100
|
-
closing = None
|
|
101
|
-
for index in range(1, len(lines)):
|
|
102
|
-
if lines[index].strip() in {"---", "..."}:
|
|
103
|
-
closing = index
|
|
104
|
-
break
|
|
105
|
-
if closing is None:
|
|
106
|
-
return {}, raw
|
|
107
|
-
data: Dict[str, Any] = {}
|
|
108
|
-
current_key: Optional[str] = None
|
|
109
|
-
for line in lines[1:closing]:
|
|
110
|
-
stripped = line.strip()
|
|
111
|
-
if not stripped or stripped.startswith("#"):
|
|
112
|
-
continue
|
|
113
|
-
if stripped.startswith("- "):
|
|
114
|
-
if current_key is not None:
|
|
115
|
-
bucket = data.setdefault(current_key, [])
|
|
116
|
-
if isinstance(bucket, list):
|
|
117
|
-
bucket.append(_scalar(stripped[2:]))
|
|
118
|
-
continue
|
|
119
|
-
key, sep, value = stripped.partition(":")
|
|
120
|
-
if not sep:
|
|
121
|
-
continue
|
|
122
|
-
current_key = key.strip()
|
|
123
|
-
rest = value.strip()
|
|
124
|
-
if not rest:
|
|
125
|
-
data[current_key] = []
|
|
126
|
-
continue
|
|
127
|
-
data[current_key] = _yaml_value(rest)
|
|
128
|
-
body = "\n".join(lines[closing + 1:])
|
|
129
|
-
return data, body
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def _scalar(value: str) -> str:
|
|
133
|
-
return str(value).strip().strip("'\"").strip()
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def _yaml_value(value: str) -> Any:
|
|
137
|
-
text = value.strip()
|
|
138
|
-
if text.startswith("[") and text.endswith("]"):
|
|
139
|
-
return [_scalar(part) for part in text[1:-1].split(",") if _scalar(part)]
|
|
140
|
-
if "," in text:
|
|
141
|
-
return [_scalar(part) for part in text.split(",") if _scalar(part)]
|
|
142
|
-
return _scalar(text)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def frontmatter_tags(frontmatter: Dict[str, Any]) -> List[str]:
|
|
146
|
-
"""Tags from a parsed frontmatter block, de-duplicated case-insensitively."""
|
|
147
|
-
collected: List[str] = []
|
|
148
|
-
seen: set = set()
|
|
149
|
-
for key in ("tags", "tag"):
|
|
150
|
-
value = frontmatter.get(key)
|
|
151
|
-
if value is None:
|
|
152
|
-
continue
|
|
153
|
-
candidates = value if isinstance(value, list) else [value]
|
|
154
|
-
for candidate in candidates:
|
|
155
|
-
tag = str(candidate or "").strip().lstrip("#").strip()
|
|
156
|
-
if not tag or tag.lower() in seen:
|
|
157
|
-
continue
|
|
158
|
-
seen.add(tag.lower())
|
|
159
|
-
collected.append(tag)
|
|
160
|
-
return collected
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def extract_link_targets(body: str) -> List[str]:
|
|
164
|
-
"""Wikilink and relative-markdown-link targets, in document order.
|
|
165
|
-
|
|
166
|
-
Aliases (``|``), headings (``#``), and block refs (``^``) are stripped so
|
|
167
|
-
``[[Design#Rationale|why]]`` resolves to the note ``Design``. External
|
|
168
|
-
URLs are not vault links and are dropped.
|
|
169
|
-
"""
|
|
170
|
-
targets: List[str] = []
|
|
171
|
-
seen: set = set()
|
|
172
|
-
|
|
173
|
-
def _add(raw: str) -> None:
|
|
174
|
-
target = _normalize_target(raw)
|
|
175
|
-
if not target or target.lower() in seen:
|
|
176
|
-
return
|
|
177
|
-
seen.add(target.lower())
|
|
178
|
-
targets.append(target)
|
|
179
|
-
|
|
180
|
-
for match in _WIKILINK_RE.finditer(body or ""):
|
|
181
|
-
_add(match.group(1).split("|", 1)[0])
|
|
182
|
-
for match in _MD_LINK_RE.finditer(body or ""):
|
|
183
|
-
_add(unquote(match.group(1)))
|
|
184
|
-
return targets
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
def _normalize_target(raw: str) -> str:
|
|
188
|
-
target = str(raw or "").strip()
|
|
189
|
-
if not target or target.startswith(_EXTERNAL_PREFIXES):
|
|
190
|
-
return ""
|
|
191
|
-
target = target.split("#", 1)[0].split("^", 1)[0].strip()
|
|
192
|
-
if not target:
|
|
193
|
-
return ""
|
|
194
|
-
suffix = Path(target).suffix.lower()
|
|
195
|
-
# A relative link to an image or a PDF is an attachment, not a note.
|
|
196
|
-
if suffix and suffix not in NOTE_EXTENSIONS:
|
|
197
|
-
return ""
|
|
198
|
-
return target
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def _index_key(value: str) -> str:
|
|
202
|
-
text = str(value or "").strip().replace("\\", "/")
|
|
203
|
-
while text.startswith("./"):
|
|
204
|
-
text = text[2:]
|
|
205
|
-
text = text.lstrip("/")
|
|
206
|
-
lowered = text.lower()
|
|
207
|
-
for extension in NOTE_EXTENSIONS:
|
|
208
|
-
if lowered.endswith(extension):
|
|
209
|
-
return lowered[: -len(extension)]
|
|
210
|
-
return lowered
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
class ObsidianVaultBridge:
|
|
214
|
-
"""Reads an approved external vault and feeds it to the ingestion gate."""
|
|
215
|
-
|
|
216
|
-
def __init__(
|
|
217
|
-
self,
|
|
218
|
-
*,
|
|
219
|
-
pipeline: Any,
|
|
220
|
-
knowledge_graph: Any = None,
|
|
221
|
-
max_files: int = DEFAULT_MAX_FILES,
|
|
222
|
-
max_file_bytes: int = DEFAULT_MAX_FILE_BYTES,
|
|
223
|
-
) -> None:
|
|
224
|
-
self._pipeline = pipeline
|
|
225
|
-
self._kg = knowledge_graph
|
|
226
|
-
self._max_files = max(1, int(max_files))
|
|
227
|
-
self._max_file_bytes = max(1, int(max_file_bytes))
|
|
228
|
-
|
|
229
|
-
def available(self) -> bool:
|
|
230
|
-
return self._pipeline is not None and bool(self._pipeline.available())
|
|
231
|
-
|
|
232
|
-
# ── scanning ─────────────────────────────────────────────────────────────
|
|
233
|
-
def scan(self, vault_path: Any) -> Dict[str, Any]:
|
|
234
|
-
"""Walk a vault into parsed notes plus a resolved link graph.
|
|
235
|
-
|
|
236
|
-
Pure read: no ingest, no graph write. ``sync(dry_run=True)`` is this
|
|
237
|
-
with counts, which is what the API returns before anyone commits.
|
|
238
|
-
"""
|
|
239
|
-
report: Dict[str, Any] = {
|
|
240
|
-
"vault": str(vault_path),
|
|
241
|
-
"scanned": 0,
|
|
242
|
-
"notes": [],
|
|
243
|
-
"skipped": {"empty": 0, "too_large": 0, "unreadable": 0},
|
|
244
|
-
"truncated": False,
|
|
245
|
-
"errors": [],
|
|
246
|
-
}
|
|
247
|
-
try:
|
|
248
|
-
root = Path(vault_path).expanduser()
|
|
249
|
-
except TypeError:
|
|
250
|
-
report["status"] = "failed"
|
|
251
|
-
report["detail"] = f"invalid vault path: {vault_path!r}"
|
|
252
|
-
return report
|
|
253
|
-
if not root.is_dir():
|
|
254
|
-
report["status"] = "failed"
|
|
255
|
-
report["detail"] = f"not a vault directory: {root}"
|
|
256
|
-
return report
|
|
257
|
-
report["vault"] = str(root)
|
|
258
|
-
notes: List[VaultNote] = self._collect(root, report)
|
|
259
|
-
report["notes"] = notes
|
|
260
|
-
report["status"] = "ok"
|
|
261
|
-
return report
|
|
262
|
-
|
|
263
|
-
def _collect(self, root: Path, report: Dict[str, Any]) -> List[VaultNote]:
|
|
264
|
-
notes: List[VaultNote] = []
|
|
265
|
-
skipped = report["skipped"]
|
|
266
|
-
errors: List[Dict[str, Any]] = report["errors"]
|
|
267
|
-
for dirpath, dirnames, filenames in os.walk(root):
|
|
268
|
-
dirnames[:] = sorted(
|
|
269
|
-
name for name in dirnames
|
|
270
|
-
if not name.startswith(".") and name not in SKIP_DIRS
|
|
271
|
-
)
|
|
272
|
-
current = Path(dirpath)
|
|
273
|
-
for name in sorted(filenames):
|
|
274
|
-
if Path(name).suffix.lower() not in NOTE_EXTENSIONS:
|
|
275
|
-
continue
|
|
276
|
-
if name.startswith("."):
|
|
277
|
-
continue
|
|
278
|
-
report["scanned"] += 1
|
|
279
|
-
path = current / name
|
|
280
|
-
if len(notes) >= self._max_files:
|
|
281
|
-
report["truncated"] = True
|
|
282
|
-
continue
|
|
283
|
-
try:
|
|
284
|
-
size = path.stat().st_size
|
|
285
|
-
if size > self._max_file_bytes:
|
|
286
|
-
skipped["too_large"] += 1
|
|
287
|
-
continue
|
|
288
|
-
text = path.read_text(encoding="utf-8", errors="ignore")
|
|
289
|
-
except OSError as exc:
|
|
290
|
-
skipped["unreadable"] += 1
|
|
291
|
-
if len(errors) < ERROR_REPORT_CAP:
|
|
292
|
-
errors.append({"path": str(path), "status": "unreadable", "detail": str(exc)})
|
|
293
|
-
continue
|
|
294
|
-
frontmatter, body = parse_frontmatter(text)
|
|
295
|
-
if not body.strip():
|
|
296
|
-
# A note that is only frontmatter has nothing to recall.
|
|
297
|
-
skipped["empty"] += 1
|
|
298
|
-
continue
|
|
299
|
-
notes.append(
|
|
300
|
-
VaultNote(
|
|
301
|
-
path=path,
|
|
302
|
-
relative_path=path.relative_to(root).as_posix(),
|
|
303
|
-
title=str(frontmatter.get("title") or path.stem),
|
|
304
|
-
body=body,
|
|
305
|
-
frontmatter=frontmatter,
|
|
306
|
-
tags=frontmatter_tags(frontmatter),
|
|
307
|
-
link_targets=extract_link_targets(body),
|
|
308
|
-
)
|
|
309
|
-
)
|
|
310
|
-
return notes
|
|
311
|
-
|
|
312
|
-
# ── link resolution ──────────────────────────────────────────────────────
|
|
313
|
-
@staticmethod
|
|
314
|
-
def resolve_links(notes: List[VaultNote]) -> Tuple[Dict[str, List[str]], List[Dict[str, str]]]:
|
|
315
|
-
"""``(relative_path → linked relative paths, unresolved reports)``.
|
|
316
|
-
|
|
317
|
-
Exact relative path wins; otherwise a unique basename wins (Obsidian's
|
|
318
|
-
own shortest-path rule). A basename shared by two notes is reported as
|
|
319
|
-
``ambiguous`` rather than guessed, and a target that matches nothing is
|
|
320
|
-
reported as ``missing``.
|
|
321
|
-
"""
|
|
322
|
-
by_path: Dict[str, str] = {}
|
|
323
|
-
by_name: Dict[str, List[str]] = {}
|
|
324
|
-
for note in notes:
|
|
325
|
-
by_path[_index_key(note.relative_path)] = note.relative_path
|
|
326
|
-
by_name.setdefault(_index_key(Path(note.relative_path).name), []).append(
|
|
327
|
-
note.relative_path
|
|
328
|
-
)
|
|
329
|
-
resolved: Dict[str, List[str]] = {}
|
|
330
|
-
unresolved: List[Dict[str, str]] = []
|
|
331
|
-
for note in notes:
|
|
332
|
-
links: List[str] = []
|
|
333
|
-
for raw in note.link_targets:
|
|
334
|
-
key = _index_key(raw)
|
|
335
|
-
target = by_path.get(key)
|
|
336
|
-
if target is None:
|
|
337
|
-
candidates = by_name.get(key) or []
|
|
338
|
-
if len(candidates) == 1:
|
|
339
|
-
target = candidates[0]
|
|
340
|
-
else:
|
|
341
|
-
unresolved.append({
|
|
342
|
-
"from": note.relative_path,
|
|
343
|
-
"target": raw,
|
|
344
|
-
"reason": "ambiguous" if candidates else "missing",
|
|
345
|
-
})
|
|
346
|
-
continue
|
|
347
|
-
if target != note.relative_path and target not in links:
|
|
348
|
-
links.append(target)
|
|
349
|
-
resolved[note.relative_path] = links
|
|
350
|
-
return resolved, unresolved
|
|
351
|
-
|
|
352
|
-
# ── sync ─────────────────────────────────────────────────────────────────
|
|
353
|
-
def sync(
|
|
354
|
-
self,
|
|
355
|
-
vault_path: Any,
|
|
356
|
-
*,
|
|
357
|
-
owner: Optional[str] = None,
|
|
358
|
-
workspace_id: Optional[str] = None,
|
|
359
|
-
user_email: Optional[str] = None,
|
|
360
|
-
dry_run: bool = False,
|
|
361
|
-
) -> Dict[str, Any]:
|
|
362
|
-
"""Ingest every note in an approved vault, then wire its structure.
|
|
363
|
-
|
|
364
|
-
Returns a summary the API hands back verbatim. ``dry_run`` reports what
|
|
365
|
-
a real run would touch — note count, resolvable links, tags, and the
|
|
366
|
-
pending list — without a single write.
|
|
367
|
-
"""
|
|
368
|
-
if not self.available():
|
|
369
|
-
return {
|
|
370
|
-
"status": "unavailable",
|
|
371
|
-
"vault": str(vault_path),
|
|
372
|
-
"detail": "Knowledge Graph ingestion is disabled (LATTICEAI_ENABLE_GRAPH).",
|
|
373
|
-
}
|
|
374
|
-
scan = self.scan(vault_path)
|
|
375
|
-
if scan.get("status") != "ok":
|
|
376
|
-
return {
|
|
377
|
-
"status": "failed",
|
|
378
|
-
"vault": scan["vault"],
|
|
379
|
-
"detail": scan.get("detail"),
|
|
380
|
-
"scanned": scan["scanned"],
|
|
381
|
-
}
|
|
382
|
-
notes: List[VaultNote] = scan["notes"]
|
|
383
|
-
resolved, unresolved = self.resolve_links(notes)
|
|
384
|
-
tags = sorted({tag for note in notes for tag in note.tags}, key=str.lower)
|
|
385
|
-
backlinks: Dict[str, List[str]] = {note.relative_path: [] for note in notes}
|
|
386
|
-
for source, targets in resolved.items():
|
|
387
|
-
for target in targets:
|
|
388
|
-
backlinks[target].append(source)
|
|
389
|
-
|
|
390
|
-
summary: Dict[str, Any] = {
|
|
391
|
-
"status": "ok",
|
|
392
|
-
"vault": scan["vault"],
|
|
393
|
-
"dry_run": bool(dry_run),
|
|
394
|
-
"scanned": scan["scanned"],
|
|
395
|
-
"notes": len(notes),
|
|
396
|
-
"ingested": 0,
|
|
397
|
-
"duplicate": 0,
|
|
398
|
-
"failed": 0,
|
|
399
|
-
"skipped": scan["skipped"],
|
|
400
|
-
"truncated": scan["truncated"],
|
|
401
|
-
"tags": len(tags),
|
|
402
|
-
"links": {
|
|
403
|
-
"resolved": sum(len(targets) for targets in resolved.values()),
|
|
404
|
-
"written": 0,
|
|
405
|
-
"unresolved_count": len(unresolved),
|
|
406
|
-
"unresolved": unresolved[:UNRESOLVED_REPORT_CAP],
|
|
407
|
-
},
|
|
408
|
-
"edges": {"status": "none", "references": 0, "tags": 0, "detail": None},
|
|
409
|
-
"errors": list(scan["errors"]),
|
|
410
|
-
}
|
|
411
|
-
if dry_run:
|
|
412
|
-
summary["status"] = "dry_run"
|
|
413
|
-
return summary
|
|
414
|
-
|
|
415
|
-
node_ids = self._ingest_notes(
|
|
416
|
-
notes,
|
|
417
|
-
vault=scan["vault"],
|
|
418
|
-
resolved=resolved,
|
|
419
|
-
backlinks=backlinks,
|
|
420
|
-
owner=owner,
|
|
421
|
-
workspace_id=workspace_id,
|
|
422
|
-
user_email=user_email,
|
|
423
|
-
summary=summary,
|
|
424
|
-
)
|
|
425
|
-
self._write_structure(
|
|
426
|
-
notes,
|
|
427
|
-
node_ids=node_ids,
|
|
428
|
-
resolved=resolved,
|
|
429
|
-
vault=scan["vault"],
|
|
430
|
-
owner=owner,
|
|
431
|
-
workspace_id=workspace_id,
|
|
432
|
-
summary=summary,
|
|
433
|
-
)
|
|
434
|
-
if summary["failed"] or summary["edges"]["status"] == "failed":
|
|
435
|
-
summary["status"] = "partial"
|
|
436
|
-
return summary
|
|
437
|
-
|
|
438
|
-
def _ingest_notes(
|
|
439
|
-
self,
|
|
440
|
-
notes: List[VaultNote],
|
|
441
|
-
*,
|
|
442
|
-
vault: str,
|
|
443
|
-
resolved: Dict[str, List[str]],
|
|
444
|
-
backlinks: Dict[str, List[str]],
|
|
445
|
-
owner: Optional[str],
|
|
446
|
-
workspace_id: Optional[str],
|
|
447
|
-
user_email: Optional[str],
|
|
448
|
-
summary: Dict[str, Any],
|
|
449
|
-
) -> Dict[str, str]:
|
|
450
|
-
node_ids: Dict[str, str] = {}
|
|
451
|
-
errors: List[Dict[str, Any]] = summary["errors"]
|
|
452
|
-
for note in notes:
|
|
453
|
-
item = IngestionItem(
|
|
454
|
-
source_type=SOURCE_TYPE,
|
|
455
|
-
title=note.title,
|
|
456
|
-
text=note.body,
|
|
457
|
-
source_uri=str(note.path),
|
|
458
|
-
mime_type="text/markdown",
|
|
459
|
-
owner=owner,
|
|
460
|
-
workspace_id=workspace_id,
|
|
461
|
-
metadata={
|
|
462
|
-
"vault": vault,
|
|
463
|
-
"relative_path": note.relative_path,
|
|
464
|
-
"frontmatter": note.frontmatter,
|
|
465
|
-
"tags": note.tags,
|
|
466
|
-
"wikilinks": note.link_targets,
|
|
467
|
-
"links": resolved.get(note.relative_path, []),
|
|
468
|
-
"backlinks": backlinks.get(note.relative_path, []),
|
|
469
|
-
},
|
|
470
|
-
)
|
|
471
|
-
result = self._pipeline.ingest(item, user_email=user_email or owner)
|
|
472
|
-
if result.status != "ok":
|
|
473
|
-
summary["failed"] += 1
|
|
474
|
-
if len(errors) < ERROR_REPORT_CAP:
|
|
475
|
-
errors.append({
|
|
476
|
-
"path": note.relative_path,
|
|
477
|
-
"status": result.status,
|
|
478
|
-
"detail": result.detail,
|
|
479
|
-
})
|
|
480
|
-
continue
|
|
481
|
-
if result.duplicate:
|
|
482
|
-
summary["duplicate"] += 1
|
|
483
|
-
else:
|
|
484
|
-
summary["ingested"] += 1
|
|
485
|
-
if result.node_id:
|
|
486
|
-
node_ids[note.relative_path] = result.node_id
|
|
487
|
-
return node_ids
|
|
488
|
-
|
|
489
|
-
def _write_structure(
|
|
490
|
-
self,
|
|
491
|
-
notes: List[VaultNote],
|
|
492
|
-
*,
|
|
493
|
-
node_ids: Dict[str, str],
|
|
494
|
-
resolved: Dict[str, List[str]],
|
|
495
|
-
vault: str,
|
|
496
|
-
owner: Optional[str],
|
|
497
|
-
workspace_id: Optional[str],
|
|
498
|
-
summary: Dict[str, Any],
|
|
499
|
-
) -> None:
|
|
500
|
-
"""Write link + tag edges through the store's public import door."""
|
|
501
|
-
edges: List[Dict[str, Any]] = []
|
|
502
|
-
topic_nodes: Dict[str, Dict[str, Any]] = {}
|
|
503
|
-
for note in notes:
|
|
504
|
-
from_id = node_ids.get(note.relative_path)
|
|
505
|
-
if from_id is None:
|
|
506
|
-
continue
|
|
507
|
-
for target in resolved.get(note.relative_path, []):
|
|
508
|
-
to_id = node_ids.get(target)
|
|
509
|
-
if to_id is None:
|
|
510
|
-
continue
|
|
511
|
-
edges.append(self._edge_row(from_id, to_id, LINK_RELATION, {
|
|
512
|
-
"source": SOURCE_TYPE,
|
|
513
|
-
"vault": vault,
|
|
514
|
-
"relation": "wikilink",
|
|
515
|
-
"from_note": note.relative_path,
|
|
516
|
-
"to_note": target,
|
|
517
|
-
}))
|
|
518
|
-
for tag in note.tags:
|
|
519
|
-
topic_id = _scoped_slug_id("topic", tag, workspace_id)
|
|
520
|
-
topic_nodes.setdefault(topic_id, self._topic_row(
|
|
521
|
-
topic_id, tag, vault=vault, owner=owner, workspace_id=workspace_id,
|
|
522
|
-
))
|
|
523
|
-
edges.append(self._edge_row(from_id, topic_id, TAG_RELATION, {
|
|
524
|
-
"source": SOURCE_TYPE,
|
|
525
|
-
"vault": vault,
|
|
526
|
-
"relation": "frontmatter_tag",
|
|
527
|
-
"tag": tag,
|
|
528
|
-
}))
|
|
529
|
-
reference_edges = sum(1 for edge in edges if edge["type"] == LINK_RELATION)
|
|
530
|
-
tag_edges = len(edges) - reference_edges
|
|
531
|
-
if not edges:
|
|
532
|
-
return
|
|
533
|
-
if self._kg is None:
|
|
534
|
-
summary["edges"] = {
|
|
535
|
-
"status": "skipped",
|
|
536
|
-
"references": 0,
|
|
537
|
-
"tags": 0,
|
|
538
|
-
"detail": "no Knowledge Graph store is bound; notes were ingested without link edges",
|
|
539
|
-
}
|
|
540
|
-
return
|
|
541
|
-
try:
|
|
542
|
-
outcome = self._kg.import_graph_data(
|
|
543
|
-
{
|
|
544
|
-
"nodes": list(topic_nodes.values()),
|
|
545
|
-
"edges": edges,
|
|
546
|
-
"chunks": [],
|
|
547
|
-
"knowledge_sources": [],
|
|
548
|
-
"provenance": [],
|
|
549
|
-
},
|
|
550
|
-
mode="merge",
|
|
551
|
-
)
|
|
552
|
-
except Exception as exc: # noqa: BLE001 — notes already landed; report, never crash
|
|
553
|
-
summary["edges"] = {
|
|
554
|
-
"status": "failed",
|
|
555
|
-
"references": 0,
|
|
556
|
-
"tags": 0,
|
|
557
|
-
"detail": f"link edges could not be written: {exc}",
|
|
558
|
-
}
|
|
559
|
-
return
|
|
560
|
-
summary["links"]["written"] = reference_edges
|
|
561
|
-
summary["edges"] = {
|
|
562
|
-
"status": "written",
|
|
563
|
-
"references": reference_edges,
|
|
564
|
-
"tags": tag_edges,
|
|
565
|
-
"topics": len(topic_nodes),
|
|
566
|
-
"detail": None,
|
|
567
|
-
"index": outcome.get("index"),
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
# Row shapes are shared with every other bridge (v11.2.0): two copies of
|
|
571
|
-
# "what an edge row looks like" is two places to forget an encoding.
|
|
572
|
-
_edge_row = staticmethod(edge_row)
|
|
573
|
-
|
|
574
|
-
@staticmethod
|
|
575
|
-
def _topic_row(
|
|
576
|
-
topic_id: str,
|
|
577
|
-
tag: str,
|
|
578
|
-
*,
|
|
579
|
-
vault: str,
|
|
580
|
-
owner: Optional[str],
|
|
581
|
-
workspace_id: Optional[str],
|
|
582
|
-
) -> Dict[str, Any]:
|
|
583
|
-
return topic_row(
|
|
584
|
-
topic_id,
|
|
585
|
-
tag,
|
|
586
|
-
summary=f"Obsidian tag #{tag}",
|
|
587
|
-
metadata={
|
|
588
|
-
"topic": tag,
|
|
589
|
-
"source": SOURCE_TYPE,
|
|
590
|
-
"vault": vault,
|
|
591
|
-
"owner": owner,
|
|
592
|
-
"workspace_id": workspace_id,
|
|
593
|
-
},
|
|
594
|
-
)
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
__all__ = [
|
|
598
|
-
"DEFAULT_MAX_FILES",
|
|
599
|
-
"DEFAULT_MAX_FILE_BYTES",
|
|
600
|
-
"LINK_RELATION",
|
|
601
|
-
"NOTE_EXTENSIONS",
|
|
602
|
-
"SOURCE_TYPE",
|
|
603
|
-
"TAG_RELATION",
|
|
604
|
-
"ObsidianVaultBridge",
|
|
605
|
-
"VaultNote",
|
|
606
|
-
"extract_link_targets",
|
|
607
|
-
"frontmatter_tags",
|
|
608
|
-
"parse_frontmatter",
|
|
609
|
-
]
|