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,824 +0,0 @@
|
|
|
1
|
-
"""Background Brain synthesis — every output is a proposal (v11.1.0).
|
|
2
|
-
|
|
3
|
-
The Brain stops being a filing cabinet you query and starts noticing things:
|
|
4
|
-
that two memories disagree, that a topic keeps recurring without ever being
|
|
5
|
-
named, that two notes always show up together but were never linked, that a
|
|
6
|
-
pile of chat fragments has decayed into noise.
|
|
7
|
-
|
|
8
|
-
**Nothing here writes knowledge.** Every observation leaves this module as a
|
|
9
|
-
review proposal (``ReviewQueueService.create``) and is applied only when a
|
|
10
|
-
person approves it — :func:`resolve_contradiction` is the single door through
|
|
11
|
-
which a synthesis conclusion ever reaches the graph, and it opens only after
|
|
12
|
-
``approve()`` has returned. Tests assert that directly.
|
|
13
|
-
|
|
14
|
-
Design contracts
|
|
15
|
-
----------------
|
|
16
|
-
* **Deterministic.** Token overlap, degree, and clock arithmetic only — the
|
|
17
|
-
same graph yields the same proposals. A ``summarizer`` callable may be
|
|
18
|
-
injected to make the *wording* of the weekly brief nicer; it can never
|
|
19
|
-
change what is proposed, and a summarizer that raises is ignored.
|
|
20
|
-
* **Event-driven.** :class:`SynthesisTrigger` counts successful ingests and
|
|
21
|
-
fires at ``LATTICEAI_SYNTHESIS_THRESHOLD`` (default 25) new nodes. It holds
|
|
22
|
-
a counter and nothing else, so a caller can drive it from an ingestion
|
|
23
|
-
pipeline, a scheduler, or a unit test without a server.
|
|
24
|
-
* **Brain Core isolation.** ``review_queue`` is duck-typed
|
|
25
|
-
(``create``/``list``/``get``/``approve``); this module never imports
|
|
26
|
-
``latticeai``.
|
|
27
|
-
* **Idempotent proposals.** A subject already sitting open in the review queue
|
|
28
|
-
is not proposed twice — the inbox is a place to decide, not a firehose.
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
from __future__ import annotations
|
|
32
|
-
|
|
33
|
-
import logging
|
|
34
|
-
import os
|
|
35
|
-
from datetime import datetime, timezone
|
|
36
|
-
from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Tuple
|
|
37
|
-
|
|
38
|
-
from .gates import FeatureGate
|
|
39
|
-
from .graph.proactive import ProactiveBrain
|
|
40
|
-
from .graph.schema import KGStoreV2
|
|
41
|
-
from .quality import content_signature
|
|
42
|
-
from .utils import now_iso
|
|
43
|
-
|
|
44
|
-
logger = logging.getLogger(__name__)
|
|
45
|
-
|
|
46
|
-
#: Review-queue source for everything this module raises. It is an existing
|
|
47
|
-
#: source (the Review Center already renders it) — synthesis is a knowledge
|
|
48
|
-
#: change digest, so it does not need a surface of its own.
|
|
49
|
-
SYNTHESIS_REVIEW_SOURCE = "kg_change_digest"
|
|
50
|
-
|
|
51
|
-
CONTRADICTION_KIND = "contradiction"
|
|
52
|
-
CONCEPT_KIND = "concept_cluster"
|
|
53
|
-
EDGE_KIND = "missing_edge"
|
|
54
|
-
CONSOLIDATION_KIND = "consolidation"
|
|
55
|
-
|
|
56
|
-
#: How a user may settle a contradiction. ``keep_both_temporal`` is the
|
|
57
|
-
#: honest default answer for a Brain: both memories were true, just not at the
|
|
58
|
-
#: same time.
|
|
59
|
-
CONTRADICTION_RESOLUTIONS = ("keep_old", "replace", "keep_both_temporal")
|
|
60
|
-
|
|
61
|
-
SYNTHESIS_THRESHOLD_ENV = "LATTICEAI_SYNTHESIS_THRESHOLD"
|
|
62
|
-
DEFAULT_SYNTHESIS_THRESHOLD = 25
|
|
63
|
-
|
|
64
|
-
#: Whether the Brain is allowed to start a pass *by itself*. Default on — this
|
|
65
|
-
#: is the behaviour 11.1.0 shipped — and it governs only the automatic path
|
|
66
|
-
#: (:meth:`BrainSynthesizer.run_if_due`). An explicit run a person asked for is
|
|
67
|
-
#: never gated: turning "tidy up on its own" off means the Brain stops deciding
|
|
68
|
-
#: when, not that the button stops working.
|
|
69
|
-
SYNTHESIS_ENV = "LATTICEAI_SYNTHESIS"
|
|
70
|
-
SYNTHESIS_GATE = FeatureGate(
|
|
71
|
-
SYNTHESIS_ENV,
|
|
72
|
-
default=True,
|
|
73
|
-
name="synthesis",
|
|
74
|
-
detail="The Brain reviews accumulated material on its own and proposes tidy-ups.",
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
_MIN_SHARED_TOKENS = 3
|
|
78
|
-
_MIN_CLUSTER_MEMBERS = 3
|
|
79
|
-
_EDGE_SIMILARITY = 0.35
|
|
80
|
-
_MAX_PROPOSALS_PER_KIND = 5
|
|
81
|
-
_COMMON_TOKEN_RATIO = 0.4
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def _default_threshold() -> int:
|
|
85
|
-
"""Read the ingest threshold from the environment (bad values → default)."""
|
|
86
|
-
raw = os.getenv(SYNTHESIS_THRESHOLD_ENV, "").strip()
|
|
87
|
-
try:
|
|
88
|
-
value = int(raw)
|
|
89
|
-
except ValueError:
|
|
90
|
-
return DEFAULT_SYNTHESIS_THRESHOLD
|
|
91
|
-
return value if value > 0 else DEFAULT_SYNTHESIS_THRESHOLD
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def _text_of(node: Dict[str, Any]) -> str:
|
|
95
|
-
title = str(node.get("title") or "").strip()
|
|
96
|
-
summary = str(node.get("summary") or "").strip()
|
|
97
|
-
return f"{title} {summary}".strip()
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def _title_of(node: Dict[str, Any]) -> str:
|
|
101
|
-
return str(node.get("title") or node.get("id") or "").strip()
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
# ── ingest-driven trigger ────────────────────────────────────────────────────
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
class SynthesisTrigger:
|
|
108
|
-
"""Counts successful ingests and reports when a synthesis run is due.
|
|
109
|
-
|
|
110
|
-
Deliberately tiny and side-effect free: it owns *when*, never *what*. The
|
|
111
|
-
ingestion pipeline (or any caller with an ingest result) feeds it through
|
|
112
|
-
:meth:`observe_ingest`; the counter resets on every fire, so a long import
|
|
113
|
-
schedules a run per ``threshold`` items rather than one at the end.
|
|
114
|
-
"""
|
|
115
|
-
|
|
116
|
-
def __init__(self, *, threshold: Optional[int] = None) -> None:
|
|
117
|
-
resolved = _default_threshold() if threshold is None else int(threshold)
|
|
118
|
-
self.threshold = max(1, resolved)
|
|
119
|
-
self._pending = 0
|
|
120
|
-
self._fired = 0
|
|
121
|
-
self._last_fired_at: Optional[str] = None
|
|
122
|
-
|
|
123
|
-
def record(self, count: int = 1) -> bool:
|
|
124
|
-
"""Add ``count`` new nodes; ``True`` when that reaches the threshold."""
|
|
125
|
-
self._pending += max(0, int(count))
|
|
126
|
-
if self._pending < self.threshold:
|
|
127
|
-
return False
|
|
128
|
-
self._pending = 0
|
|
129
|
-
self._fired += 1
|
|
130
|
-
self._last_fired_at = now_iso()
|
|
131
|
-
return True
|
|
132
|
-
|
|
133
|
-
def observe_ingest(self, result: Any) -> bool:
|
|
134
|
-
"""Count one ingest result (dataclass or dict); ``True`` when due.
|
|
135
|
-
|
|
136
|
-
Only a successful, non-duplicate ingest is new knowledge — a blocked,
|
|
137
|
-
failed, or deduplicated item must not push the Brain toward a run it
|
|
138
|
-
has nothing to say in.
|
|
139
|
-
"""
|
|
140
|
-
status = str(_field(result, "status") or "")
|
|
141
|
-
duplicate = bool(_field(result, "duplicate"))
|
|
142
|
-
if status != "ok" or duplicate:
|
|
143
|
-
return False
|
|
144
|
-
return self.record(1)
|
|
145
|
-
|
|
146
|
-
def status(self) -> Dict[str, Any]:
|
|
147
|
-
return {
|
|
148
|
-
"threshold": self.threshold,
|
|
149
|
-
"pending": self._pending,
|
|
150
|
-
"runs": self._fired,
|
|
151
|
-
"last_fired_at": self._last_fired_at,
|
|
152
|
-
"due_in": self.threshold - self._pending,
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def _field(source: Any, name: str) -> Any:
|
|
157
|
-
if isinstance(source, dict):
|
|
158
|
-
return source.get(name)
|
|
159
|
-
return getattr(source, name, None)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
# ── proposal desk ────────────────────────────────────────────────────────────
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
class ProposalDesk:
|
|
166
|
-
"""The only way this module reaches the Brain: the review queue.
|
|
167
|
-
|
|
168
|
-
Wraps ``ReviewQueueService`` with one extra rule — a subject that is
|
|
169
|
-
already waiting for a decision is not proposed again. ``proposal_key`` in
|
|
170
|
-
the payload is that subject's identity.
|
|
171
|
-
"""
|
|
172
|
-
|
|
173
|
-
def __init__(
|
|
174
|
-
self,
|
|
175
|
-
review_queue: Any,
|
|
176
|
-
*,
|
|
177
|
-
user_email: Optional[str] = None,
|
|
178
|
-
workspace_id: Optional[str] = None,
|
|
179
|
-
) -> None:
|
|
180
|
-
self._queue = review_queue
|
|
181
|
-
self._user_email = user_email
|
|
182
|
-
self._workspace_id = workspace_id
|
|
183
|
-
self.suppressed = 0
|
|
184
|
-
|
|
185
|
-
def open_keys(self) -> Set[str]:
|
|
186
|
-
"""Proposal keys currently awaiting a decision."""
|
|
187
|
-
try:
|
|
188
|
-
listing = self._queue.list(
|
|
189
|
-
workspace_id=self._workspace_id,
|
|
190
|
-
source=SYNTHESIS_REVIEW_SOURCE,
|
|
191
|
-
)
|
|
192
|
-
except Exception: # noqa: BLE001 — an unreadable inbox must not block
|
|
193
|
-
logger.exception("review queue listing failed")
|
|
194
|
-
return set()
|
|
195
|
-
keys: Set[str] = set()
|
|
196
|
-
for item in listing.get("items") or []:
|
|
197
|
-
if str(item.get("effective_status") or item.get("status")) not in {
|
|
198
|
-
"pending",
|
|
199
|
-
"snoozed",
|
|
200
|
-
}:
|
|
201
|
-
continue
|
|
202
|
-
key = str((item.get("payload") or {}).get("proposal_key") or "")
|
|
203
|
-
if key:
|
|
204
|
-
keys.add(key)
|
|
205
|
-
return keys
|
|
206
|
-
|
|
207
|
-
def propose(
|
|
208
|
-
self,
|
|
209
|
-
*,
|
|
210
|
-
kind: str,
|
|
211
|
-
key: str,
|
|
212
|
-
title: str,
|
|
213
|
-
summary: str,
|
|
214
|
-
payload: Dict[str, Any],
|
|
215
|
-
open_keys: Set[str],
|
|
216
|
-
) -> Optional[Dict[str, Any]]:
|
|
217
|
-
"""Create one review item, or ``None`` when the subject is already open."""
|
|
218
|
-
if key in open_keys:
|
|
219
|
-
self.suppressed += 1
|
|
220
|
-
return None
|
|
221
|
-
open_keys.add(key)
|
|
222
|
-
item = self._queue.create(
|
|
223
|
-
title=title,
|
|
224
|
-
summary=summary,
|
|
225
|
-
source=SYNTHESIS_REVIEW_SOURCE,
|
|
226
|
-
kind=kind,
|
|
227
|
-
payload={**payload, "proposal_key": key, "summary_ko": summary},
|
|
228
|
-
provenance={"pipeline": "brain-synthesis", "proposal_key": key},
|
|
229
|
-
user_email=self._user_email,
|
|
230
|
-
workspace_id=self._workspace_id,
|
|
231
|
-
)
|
|
232
|
-
return item
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
def _pair_key(prefix: str, left: str, right: str) -> str:
|
|
236
|
-
low, high = sorted((str(left), str(right)))
|
|
237
|
-
return f"{prefix}:{low}|{high}"
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
# ── 2.1 contradiction → proposal ─────────────────────────────────────────────
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
def propose_contradictions(
|
|
244
|
-
store: Any,
|
|
245
|
-
review_queue: Any,
|
|
246
|
-
*,
|
|
247
|
-
workspace_id: Optional[str] = None,
|
|
248
|
-
user_email: Optional[str] = None,
|
|
249
|
-
limit: Optional[int] = None,
|
|
250
|
-
sample: Optional[Dict[str, List[Dict[str, Any]]]] = None,
|
|
251
|
-
brain: Optional[ProactiveBrain] = None,
|
|
252
|
-
max_proposals: int = _MAX_PROPOSALS_PER_KIND,
|
|
253
|
-
) -> Dict[str, Any]:
|
|
254
|
-
"""Turn detected contradictions into decisions the user can actually make.
|
|
255
|
-
|
|
256
|
-
Reads ``ProactiveBrain.detect_contradictions`` and raises one review item
|
|
257
|
-
per contradicting pair, carrying both memories, which one is older, and
|
|
258
|
-
the three ways out (keep the old one / replace it / keep both with time
|
|
259
|
-
ranges). Nothing is written to the graph here.
|
|
260
|
-
"""
|
|
261
|
-
detector = brain or ProactiveBrain(store)
|
|
262
|
-
data = sample if sample is not None else detector.sample(
|
|
263
|
-
workspace_id=workspace_id, limit=limit
|
|
264
|
-
)
|
|
265
|
-
found = detector.contradictions_in(data["nodes"], data["edges"])
|
|
266
|
-
by_id = {str(node.get("id")): node for node in data["nodes"]}
|
|
267
|
-
desk = ProposalDesk(review_queue, user_email=user_email, workspace_id=workspace_id)
|
|
268
|
-
open_keys = desk.open_keys()
|
|
269
|
-
proposed: List[Dict[str, Any]] = []
|
|
270
|
-
for pair in found.get("node_pairs") or []:
|
|
271
|
-
if len(proposed) >= max_proposals:
|
|
272
|
-
break
|
|
273
|
-
item = _propose_one_contradiction(desk, pair, by_id, open_keys)
|
|
274
|
-
if item is not None:
|
|
275
|
-
proposed.append(item)
|
|
276
|
-
return {
|
|
277
|
-
"available": True,
|
|
278
|
-
"pairs_detected": len(found.get("node_pairs") or []),
|
|
279
|
-
"proposed": proposed,
|
|
280
|
-
"proposed_count": len(proposed),
|
|
281
|
-
"suppressed": desk.suppressed,
|
|
282
|
-
"nodes_scanned": len(data["nodes"]),
|
|
283
|
-
"generated_at": now_iso(),
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
def _propose_one_contradiction(
|
|
288
|
-
desk: ProposalDesk,
|
|
289
|
-
pair: Dict[str, Any],
|
|
290
|
-
by_id: Dict[str, Dict[str, Any]],
|
|
291
|
-
open_keys: Set[str],
|
|
292
|
-
) -> Optional[Dict[str, Any]]:
|
|
293
|
-
left_id = str(pair.get("left_id") or "")
|
|
294
|
-
right_id = str(pair.get("right_id") or "")
|
|
295
|
-
if not left_id or not right_id or left_id == right_id:
|
|
296
|
-
return None
|
|
297
|
-
older_id, newer_id = _order_by_age(left_id, right_id, by_id)
|
|
298
|
-
older = by_id.get(older_id) or {}
|
|
299
|
-
newer = by_id.get(newer_id) or {}
|
|
300
|
-
older_title = _title_of(older) or older_id
|
|
301
|
-
newer_title = _title_of(newer) or newer_id
|
|
302
|
-
summary = (
|
|
303
|
-
f"'{older_title}'와(과) '{newer_title}'가 서로 어긋납니다. "
|
|
304
|
-
"예전 기억을 그대로 둘지, 새 기억으로 바꿀지, "
|
|
305
|
-
"둘 다 남기고 각각 언제 맞았는지 표시할지 골라주세요."
|
|
306
|
-
)
|
|
307
|
-
return desk.propose(
|
|
308
|
-
kind=CONTRADICTION_KIND,
|
|
309
|
-
key=_pair_key(CONTRADICTION_KIND, older_id, newer_id),
|
|
310
|
-
title=f"모순된 기억: {older_title} ↔ {newer_title}",
|
|
311
|
-
summary=summary,
|
|
312
|
-
open_keys=open_keys,
|
|
313
|
-
payload={
|
|
314
|
-
"older": _memory_brief(older_id, older, pair, "left"),
|
|
315
|
-
"newer": _memory_brief(newer_id, newer, pair, "right"),
|
|
316
|
-
"signal": pair.get("signal"),
|
|
317
|
-
"options": [
|
|
318
|
-
{"id": "keep_old", "label": "예전 기억을 유지"},
|
|
319
|
-
{"id": "replace", "label": "새 기억으로 교체"},
|
|
320
|
-
{"id": "keep_both_temporal", "label": "둘 다 유지하고 기간 표시"},
|
|
321
|
-
],
|
|
322
|
-
},
|
|
323
|
-
)
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
def _memory_brief(
|
|
327
|
-
node_id: str, node: Dict[str, Any], pair: Dict[str, Any], side: str
|
|
328
|
-
) -> Dict[str, Any]:
|
|
329
|
-
content = pair.get(f"{side}_content")
|
|
330
|
-
if str(pair.get(f"{side}_id") or "") != node_id:
|
|
331
|
-
other = "right" if side == "left" else "left"
|
|
332
|
-
content = pair.get(f"{other}_content")
|
|
333
|
-
return {
|
|
334
|
-
"id": node_id,
|
|
335
|
-
"title": _title_of(node),
|
|
336
|
-
"type": node.get("type"),
|
|
337
|
-
"updated_at": node.get("updated_at"),
|
|
338
|
-
"content": str(content or "")[:200],
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
def _order_by_age(
|
|
343
|
-
left_id: str, right_id: str, by_id: Dict[str, Dict[str, Any]]
|
|
344
|
-
) -> Tuple[str, str]:
|
|
345
|
-
"""(older, newer) by ``updated_at``; ties break on id so it is stable."""
|
|
346
|
-
left_ts = str((by_id.get(left_id) or {}).get("updated_at") or "")
|
|
347
|
-
right_ts = str((by_id.get(right_id) or {}).get("updated_at") or "")
|
|
348
|
-
if (left_ts, left_id) <= (right_ts, right_id):
|
|
349
|
-
return left_id, right_id
|
|
350
|
-
return right_id, left_id
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
class ContradictionResolutionError(ValueError):
|
|
354
|
-
"""Raised when a contradiction cannot be resolved as asked."""
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
def resolve_contradiction(
|
|
358
|
-
store: Any,
|
|
359
|
-
review_queue: Any,
|
|
360
|
-
item_id: str,
|
|
361
|
-
*,
|
|
362
|
-
resolution: str,
|
|
363
|
-
workspace_id: Optional[str] = None,
|
|
364
|
-
at: Optional[str] = None,
|
|
365
|
-
) -> Dict[str, Any]:
|
|
366
|
-
"""Approve a contradiction proposal and stamp the graph accordingly.
|
|
367
|
-
|
|
368
|
-
This is the **only** write path in the module, and it is gated twice: the
|
|
369
|
-
item must be a contradiction proposal, and ``review_queue.approve`` must
|
|
370
|
-
return before a single column is touched. A rejected or unknown resolution
|
|
371
|
-
raises before the approval, so a bad request cannot burn the item.
|
|
372
|
-
|
|
373
|
-
* ``replace`` — the older memory stops being true now and points at its
|
|
374
|
-
successor (``valid_to`` + ``superseded_by``).
|
|
375
|
-
* ``keep_old`` — the *newer* memory is the mistake: it is retired and
|
|
376
|
-
points back at the older one.
|
|
377
|
-
* ``keep_both_temporal`` — both stay, with adjoining windows: the older
|
|
378
|
-
one was true until now, the newer one from now on.
|
|
379
|
-
"""
|
|
380
|
-
if resolution not in CONTRADICTION_RESOLUTIONS:
|
|
381
|
-
raise ContradictionResolutionError(
|
|
382
|
-
f"resolution must be one of {list(CONTRADICTION_RESOLUTIONS)}"
|
|
383
|
-
)
|
|
384
|
-
item = review_queue.get(item_id, workspace_id=workspace_id)
|
|
385
|
-
if str(item.get("kind")) != CONTRADICTION_KIND:
|
|
386
|
-
raise ContradictionResolutionError(
|
|
387
|
-
f"review item {item_id} is not a contradiction proposal"
|
|
388
|
-
)
|
|
389
|
-
payload = dict(item.get("payload") or {})
|
|
390
|
-
older_id = str((payload.get("older") or {}).get("id") or "")
|
|
391
|
-
newer_id = str((payload.get("newer") or {}).get("id") or "")
|
|
392
|
-
if not older_id or not newer_id:
|
|
393
|
-
raise ContradictionResolutionError(
|
|
394
|
-
f"review item {item_id} carries no memory pair to resolve"
|
|
395
|
-
)
|
|
396
|
-
|
|
397
|
-
approved = review_queue.approve(item_id, workspace_id=workspace_id)
|
|
398
|
-
moment = at or now_iso()
|
|
399
|
-
stamps = _stamp_resolution(store, older_id, newer_id, resolution, moment)
|
|
400
|
-
return {
|
|
401
|
-
"item_id": item_id,
|
|
402
|
-
"resolution": resolution,
|
|
403
|
-
"status": str(approved.get("status") or ""),
|
|
404
|
-
"applied_at": moment,
|
|
405
|
-
"stamps": stamps,
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
def _stamp_resolution(
|
|
410
|
-
store: Any, older_id: str, newer_id: str, resolution: str, moment: str
|
|
411
|
-
) -> List[Dict[str, Any]]:
|
|
412
|
-
v2 = KGStoreV2(getattr(store, "db_path", store))
|
|
413
|
-
if resolution == "keep_old":
|
|
414
|
-
plan = [{"node_id": newer_id, "valid_to": moment, "superseded_by": older_id}]
|
|
415
|
-
elif resolution == "replace":
|
|
416
|
-
plan = [
|
|
417
|
-
{"node_id": older_id, "valid_to": moment, "superseded_by": newer_id},
|
|
418
|
-
{"node_id": newer_id, "valid_from": moment},
|
|
419
|
-
]
|
|
420
|
-
else: # keep_both_temporal — validated by the caller
|
|
421
|
-
plan = [
|
|
422
|
-
{"node_id": older_id, "valid_to": moment},
|
|
423
|
-
{"node_id": newer_id, "valid_from": moment},
|
|
424
|
-
]
|
|
425
|
-
applied: List[Dict[str, Any]] = []
|
|
426
|
-
for stamp in plan:
|
|
427
|
-
node_id = stamp["node_id"]
|
|
428
|
-
fields = {key: value for key, value in stamp.items() if key != "node_id"}
|
|
429
|
-
applied.append(
|
|
430
|
-
{
|
|
431
|
-
"node_id": node_id,
|
|
432
|
-
**fields,
|
|
433
|
-
"updated": v2.stamp_node_validity(node_id, **fields),
|
|
434
|
-
}
|
|
435
|
-
)
|
|
436
|
-
return applied
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
# ── 2.4 importance / decay → consolidation proposals ─────────────────────────
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
def propose_consolidation(
|
|
443
|
-
store: Any,
|
|
444
|
-
review_queue: Any,
|
|
445
|
-
*,
|
|
446
|
-
workspace_id: Optional[str] = None,
|
|
447
|
-
user_email: Optional[str] = None,
|
|
448
|
-
limit: Optional[int] = None,
|
|
449
|
-
sample: Optional[Dict[str, List[Dict[str, Any]]]] = None,
|
|
450
|
-
brain: Optional[ProactiveBrain] = None,
|
|
451
|
-
min_candidates: int = 3,
|
|
452
|
-
max_proposals: int = _MAX_PROPOSALS_PER_KIND,
|
|
453
|
-
) -> Dict[str, Any]:
|
|
454
|
-
"""Offer to fold the least-used episodic memories into one summary node.
|
|
455
|
-
|
|
456
|
-
One proposal per batch, not per fragment: "these 12 chat scraps have not
|
|
457
|
-
been touched in months — shall I fold them into a single summary?" is a
|
|
458
|
-
decision a person can make; twelve of them is a chore.
|
|
459
|
-
"""
|
|
460
|
-
detector = brain or ProactiveBrain(store)
|
|
461
|
-
report = detector.importance_report(
|
|
462
|
-
workspace_id=workspace_id, limit=limit, sample=sample
|
|
463
|
-
)
|
|
464
|
-
candidates = list(report.get("candidates") or [])[: max(1, int(max_proposals) * 4)]
|
|
465
|
-
desk = ProposalDesk(review_queue, user_email=user_email, workspace_id=workspace_id)
|
|
466
|
-
proposed: List[Dict[str, Any]] = []
|
|
467
|
-
if len(candidates) >= max(1, int(min_candidates)):
|
|
468
|
-
open_keys = desk.open_keys()
|
|
469
|
-
titles = ", ".join(str(c.get("title") or c.get("id")) for c in candidates[:3])
|
|
470
|
-
item = desk.propose(
|
|
471
|
-
kind=CONSOLIDATION_KIND,
|
|
472
|
-
key="consolidation:" + "|".join(sorted(str(c.get("id")) for c in candidates)),
|
|
473
|
-
title=f"오래된 기록 {len(candidates)}건 정리",
|
|
474
|
-
summary=(
|
|
475
|
-
f"{titles} 등 {len(candidates)}건이 오랫동안 쓰이지 않았습니다. "
|
|
476
|
-
"하나의 요약으로 묶어둘까요? 원본은 그대로 남습니다."
|
|
477
|
-
),
|
|
478
|
-
open_keys=open_keys,
|
|
479
|
-
payload={
|
|
480
|
-
"candidates": candidates,
|
|
481
|
-
"half_life_days": report.get("half_life_days"),
|
|
482
|
-
"access_source": report.get("access_source"),
|
|
483
|
-
},
|
|
484
|
-
)
|
|
485
|
-
if item is not None:
|
|
486
|
-
proposed.append(item)
|
|
487
|
-
return {
|
|
488
|
-
"available": True,
|
|
489
|
-
"candidate_count": len(candidates),
|
|
490
|
-
"proposed": proposed,
|
|
491
|
-
"proposed_count": len(proposed),
|
|
492
|
-
"suppressed": desk.suppressed,
|
|
493
|
-
"report": report,
|
|
494
|
-
"generated_at": now_iso(),
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
# ── 2.3 background synthesis job ─────────────────────────────────────────────
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
class BrainSynthesizer:
|
|
502
|
-
"""Event-driven synthesis: contradictions, concepts, links, consolidation.
|
|
503
|
-
|
|
504
|
-
``run()`` takes one graph sample and hands the same rows to every pass, so
|
|
505
|
-
a run is a single consistent view of the Brain. Every output is a review
|
|
506
|
-
proposal; the returned ``brief`` is read-only text for the Brain Brief.
|
|
507
|
-
"""
|
|
508
|
-
|
|
509
|
-
def __init__(
|
|
510
|
-
self,
|
|
511
|
-
store: Any,
|
|
512
|
-
review_queue: Any,
|
|
513
|
-
*,
|
|
514
|
-
trigger: Optional[SynthesisTrigger] = None,
|
|
515
|
-
summarizer: Optional[Callable[[str], str]] = None,
|
|
516
|
-
sample_limit: Optional[int] = None,
|
|
517
|
-
) -> None:
|
|
518
|
-
self._store = store
|
|
519
|
-
self._queue = review_queue
|
|
520
|
-
self.trigger = trigger or SynthesisTrigger()
|
|
521
|
-
self._summarizer = summarizer
|
|
522
|
-
self._sample_limit = sample_limit
|
|
523
|
-
|
|
524
|
-
# -- trigger seam ----------------------------------------------------
|
|
525
|
-
def observe_ingest(self, result: Any) -> bool:
|
|
526
|
-
"""Feed one ingest result to the trigger; ``True`` when a run is due."""
|
|
527
|
-
return self.trigger.observe_ingest(result)
|
|
528
|
-
|
|
529
|
-
def run_if_due(
|
|
530
|
-
self, result: Any, *, workspace_id: Optional[str] = None,
|
|
531
|
-
user_email: Optional[str] = None,
|
|
532
|
-
) -> Optional[Dict[str, Any]]:
|
|
533
|
-
"""Run synthesis only when this ingest pushed the counter over.
|
|
534
|
-
|
|
535
|
-
The gate is checked *before* the counter moves, so "off" means off: a
|
|
536
|
-
long import while the switch is down does not bank a pass that fires the
|
|
537
|
-
instant someone turns it back on.
|
|
538
|
-
"""
|
|
539
|
-
if not SYNTHESIS_GATE.enabled():
|
|
540
|
-
return None
|
|
541
|
-
if not self.observe_ingest(result):
|
|
542
|
-
return None
|
|
543
|
-
return self.run(workspace_id=workspace_id, user_email=user_email)
|
|
544
|
-
|
|
545
|
-
# -- the run ---------------------------------------------------------
|
|
546
|
-
def run(
|
|
547
|
-
self, *, workspace_id: Optional[str] = None, user_email: Optional[str] = None,
|
|
548
|
-
) -> Dict[str, Any]:
|
|
549
|
-
brain = ProactiveBrain(self._store)
|
|
550
|
-
sample = brain.sample(workspace_id=workspace_id, limit=self._sample_limit)
|
|
551
|
-
contradictions = propose_contradictions(
|
|
552
|
-
self._store, self._queue,
|
|
553
|
-
workspace_id=workspace_id, user_email=user_email,
|
|
554
|
-
sample=sample, brain=brain,
|
|
555
|
-
)
|
|
556
|
-
consolidation = propose_consolidation(
|
|
557
|
-
self._store, self._queue,
|
|
558
|
-
workspace_id=workspace_id, user_email=user_email,
|
|
559
|
-
sample=sample, brain=brain,
|
|
560
|
-
)
|
|
561
|
-
concepts = self._propose_concepts(sample, workspace_id, user_email)
|
|
562
|
-
links = self._propose_links(sample, workspace_id, user_email)
|
|
563
|
-
counts = {
|
|
564
|
-
"contradictions": contradictions["proposed_count"],
|
|
565
|
-
"concepts": len(concepts["proposed"]),
|
|
566
|
-
"links": len(links["proposed"]),
|
|
567
|
-
"consolidation": consolidation["proposed_count"],
|
|
568
|
-
}
|
|
569
|
-
return {
|
|
570
|
-
"nodes_scanned": len(sample["nodes"]),
|
|
571
|
-
"edges_scanned": len(sample["edges"]),
|
|
572
|
-
"counts": counts,
|
|
573
|
-
"proposed_total": sum(counts.values()),
|
|
574
|
-
"suppressed": (
|
|
575
|
-
contradictions["suppressed"] + consolidation["suppressed"]
|
|
576
|
-
+ concepts["suppressed"] + links["suppressed"]
|
|
577
|
-
),
|
|
578
|
-
"contradictions": contradictions,
|
|
579
|
-
"concepts": concepts,
|
|
580
|
-
"links": links,
|
|
581
|
-
"consolidation": consolidation,
|
|
582
|
-
"brief": self.brief_section(sample=sample, counts=counts),
|
|
583
|
-
"trigger": self.trigger.status(),
|
|
584
|
-
"generated_at": now_iso(),
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
# -- (b) higher-level concept proposals -------------------------------
|
|
588
|
-
def _propose_concepts(
|
|
589
|
-
self,
|
|
590
|
-
sample: Dict[str, List[Dict[str, Any]]],
|
|
591
|
-
workspace_id: Optional[str],
|
|
592
|
-
user_email: Optional[str],
|
|
593
|
-
) -> Dict[str, Any]:
|
|
594
|
-
clusters = _concept_clusters(sample["nodes"])
|
|
595
|
-
desk = ProposalDesk(
|
|
596
|
-
self._queue, user_email=user_email, workspace_id=workspace_id
|
|
597
|
-
)
|
|
598
|
-
open_keys = desk.open_keys()
|
|
599
|
-
proposed: List[Dict[str, Any]] = []
|
|
600
|
-
for cluster in clusters[:_MAX_PROPOSALS_PER_KIND]:
|
|
601
|
-
titles = ", ".join(member["title"] for member in cluster["members"][:3])
|
|
602
|
-
item = desk.propose(
|
|
603
|
-
kind=CONCEPT_KIND,
|
|
604
|
-
key=f"{CONCEPT_KIND}:{cluster['token']}",
|
|
605
|
-
title=f"새 주제 후보: {cluster['token']}",
|
|
606
|
-
summary=(
|
|
607
|
-
f"'{cluster['token']}'가 {cluster['size']}개의 기억에 반복해서 "
|
|
608
|
-
f"나타납니다({titles} 등). 하나의 주제로 묶어둘까요?"
|
|
609
|
-
),
|
|
610
|
-
open_keys=open_keys,
|
|
611
|
-
payload={
|
|
612
|
-
"token": cluster["token"],
|
|
613
|
-
"members": cluster["members"],
|
|
614
|
-
"size": cluster["size"],
|
|
615
|
-
"node_type": "Concept",
|
|
616
|
-
},
|
|
617
|
-
)
|
|
618
|
-
if item is not None:
|
|
619
|
-
proposed.append(item)
|
|
620
|
-
return {
|
|
621
|
-
"clusters": clusters,
|
|
622
|
-
"proposed": proposed,
|
|
623
|
-
"suppressed": desk.suppressed,
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
# -- (c) "always together, never linked" -------------------------------
|
|
627
|
-
def _propose_links(
|
|
628
|
-
self,
|
|
629
|
-
sample: Dict[str, List[Dict[str, Any]]],
|
|
630
|
-
workspace_id: Optional[str],
|
|
631
|
-
user_email: Optional[str],
|
|
632
|
-
) -> Dict[str, Any]:
|
|
633
|
-
pairs = _unlinked_pairs(sample["nodes"], sample["edges"])
|
|
634
|
-
desk = ProposalDesk(
|
|
635
|
-
self._queue, user_email=user_email, workspace_id=workspace_id
|
|
636
|
-
)
|
|
637
|
-
open_keys = desk.open_keys()
|
|
638
|
-
proposed: List[Dict[str, Any]] = []
|
|
639
|
-
for pair in pairs[:_MAX_PROPOSALS_PER_KIND]:
|
|
640
|
-
item = desk.propose(
|
|
641
|
-
kind=EDGE_KIND,
|
|
642
|
-
key=_pair_key(EDGE_KIND, pair["left"]["id"], pair["right"]["id"]),
|
|
643
|
-
title=(
|
|
644
|
-
f"연결 제안: {pair['left']['title']} ↔ {pair['right']['title']}"
|
|
645
|
-
),
|
|
646
|
-
summary=(
|
|
647
|
-
f"'{pair['left']['title']}'와(과) '{pair['right']['title']}'는 "
|
|
648
|
-
f"자주 같이 등장하는데 아직 이어져 있지 않습니다"
|
|
649
|
-
f"(겹침 {int(pair['similarity'] * 100)}%). 연결할까요?"
|
|
650
|
-
),
|
|
651
|
-
open_keys=open_keys,
|
|
652
|
-
payload={
|
|
653
|
-
"source": pair["left"],
|
|
654
|
-
"target": pair["right"],
|
|
655
|
-
"edge_type": "RELATED_TO",
|
|
656
|
-
"similarity": pair["similarity"],
|
|
657
|
-
"shared_tokens": pair["shared_tokens"],
|
|
658
|
-
},
|
|
659
|
-
)
|
|
660
|
-
if item is not None:
|
|
661
|
-
proposed.append(item)
|
|
662
|
-
return {"pairs": pairs, "proposed": proposed, "suppressed": desk.suppressed}
|
|
663
|
-
|
|
664
|
-
# -- (a) weekly brief text --------------------------------------------
|
|
665
|
-
def brief_section(
|
|
666
|
-
self,
|
|
667
|
-
*,
|
|
668
|
-
sample: Optional[Dict[str, List[Dict[str, Any]]]] = None,
|
|
669
|
-
counts: Optional[Dict[str, int]] = None,
|
|
670
|
-
workspace_id: Optional[str] = None,
|
|
671
|
-
) -> Dict[str, Any]:
|
|
672
|
-
"""Plain-language "what the Brain noticed" text for the Brain Brief.
|
|
673
|
-
|
|
674
|
-
Deterministic by default. When a ``summarizer`` is injected the same
|
|
675
|
-
facts are handed to it for nicer wording — and if it fails, the
|
|
676
|
-
deterministic sentence stands. The model is never the source of the
|
|
677
|
-
numbers.
|
|
678
|
-
"""
|
|
679
|
-
if sample is None:
|
|
680
|
-
sample = ProactiveBrain(self._store).sample(
|
|
681
|
-
workspace_id=workspace_id, limit=self._sample_limit
|
|
682
|
-
)
|
|
683
|
-
counts = counts or {}
|
|
684
|
-
window = _recent_window(sample["nodes"])
|
|
685
|
-
headline = (
|
|
686
|
-
f"최근 7일 동안 새 기억 {window['recent']}건이 쌓였고, "
|
|
687
|
-
f"Brain이 검토할 거리 {sum(counts.values())}건을 찾았습니다."
|
|
688
|
-
)
|
|
689
|
-
lines = [
|
|
690
|
-
f"모순되는 기억 {counts.get('contradictions', 0)}쌍",
|
|
691
|
-
f"새 주제 후보 {counts.get('concepts', 0)}건",
|
|
692
|
-
f"빠진 연결 {counts.get('links', 0)}건",
|
|
693
|
-
f"정리할 오래된 기록 {counts.get('consolidation', 0)}묶음",
|
|
694
|
-
]
|
|
695
|
-
return {
|
|
696
|
-
"headline": self._summarize(headline, lines),
|
|
697
|
-
"deterministic_headline": headline,
|
|
698
|
-
"lines": lines,
|
|
699
|
-
"recent_nodes": window["recent"],
|
|
700
|
-
"window_days": window["days"],
|
|
701
|
-
"counts": dict(counts),
|
|
702
|
-
"generated_at": now_iso(),
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
def _summarize(self, headline: str, lines: Sequence[str]) -> str:
|
|
706
|
-
if self._summarizer is None:
|
|
707
|
-
return headline
|
|
708
|
-
try:
|
|
709
|
-
written = str(self._summarizer("\n".join([headline, *lines])) or "").strip()
|
|
710
|
-
except Exception: # noqa: BLE001 — a model may never break the brief
|
|
711
|
-
logger.exception("synthesis summarizer failed")
|
|
712
|
-
return headline
|
|
713
|
-
return written or headline
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
def _recent_window(nodes: List[Dict[str, Any]], *, days: int = 7) -> Dict[str, int]:
|
|
717
|
-
cutoff = datetime.now(timezone.utc).timestamp() - days * 86400
|
|
718
|
-
recent = 0
|
|
719
|
-
for node in nodes:
|
|
720
|
-
text = str(node.get("updated_at") or "").strip()
|
|
721
|
-
try:
|
|
722
|
-
parsed = datetime.fromisoformat(text.replace("Z", "+00:00"))
|
|
723
|
-
except ValueError:
|
|
724
|
-
continue
|
|
725
|
-
if parsed.tzinfo is None:
|
|
726
|
-
# The store writes naive *local* stamps (utils.now_iso); reading
|
|
727
|
-
# them as UTC would shift the window by the machine's offset.
|
|
728
|
-
parsed = parsed.astimezone()
|
|
729
|
-
if parsed.timestamp() >= cutoff:
|
|
730
|
-
recent += 1
|
|
731
|
-
return {"recent": recent, "days": days}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
def _concept_clusters(nodes: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
735
|
-
"""Tokens that recur across many memories but are nobody's topic yet.
|
|
736
|
-
|
|
737
|
-
A token shared by at least three memories is a candidate; a token present
|
|
738
|
-
in most of the sample is boilerplate, not a topic, and is dropped.
|
|
739
|
-
"""
|
|
740
|
-
index: Dict[str, List[Dict[str, Any]]] = {}
|
|
741
|
-
existing = {str(node.get("title") or "").strip().lower() for node in nodes}
|
|
742
|
-
for node in nodes:
|
|
743
|
-
text = _text_of(node)
|
|
744
|
-
if len(text) < 3:
|
|
745
|
-
continue
|
|
746
|
-
for token in content_signature(text):
|
|
747
|
-
index.setdefault(token, []).append(node)
|
|
748
|
-
ceiling = max(_MIN_CLUSTER_MEMBERS, int(len(nodes) * _COMMON_TOKEN_RATIO))
|
|
749
|
-
clusters: List[Dict[str, Any]] = []
|
|
750
|
-
for token, members in index.items():
|
|
751
|
-
if len(members) < _MIN_CLUSTER_MEMBERS or len(members) > ceiling:
|
|
752
|
-
continue
|
|
753
|
-
if token in existing:
|
|
754
|
-
continue # the topic node already exists
|
|
755
|
-
clusters.append(
|
|
756
|
-
{
|
|
757
|
-
"token": token,
|
|
758
|
-
"size": len(members),
|
|
759
|
-
"members": [
|
|
760
|
-
{"id": str(m.get("id")), "title": _title_of(m)} for m in members[:8]
|
|
761
|
-
],
|
|
762
|
-
}
|
|
763
|
-
)
|
|
764
|
-
clusters.sort(key=lambda item: (-item["size"], item["token"]))
|
|
765
|
-
return clusters
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
def _unlinked_pairs(
|
|
769
|
-
nodes: List[Dict[str, Any]], edges: List[Dict[str, Any]]
|
|
770
|
-
) -> List[Dict[str, Any]]:
|
|
771
|
-
"""Memory pairs with strong token overlap and no edge between them."""
|
|
772
|
-
linked = {
|
|
773
|
-
_pair_key("e", str(edge.get("source") or ""), str(edge.get("target") or ""))
|
|
774
|
-
for edge in edges
|
|
775
|
-
}
|
|
776
|
-
signatures = [
|
|
777
|
-
(node, content_signature(_text_of(node)))
|
|
778
|
-
for node in nodes
|
|
779
|
-
if len(_text_of(node)) >= 3
|
|
780
|
-
]
|
|
781
|
-
pairs: List[Dict[str, Any]] = []
|
|
782
|
-
for index, (left, left_sig) in enumerate(signatures):
|
|
783
|
-
for right, right_sig in signatures[index + 1:]:
|
|
784
|
-
shared = left_sig & right_sig
|
|
785
|
-
if len(shared) < _MIN_SHARED_TOKENS:
|
|
786
|
-
continue
|
|
787
|
-
left_id, right_id = str(left.get("id")), str(right.get("id"))
|
|
788
|
-
if _pair_key("e", left_id, right_id) in linked:
|
|
789
|
-
continue
|
|
790
|
-
# Jaccard, same definition the proactive duplicate scorer uses.
|
|
791
|
-
# The union cannot be empty here: `shared` already has 3 tokens.
|
|
792
|
-
similarity = len(shared) / len(left_sig | right_sig)
|
|
793
|
-
if similarity < _EDGE_SIMILARITY:
|
|
794
|
-
continue
|
|
795
|
-
pairs.append(
|
|
796
|
-
{
|
|
797
|
-
"left": {"id": left_id, "title": _title_of(left)},
|
|
798
|
-
"right": {"id": right_id, "title": _title_of(right)},
|
|
799
|
-
"similarity": round(similarity, 4),
|
|
800
|
-
"shared_tokens": sorted(shared)[:8],
|
|
801
|
-
}
|
|
802
|
-
)
|
|
803
|
-
pairs.sort(key=lambda item: (-item["similarity"], item["left"]["id"]))
|
|
804
|
-
return pairs
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
__all__ = [
|
|
808
|
-
"BrainSynthesizer",
|
|
809
|
-
"CONCEPT_KIND",
|
|
810
|
-
"CONSOLIDATION_KIND",
|
|
811
|
-
"CONTRADICTION_KIND",
|
|
812
|
-
"CONTRADICTION_RESOLUTIONS",
|
|
813
|
-
"ContradictionResolutionError",
|
|
814
|
-
"EDGE_KIND",
|
|
815
|
-
"ProposalDesk",
|
|
816
|
-
"SYNTHESIS_ENV",
|
|
817
|
-
"SYNTHESIS_GATE",
|
|
818
|
-
"SYNTHESIS_REVIEW_SOURCE",
|
|
819
|
-
"SYNTHESIS_THRESHOLD_ENV",
|
|
820
|
-
"SynthesisTrigger",
|
|
821
|
-
"propose_consolidation",
|
|
822
|
-
"propose_contradictions",
|
|
823
|
-
"resolve_contradiction",
|
|
824
|
-
]
|