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,290 +0,0 @@
|
|
|
1
|
-
"""Approximate nearest-neighbour index backed by optional ``hnswlib``.
|
|
2
|
-
|
|
3
|
-
``hnswlib`` is a compiled extension. It is **not** a core dependency: the
|
|
4
|
-
install stays pure-Python-plus-wheels for everyone who never asked for ANN,
|
|
5
|
-
and this backend is reachable only through the ``hnsw`` optional extra
|
|
6
|
-
(``pip install "ltcai[hnsw]"``) plus an explicit
|
|
7
|
-
``LATTICEAI_VECTOR_INDEX=hnsw``. When the module is missing the import is
|
|
8
|
-
caught here and reported as a reason string; the selector then falls back to
|
|
9
|
-
the exact scan and says so, rather than failing a search.
|
|
10
|
-
|
|
11
|
-
The graph search index is a **derivative**. SQLite remains the source of
|
|
12
|
-
truth, so the ``.hnsw`` sidecar next to the brain database is disposable: if
|
|
13
|
-
it is deleted, corrupt, or built for a different embedder/row-set, the
|
|
14
|
-
fingerprint check rejects it and the caller rebuilds from
|
|
15
|
-
``vector_embeddings``.
|
|
16
|
-
|
|
17
|
-
Approximation is reported, never hidden: this backend sets ``approx = True``
|
|
18
|
-
and ``exhaustive = False``, and ``scripts/bench_vector_index.py`` measures
|
|
19
|
-
recall@k against the brute-force backend so the cost of the speed is a
|
|
20
|
-
number, not a promise.
|
|
21
|
-
"""
|
|
22
|
-
|
|
23
|
-
from __future__ import annotations
|
|
24
|
-
|
|
25
|
-
import json
|
|
26
|
-
from pathlib import Path
|
|
27
|
-
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple
|
|
28
|
-
|
|
29
|
-
from .base import IndexItem, IndexStats, ScoredId, score_floor
|
|
30
|
-
|
|
31
|
-
HNSW_BACKEND = "hnsw"
|
|
32
|
-
HNSWLIB_MODULE = "hnswlib"
|
|
33
|
-
#: Sidecar suffix for the persisted graph (a derivative of the SQLite rows).
|
|
34
|
-
HNSW_SUFFIX = ".hnsw"
|
|
35
|
-
#: Companion JSON: fingerprint + label→id mapping. Without it the binary
|
|
36
|
-
#: graph is unreadable, because hnswlib only stores integer labels.
|
|
37
|
-
HNSW_META_SUFFIX = ".hnsw.meta.json"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def load_hnswlib() -> Tuple[Optional[Any], Optional[str]]:
|
|
41
|
-
"""Import ``hnswlib`` or explain why it is unavailable (never raises)."""
|
|
42
|
-
try:
|
|
43
|
-
import hnswlib # noqa: PLC0415 — guarded, optional, import-on-demand
|
|
44
|
-
except Exception as exc: # noqa: BLE001 — any import failure is "no ANN"
|
|
45
|
-
return None, (
|
|
46
|
-
f"{HNSWLIB_MODULE} is not available ({exc}); install the optional "
|
|
47
|
-
'extra with `pip install "ltcai[hnsw]"`'
|
|
48
|
-
)
|
|
49
|
-
return hnswlib, None
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def hnswlib_available() -> bool:
|
|
53
|
-
"""True when the optional ANN engine can actually be imported."""
|
|
54
|
-
module, _ = load_hnswlib()
|
|
55
|
-
return module is not None
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def sidecar_paths(db_path: Any) -> Tuple[Path, Path]:
|
|
59
|
-
"""``(index file, meta file)`` for a brain database path."""
|
|
60
|
-
base = Path(db_path)
|
|
61
|
-
return (
|
|
62
|
-
base.with_suffix(HNSW_SUFFIX),
|
|
63
|
-
base.with_suffix(HNSW_META_SUFFIX),
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
class HnswIndex:
|
|
68
|
-
"""Small-M HNSW graph over the brain's embeddings."""
|
|
69
|
-
|
|
70
|
-
def __init__(
|
|
71
|
-
self,
|
|
72
|
-
*,
|
|
73
|
-
dim: int,
|
|
74
|
-
space: str = "cosine",
|
|
75
|
-
ef_construction: int = 200,
|
|
76
|
-
m: int = 16,
|
|
77
|
-
ef_search: int = 64,
|
|
78
|
-
) -> None:
|
|
79
|
-
self._dim = int(dim)
|
|
80
|
-
self._space = str(space)
|
|
81
|
-
self._ef_construction = int(ef_construction)
|
|
82
|
-
self._m = int(m)
|
|
83
|
-
self._ef_search = int(ef_search)
|
|
84
|
-
self._module, self._detail = load_hnswlib()
|
|
85
|
-
self._vectors: Dict[str, List[float]] = {}
|
|
86
|
-
self._metadata: Dict[str, Dict[str, Any]] = {}
|
|
87
|
-
self._labels: List[str] = []
|
|
88
|
-
self._ann: Any = None
|
|
89
|
-
self._dirty = True
|
|
90
|
-
self._loaded = False
|
|
91
|
-
|
|
92
|
-
# ── identity ─────────────────────────────────────────────────────────────
|
|
93
|
-
@property
|
|
94
|
-
def backend(self) -> str:
|
|
95
|
-
return HNSW_BACKEND
|
|
96
|
-
|
|
97
|
-
@property
|
|
98
|
-
def approx(self) -> bool:
|
|
99
|
-
return True
|
|
100
|
-
|
|
101
|
-
@property
|
|
102
|
-
def exhaustive(self) -> bool:
|
|
103
|
-
return False
|
|
104
|
-
|
|
105
|
-
@property
|
|
106
|
-
def available(self) -> bool:
|
|
107
|
-
return self._module is not None
|
|
108
|
-
|
|
109
|
-
@property
|
|
110
|
-
def unavailable_detail(self) -> Optional[str]:
|
|
111
|
-
return self._detail
|
|
112
|
-
|
|
113
|
-
@property
|
|
114
|
-
def loaded_from_sidecar(self) -> bool:
|
|
115
|
-
"""True when the graph came off disk instead of being rebuilt."""
|
|
116
|
-
return self._loaded
|
|
117
|
-
|
|
118
|
-
# ── mutation ─────────────────────────────────────────────────────────────
|
|
119
|
-
def _reject_mutation_after_load(self) -> None:
|
|
120
|
-
if self._loaded:
|
|
121
|
-
raise RuntimeError(
|
|
122
|
-
"this HnswIndex was loaded from a sidecar and holds no source "
|
|
123
|
-
"vectors; rebuild it from vector_embeddings before mutating"
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
def add(
|
|
127
|
-
self,
|
|
128
|
-
id: str,
|
|
129
|
-
vector: Sequence[float],
|
|
130
|
-
metadata: Optional[Mapping[str, Any]] = None,
|
|
131
|
-
) -> None:
|
|
132
|
-
self._reject_mutation_after_load()
|
|
133
|
-
key = str(id)
|
|
134
|
-
self._vectors[key] = [float(value) for value in vector]
|
|
135
|
-
self._metadata[key] = dict(metadata or {})
|
|
136
|
-
self._dirty = True
|
|
137
|
-
|
|
138
|
-
def remove(self, id: str) -> None:
|
|
139
|
-
self._reject_mutation_after_load()
|
|
140
|
-
key = str(id)
|
|
141
|
-
self._vectors.pop(key, None)
|
|
142
|
-
self._metadata.pop(key, None)
|
|
143
|
-
self._dirty = True
|
|
144
|
-
|
|
145
|
-
def rebuild(self, items: Iterable[IndexItem]) -> None:
|
|
146
|
-
self._vectors.clear()
|
|
147
|
-
self._metadata.clear()
|
|
148
|
-
self._loaded = False
|
|
149
|
-
for item_id, vector, metadata in items:
|
|
150
|
-
self.add(item_id, vector, metadata)
|
|
151
|
-
self._dirty = True
|
|
152
|
-
|
|
153
|
-
# ── graph construction ───────────────────────────────────────────────────
|
|
154
|
-
def _new_graph(self, module: Any, capacity: int) -> Any:
|
|
155
|
-
graph = module.Index(space=self._space, dim=self._dim)
|
|
156
|
-
graph.init_index(
|
|
157
|
-
max_elements=max(1, capacity),
|
|
158
|
-
ef_construction=self._ef_construction,
|
|
159
|
-
M=self._m,
|
|
160
|
-
)
|
|
161
|
-
return graph
|
|
162
|
-
|
|
163
|
-
def _ensure_graph(self) -> Any:
|
|
164
|
-
"""Build the graph if the held vectors changed (None when disabled)."""
|
|
165
|
-
module = self._module
|
|
166
|
-
if module is None:
|
|
167
|
-
return None
|
|
168
|
-
if self._ann is not None and not self._dirty:
|
|
169
|
-
return self._ann
|
|
170
|
-
labels = list(self._vectors)
|
|
171
|
-
graph = self._new_graph(module, len(labels))
|
|
172
|
-
if labels:
|
|
173
|
-
graph.add_items(
|
|
174
|
-
[self._vectors[key] for key in labels],
|
|
175
|
-
list(range(len(labels))),
|
|
176
|
-
)
|
|
177
|
-
self._ann = graph
|
|
178
|
-
self._labels = labels
|
|
179
|
-
self._dirty = False
|
|
180
|
-
return graph
|
|
181
|
-
|
|
182
|
-
# ── search ───────────────────────────────────────────────────────────────
|
|
183
|
-
def search(
|
|
184
|
-
self,
|
|
185
|
-
query: Sequence[float],
|
|
186
|
-
top_k: int,
|
|
187
|
-
filter: Optional[Mapping[str, Any]] = None,
|
|
188
|
-
) -> List[ScoredId]:
|
|
189
|
-
graph = self._ensure_graph()
|
|
190
|
-
if graph is None or not self._labels:
|
|
191
|
-
return []
|
|
192
|
-
floor = score_floor(filter)
|
|
193
|
-
wanted = max(1, min(int(top_k), len(self._labels)))
|
|
194
|
-
graph.set_ef(max(self._ef_search, wanted))
|
|
195
|
-
labels, distances = graph.knn_query([list(query)], k=wanted)
|
|
196
|
-
scored: List[ScoredId] = []
|
|
197
|
-
for label, distance in zip(labels[0], distances[0], strict=True):
|
|
198
|
-
# cosine/ip space: hnswlib returns 1 - similarity as the distance.
|
|
199
|
-
score = 1.0 - float(distance)
|
|
200
|
-
if score < floor:
|
|
201
|
-
continue
|
|
202
|
-
scored.append((self._labels[int(label)], score))
|
|
203
|
-
return scored
|
|
204
|
-
|
|
205
|
-
def stats(self) -> IndexStats:
|
|
206
|
-
return IndexStats(
|
|
207
|
-
backend=self.backend,
|
|
208
|
-
size=len(self._labels) if self._loaded else len(self._vectors),
|
|
209
|
-
dim=self._dim,
|
|
210
|
-
approx=True,
|
|
211
|
-
exhaustive=False,
|
|
212
|
-
available=self.available,
|
|
213
|
-
detail=self._detail,
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
# ── sidecar persistence (a derivative — safe to delete) ──────────────────
|
|
217
|
-
def save(self, db_path: Any, *, fingerprint: str) -> bool:
|
|
218
|
-
"""Write the graph + label map beside the brain database.
|
|
219
|
-
|
|
220
|
-
Returns False (never raises) when ANN is unavailable, the index is
|
|
221
|
-
empty, or the filesystem refuses the write: a missing sidecar only
|
|
222
|
-
costs a rebuild on the next search.
|
|
223
|
-
"""
|
|
224
|
-
graph = self._ensure_graph()
|
|
225
|
-
if graph is None or not self._labels:
|
|
226
|
-
return False
|
|
227
|
-
index_path, meta_path = sidecar_paths(db_path)
|
|
228
|
-
try:
|
|
229
|
-
index_path.parent.mkdir(parents=True, exist_ok=True)
|
|
230
|
-
graph.save_index(str(index_path))
|
|
231
|
-
meta_path.write_text(
|
|
232
|
-
json.dumps(
|
|
233
|
-
{
|
|
234
|
-
"fingerprint": str(fingerprint),
|
|
235
|
-
"dim": self._dim,
|
|
236
|
-
"space": self._space,
|
|
237
|
-
"labels": self._labels,
|
|
238
|
-
},
|
|
239
|
-
ensure_ascii=False,
|
|
240
|
-
),
|
|
241
|
-
encoding="utf-8",
|
|
242
|
-
)
|
|
243
|
-
except Exception: # noqa: BLE001 — persistence is best-effort
|
|
244
|
-
return False
|
|
245
|
-
return True
|
|
246
|
-
|
|
247
|
-
def load(self, db_path: Any, *, fingerprint: str) -> bool:
|
|
248
|
-
"""Adopt a sidecar graph when it provably matches ``fingerprint``.
|
|
249
|
-
|
|
250
|
-
Any mismatch — missing file, unreadable JSON, different embedder or
|
|
251
|
-
row-set, wrong dimension — returns False so the caller rebuilds. The
|
|
252
|
-
sidecar is never trusted over SQLite.
|
|
253
|
-
"""
|
|
254
|
-
module = self._module
|
|
255
|
-
if module is None:
|
|
256
|
-
return False
|
|
257
|
-
index_path, meta_path = sidecar_paths(db_path)
|
|
258
|
-
try:
|
|
259
|
-
meta = json.loads(meta_path.read_text(encoding="utf-8"))
|
|
260
|
-
matches = str(meta["fingerprint"]) == str(fingerprint)
|
|
261
|
-
matches = matches and int(meta["dim"]) == self._dim
|
|
262
|
-
labels = [str(label) for label in meta["labels"]]
|
|
263
|
-
except Exception: # noqa: BLE001 — absent/corrupt sidecar = rebuild
|
|
264
|
-
return False
|
|
265
|
-
if not (matches and labels):
|
|
266
|
-
return False
|
|
267
|
-
try:
|
|
268
|
-
graph = module.Index(space=self._space, dim=self._dim)
|
|
269
|
-
graph.load_index(str(index_path), max_elements=len(labels))
|
|
270
|
-
except Exception: # noqa: BLE001 — corrupt binary = rebuild
|
|
271
|
-
return False
|
|
272
|
-
self._ann = graph
|
|
273
|
-
self._labels = labels
|
|
274
|
-
self._vectors.clear()
|
|
275
|
-
self._metadata.clear()
|
|
276
|
-
self._dirty = False
|
|
277
|
-
self._loaded = True
|
|
278
|
-
return True
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
__all__ = [
|
|
282
|
-
"HNSW_BACKEND",
|
|
283
|
-
"HNSW_META_SUFFIX",
|
|
284
|
-
"HNSW_SUFFIX",
|
|
285
|
-
"HNSWLIB_MODULE",
|
|
286
|
-
"HnswIndex",
|
|
287
|
-
"hnswlib_available",
|
|
288
|
-
"load_hnswlib",
|
|
289
|
-
"sidecar_paths",
|
|
290
|
-
]
|
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
"""Durable pending-embed queue: ingest now, embed a moment later.
|
|
2
|
-
|
|
3
|
-
The write path already degrades honestly — when the incremental vector sync
|
|
4
|
-
fails, ``IngestionResult.indexing_status`` becomes ``"pending"`` and
|
|
5
|
-
``index_status()`` keeps the node visible as backlog. What was missing is
|
|
6
|
-
anyone whose job it is to *come back for it*. Until a human ran a rebuild,
|
|
7
|
-
"pending" meant "unsearchable, indefinitely".
|
|
8
|
-
|
|
9
|
-
This is that worker's memory, and it is on disk for the same reason
|
|
10
|
-
``ingestion_jobs`` is: a queue that lives in one process's heap forgets its
|
|
11
|
-
backlog on restart, which is exactly when a backlog exists. The table is
|
|
12
|
-
created lazily inside whichever SQLite file the brain already uses, so the
|
|
13
|
-
existing backup/restore covers it with no manifest change.
|
|
14
|
-
|
|
15
|
-
The queue is a pure service — schedule + tick, no threads, no server, no
|
|
16
|
-
event loop. :meth:`VectorEmbedQueue.tick_async` exists for callers that live
|
|
17
|
-
on the event loop and hands the blocking work to a thread.
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
from __future__ import annotations
|
|
21
|
-
|
|
22
|
-
import asyncio
|
|
23
|
-
import logging
|
|
24
|
-
import sqlite3
|
|
25
|
-
from contextlib import contextmanager
|
|
26
|
-
from pathlib import Path
|
|
27
|
-
from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple
|
|
28
|
-
|
|
29
|
-
from ...utils import utc_now_iso
|
|
30
|
-
|
|
31
|
-
#: Job lifecycle. ``failed`` is terminal — the retry budget is exhausted.
|
|
32
|
-
VECTOR_JOB_STATUSES = ("pending", "running", "done", "failed")
|
|
33
|
-
#: How many times one node may fail before the queue stops retrying it.
|
|
34
|
-
DEFAULT_MAX_ATTEMPTS = 3
|
|
35
|
-
#: Nodes claimed per :meth:`VectorEmbedQueue.tick`.
|
|
36
|
-
DEFAULT_TICK_LIMIT = 25
|
|
37
|
-
|
|
38
|
-
LOGGER = logging.getLogger(__name__)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class VectorJobStore:
|
|
42
|
-
"""SQLite persistence for the pending-embed backlog.
|
|
43
|
-
|
|
44
|
-
Own connection per operation, always closed — ``with sqlite3.connect(...)``
|
|
45
|
-
commits but never closes, which is how 70 leaks accumulated before 10.2.0.
|
|
46
|
-
"""
|
|
47
|
-
|
|
48
|
-
def __init__(self, db_path: Any) -> None:
|
|
49
|
-
self.db_path = Path(db_path)
|
|
50
|
-
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
51
|
-
self._init_db()
|
|
52
|
-
|
|
53
|
-
@contextmanager
|
|
54
|
-
def _connect(self) -> Iterator[sqlite3.Connection]:
|
|
55
|
-
conn = sqlite3.connect(str(self.db_path))
|
|
56
|
-
conn.row_factory = sqlite3.Row
|
|
57
|
-
try:
|
|
58
|
-
with conn:
|
|
59
|
-
yield conn
|
|
60
|
-
finally:
|
|
61
|
-
conn.close()
|
|
62
|
-
|
|
63
|
-
def _init_db(self) -> None:
|
|
64
|
-
with self._connect() as conn:
|
|
65
|
-
conn.executescript(
|
|
66
|
-
"""
|
|
67
|
-
CREATE TABLE IF NOT EXISTS vector_jobs (
|
|
68
|
-
node_id TEXT PRIMARY KEY,
|
|
69
|
-
status TEXT NOT NULL DEFAULT 'pending',
|
|
70
|
-
attempts INTEGER NOT NULL DEFAULT 0,
|
|
71
|
-
detail TEXT,
|
|
72
|
-
created_at TEXT NOT NULL,
|
|
73
|
-
updated_at TEXT NOT NULL
|
|
74
|
-
);
|
|
75
|
-
CREATE INDEX IF NOT EXISTS idx_vector_jobs_status
|
|
76
|
-
ON vector_jobs(status, created_at);
|
|
77
|
-
"""
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
def enqueue(self, node_id: str, *, detail: Optional[str] = None) -> bool:
|
|
81
|
-
"""Mark ``node_id`` pending. Re-queuing an existing row is expected."""
|
|
82
|
-
node_id = str(node_id or "").strip()
|
|
83
|
-
if not node_id:
|
|
84
|
-
return False
|
|
85
|
-
now = utc_now_iso()
|
|
86
|
-
with self._connect() as conn:
|
|
87
|
-
conn.execute(
|
|
88
|
-
"""
|
|
89
|
-
INSERT INTO vector_jobs(node_id, status, attempts, detail,
|
|
90
|
-
created_at, updated_at)
|
|
91
|
-
VALUES (?, 'pending', 0, ?, ?, ?)
|
|
92
|
-
ON CONFLICT(node_id) DO UPDATE SET
|
|
93
|
-
status='pending',
|
|
94
|
-
detail=excluded.detail,
|
|
95
|
-
updated_at=excluded.updated_at
|
|
96
|
-
""",
|
|
97
|
-
(node_id, detail, now, now),
|
|
98
|
-
)
|
|
99
|
-
return True
|
|
100
|
-
|
|
101
|
-
def claim(self, limit: int) -> List[str]:
|
|
102
|
-
"""Take up to ``limit`` pending node ids and mark them running."""
|
|
103
|
-
limit = max(1, int(limit))
|
|
104
|
-
now = utc_now_iso()
|
|
105
|
-
with self._connect() as conn:
|
|
106
|
-
rows = conn.execute(
|
|
107
|
-
"""
|
|
108
|
-
SELECT node_id FROM vector_jobs
|
|
109
|
-
WHERE status='pending'
|
|
110
|
-
ORDER BY created_at ASC, node_id ASC
|
|
111
|
-
LIMIT ?
|
|
112
|
-
""",
|
|
113
|
-
(limit,),
|
|
114
|
-
).fetchall()
|
|
115
|
-
claimed = [str(row["node_id"]) for row in rows]
|
|
116
|
-
for node_id in claimed:
|
|
117
|
-
conn.execute(
|
|
118
|
-
"""
|
|
119
|
-
UPDATE vector_jobs
|
|
120
|
-
SET status='running', attempts=attempts+1, updated_at=?
|
|
121
|
-
WHERE node_id=?
|
|
122
|
-
""",
|
|
123
|
-
(now, node_id),
|
|
124
|
-
)
|
|
125
|
-
return claimed
|
|
126
|
-
|
|
127
|
-
def finish(self, node_id: str, *, status: str, detail: Optional[str]) -> None:
|
|
128
|
-
with self._connect() as conn:
|
|
129
|
-
conn.execute(
|
|
130
|
-
"UPDATE vector_jobs SET status=?, detail=?, updated_at=? "
|
|
131
|
-
"WHERE node_id=?",
|
|
132
|
-
(status, detail, utc_now_iso(), str(node_id)),
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
def attempts_for(self, node_id: str) -> int:
|
|
136
|
-
with self._connect() as conn:
|
|
137
|
-
row = conn.execute(
|
|
138
|
-
"SELECT attempts FROM vector_jobs WHERE node_id=?", (str(node_id),)
|
|
139
|
-
).fetchone()
|
|
140
|
-
return int(row["attempts"]) if row is not None else 0
|
|
141
|
-
|
|
142
|
-
def counts(self) -> Dict[str, int]:
|
|
143
|
-
"""``{status: count}`` with every known status present (zero-filled)."""
|
|
144
|
-
counts = dict.fromkeys(VECTOR_JOB_STATUSES, 0)
|
|
145
|
-
with self._connect() as conn:
|
|
146
|
-
for row in conn.execute(
|
|
147
|
-
"SELECT status, COUNT(*) AS total FROM vector_jobs GROUP BY status"
|
|
148
|
-
):
|
|
149
|
-
counts[str(row["status"])] = int(row["total"])
|
|
150
|
-
return counts
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
class VectorEmbedQueue:
|
|
154
|
-
"""Schedule nodes for background embedding, then drain them a tick at a time.
|
|
155
|
-
|
|
156
|
-
``indexer`` is normally ``KnowledgeGraphStore.index_node_incremental``: it
|
|
157
|
-
returns a status dict and never raises. A queue built without a usable
|
|
158
|
-
database is *disabled*, not silently in-memory — :meth:`describe` says so
|
|
159
|
-
and :meth:`schedule` returns False, because a backlog that evaporates on
|
|
160
|
-
restart is worse than an honest "not tracked".
|
|
161
|
-
"""
|
|
162
|
-
|
|
163
|
-
def __init__(
|
|
164
|
-
self,
|
|
165
|
-
*,
|
|
166
|
-
db_path: Optional[Any] = None,
|
|
167
|
-
indexer: Optional[Callable[[str], Any]] = None,
|
|
168
|
-
max_attempts: int = DEFAULT_MAX_ATTEMPTS,
|
|
169
|
-
) -> None:
|
|
170
|
-
self._indexer = indexer
|
|
171
|
-
self._max_attempts = max(1, int(max_attempts))
|
|
172
|
-
self._store: Optional[VectorJobStore] = None
|
|
173
|
-
self._detail: Optional[str] = None
|
|
174
|
-
if db_path is None:
|
|
175
|
-
self._detail = "no database configured; the embed backlog is not tracked"
|
|
176
|
-
else:
|
|
177
|
-
try:
|
|
178
|
-
self._store = VectorJobStore(db_path)
|
|
179
|
-
except Exception as exc: # noqa: BLE001 — never block ingestion
|
|
180
|
-
self._detail = f"vector job persistence unavailable: {exc}"
|
|
181
|
-
LOGGER.warning("vector job persistence unavailable: %s", exc)
|
|
182
|
-
|
|
183
|
-
@property
|
|
184
|
-
def available(self) -> bool:
|
|
185
|
-
return self._store is not None
|
|
186
|
-
|
|
187
|
-
def describe(self) -> Dict[str, Any]:
|
|
188
|
-
"""Whether the backlog survives a restart, and where it lives."""
|
|
189
|
-
return {
|
|
190
|
-
"persistent": self._store is not None,
|
|
191
|
-
"db_path": str(self._store.db_path) if self._store is not None else None,
|
|
192
|
-
"detail": self._detail,
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
def schedule(self, node_id: str, *, detail: Optional[str] = None) -> bool:
|
|
196
|
-
if self._store is None:
|
|
197
|
-
return False
|
|
198
|
-
try:
|
|
199
|
-
return self._store.enqueue(node_id, detail=detail)
|
|
200
|
-
except Exception as exc: # noqa: BLE001 — queueing is best-effort
|
|
201
|
-
LOGGER.warning("vector job %s could not be queued: %s", node_id, exc)
|
|
202
|
-
return False
|
|
203
|
-
|
|
204
|
-
def snapshot(self) -> Dict[str, int]:
|
|
205
|
-
"""Backlog counts by status (all zero when the queue is disabled)."""
|
|
206
|
-
if self._store is None:
|
|
207
|
-
return dict.fromkeys(VECTOR_JOB_STATUSES, 0)
|
|
208
|
-
try:
|
|
209
|
-
return self._store.counts()
|
|
210
|
-
except Exception: # noqa: BLE001 — a status read must never raise
|
|
211
|
-
return dict.fromkeys(VECTOR_JOB_STATUSES, 0)
|
|
212
|
-
|
|
213
|
-
def pending_count(self) -> int:
|
|
214
|
-
"""Nodes still owed an embedding (queued plus in flight)."""
|
|
215
|
-
counts = self.snapshot()
|
|
216
|
-
return int(counts["pending"]) + int(counts["running"])
|
|
217
|
-
|
|
218
|
-
def tick(self, limit: int = DEFAULT_TICK_LIMIT) -> Dict[str, Any]:
|
|
219
|
-
"""Drain up to ``limit`` queued nodes. Never raises.
|
|
220
|
-
|
|
221
|
-
A node whose indexing fails goes back to ``pending`` until its retry
|
|
222
|
-
budget runs out, then stays ``failed`` with the last reason attached —
|
|
223
|
-
visible backlog, not a silent retry loop.
|
|
224
|
-
"""
|
|
225
|
-
summary: Dict[str, Any] = {
|
|
226
|
-
"claimed": 0,
|
|
227
|
-
"indexed": 0,
|
|
228
|
-
"retried": 0,
|
|
229
|
-
"failed": 0,
|
|
230
|
-
"detail": self._detail,
|
|
231
|
-
}
|
|
232
|
-
store = self._store
|
|
233
|
-
if store is None:
|
|
234
|
-
return summary
|
|
235
|
-
indexer = self._indexer
|
|
236
|
-
if indexer is None:
|
|
237
|
-
summary["detail"] = "no indexer configured; queued nodes were left pending"
|
|
238
|
-
return summary
|
|
239
|
-
claimed = store.claim(limit)
|
|
240
|
-
summary["claimed"] = len(claimed)
|
|
241
|
-
for node_id in claimed:
|
|
242
|
-
status, detail = self._run_one(store, indexer, node_id)
|
|
243
|
-
if status == "done":
|
|
244
|
-
summary["indexed"] += 1
|
|
245
|
-
elif status == "pending":
|
|
246
|
-
summary["retried"] += 1
|
|
247
|
-
else:
|
|
248
|
-
summary["failed"] += 1
|
|
249
|
-
store.finish(node_id, status=status, detail=detail)
|
|
250
|
-
return summary
|
|
251
|
-
|
|
252
|
-
async def tick_async(self, limit: int = DEFAULT_TICK_LIMIT) -> Dict[str, Any]:
|
|
253
|
-
""":meth:`tick` off the event loop — embedding and SQLite both block."""
|
|
254
|
-
return await asyncio.to_thread(self.tick, limit)
|
|
255
|
-
|
|
256
|
-
def _run_one(
|
|
257
|
-
self,
|
|
258
|
-
store: VectorJobStore,
|
|
259
|
-
indexer: Callable[[str], Any],
|
|
260
|
-
node_id: str,
|
|
261
|
-
) -> Tuple[str, Optional[str]]:
|
|
262
|
-
"""Index one node → the status to persist and the reason for it."""
|
|
263
|
-
try:
|
|
264
|
-
outcome = indexer(node_id) or {}
|
|
265
|
-
except Exception as exc: # noqa: BLE001 — one bad node must not stop the drain
|
|
266
|
-
return self._retry_or_fail(store, node_id, str(exc))
|
|
267
|
-
if str(outcome.get("status") or "") == "failed":
|
|
268
|
-
detail = str(outcome.get("detail") or "unknown error")
|
|
269
|
-
return self._retry_or_fail(store, node_id, detail)
|
|
270
|
-
return "done", None
|
|
271
|
-
|
|
272
|
-
def _retry_or_fail(
|
|
273
|
-
self, store: VectorJobStore, node_id: str, detail: str
|
|
274
|
-
) -> Tuple[str, Optional[str]]:
|
|
275
|
-
attempts = store.attempts_for(node_id)
|
|
276
|
-
if attempts >= self._max_attempts:
|
|
277
|
-
return "failed", f"{detail} (gave up after {attempts} attempts)"
|
|
278
|
-
return "pending", detail
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
__all__ = [
|
|
282
|
-
"DEFAULT_MAX_ATTEMPTS",
|
|
283
|
-
"DEFAULT_TICK_LIMIT",
|
|
284
|
-
"VECTOR_JOB_STATUSES",
|
|
285
|
-
"VectorEmbedQueue",
|
|
286
|
-
"VectorJobStore",
|
|
287
|
-
]
|