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,766 +1,33 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
"""The embedder report behind ``GET /api/embeddings/status``.
|
|
2
|
+
|
|
3
|
+
``SearchService`` was v3's retrieval orchestrator: keyword, vector, graph and
|
|
4
|
+
hybrid search, the fusion weights, the image-space query channel, the index
|
|
5
|
+
rebuild. ``lattice-retrieval`` owns all of it now, and the worker keeps the one
|
|
6
|
+
question that is a fact about *this* process — which embedding provider
|
|
7
|
+
resolved, at what width, and whether it is the one that was asked for.
|
|
8
|
+
|
|
9
|
+
The v11.6.0 boundary, stated because the payload used to blur it: this reports
|
|
10
|
+
the **embedder**, not the index. ``embeddings_status`` called
|
|
11
|
+
``graph.index_status()`` to fill an ``index`` block; the worker no longer opens
|
|
12
|
+
that store, and index completeness is a native jobs route. The key stays in the
|
|
13
|
+
response, empty, because a client that reads it should see "this answer has
|
|
14
|
+
nothing to say about the index" rather than a missing field.
|
|
5
15
|
"""
|
|
6
16
|
|
|
7
17
|
from __future__ import annotations
|
|
8
18
|
|
|
9
19
|
from dataclasses import dataclass
|
|
10
|
-
from
|
|
11
|
-
from typing import Any, Dict, List, Mapping, Optional, Tuple
|
|
12
|
-
|
|
13
|
-
from lattice_brain.gates import FeatureGate
|
|
14
|
-
from lattice_brain.graph._kg_fsutil import _parse_iso, _recency_score
|
|
15
|
-
from lattice_brain.graph.image_vectors import DEFAULT_IMAGE_FUSION_WEIGHT
|
|
16
|
-
from lattice_brain.graph.retrieval_policy import resolve_policy
|
|
17
|
-
|
|
18
|
-
DEFAULT_HYBRID_WEIGHTS = {
|
|
19
|
-
"keyword": 0.35,
|
|
20
|
-
"vector": 0.40,
|
|
21
|
-
"graph": 0.25,
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
# ── text query → image space (v11.2.0) ───────────────────────────────────────
|
|
25
|
-
# 11.1.0 shipped image late fusion as API-only, because the two vector spaces
|
|
26
|
-
# are not comparable and nothing produced a query vector for the image index.
|
|
27
|
-
# A *shared-space* vision model can produce one, and this is the switch that
|
|
28
|
-
# lets the search API do it automatically. Off by default and resolved when
|
|
29
|
-
# asked, so a settings toggle can move it without a restart; with it off the
|
|
30
|
-
# hybrid response is byte-identical to 11.1.0's.
|
|
31
|
-
TEXT_IMAGE_FUSION_ENV = "LATTICEAI_TEXT_IMAGE_FUSION"
|
|
32
|
-
IMAGE_QUERY_FUSION_GATE = FeatureGate(
|
|
33
|
-
TEXT_IMAGE_FUSION_ENV,
|
|
34
|
-
default=False,
|
|
35
|
-
name="text_image_fusion",
|
|
36
|
-
detail=(
|
|
37
|
-
"Typed questions can also be scored against the image index when a "
|
|
38
|
-
"shared-space vision model is configured."
|
|
39
|
-
),
|
|
40
|
-
)
|
|
41
|
-
IMAGE_FUSION_UNAVAILABLE = (
|
|
42
|
-
"no shared-space vision model is configured, so a typed question cannot be "
|
|
43
|
-
"scored against image vectors; pictures are still found through their OCR "
|
|
44
|
-
"text and captions"
|
|
45
|
-
)
|
|
46
|
-
IMAGE_FUSION_DISABLED = (
|
|
47
|
-
"automatic image fusion is off for this install "
|
|
48
|
-
f"({TEXT_IMAGE_FUSION_ENV}); the caller may still supply an image vector"
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def _clean(text: Any, limit: int = 1000) -> str:
|
|
53
|
-
return " ".join(str(text or "").split())[:limit]
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def _result_key(result: Mapping[str, Any]) -> str:
|
|
57
|
-
return str(result.get("id") or result.get("node_id") or "")
|
|
20
|
+
from typing import Any, Dict, Mapping, Optional
|
|
58
21
|
|
|
59
22
|
|
|
60
23
|
@dataclass
|
|
61
24
|
class SearchService:
|
|
62
|
-
|
|
63
|
-
#: ``(query_text) -> image-space vector``. Defaults to the port the store
|
|
64
|
-
#: already carries (``multimodal_ports.text_to_image_embedder``), so no new
|
|
65
|
-
#: wiring is needed; the field exists so a test — or an install with its
|
|
66
|
-
#: own encoder — can supply one directly.
|
|
67
|
-
image_query_embedder: Any = None
|
|
68
|
-
|
|
69
|
-
# ── image-space query channel (v11.2.0) ──────────────────────────────
|
|
70
|
-
def image_query_status(self) -> Dict[str, Any]:
|
|
71
|
-
"""Whether a typed question can reach the image index here, and why not."""
|
|
72
|
-
port, detail = self._image_query_port()
|
|
73
|
-
return {
|
|
74
|
-
"available": port is not None,
|
|
75
|
-
"gate": IMAGE_QUERY_FUSION_GATE.describe(),
|
|
76
|
-
"detail": detail,
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
def _image_query_port(self) -> Tuple[Any, Optional[str]]:
|
|
80
|
-
"""The text→image port and, when there is none, the honest reason."""
|
|
81
|
-
if self.image_query_embedder is not None:
|
|
82
|
-
return self.image_query_embedder, None
|
|
83
|
-
ports = getattr(self.graph_store, "multimodal_ports", None)
|
|
84
|
-
port = getattr(ports, "text_to_image_embedder", None)
|
|
85
|
-
return (port, None) if port is not None else (None, IMAGE_FUSION_UNAVAILABLE)
|
|
86
|
-
|
|
87
|
-
def _image_query_vector(self, query: str) -> Tuple[Optional[List[float]], Optional[str]]:
|
|
88
|
-
port, detail = self._image_query_port()
|
|
89
|
-
if port is None:
|
|
90
|
-
return None, detail
|
|
91
|
-
try:
|
|
92
|
-
vector = [float(value) for value in (port(query) or [])]
|
|
93
|
-
except Exception as exc: # noqa: BLE001 — a broken encoder never fails a search
|
|
94
|
-
return None, f"the vision model could not embed the query: {exc}"
|
|
95
|
-
if not vector:
|
|
96
|
-
return None, "the vision model returned an empty query vector"
|
|
97
|
-
return vector, None
|
|
98
|
-
|
|
99
|
-
def _image_channel(
|
|
100
|
-
self, query: str, matches: List[Dict[str, Any]], *, requested: bool, limit: int
|
|
101
|
-
) -> Optional[Dict[str, Any]]:
|
|
102
|
-
"""Late-fuse the image index into an already-ranked match list.
|
|
103
|
-
|
|
104
|
-
Returns ``None`` when the caller did not ask, which is what keeps the
|
|
105
|
-
response byte-identical for everyone who did not opt in. When it *was*
|
|
106
|
-
asked for and could not run, it returns the reason rather than silently
|
|
107
|
-
producing the same answer as before.
|
|
108
|
-
"""
|
|
109
|
-
if not requested:
|
|
110
|
-
return None
|
|
111
|
-
report: Dict[str, Any] = {
|
|
112
|
-
"requested": True,
|
|
113
|
-
"applied": False,
|
|
114
|
-
"weight": DEFAULT_IMAGE_FUSION_WEIGHT,
|
|
115
|
-
"candidates": 0,
|
|
116
|
-
"fused": 0,
|
|
117
|
-
"detail": None,
|
|
118
|
-
}
|
|
119
|
-
if not IMAGE_QUERY_FUSION_GATE.enabled():
|
|
120
|
-
report["detail"] = IMAGE_FUSION_DISABLED
|
|
121
|
-
return {"image_fusion": report}
|
|
122
|
-
vector, detail = self._image_query_vector(query)
|
|
123
|
-
if vector is None:
|
|
124
|
-
report["detail"] = detail
|
|
125
|
-
return {"image_fusion": report}
|
|
126
|
-
from lattice_brain.graph.image_vectors import image_similarity_search
|
|
127
|
-
|
|
128
|
-
try:
|
|
129
|
-
found = image_similarity_search(
|
|
130
|
-
self.graph_store, vector, top_k=max(1, int(limit) * 2)
|
|
131
|
-
)
|
|
132
|
-
except Exception as exc: # noqa: BLE001 — the text answer must survive
|
|
133
|
-
report["detail"] = f"image index unavailable: {exc}"
|
|
134
|
-
return {"image_fusion": report}
|
|
135
|
-
report["candidates"] = int(found.get("candidates") or 0)
|
|
136
|
-
scores = {
|
|
137
|
-
str(match["node_id"]): float(match["score"])
|
|
138
|
-
for match in found.get("matches") or []
|
|
139
|
-
}
|
|
140
|
-
report["fused"] = self._blend_image_scores(matches, scores)
|
|
141
|
-
report["applied"] = True
|
|
142
|
-
report["detail"] = found.get("detail")
|
|
143
|
-
return {"image_fusion": report}
|
|
144
|
-
|
|
145
|
-
@staticmethod
|
|
146
|
-
def _blend_image_scores(
|
|
147
|
-
matches: List[Dict[str, Any]],
|
|
148
|
-
scores: Dict[str, float],
|
|
149
|
-
*,
|
|
150
|
-
weight: float = DEFAULT_IMAGE_FUSION_WEIGHT,
|
|
151
|
-
) -> int:
|
|
152
|
-
"""The graph layer's blend, over this layer's ``source_scores`` shape.
|
|
153
|
-
|
|
154
|
-
Late fusion by construction: the two rankings were produced
|
|
155
|
-
independently and only the final numbers meet, so neither space is ever
|
|
156
|
-
asked to interpret the other's.
|
|
157
|
-
"""
|
|
158
|
-
touched = 0
|
|
159
|
-
for match in matches:
|
|
160
|
-
raw = scores.get(_result_key(match))
|
|
161
|
-
if raw is None:
|
|
162
|
-
continue
|
|
163
|
-
touched += 1
|
|
164
|
-
match.setdefault("source_scores", {})["image"] = round(raw, 6)
|
|
165
|
-
blended = (1.0 - weight) * float(match.get("score") or 0.0) + weight * raw
|
|
166
|
-
match["score"] = round(blended, 6)
|
|
167
|
-
if touched:
|
|
168
|
-
matches.sort(key=lambda item: float(item.get("score") or 0.0), reverse=True)
|
|
169
|
-
for rank, match in enumerate(matches, start=1):
|
|
170
|
-
match["rank"] = rank
|
|
171
|
-
return touched
|
|
25
|
+
"""The resolved embedder, shaped for the Models → Embeddings view."""
|
|
172
26
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def _scope(
|
|
179
|
-
self,
|
|
180
|
-
matches,
|
|
181
|
-
allowed_workspaces,
|
|
182
|
-
*,
|
|
183
|
-
include_legacy_global: bool = False,
|
|
184
|
-
):
|
|
185
|
-
"""Drop matches scoped to workspaces the caller is not a member of
|
|
186
|
-
(None = no scoping; legacy-global requires an explicit opt-in)."""
|
|
187
|
-
if allowed_workspaces is None:
|
|
188
|
-
return matches
|
|
189
|
-
graph = self._require_graph()
|
|
190
|
-
try:
|
|
191
|
-
return graph.filter_scoped_nodes(
|
|
192
|
-
matches,
|
|
193
|
-
allowed_workspaces,
|
|
194
|
-
include_legacy_global=include_legacy_global,
|
|
195
|
-
)
|
|
196
|
-
except TypeError:
|
|
197
|
-
# Compatibility for pre-opt-in graph implementations. Rebuild the
|
|
198
|
-
# policy here instead of invoking their legacy fail-open filter.
|
|
199
|
-
candidates = list(matches)
|
|
200
|
-
scopes = graph.workspaces_of(
|
|
201
|
-
[item.get("id") or item.get("node_id") for item in candidates]
|
|
202
|
-
)
|
|
203
|
-
allowed = {str(item) for item in allowed_workspaces if item}
|
|
204
|
-
visible = []
|
|
205
|
-
for item in candidates:
|
|
206
|
-
node_id = str(item.get("id") or item.get("node_id") or "")
|
|
207
|
-
if not node_id or node_id not in scopes:
|
|
208
|
-
continue
|
|
209
|
-
scope = scopes[node_id]
|
|
210
|
-
if scope is None and include_legacy_global or scope is not None and str(scope) in allowed:
|
|
211
|
-
visible.append(item)
|
|
212
|
-
return visible
|
|
213
|
-
|
|
214
|
-
def _graph_search(
|
|
215
|
-
self,
|
|
216
|
-
graph,
|
|
217
|
-
query: str,
|
|
218
|
-
*,
|
|
219
|
-
limit: int,
|
|
220
|
-
allowed_workspaces=None,
|
|
221
|
-
include_legacy_global: bool = False,
|
|
222
|
-
):
|
|
223
|
-
try:
|
|
224
|
-
return graph.search(
|
|
225
|
-
query,
|
|
226
|
-
limit,
|
|
227
|
-
allowed_workspaces=allowed_workspaces,
|
|
228
|
-
include_legacy_global=include_legacy_global,
|
|
229
|
-
)
|
|
230
|
-
except TypeError:
|
|
231
|
-
payload = graph.search(query, limit)
|
|
232
|
-
if allowed_workspaces is not None:
|
|
233
|
-
payload = {
|
|
234
|
-
**payload,
|
|
235
|
-
"matches": self._scope(
|
|
236
|
-
payload.get("matches", []),
|
|
237
|
-
allowed_workspaces,
|
|
238
|
-
include_legacy_global=include_legacy_global,
|
|
239
|
-
),
|
|
240
|
-
}
|
|
241
|
-
return payload
|
|
242
|
-
|
|
243
|
-
def _relationship_search(
|
|
244
|
-
self,
|
|
245
|
-
graph,
|
|
246
|
-
*,
|
|
247
|
-
allowed_workspaces=None,
|
|
248
|
-
include_legacy_global: bool = False,
|
|
249
|
-
**kwargs,
|
|
250
|
-
):
|
|
251
|
-
try:
|
|
252
|
-
return graph.relationship_search(
|
|
253
|
-
**kwargs,
|
|
254
|
-
allowed_workspaces=allowed_workspaces,
|
|
255
|
-
include_legacy_global=include_legacy_global,
|
|
256
|
-
)
|
|
257
|
-
except TypeError:
|
|
258
|
-
payload = graph.relationship_search(**kwargs)
|
|
259
|
-
if allowed_workspaces is not None:
|
|
260
|
-
kept = []
|
|
261
|
-
for rel in payload.get("relationships", []):
|
|
262
|
-
endpoints = [
|
|
263
|
-
{"id": (rel.get("source") or {}).get("id")},
|
|
264
|
-
{"id": (rel.get("target") or {}).get("id")},
|
|
265
|
-
]
|
|
266
|
-
if len(
|
|
267
|
-
self._scope(
|
|
268
|
-
endpoints,
|
|
269
|
-
allowed_workspaces,
|
|
270
|
-
include_legacy_global=include_legacy_global,
|
|
271
|
-
)
|
|
272
|
-
) == 2:
|
|
273
|
-
kept.append(rel)
|
|
274
|
-
payload = {**payload, "relationships": kept}
|
|
275
|
-
return payload
|
|
276
|
-
|
|
277
|
-
def _traverse(
|
|
278
|
-
self,
|
|
279
|
-
graph,
|
|
280
|
-
node_id: str,
|
|
281
|
-
*,
|
|
282
|
-
depth: int,
|
|
283
|
-
limit: int,
|
|
284
|
-
allowed_workspaces=None,
|
|
285
|
-
include_legacy_global: bool = False,
|
|
286
|
-
):
|
|
287
|
-
try:
|
|
288
|
-
return graph.traverse(
|
|
289
|
-
node_id,
|
|
290
|
-
depth=depth,
|
|
291
|
-
limit=limit,
|
|
292
|
-
allowed_workspaces=allowed_workspaces,
|
|
293
|
-
include_legacy_global=include_legacy_global,
|
|
294
|
-
)
|
|
295
|
-
except TypeError:
|
|
296
|
-
neighborhood = graph.traverse(node_id, depth=depth, limit=limit)
|
|
297
|
-
if allowed_workspaces is not None:
|
|
298
|
-
nodes = self._scope(
|
|
299
|
-
neighborhood.get("nodes", []),
|
|
300
|
-
allowed_workspaces,
|
|
301
|
-
include_legacy_global=include_legacy_global,
|
|
302
|
-
)
|
|
303
|
-
kept = {item.get("id") for item in nodes}
|
|
304
|
-
edges = [
|
|
305
|
-
edge for edge in neighborhood.get("edges", [])
|
|
306
|
-
if edge.get("from") in kept and edge.get("to") in kept
|
|
307
|
-
]
|
|
308
|
-
neighborhood = {**neighborhood, "nodes": nodes, "edges": edges}
|
|
309
|
-
return neighborhood
|
|
310
|
-
|
|
311
|
-
def _get_node(
|
|
312
|
-
self,
|
|
313
|
-
graph,
|
|
314
|
-
node_id: str,
|
|
315
|
-
*,
|
|
316
|
-
allowed_workspaces=None,
|
|
317
|
-
include_legacy_global: bool = False,
|
|
318
|
-
):
|
|
319
|
-
try:
|
|
320
|
-
return graph.get_node(
|
|
321
|
-
node_id,
|
|
322
|
-
allowed_workspaces=allowed_workspaces,
|
|
323
|
-
include_legacy_global=include_legacy_global,
|
|
324
|
-
)
|
|
325
|
-
except TypeError:
|
|
326
|
-
node = graph.get_node(node_id)
|
|
327
|
-
if allowed_workspaces is not None:
|
|
328
|
-
visible = self._scope(
|
|
329
|
-
[node],
|
|
330
|
-
allowed_workspaces,
|
|
331
|
-
include_legacy_global=include_legacy_global,
|
|
332
|
-
)
|
|
333
|
-
if not visible:
|
|
334
|
-
raise ValueError(f"graph node not found: {node_id}")
|
|
335
|
-
return visible[0]
|
|
336
|
-
return node
|
|
337
|
-
|
|
338
|
-
def keyword_search(
|
|
339
|
-
self,
|
|
340
|
-
query: str,
|
|
341
|
-
*,
|
|
342
|
-
limit: int = 30,
|
|
343
|
-
allowed_workspaces=None,
|
|
344
|
-
include_legacy_global: bool = False,
|
|
345
|
-
) -> Dict[str, Any]:
|
|
346
|
-
graph = self._require_graph()
|
|
347
|
-
payload = self._graph_search(
|
|
348
|
-
graph,
|
|
349
|
-
query,
|
|
350
|
-
limit=limit,
|
|
351
|
-
allowed_workspaces=allowed_workspaces,
|
|
352
|
-
include_legacy_global=include_legacy_global,
|
|
353
|
-
)
|
|
354
|
-
matches = []
|
|
355
|
-
for rank, match in enumerate(payload.get("matches", []), start=1):
|
|
356
|
-
matches.append({
|
|
357
|
-
"id": match["id"],
|
|
358
|
-
"node_id": match["id"],
|
|
359
|
-
"item_type": "node",
|
|
360
|
-
"type": match.get("type"),
|
|
361
|
-
"title": match.get("title"),
|
|
362
|
-
"summary": _clean(match.get("summary")),
|
|
363
|
-
"score": round(1.0 / rank, 6),
|
|
364
|
-
"rank": rank,
|
|
365
|
-
"sources": ["keyword"],
|
|
366
|
-
"source_scores": {"keyword": round(1.0 / rank, 6)},
|
|
367
|
-
"metadata": match.get("metadata") or {},
|
|
368
|
-
"updated_at": match.get("updated_at"),
|
|
369
|
-
})
|
|
370
|
-
return {
|
|
371
|
-
"query": query,
|
|
372
|
-
"mode": "keyword",
|
|
373
|
-
"matches": self._scope(
|
|
374
|
-
matches,
|
|
375
|
-
allowed_workspaces,
|
|
376
|
-
include_legacy_global=include_legacy_global,
|
|
377
|
-
),
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
def vector_search(
|
|
381
|
-
self,
|
|
382
|
-
query: str,
|
|
383
|
-
*,
|
|
384
|
-
limit: int = 30,
|
|
385
|
-
min_score: float = 0.0,
|
|
386
|
-
allowed_workspaces=None,
|
|
387
|
-
include_legacy_global: bool = False,
|
|
388
|
-
) -> Dict[str, Any]:
|
|
389
|
-
graph = self._require_graph()
|
|
390
|
-
payload = graph.vector_search(query, limit=limit, min_score=min_score)
|
|
391
|
-
matches = []
|
|
392
|
-
for rank, match in enumerate(payload.get("matches", []), start=1):
|
|
393
|
-
score = float(match.get("score") or 0.0)
|
|
394
|
-
matches.append({
|
|
395
|
-
"id": match.get("id"),
|
|
396
|
-
"node_id": match.get("node_id"),
|
|
397
|
-
"item_type": match.get("item_type"),
|
|
398
|
-
"type": match.get("type"),
|
|
399
|
-
"title": match.get("title"),
|
|
400
|
-
"summary": _clean(match.get("summary")),
|
|
401
|
-
"score": round(score, 6),
|
|
402
|
-
"rank": rank,
|
|
403
|
-
"sources": ["vector"],
|
|
404
|
-
"source_scores": {"vector": round(score, 6)},
|
|
405
|
-
"metadata": match.get("metadata") or {},
|
|
406
|
-
"updated_at": match.get("updated_at"),
|
|
407
|
-
})
|
|
408
|
-
result = {
|
|
409
|
-
"query": query,
|
|
410
|
-
"mode": "vector",
|
|
411
|
-
"embedding_model": payload.get("embedding_model"),
|
|
412
|
-
"embedding_dim": payload.get("embedding_dim"),
|
|
413
|
-
"matches": self._scope(
|
|
414
|
-
matches,
|
|
415
|
-
allowed_workspaces,
|
|
416
|
-
include_legacy_global=include_legacy_global,
|
|
417
|
-
),
|
|
418
|
-
}
|
|
419
|
-
# Honest recall passthrough: the graph layer scores a capped slice of a
|
|
420
|
-
# large vector index and says so. Dropping it here would turn "partial
|
|
421
|
-
# recall" into "these are all the matches" at the API boundary. It also
|
|
422
|
-
# rides into hybrid_search()["channels"]["vector"] for free.
|
|
423
|
-
recall = payload.get("recall")
|
|
424
|
-
if isinstance(recall, dict):
|
|
425
|
-
result["recall"] = recall
|
|
426
|
-
return result
|
|
427
|
-
|
|
428
|
-
def graph_search(
|
|
429
|
-
self,
|
|
430
|
-
query: str,
|
|
431
|
-
*,
|
|
432
|
-
limit: int = 30,
|
|
433
|
-
expand_depth: int = 1,
|
|
434
|
-
allowed_workspaces=None,
|
|
435
|
-
include_legacy_global: bool = False,
|
|
436
|
-
) -> Dict[str, Any]:
|
|
437
|
-
graph = self._require_graph()
|
|
438
|
-
limit = max(1, min(int(limit or 30), 100))
|
|
439
|
-
expand_depth = max(0, min(int(expand_depth or 1), 3))
|
|
440
|
-
direct = self._graph_search(
|
|
441
|
-
graph,
|
|
442
|
-
query,
|
|
443
|
-
limit=max(limit, 10),
|
|
444
|
-
allowed_workspaces=allowed_workspaces,
|
|
445
|
-
include_legacy_global=include_legacy_global,
|
|
446
|
-
).get("matches", [])
|
|
447
|
-
relationships = self._relationship_search(
|
|
448
|
-
graph,
|
|
449
|
-
query=query,
|
|
450
|
-
limit=limit,
|
|
451
|
-
allowed_workspaces=allowed_workspaces,
|
|
452
|
-
include_legacy_global=include_legacy_global,
|
|
453
|
-
).get("relationships", [])
|
|
454
|
-
by_id: Dict[str, Dict[str, Any]] = {}
|
|
455
|
-
|
|
456
|
-
def add_node(node: Mapping[str, Any], score: float, reason: str, edge: Optional[Mapping[str, Any]] = None) -> None:
|
|
457
|
-
node_id = str(node.get("id") or "")
|
|
458
|
-
if not node_id:
|
|
459
|
-
return
|
|
460
|
-
current = by_id.get(node_id)
|
|
461
|
-
if not current:
|
|
462
|
-
current = {
|
|
463
|
-
"id": node_id,
|
|
464
|
-
"node_id": node_id,
|
|
465
|
-
"item_type": "node",
|
|
466
|
-
"type": node.get("type"),
|
|
467
|
-
"title": node.get("title"),
|
|
468
|
-
"summary": _clean(node.get("summary")),
|
|
469
|
-
"score": 0.0,
|
|
470
|
-
"sources": ["graph"],
|
|
471
|
-
"source_scores": {"graph": 0.0},
|
|
472
|
-
"metadata": node.get("metadata") or {},
|
|
473
|
-
"updated_at": node.get("updated_at"),
|
|
474
|
-
"graph_context": [],
|
|
475
|
-
}
|
|
476
|
-
by_id[node_id] = current
|
|
477
|
-
current["score"] = max(float(current["score"]), score)
|
|
478
|
-
current["source_scores"]["graph"] = max(float(current["source_scores"]["graph"]), score)
|
|
479
|
-
context: Dict[str, Any] = {"reason": reason}
|
|
480
|
-
if edge:
|
|
481
|
-
context["relationship"] = {
|
|
482
|
-
"id": edge.get("id"),
|
|
483
|
-
"type": edge.get("type"),
|
|
484
|
-
"weight": edge.get("weight"),
|
|
485
|
-
"from": edge.get("from") or (edge.get("source") or {}).get("id"),
|
|
486
|
-
"to": edge.get("to") or (edge.get("target") or {}).get("id"),
|
|
487
|
-
}
|
|
488
|
-
current["graph_context"].append(context)
|
|
489
|
-
|
|
490
|
-
for rank, match in enumerate(direct, start=1):
|
|
491
|
-
add_node(match, 1.0 / rank, "direct_match")
|
|
492
|
-
if expand_depth <= 0:
|
|
493
|
-
continue
|
|
494
|
-
try:
|
|
495
|
-
neighborhood = self._traverse(
|
|
496
|
-
graph,
|
|
497
|
-
match["id"],
|
|
498
|
-
depth=expand_depth,
|
|
499
|
-
limit=limit * 3,
|
|
500
|
-
allowed_workspaces=allowed_workspaces,
|
|
501
|
-
include_legacy_global=include_legacy_global,
|
|
502
|
-
)
|
|
503
|
-
except Exception:
|
|
504
|
-
neighborhood = {"nodes": [], "edges": []}
|
|
505
|
-
edge_by_pair = {
|
|
506
|
-
(edge.get("from"), edge.get("to")): edge
|
|
507
|
-
for edge in neighborhood.get("edges", [])
|
|
508
|
-
}
|
|
509
|
-
for node in neighborhood.get("nodes", []):
|
|
510
|
-
if node.get("id") == match.get("id"):
|
|
511
|
-
continue
|
|
512
|
-
related_edge = None
|
|
513
|
-
for pair, edge in edge_by_pair.items():
|
|
514
|
-
if match.get("id") in pair and node.get("id") in pair:
|
|
515
|
-
related_edge = edge
|
|
516
|
-
break
|
|
517
|
-
add_node(node, 0.45 / rank, "neighbor_expansion", related_edge)
|
|
518
|
-
|
|
519
|
-
for rank, rel in enumerate(relationships, start=1):
|
|
520
|
-
rel_score = 0.75 / rank
|
|
521
|
-
add_node(rel.get("source") or {}, rel_score, "relationship_match", rel)
|
|
522
|
-
add_node(rel.get("target") or {}, rel_score, "relationship_match", rel)
|
|
523
|
-
|
|
524
|
-
matches = sorted(by_id.values(), key=lambda item: item["score"], reverse=True)[:limit]
|
|
525
|
-
for rank, match in enumerate(matches, start=1):
|
|
526
|
-
match["rank"] = rank
|
|
527
|
-
match["score"] = round(float(match["score"]), 6)
|
|
528
|
-
match["source_scores"]["graph"] = round(float(match["source_scores"]["graph"]), 6)
|
|
529
|
-
return {
|
|
530
|
-
"query": query,
|
|
531
|
-
"mode": "graph",
|
|
532
|
-
"expand_depth": expand_depth,
|
|
533
|
-
"matches": self._scope(
|
|
534
|
-
matches,
|
|
535
|
-
allowed_workspaces,
|
|
536
|
-
include_legacy_global=include_legacy_global,
|
|
537
|
-
),
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
def hybrid_search(
|
|
541
|
-
self,
|
|
542
|
-
query: str,
|
|
543
|
-
*,
|
|
544
|
-
limit: int = 30,
|
|
545
|
-
keyword_limit: int = 30,
|
|
546
|
-
vector_limit: int = 30,
|
|
547
|
-
graph_limit: int = 30,
|
|
548
|
-
weights: Optional[Mapping[str, float]] = None,
|
|
549
|
-
allowed_workspaces=None,
|
|
550
|
-
include_legacy_global: bool = False,
|
|
551
|
-
image_fusion: bool = False,
|
|
552
|
-
) -> Dict[str, Any]:
|
|
553
|
-
# Single retrieval policy (review Wave 0.2): when the caller does not
|
|
554
|
-
# pin explicit weights, resolve the query class + per-class channel
|
|
555
|
-
# weights + deterministic rewrite from lattice_brain.graph.
|
|
556
|
-
# retrieval_policy (the same policy the graph-layer hybrid consults).
|
|
557
|
-
# Explicit weights still win (and disable rewrite/decay), and the
|
|
558
|
-
# "fact" class equals DEFAULT_HYBRID_WEIGHTS, so pinned-weight callers
|
|
559
|
-
# and fact-class queries behave exactly as before.
|
|
560
|
-
query_class: Optional[str] = None
|
|
561
|
-
search_query = query
|
|
562
|
-
rewrite_rules: List[str] = []
|
|
563
|
-
recency_half_life_days: Optional[float] = None
|
|
564
|
-
if weights is None:
|
|
565
|
-
policy = resolve_policy(query)
|
|
566
|
-
query_class = policy["query_class"]
|
|
567
|
-
weights = dict(policy["weights"])
|
|
568
|
-
rewrite_rules = list(policy["rewrite_rules"])
|
|
569
|
-
recency_half_life_days = policy["recency_half_life_days"]
|
|
570
|
-
if policy["search_query"] and policy["search_query"] != policy["original_query"]:
|
|
571
|
-
search_query = policy["search_query"]
|
|
572
|
-
else:
|
|
573
|
-
weights = {**DEFAULT_HYBRID_WEIGHTS, **dict(weights)}
|
|
574
|
-
# Scope each channel at the source so out-of-scope rows never enter the
|
|
575
|
-
# fusion set (defense-in-depth — the fused result is re-scoped below too).
|
|
576
|
-
channels = {
|
|
577
|
-
"keyword": self.keyword_search(
|
|
578
|
-
search_query,
|
|
579
|
-
limit=keyword_limit,
|
|
580
|
-
allowed_workspaces=allowed_workspaces,
|
|
581
|
-
include_legacy_global=include_legacy_global,
|
|
582
|
-
),
|
|
583
|
-
"vector": self.vector_search(
|
|
584
|
-
search_query,
|
|
585
|
-
limit=vector_limit,
|
|
586
|
-
allowed_workspaces=allowed_workspaces,
|
|
587
|
-
include_legacy_global=include_legacy_global,
|
|
588
|
-
),
|
|
589
|
-
"graph": self.graph_search(
|
|
590
|
-
search_query,
|
|
591
|
-
limit=graph_limit,
|
|
592
|
-
allowed_workspaces=allowed_workspaces,
|
|
593
|
-
include_legacy_global=include_legacy_global,
|
|
594
|
-
),
|
|
595
|
-
}
|
|
596
|
-
fused: Dict[str, Dict[str, Any]] = {}
|
|
597
|
-
for source, payload in channels.items():
|
|
598
|
-
source_weight = float(weights.get(source, 0.0))
|
|
599
|
-
for rank, result in enumerate(payload.get("matches", []), start=1):
|
|
600
|
-
key = _result_key(result)
|
|
601
|
-
if not key:
|
|
602
|
-
continue
|
|
603
|
-
source_score = float((result.get("source_scores") or {}).get(source, result.get("score") or 0.0))
|
|
604
|
-
rank_score = 1.0 / rank
|
|
605
|
-
contribution = source_weight * max(source_score, rank_score)
|
|
606
|
-
current = fused.get(key)
|
|
607
|
-
if not current:
|
|
608
|
-
current = {
|
|
609
|
-
**result,
|
|
610
|
-
"sources": [],
|
|
611
|
-
"source_scores": {},
|
|
612
|
-
"score": 0.0,
|
|
613
|
-
}
|
|
614
|
-
fused[key] = current
|
|
615
|
-
current["score"] = float(current["score"]) + contribution
|
|
616
|
-
if source not in current["sources"]:
|
|
617
|
-
current["sources"].append(source)
|
|
618
|
-
current["source_scores"][source] = round(source_score, 6)
|
|
619
|
-
if result.get("graph_context"):
|
|
620
|
-
current.setdefault("graph_context", [])
|
|
621
|
-
current["graph_context"].extend(result.get("graph_context") or [])
|
|
622
|
-
|
|
623
|
-
# Recency-class age decay (retrieval_policy): dampen each fused score
|
|
624
|
-
# into the [0.5, 1.0] band so old-but-relevant items sink without ever
|
|
625
|
-
# being zeroed. Missing/unparseable updated_at keeps multiplier 1.0 —
|
|
626
|
-
# unknown age is not evidence of staleness. Other classes skip this
|
|
627
|
-
# block byte-identically.
|
|
628
|
-
if recency_half_life_days is not None:
|
|
629
|
-
decay_now = datetime.now()
|
|
630
|
-
for item in fused.values():
|
|
631
|
-
stamp = item.get("updated_at")
|
|
632
|
-
if _parse_iso(stamp):
|
|
633
|
-
multiplier = 0.5 + 0.5 * _recency_score(
|
|
634
|
-
stamp, now=decay_now, half_life_days=recency_half_life_days
|
|
635
|
-
)
|
|
636
|
-
else:
|
|
637
|
-
multiplier = 1.0
|
|
638
|
-
item["source_scores"]["age_decay"] = round(multiplier, 6)
|
|
639
|
-
item["score"] = float(item["score"]) * multiplier
|
|
640
|
-
|
|
641
|
-
matches = self._scope(
|
|
642
|
-
sorted(fused.values(), key=lambda item: item["score"], reverse=True),
|
|
643
|
-
allowed_workspaces,
|
|
644
|
-
include_legacy_global=include_legacy_global,
|
|
645
|
-
)[: max(1, min(limit, 100))]
|
|
646
|
-
for rank, match in enumerate(matches, start=1):
|
|
647
|
-
match["rank"] = rank
|
|
648
|
-
match["score"] = round(float(match["score"]), 6)
|
|
649
|
-
match["fusion"] = {
|
|
650
|
-
"weights": weights,
|
|
651
|
-
"sources": match.get("sources", []),
|
|
652
|
-
}
|
|
653
|
-
if query_class is not None:
|
|
654
|
-
match["fusion"]["query_class"] = query_class
|
|
655
|
-
multimodal = self._image_channel(
|
|
656
|
-
query, matches, requested=image_fusion, limit=limit,
|
|
657
|
-
)
|
|
658
|
-
report: Dict[str, Any] = {
|
|
659
|
-
"query": query,
|
|
660
|
-
"mode": "hybrid",
|
|
661
|
-
"query_class": query_class,
|
|
662
|
-
"weights": weights,
|
|
663
|
-
"policy": {"search_query": search_query, "rewrite_rules": rewrite_rules},
|
|
664
|
-
"channels": {
|
|
665
|
-
name: {
|
|
666
|
-
key: value
|
|
667
|
-
for key, value in payload.items()
|
|
668
|
-
if key not in {"matches"}
|
|
669
|
-
}
|
|
670
|
-
for name, payload in channels.items()
|
|
671
|
-
},
|
|
672
|
-
"matches": matches,
|
|
673
|
-
}
|
|
674
|
-
# Additive key only when the caller asked, so an untouched response is
|
|
675
|
-
# the one 11.1.0 returned.
|
|
676
|
-
if multimodal is not None:
|
|
677
|
-
report["multimodal"] = multimodal
|
|
678
|
-
return report
|
|
679
|
-
|
|
680
|
-
def graph(
|
|
681
|
-
self,
|
|
682
|
-
*,
|
|
683
|
-
limit: int = 300,
|
|
684
|
-
allowed_workspaces=None,
|
|
685
|
-
include_legacy_global: bool = False,
|
|
686
|
-
) -> Dict[str, Any]:
|
|
687
|
-
graph = self._require_graph()
|
|
688
|
-
try:
|
|
689
|
-
return graph.graph(
|
|
690
|
-
limit=limit,
|
|
691
|
-
allowed_workspaces=allowed_workspaces,
|
|
692
|
-
include_legacy_global=include_legacy_global,
|
|
693
|
-
)
|
|
694
|
-
except TypeError:
|
|
695
|
-
payload = graph.graph(limit=limit)
|
|
696
|
-
if allowed_workspaces is not None:
|
|
697
|
-
nodes = self._scope(
|
|
698
|
-
payload.get("nodes", []),
|
|
699
|
-
allowed_workspaces,
|
|
700
|
-
include_legacy_global=include_legacy_global,
|
|
701
|
-
)
|
|
702
|
-
kept = {node.get("id") for node in nodes}
|
|
703
|
-
edges = [
|
|
704
|
-
edge for edge in payload.get("edges", [])
|
|
705
|
-
if edge.get("from") in kept and edge.get("to") in kept
|
|
706
|
-
]
|
|
707
|
-
payload = {**payload, "nodes": nodes, "edges": edges}
|
|
708
|
-
return payload
|
|
709
|
-
|
|
710
|
-
def node(
|
|
711
|
-
self,
|
|
712
|
-
node_id: str,
|
|
713
|
-
*,
|
|
714
|
-
include_neighbors: bool = True,
|
|
715
|
-
depth: int = 1,
|
|
716
|
-
limit: int = 100,
|
|
717
|
-
allowed_workspaces=None,
|
|
718
|
-
include_legacy_global: bool = False,
|
|
719
|
-
) -> Dict[str, Any]:
|
|
720
|
-
graph = self._require_graph()
|
|
721
|
-
node = self._get_node(
|
|
722
|
-
graph,
|
|
723
|
-
node_id,
|
|
724
|
-
allowed_workspaces=allowed_workspaces,
|
|
725
|
-
include_legacy_global=include_legacy_global,
|
|
726
|
-
)
|
|
727
|
-
payload = {"node": node}
|
|
728
|
-
if include_neighbors:
|
|
729
|
-
neighborhood = self._traverse(
|
|
730
|
-
graph,
|
|
731
|
-
node_id,
|
|
732
|
-
depth=depth,
|
|
733
|
-
limit=limit,
|
|
734
|
-
allowed_workspaces=allowed_workspaces,
|
|
735
|
-
include_legacy_global=include_legacy_global,
|
|
736
|
-
)
|
|
737
|
-
payload["neighborhood"] = neighborhood
|
|
738
|
-
return payload
|
|
739
|
-
|
|
740
|
-
def relationships(
|
|
741
|
-
self,
|
|
742
|
-
*,
|
|
743
|
-
query: str = "",
|
|
744
|
-
node_id: str = "",
|
|
745
|
-
relationship_type: str = "",
|
|
746
|
-
limit: int = 30,
|
|
747
|
-
allowed_workspaces=None,
|
|
748
|
-
include_legacy_global: bool = False,
|
|
749
|
-
) -> Dict[str, Any]:
|
|
750
|
-
graph = self._require_graph()
|
|
751
|
-
payload = self._relationship_search(
|
|
752
|
-
graph,
|
|
753
|
-
query=query,
|
|
754
|
-
node_id=node_id,
|
|
755
|
-
relationship_type=relationship_type,
|
|
756
|
-
limit=limit,
|
|
757
|
-
allowed_workspaces=allowed_workspaces,
|
|
758
|
-
include_legacy_global=include_legacy_global,
|
|
759
|
-
)
|
|
760
|
-
return payload
|
|
761
|
-
|
|
762
|
-
def index_status(self) -> Dict[str, Any]:
|
|
763
|
-
return self._require_graph().index_status()
|
|
27
|
+
#: A ``ResolvedEmbedder`` — the same object ``phase_brain`` hands the
|
|
28
|
+
#: compute seams, so what this reports and what ``POST /worker/embed``
|
|
29
|
+
#: produces can never disagree.
|
|
30
|
+
embedder: Any = None
|
|
764
31
|
|
|
765
32
|
def embeddings_status(
|
|
766
33
|
self,
|
|
@@ -770,34 +37,32 @@ class SearchService:
|
|
|
770
37
|
) -> Dict[str, Any]:
|
|
771
38
|
"""Report the active embedding provider for the Models → Embeddings UI.
|
|
772
39
|
|
|
773
|
-
Combines the resolved-provider info (requested vs active, fallback,
|
|
774
|
-
health) with the vector index's identity and last build time. The
|
|
775
40
|
``state`` is one of ``production`` | ``fallback`` | ``unavailable`` so
|
|
776
|
-
the UI never shows a down provider as live.
|
|
41
|
+
the UI never shows a down provider as live. ``refresh`` re-probes the
|
|
42
|
+
provider rather than reading the health recorded at resolution time.
|
|
777
43
|
"""
|
|
778
44
|
resolved = dict(resolved or {})
|
|
779
|
-
|
|
780
|
-
embedder = getattr(graph, "_embedding_model", None)
|
|
45
|
+
provider = getattr(self.embedder, "provider", None)
|
|
781
46
|
|
|
782
47
|
meta: Dict[str, Any] = {}
|
|
783
|
-
if
|
|
48
|
+
if provider is not None and hasattr(provider, "metadata"):
|
|
784
49
|
try:
|
|
785
|
-
meta = dict(
|
|
50
|
+
meta = dict(provider.metadata())
|
|
786
51
|
except Exception:
|
|
787
52
|
meta = {}
|
|
788
53
|
else: # legacy LocalEmbeddingModel
|
|
789
54
|
meta = {
|
|
790
55
|
"provider": "hash",
|
|
791
|
-
"model": getattr(
|
|
792
|
-
"model_id": getattr(
|
|
793
|
-
"dim": getattr(
|
|
56
|
+
"model": getattr(provider, "model_id", "lattice-local-hash-v1"),
|
|
57
|
+
"model_id": getattr(provider, "model_id", "lattice-local-hash-v1"),
|
|
58
|
+
"dim": getattr(provider, "dim", 384),
|
|
794
59
|
"grade": "fallback",
|
|
795
60
|
}
|
|
796
61
|
|
|
797
62
|
health = resolved.get("health") or {"status": "unknown", "detail": ""}
|
|
798
|
-
if refresh and
|
|
63
|
+
if refresh and provider is not None and hasattr(provider, "health"):
|
|
799
64
|
try:
|
|
800
|
-
health =
|
|
65
|
+
health = provider.health()
|
|
801
66
|
except Exception as exc: # pragma: no cover - defensive
|
|
802
67
|
health = {"status": "unavailable", "detail": str(exc)}
|
|
803
68
|
|
|
@@ -808,28 +73,6 @@ class SearchService:
|
|
|
808
73
|
else:
|
|
809
74
|
state = "fallback" if grade == "fallback" else "production"
|
|
810
75
|
|
|
811
|
-
index: Dict[str, Any] = {}
|
|
812
|
-
last_indexed_at = None
|
|
813
|
-
if graph is not None:
|
|
814
|
-
try:
|
|
815
|
-
status = graph.index_status()
|
|
816
|
-
index = {
|
|
817
|
-
"status": status.get("status"),
|
|
818
|
-
"source_items": status.get("source_items"),
|
|
819
|
-
"indexed_items": status.get("indexed_items"),
|
|
820
|
-
"ready_items": status.get("ready_items"),
|
|
821
|
-
"pending_items": status.get("pending_items"),
|
|
822
|
-
"stale_items": status.get("stale_items"),
|
|
823
|
-
"embedding_model": (status.get("storage") or {}).get("embedding_model"),
|
|
824
|
-
"embedding_dim": (status.get("storage") or {}).get("embedding_dim"),
|
|
825
|
-
}
|
|
826
|
-
for op in status.get("operations", []):
|
|
827
|
-
if op.get("status") == "completed" and op.get("completed_at"):
|
|
828
|
-
last_indexed_at = op.get("completed_at")
|
|
829
|
-
break
|
|
830
|
-
except Exception as exc: # pragma: no cover - defensive
|
|
831
|
-
index = {"error": str(exc)}
|
|
832
|
-
|
|
833
76
|
return {
|
|
834
77
|
"provider": meta.get("provider"),
|
|
835
78
|
"requested_provider": resolved.get("requested_provider") or meta.get("provider"),
|
|
@@ -842,14 +85,13 @@ class SearchService:
|
|
|
842
85
|
"fell_back": fell_back,
|
|
843
86
|
"health": health,
|
|
844
87
|
"detail": resolved.get("detail", ""),
|
|
845
|
-
|
|
846
|
-
|
|
88
|
+
# Index completeness is native (lattice-jobs). Present and empty:
|
|
89
|
+
# a reader learns this answer does not speak for the index, which
|
|
90
|
+
# a missing key would not tell them.
|
|
91
|
+
"last_indexed_at": None,
|
|
92
|
+
"index": {},
|
|
847
93
|
"available_providers": list(resolved.get("available_providers") or []),
|
|
848
94
|
}
|
|
849
95
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
full=full,
|
|
853
|
-
include_nodes=include_nodes,
|
|
854
|
-
include_chunks=include_chunks,
|
|
855
|
-
)
|
|
96
|
+
|
|
97
|
+
__all__ = ["SearchService"]
|