ltcai 11.5.2 → 11.6.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 +91 -148
- package/bin/ltcai.js +234 -24
- package/docs/CHANGELOG.md +75 -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 +21 -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-CToZnOHz.js} +1 -1
- package/static/app/assets/{AdminConsole-cf4npybT.js → AdminConsole-D5kbrBu8.js} +1 -1
- package/static/app/assets/{Brain-3VCSHFcn.js → Brain-C6zCdv4S.js} +1 -1
- package/static/app/assets/{BrainHome-Qm8eaztx.js → BrainHome-D4n3EDUt.js} +1 -1
- package/static/app/assets/{BrainSignals-DS9BtKOW.js → BrainSignals-CzeZ-vQ9.js} +1 -1
- package/static/app/assets/{Capture-DiQ219jW.js → Capture-DPcyTczq.js} +1 -1
- package/static/app/assets/{Chronicle-BGvuAchH.js → Chronicle-D3Nx-oWS.js} +1 -1
- package/static/app/assets/{CommandPalette-Bqhm0Urn.js → CommandPalette-DPPvpp28.js} +1 -1
- package/static/app/assets/{Library-BV6NnF0a.js → Library-Co2qTu3I.js} +1 -1
- package/static/app/assets/{LivingBrain-GzenJchP.js → LivingBrain-Bsmzwy8_.js} +1 -1
- package/static/app/assets/{ProductFlow-DEP6-vML.js → ProductFlow-CBLdmesT.js} +1 -1
- package/static/app/assets/{ReviewCard-CNZ7XjWG.js → ReviewCard-CfR6DLs7.js} +1 -1
- package/static/app/assets/{System-CieofHQa.js → System-Ce-P2YL6.js} +1 -1
- package/static/app/assets/arrow-left-DD5jFGHV.js +1 -0
- package/static/app/assets/{bot-B_K1Tdmw.js → bot-C5-XIww2.js} +1 -1
- package/static/app/assets/{brain-DWyaV1L1.js → brain-CFRJUvdX.js} +1 -1
- package/static/app/assets/{button-aTn4s84A.js → button-DI1KcQP-.js} +1 -1
- package/static/app/assets/circle-check-CY20XMCh.js +1 -0
- package/static/app/assets/{circle-pause-xKgeGXkT.js → circle-pause-C8HHyood.js} +1 -1
- package/static/app/assets/{circle-play-DkT6tYPX.js → circle-play-Cufc235E.js} +1 -1
- package/static/app/assets/{cpu-85xYObUC.js → cpu-DW2_JVsv.js} +1 -1
- package/static/app/assets/{download-B5Fm7YXo.js → download-gbdKTBb8.js} +1 -1
- package/static/app/assets/{folder-open-kk2Xa52u.js → folder-open-C9ydyX33.js} +1 -1
- package/static/app/assets/{hard-drive-DkA3zBW_.js → hard-drive-BxNMzBM3.js} +1 -1
- package/static/app/assets/index-BovSWRmQ.css +2 -0
- package/static/app/assets/{index-BMPdTmlY.js → index-q9XyDY7A.js} +3 -3
- package/static/app/assets/{input-B0nRf2jO.js → input-GLD94Y_K.js} +1 -1
- package/static/app/assets/{link-2-Dwb4gnTc.js → link-2-B4Y0gz1S.js} +1 -1
- package/static/app/assets/{permissionCopy-CQDUBrOZ.js → permissionCopy-CxrV0fvU.js} +1 -1
- package/static/app/assets/{primitives-SNp0LRJz.js → primitives-2ANdAsu2.js} +1 -1
- package/static/app/assets/search-DTxtjQ07.js +1 -0
- package/static/app/assets/{share-2-BsrxFglO.js → share-2-DxArfiuI.js} +1 -1
- package/static/app/assets/{shield-alert-5BStfp2_.js → shield-alert-CgftuFFs.js} +1 -1
- package/static/app/assets/{textarea-Cg8IUA-k.js → textarea-BHVBAoCr.js} +1 -1
- package/static/app/assets/{useFocusTrap-CYKvE46M.js → useFocusTrap-Dzq2j_OG.js} +1 -1
- package/static/app/assets/{useMutation-CSn9t1op.js → useMutation-BcqfMLNq.js} +1 -1
- package/static/app/assets/{useQuery-CY2OI2uy.js → useQuery-DBzLKCOv.js} +1 -1
- package/static/app/assets/{utils-Ddol2RWD.js → utils-B-Aah1bO.js} +1 -1
- package/static/app/assets/{workspace-BqDwOz_p.js → workspace-B6fFhoko.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/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/search-BcHqkjoy.js +0 -1
|
@@ -1,676 +0,0 @@
|
|
|
1
|
-
"""Lattice AI Auto Graph Curator.
|
|
2
|
-
|
|
3
|
-
피드백 #4 (lattice_ai_auto_graph_direction.txt) 반영.
|
|
4
|
-
|
|
5
|
-
핵심 방향:
|
|
6
|
-
- 사용자는 노드/엣지를 직접 만들지 않는다.
|
|
7
|
-
- 대화/파일/작업 로그 → topic candidate → cluster → promoted node
|
|
8
|
-
→ derived thread edge → 자동 레이아웃.
|
|
9
|
-
- 너무 많은 노드를 만들지 않고, 알리아스를 자동 병합.
|
|
10
|
-
- secret/API key/private key 같은 원문은 그래프에 들어가면 안 된다.
|
|
11
|
-
|
|
12
|
-
이 모듈은 텍스트 단위 토픽 후보 추출, 클러스터링/병합, 노드 승격 판정,
|
|
13
|
-
파생 이야기 엣지 생성, 큐레이션(중요도 점수)을 담당하는 가벼운 헬퍼다.
|
|
14
|
-
무거운 의존성 없이 동작하므로 기존 knowledge_graph.py 위에 얹어 쓸 수 있다.
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
from __future__ import annotations
|
|
18
|
-
|
|
19
|
-
import logging
|
|
20
|
-
import math
|
|
21
|
-
import re
|
|
22
|
-
import time
|
|
23
|
-
from dataclasses import dataclass, field
|
|
24
|
-
from typing import Any, Dict, Iterable, List, Optional, Sequence, Set
|
|
25
|
-
|
|
26
|
-
logger = logging.getLogger(__name__)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
# ── Secret / sensitive patterns to NEVER include in graph ─────────────────────
|
|
30
|
-
|
|
31
|
-
SECRET_PATTERNS: List[re.Pattern] = [
|
|
32
|
-
re.compile(r"(?i)\b(?:api[_-]?key|secret|access[_-]?token|password|passwd|pwd|bearer)\s*[:=]\s*\S+"),
|
|
33
|
-
re.compile(r"sk-[A-Za-z0-9]{20,}"),
|
|
34
|
-
re.compile(r"-----BEGIN [A-Z ]+PRIVATE KEY-----[\s\S]+?-----END [A-Z ]+PRIVATE KEY-----"),
|
|
35
|
-
re.compile(r"AKIA[0-9A-Z]{16}"), # AWS access key
|
|
36
|
-
re.compile(r"ghp_[A-Za-z0-9]{30,}"), # GitHub PAT
|
|
37
|
-
re.compile(r"xox[baprs]-[A-Za-z0-9-]{10,}"), # Slack token
|
|
38
|
-
]
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def contains_secret(text: str) -> bool:
|
|
42
|
-
if not text:
|
|
43
|
-
return False
|
|
44
|
-
for pat in SECRET_PATTERNS:
|
|
45
|
-
if pat.search(text):
|
|
46
|
-
return True
|
|
47
|
-
return False
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def mask_secrets(text: str) -> str:
|
|
51
|
-
"""문자열 안의 secret을 마스킹한다. 그래프 저장 직전에 한 번 더 거쳐야 한다."""
|
|
52
|
-
if not text:
|
|
53
|
-
return text
|
|
54
|
-
out = text
|
|
55
|
-
for pat in SECRET_PATTERNS:
|
|
56
|
-
out = pat.sub("[REDACTED]", out)
|
|
57
|
-
return out
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
# ── Stopwords (KO + EN) ───────────────────────────────────────────────────────
|
|
61
|
-
|
|
62
|
-
_STOPWORDS: Set[str] = {
|
|
63
|
-
# 한국어
|
|
64
|
-
"그리고", "그러나", "또한", "하지만", "그런데", "그래서", "이것", "저것",
|
|
65
|
-
"이번", "저번", "지금", "어제", "오늘", "내일", "에서", "에게", "에는",
|
|
66
|
-
"되어", "있다", "없다", "있는", "없는", "같은", "처럼", "위해", "통해",
|
|
67
|
-
"에서의", "에서는", "라고", "이라고", "이다", "이며", "이고", "되는",
|
|
68
|
-
# 영어
|
|
69
|
-
"the", "and", "for", "are", "but", "not", "you", "can", "with", "this",
|
|
70
|
-
"that", "from", "into", "have", "has", "your", "any", "all", "one", "out",
|
|
71
|
-
"use", "using", "used", "about", "via", "per", "let", "let's", "we'll",
|
|
72
|
-
"i'll", "as", "be", "is", "it", "an", "or", "to", "of", "in", "on",
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
# item 5: 한국어 그래프 노이즈를 줄이기 위한 일반어 blacklist 강화.
|
|
76
|
-
# 의미를 담지 않는 흔한 단어들. (코드/도메인 고유명사는 제외)
|
|
77
|
-
_GENERIC_BLACKLIST: Set[str] = {
|
|
78
|
-
# 한국어 일반어
|
|
79
|
-
"내용", "관련", "사용", "경우", "부분", "정도", "생각", "방법", "진행",
|
|
80
|
-
"확인", "작업", "설정", "추가", "수정", "정보", "결과", "상태", "기준",
|
|
81
|
-
"그것", "그거", "여기", "거기", "이거", "저거", "무엇", "어떤", "관해",
|
|
82
|
-
"그냥", "정말", "조금", "많이", "다시", "먼저", "현재", "다음", "이전",
|
|
83
|
-
# 영어 일반어
|
|
84
|
-
"thing", "things", "stuff", "etc", "really", "just", "like", "make",
|
|
85
|
-
"made", "want", "need", "good", "work", "works", "very", "more", "most",
|
|
86
|
-
"some", "such", "then", "than", "also", "here", "there", "what", "which",
|
|
87
|
-
"when", "where", "will", "would", "should", "could", "does", "done",
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
# item 5: 파일 확장자 토큰. 파일명에서 떨어져 나온 노이즈라 노드 후보로 부적절.
|
|
91
|
-
_FILE_EXT_TOKENS: Set[str] = {
|
|
92
|
-
"py", "js", "ts", "tsx", "jsx", "json", "md", "txt", "csv", "tsv",
|
|
93
|
-
"png", "jpg", "jpeg", "gif", "svg", "webp", "pdf", "html", "css",
|
|
94
|
-
"yml", "yaml", "toml", "sh", "bash", "zsh", "log", "ipynb", "xml",
|
|
95
|
-
"lock", "cfg", "ini", "env", "bin", "exe", "zip", "tar", "gz",
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
_FILTER_TOKENS: Set[str] = _STOPWORDS | _GENERIC_BLACKLIST | _FILE_EXT_TOKENS
|
|
99
|
-
|
|
100
|
-
# item 5: 한국어 조사. 토큰 끝에서 제거해 "그래프를"/"그래프가"/"그래프" 를 하나로 모은다.
|
|
101
|
-
_JOSA_SUFFIXES: List[str] = sorted(
|
|
102
|
-
[
|
|
103
|
-
"으로는", "에서는", "에서의", "에게서", "이라는", "이라고", "라는", "라고",
|
|
104
|
-
"으로", "에서", "에게", "한테", "까지", "부터", "보다", "처럼", "마다",
|
|
105
|
-
"조차", "밖에", "라도", "이나", "에는", "에도", "께서", "이란",
|
|
106
|
-
"은", "는", "이", "가", "을", "를", "와", "과", "에", "의", "도",
|
|
107
|
-
"만", "로", "나", "께", "란",
|
|
108
|
-
],
|
|
109
|
-
key=len,
|
|
110
|
-
reverse=True,
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def _strip_josa(token: str) -> str:
|
|
115
|
-
"""한국어 토큰 끝의 조사를 제거한다. (영문/혼합 토큰은 그대로)"""
|
|
116
|
-
if not re.search(r"[가-힣]", token):
|
|
117
|
-
return token
|
|
118
|
-
for suf in _JOSA_SUFFIXES:
|
|
119
|
-
if token.endswith(suf) and len(token) - len(suf) >= 2:
|
|
120
|
-
return token[: -len(suf)]
|
|
121
|
-
return token
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def _tokenize(text: str) -> List[str]:
|
|
125
|
-
if not text:
|
|
126
|
-
return []
|
|
127
|
-
# 한글/영문/숫자만 남김
|
|
128
|
-
cleaned = re.sub(r"[^0-9A-Za-z가-힣\s]", " ", text)
|
|
129
|
-
tokens = [t for t in cleaned.split() if t]
|
|
130
|
-
out = []
|
|
131
|
-
for t in tokens:
|
|
132
|
-
low = _strip_josa(t.lower())
|
|
133
|
-
if len(low) < 2:
|
|
134
|
-
continue
|
|
135
|
-
if low in _FILTER_TOKENS:
|
|
136
|
-
continue
|
|
137
|
-
out.append(low)
|
|
138
|
-
return out
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def _ngrams(tokens: Sequence[str], n: int = 2) -> List[str]:
|
|
142
|
-
if len(tokens) < n:
|
|
143
|
-
return []
|
|
144
|
-
return [" ".join(tokens[i : i + n]) for i in range(len(tokens) - n + 1)]
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
# ── Topic candidates ──────────────────────────────────────────────────────────
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
@dataclass
|
|
151
|
-
class TopicCandidate:
|
|
152
|
-
label: str
|
|
153
|
-
score: float
|
|
154
|
-
sources: List[str] = field(default_factory=list)
|
|
155
|
-
aliases: Set[str] = field(default_factory=set)
|
|
156
|
-
|
|
157
|
-
def to_dict(self) -> Dict[str, Any]:
|
|
158
|
-
return {
|
|
159
|
-
"label": self.label,
|
|
160
|
-
"score": self.score,
|
|
161
|
-
"sources": list(self.sources),
|
|
162
|
-
"aliases": sorted(self.aliases),
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def extract_topic_candidates(
|
|
167
|
-
documents: Iterable[Dict[str, Any]],
|
|
168
|
-
*,
|
|
169
|
-
min_score: float = 1.5,
|
|
170
|
-
top_k: int = 50,
|
|
171
|
-
) -> List[TopicCandidate]:
|
|
172
|
-
"""대화/파일/작업 로그 documents에서 topic candidate를 뽑는다.
|
|
173
|
-
|
|
174
|
-
documents: [{"id": str, "text": str, "kind": "chat|file|task", "weight": float}]
|
|
175
|
-
"""
|
|
176
|
-
counts: Dict[str, float] = {}
|
|
177
|
-
sources: Dict[str, List[str]] = {}
|
|
178
|
-
|
|
179
|
-
for doc in documents:
|
|
180
|
-
text = str(doc.get("text") or "")
|
|
181
|
-
# secret이 섞여 있으면 제거하고 진행
|
|
182
|
-
text = mask_secrets(text)
|
|
183
|
-
weight = float(doc.get("weight") or 1.0)
|
|
184
|
-
kind = str(doc.get("kind") or "chat")
|
|
185
|
-
if kind == "file":
|
|
186
|
-
weight *= 1.5 # 파일은 신호가 강함
|
|
187
|
-
elif kind == "task":
|
|
188
|
-
weight *= 1.2
|
|
189
|
-
|
|
190
|
-
tokens = _tokenize(text)
|
|
191
|
-
if not tokens:
|
|
192
|
-
continue
|
|
193
|
-
|
|
194
|
-
# 단어 + 2gram 두 가지 모두 후보로 둔다
|
|
195
|
-
bag = list(set(tokens + _ngrams(tokens, 2)))
|
|
196
|
-
seen_in_doc: Set[str] = set()
|
|
197
|
-
for term in bag:
|
|
198
|
-
if term in seen_in_doc:
|
|
199
|
-
continue # pragma: no cover — unreachable: bag is list(set(...)), already deduplicated
|
|
200
|
-
seen_in_doc.add(term)
|
|
201
|
-
counts[term] = counts.get(term, 0.0) + weight
|
|
202
|
-
sources.setdefault(term, []).append(str(doc.get("id") or ""))
|
|
203
|
-
|
|
204
|
-
# log-normalize and filter
|
|
205
|
-
candidates: List[TopicCandidate] = []
|
|
206
|
-
for term, score in counts.items():
|
|
207
|
-
if score < min_score:
|
|
208
|
-
continue
|
|
209
|
-
term_sources = sources.get(term, [])
|
|
210
|
-
# item 5: 같은 대화/폴더(단일 출처)에서만 반복된 단어는 감점한다.
|
|
211
|
-
# 여러 출처에서 반복된 개념일수록 가산해 "진짜 주제"만 위로 올린다.
|
|
212
|
-
distinct_sources = len({s for s in term_sources if s})
|
|
213
|
-
if distinct_sources <= 1:
|
|
214
|
-
diversity = 0.5 # 단일 출처 노이즈 감점
|
|
215
|
-
else:
|
|
216
|
-
diversity = 1.0 + 0.15 * math.log(distinct_sources)
|
|
217
|
-
normalized = math.log(1.0 + score) * (1.0 + 0.05 * len(term.split())) * diversity
|
|
218
|
-
candidates.append(
|
|
219
|
-
TopicCandidate(
|
|
220
|
-
label=term,
|
|
221
|
-
score=round(normalized, 4),
|
|
222
|
-
sources=term_sources[:20],
|
|
223
|
-
)
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
candidates.sort(key=lambda c: c.score, reverse=True)
|
|
227
|
-
return candidates[:top_k]
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
# ── Alias normalization / merging ─────────────────────────────────────────────
|
|
231
|
-
|
|
232
|
-
DEFAULT_ALIAS_GROUPS: List[List[str]] = [
|
|
233
|
-
["lattice ai", "latticeai", "래티스 ai", "래티스ai", "내 앱", "내 ai"],
|
|
234
|
-
["gemma-4", "gemma 4", "google gemma"],
|
|
235
|
-
["gemma 4", "gemma4", "google gemma 4"],
|
|
236
|
-
["llama 4", "llama4", "meta llama 4", "llama scout"],
|
|
237
|
-
]
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
def build_alias_index(groups: Optional[List[List[str]]] = None) -> Dict[str, str]:
|
|
241
|
-
groups = groups or DEFAULT_ALIAS_GROUPS
|
|
242
|
-
idx: Dict[str, str] = {}
|
|
243
|
-
for grp in groups:
|
|
244
|
-
if not grp:
|
|
245
|
-
continue
|
|
246
|
-
canon = grp[0].lower().strip()
|
|
247
|
-
for alias in grp:
|
|
248
|
-
idx[alias.lower().strip()] = canon
|
|
249
|
-
return idx
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
def cluster_candidates(
|
|
253
|
-
candidates: List[TopicCandidate],
|
|
254
|
-
alias_index: Optional[Dict[str, str]] = None,
|
|
255
|
-
) -> List[TopicCandidate]:
|
|
256
|
-
"""비슷한 라벨을 자동 병합한다."""
|
|
257
|
-
alias_index = alias_index or build_alias_index()
|
|
258
|
-
merged: Dict[str, TopicCandidate] = {}
|
|
259
|
-
|
|
260
|
-
def canon_of(label: str) -> str:
|
|
261
|
-
low = label.lower().strip()
|
|
262
|
-
if low in alias_index:
|
|
263
|
-
return alias_index[low]
|
|
264
|
-
# 단순 정규화: 공백/하이픈 통일
|
|
265
|
-
norm = re.sub(r"[-_]+", " ", low)
|
|
266
|
-
norm = re.sub(r"\s+", " ", norm).strip()
|
|
267
|
-
return norm
|
|
268
|
-
|
|
269
|
-
for c in candidates:
|
|
270
|
-
key = canon_of(c.label)
|
|
271
|
-
if key in merged:
|
|
272
|
-
existing = merged[key]
|
|
273
|
-
existing.score += c.score * 0.6 # 중복일수록 score는 약간 가산
|
|
274
|
-
existing.aliases.add(c.label)
|
|
275
|
-
existing.sources = list({*existing.sources, *c.sources})[:50]
|
|
276
|
-
else:
|
|
277
|
-
cand = TopicCandidate(
|
|
278
|
-
label=key,
|
|
279
|
-
score=c.score,
|
|
280
|
-
sources=list(c.sources),
|
|
281
|
-
aliases={c.label} if c.label.lower() != key else set(),
|
|
282
|
-
)
|
|
283
|
-
merged[key] = cand
|
|
284
|
-
|
|
285
|
-
return sorted(merged.values(), key=lambda x: x.score, reverse=True)
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
# ── Promotion rules ───────────────────────────────────────────────────────────
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
@dataclass
|
|
292
|
-
class PromotionDecision:
|
|
293
|
-
candidate: TopicCandidate
|
|
294
|
-
promote: bool
|
|
295
|
-
reason: str
|
|
296
|
-
importance: float
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
def should_promote(
|
|
300
|
-
candidate: TopicCandidate,
|
|
301
|
-
*,
|
|
302
|
-
existing_node_labels: Optional[Set[str]] = None,
|
|
303
|
-
min_sources: int = 2,
|
|
304
|
-
min_importance: float = 1.0,
|
|
305
|
-
) -> PromotionDecision:
|
|
306
|
-
existing_node_labels = existing_node_labels or set()
|
|
307
|
-
# 1. secret 라벨이면 절대 승격 금지
|
|
308
|
-
if contains_secret(candidate.label):
|
|
309
|
-
return PromotionDecision(candidate, False, "contains secret", 0.0)
|
|
310
|
-
# 2. 이미 같은 라벨의 노드가 있으면 승격하지 않음 (alias로 들어감)
|
|
311
|
-
if candidate.label in existing_node_labels:
|
|
312
|
-
return PromotionDecision(candidate, False, "duplicate of existing node", candidate.score)
|
|
313
|
-
# 3. 출처가 너무 적으면 노이즈로 간주
|
|
314
|
-
if len(set(candidate.sources)) < min_sources:
|
|
315
|
-
return PromotionDecision(candidate, False, "too few sources", candidate.score)
|
|
316
|
-
# 4. 너무 짧은 라벨(단어 1자) 거부
|
|
317
|
-
if len(candidate.label) < 2:
|
|
318
|
-
return PromotionDecision(candidate, False, "label too short", candidate.score)
|
|
319
|
-
|
|
320
|
-
importance = candidate.score
|
|
321
|
-
if importance < min_importance:
|
|
322
|
-
return PromotionDecision(candidate, False, "importance below threshold", importance)
|
|
323
|
-
|
|
324
|
-
return PromotionDecision(candidate, True, "promoted", importance)
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
# ── Thread edges (파생 이야기) ────────────────────────────────────────────────
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
@dataclass
|
|
331
|
-
class ThreadEdge:
|
|
332
|
-
source: str
|
|
333
|
-
target: str
|
|
334
|
-
story: str
|
|
335
|
-
evidence: List[str] = field(default_factory=list)
|
|
336
|
-
created_at: float = field(default_factory=time.time)
|
|
337
|
-
|
|
338
|
-
def to_dict(self) -> Dict[str, Any]:
|
|
339
|
-
return {
|
|
340
|
-
"source": self.source,
|
|
341
|
-
"target": self.target,
|
|
342
|
-
"story": self.story,
|
|
343
|
-
"evidence": list(self.evidence),
|
|
344
|
-
"created_at": self.created_at,
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
def derive_thread_story(
|
|
349
|
-
source_label: str,
|
|
350
|
-
target_label: str,
|
|
351
|
-
*,
|
|
352
|
-
snippets: Iterable[str],
|
|
353
|
-
max_len: int = 220,
|
|
354
|
-
) -> str:
|
|
355
|
-
"""간단한 1~2문장 파생 이야기를 만든다. 빠르고 결정적."""
|
|
356
|
-
cleaned: List[str] = []
|
|
357
|
-
for s in snippets:
|
|
358
|
-
if not s:
|
|
359
|
-
continue
|
|
360
|
-
sm = mask_secrets(str(s))
|
|
361
|
-
# 가장 의미있어 보이는 첫 문장만 따온다
|
|
362
|
-
sentences = re.split(r"[.!?\n]+", sm)
|
|
363
|
-
for sent in sentences:
|
|
364
|
-
t = sent.strip()
|
|
365
|
-
if 8 <= len(t) <= max_len:
|
|
366
|
-
cleaned.append(t)
|
|
367
|
-
break
|
|
368
|
-
if len(cleaned) >= 2:
|
|
369
|
-
break
|
|
370
|
-
if not cleaned:
|
|
371
|
-
return f"{source_label}에서 {target_label}로 이어지는 흐름이 발견되었습니다."
|
|
372
|
-
joined = ". ".join(cleaned[:2])
|
|
373
|
-
return joined[:max_len]
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
# ── Curation (중요도 기반 hide/show) ──────────────────────────────────────────
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
def curate_nodes(
|
|
380
|
-
nodes: List[Dict[str, Any]],
|
|
381
|
-
*,
|
|
382
|
-
max_visible: int = 20,
|
|
383
|
-
behavior_signals: Optional[Dict[str, Dict[str, float]]] = None,
|
|
384
|
-
decay_seconds: float = 60 * 60 * 24 * 14, # 2주
|
|
385
|
-
now: Optional[float] = None,
|
|
386
|
-
) -> List[Dict[str, Any]]:
|
|
387
|
-
"""노드 리스트에 visible/score 정보를 부여한다.
|
|
388
|
-
|
|
389
|
-
nodes: [{"id": str, "label": str, "importance": float, "updated_at": float}]
|
|
390
|
-
behavior_signals: {node_id: {"clicks": int, "searches": int}} 형태.
|
|
391
|
-
"""
|
|
392
|
-
now = now or time.time()
|
|
393
|
-
behavior_signals = behavior_signals or {}
|
|
394
|
-
enriched: List[Dict[str, Any]] = []
|
|
395
|
-
|
|
396
|
-
for n in nodes:
|
|
397
|
-
importance = float(n.get("importance") or 0.0)
|
|
398
|
-
updated_at = float(n.get("updated_at") or now)
|
|
399
|
-
age = max(0.0, now - updated_at)
|
|
400
|
-
decay = math.exp(-age / decay_seconds) if decay_seconds > 0 else 1.0
|
|
401
|
-
sig = behavior_signals.get(str(n.get("id") or ""), {})
|
|
402
|
-
boost = (
|
|
403
|
-
0.4 * math.log(1.0 + float(sig.get("clicks") or 0))
|
|
404
|
-
+ 0.6 * math.log(1.0 + float(sig.get("searches") or 0))
|
|
405
|
-
)
|
|
406
|
-
final_score = round(importance * decay + boost, 4)
|
|
407
|
-
enriched.append({**n, "curated_score": final_score})
|
|
408
|
-
|
|
409
|
-
enriched.sort(key=lambda x: x.get("curated_score", 0.0), reverse=True)
|
|
410
|
-
for i, n in enumerate(enriched):
|
|
411
|
-
n["visible"] = i < max_visible
|
|
412
|
-
return enriched
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
# ── Noise reduction (backlog #10, review §7.2 D) ─────────────────────────────
|
|
416
|
-
# Relation-verb normalization dictionary (ko/en). Keys are canonical verbs;
|
|
417
|
-
# values are the free-string labels observed in the legacy edge table. The
|
|
418
|
-
# canonical labels stay aligned with the v2 EdgeType legacy mapping so this
|
|
419
|
-
# never fights the schema normalization.
|
|
420
|
-
|
|
421
|
-
RELATION_VERB_GROUPS: Dict[str, List[str]] = {
|
|
422
|
-
"created": [
|
|
423
|
-
"creates", "create", "만들다", "만든", "만들었다", "만듦", "만들어냄",
|
|
424
|
-
"생성함", "생성", "생성했다", "작성함", "작성", "작성했다",
|
|
425
|
-
],
|
|
426
|
-
"mentions": ["mention", "언급함", "언급", "언급했다", "언급됨"],
|
|
427
|
-
"contains": ["contain", "포함함", "포함", "포함했다", "포함됨"],
|
|
428
|
-
"uses": ["use", "used", "사용함", "사용", "사용했다", "이용함", "이용"],
|
|
429
|
-
"related_to": ["related", "relates_to", "관련", "관련됨", "관련있음", "연관됨", "연관"],
|
|
430
|
-
"fixed": ["fixes", "fix", "수정함", "수정", "수정했다", "고침", "고쳤다"],
|
|
431
|
-
"decided": ["decides", "decide", "결정함", "결정", "결정했다"],
|
|
432
|
-
"uploaded": ["uploads", "upload", "업로드함", "업로드", "올림", "올렸다"],
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
def build_relation_verb_index(
|
|
437
|
-
groups: Optional[Dict[str, List[str]]] = None,
|
|
438
|
-
) -> Dict[str, str]:
|
|
439
|
-
"""{alias(lower) → canonical} — canonical labels map to themselves."""
|
|
440
|
-
groups = groups or RELATION_VERB_GROUPS
|
|
441
|
-
index: Dict[str, str] = {}
|
|
442
|
-
for canonical, aliases in groups.items():
|
|
443
|
-
canon = str(canonical).strip().lower()
|
|
444
|
-
index[canon] = canon
|
|
445
|
-
for alias in aliases:
|
|
446
|
-
index[str(alias).strip().lower()] = canon
|
|
447
|
-
return index
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
def normalize_relation_verb(
|
|
451
|
-
verb: str,
|
|
452
|
-
*,
|
|
453
|
-
index: Optional[Dict[str, str]] = None,
|
|
454
|
-
) -> str:
|
|
455
|
-
"""Map a free-string edge verb to its canonical form (identity if unknown).
|
|
456
|
-
|
|
457
|
-
Korean verbs are additionally tried with the trailing josa stripped so
|
|
458
|
-
"생성함을" style variants still normalize; unknown labels pass through
|
|
459
|
-
unchanged (lossless — this function is a rename map, not a filter).
|
|
460
|
-
"""
|
|
461
|
-
raw = str(verb or "").strip()
|
|
462
|
-
if not raw:
|
|
463
|
-
return raw
|
|
464
|
-
index = index if index is not None else build_relation_verb_index()
|
|
465
|
-
low = raw.lower()
|
|
466
|
-
if low in index:
|
|
467
|
-
return index[low]
|
|
468
|
-
stripped = _strip_josa(low)
|
|
469
|
-
if stripped in index:
|
|
470
|
-
return index[stripped]
|
|
471
|
-
return raw
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
# v4 write-door enum labels are SCREAMING_SNAKE_CASE ASCII; those are already
|
|
475
|
-
# canonical schema taxonomy and must never be rewritten by the verb dictionary.
|
|
476
|
-
_V4_ENUM_LABEL_RE = re.compile(r"^[A-Z][A-Z0-9_]*$")
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
def plan_relation_normalization(
|
|
480
|
-
edge_types: Iterable[str],
|
|
481
|
-
*,
|
|
482
|
-
index: Optional[Dict[str, str]] = None,
|
|
483
|
-
) -> Dict[str, str]:
|
|
484
|
-
"""{observed_type → canonical} for every type the dictionary changes.
|
|
485
|
-
|
|
486
|
-
Skips v4-canonical enum labels (``MENTIONS``, ``INDEXED_FROM``, …): this
|
|
487
|
-
plan targets the free-string verbs of pre-v4 rows, not the schema enum.
|
|
488
|
-
"""
|
|
489
|
-
index = index if index is not None else build_relation_verb_index()
|
|
490
|
-
plan: Dict[str, str] = {}
|
|
491
|
-
for edge_type in edge_types:
|
|
492
|
-
original = str(edge_type or "")
|
|
493
|
-
if _V4_ENUM_LABEL_RE.match(original):
|
|
494
|
-
continue
|
|
495
|
-
canonical = normalize_relation_verb(original, index=index)
|
|
496
|
-
if canonical and canonical != original:
|
|
497
|
-
plan[original] = canonical
|
|
498
|
-
return plan
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
def plan_concept_noise_reduction(
|
|
502
|
-
concepts: Iterable[Dict[str, Any]],
|
|
503
|
-
total_docs: int,
|
|
504
|
-
*,
|
|
505
|
-
max_df_ratio: float = 0.8,
|
|
506
|
-
min_doc_frequency: int = 1,
|
|
507
|
-
min_corpus_docs: int = 5,
|
|
508
|
-
) -> Dict[str, List[Dict[str, Any]]]:
|
|
509
|
-
"""Decide which heuristic concept nodes are graph noise.
|
|
510
|
-
|
|
511
|
-
``concepts``: ``[{"id", "label", "df", "heuristic"}]`` where ``df`` is the
|
|
512
|
-
number of distinct content documents linking to the concept.
|
|
513
|
-
|
|
514
|
-
Rules (dry-run friendly — pure decision, no side effects):
|
|
515
|
-
|
|
516
|
-
* **never** flag ``heuristic=False`` nodes (explicit user-created nodes
|
|
517
|
-
are untouchable, whatever their stats);
|
|
518
|
-
* low IDF: with a corpus of at least ``min_corpus_docs`` documents, a
|
|
519
|
-
concept appearing in ``> max_df_ratio`` of them separates nothing —
|
|
520
|
-
remove;
|
|
521
|
-
* frequency floor: ``df < min_doc_frequency`` (orphaned/near-orphaned
|
|
522
|
-
auto concepts) — remove.
|
|
523
|
-
"""
|
|
524
|
-
total = max(0, int(total_docs))
|
|
525
|
-
remove: List[Dict[str, Any]] = []
|
|
526
|
-
keep: List[Dict[str, Any]] = []
|
|
527
|
-
for concept in concepts:
|
|
528
|
-
entry: Dict[str, Any] = {
|
|
529
|
-
"id": str(concept.get("id") or ""),
|
|
530
|
-
"label": concept.get("label"),
|
|
531
|
-
"df": int(concept.get("df") or 0),
|
|
532
|
-
"heuristic": bool(concept.get("heuristic")),
|
|
533
|
-
}
|
|
534
|
-
if not entry["heuristic"]:
|
|
535
|
-
keep.append({**entry, "reason": "user_created_protected"})
|
|
536
|
-
continue
|
|
537
|
-
df = int(entry["df"])
|
|
538
|
-
if df < int(min_doc_frequency):
|
|
539
|
-
df_ratio = (df / total) if total else 0.0
|
|
540
|
-
remove.append({**entry, "df_ratio": round(df_ratio, 4), "reason": "below_frequency_floor"})
|
|
541
|
-
continue
|
|
542
|
-
if total >= int(min_corpus_docs):
|
|
543
|
-
df_ratio = df / total
|
|
544
|
-
if df_ratio > float(max_df_ratio):
|
|
545
|
-
remove.append({**entry, "df_ratio": round(df_ratio, 4), "reason": "low_idf_ubiquitous"})
|
|
546
|
-
continue
|
|
547
|
-
keep.append({**entry, "reason": "signal"})
|
|
548
|
-
return {"remove": remove, "keep": keep}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
def plan_relation_noise_reduction(
|
|
552
|
-
edges: Iterable[Dict[str, Any]],
|
|
553
|
-
*,
|
|
554
|
-
min_cooccurrence_weight: float = 0.3,
|
|
555
|
-
max_cooccurrence_degree: int = 12,
|
|
556
|
-
) -> Dict[str, List[Dict[str, Any]]]:
|
|
557
|
-
"""Separate meaning edges from adjacency edges (review 2026-07-27 P1 #6).
|
|
558
|
-
|
|
559
|
-
``edges``: ``[{"id", "from", "to", "type", "weight", "evidence", "degree"}]``
|
|
560
|
-
where ``evidence`` is the class recorded at ingest (``"verb"`` |
|
|
561
|
-
``"cooccurrence"``) and ``degree`` is how many co-occurrence edges the
|
|
562
|
-
source node already carries.
|
|
563
|
-
|
|
564
|
-
Rules (pure decision, dry-run friendly):
|
|
565
|
-
|
|
566
|
-
* verb-backed edges are **never** demoted — the sentence carried a real
|
|
567
|
-
relation;
|
|
568
|
-
* edges with no recorded evidence class are legacy rows: kept, and marked
|
|
569
|
-
``unknown_evidence`` rather than guessed at;
|
|
570
|
-
* co-occurrence edges below ``min_cooccurrence_weight`` are noise;
|
|
571
|
-
* a node whose co-occurrence degree exceeds ``max_cooccurrence_degree`` is
|
|
572
|
-
a hub built by adjacency, not meaning — its extra co-occurrence edges
|
|
573
|
-
are demoted.
|
|
574
|
-
|
|
575
|
-
Returns ``{"keep": [...], "demote": [...]}``. "Demote" is deliberate:
|
|
576
|
-
these edges are candidates for review, not silent deletion.
|
|
577
|
-
"""
|
|
578
|
-
keep: List[Dict[str, Any]] = []
|
|
579
|
-
demote: List[Dict[str, Any]] = []
|
|
580
|
-
for edge in edges:
|
|
581
|
-
entry: Dict[str, Any] = {
|
|
582
|
-
"id": str(edge.get("id") or ""),
|
|
583
|
-
"from": edge.get("from"),
|
|
584
|
-
"to": edge.get("to"),
|
|
585
|
-
"type": edge.get("type"),
|
|
586
|
-
"evidence": str(edge.get("evidence") or ""),
|
|
587
|
-
}
|
|
588
|
-
try:
|
|
589
|
-
entry["weight"] = float(edge.get("weight") or 0.0)
|
|
590
|
-
except (TypeError, ValueError):
|
|
591
|
-
entry["weight"] = 0.0
|
|
592
|
-
try:
|
|
593
|
-
degree = int(edge.get("degree") or 0)
|
|
594
|
-
except (TypeError, ValueError):
|
|
595
|
-
degree = 0
|
|
596
|
-
entry["degree"] = degree
|
|
597
|
-
if entry["evidence"] == "verb":
|
|
598
|
-
keep.append({**entry, "reason": "verb_evidence"})
|
|
599
|
-
continue
|
|
600
|
-
if not entry["evidence"]:
|
|
601
|
-
keep.append({**entry, "reason": "unknown_evidence"})
|
|
602
|
-
continue
|
|
603
|
-
if float(entry["weight"]) < float(min_cooccurrence_weight):
|
|
604
|
-
demote.append({**entry, "reason": "weak_cooccurrence"})
|
|
605
|
-
continue
|
|
606
|
-
if degree > int(max_cooccurrence_degree):
|
|
607
|
-
demote.append({**entry, "reason": "cooccurrence_hub"})
|
|
608
|
-
continue
|
|
609
|
-
keep.append({**entry, "reason": "cooccurrence_within_budget"})
|
|
610
|
-
return {"keep": keep, "demote": demote}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
# ── End-to-end helper ─────────────────────────────────────────────────────────
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
def auto_build_graph_overlay(
|
|
617
|
-
documents: List[Dict[str, Any]],
|
|
618
|
-
*,
|
|
619
|
-
existing_node_labels: Optional[Set[str]] = None,
|
|
620
|
-
alias_index: Optional[Dict[str, str]] = None,
|
|
621
|
-
max_new_nodes: int = 8,
|
|
622
|
-
) -> Dict[str, Any]:
|
|
623
|
-
"""한 번에 토픽 추출 → 클러스터 → 승격 결정까지 수행한 결과를 돌려준다.
|
|
624
|
-
|
|
625
|
-
실제 그래프 DB에 쓰는 작업은 호출자가 담당한다. 이 함수는 부작용 없음.
|
|
626
|
-
"""
|
|
627
|
-
candidates = extract_topic_candidates(documents)
|
|
628
|
-
clustered = cluster_candidates(candidates, alias_index=alias_index)
|
|
629
|
-
|
|
630
|
-
promotions: List[Dict[str, Any]] = []
|
|
631
|
-
skipped: List[Dict[str, Any]] = []
|
|
632
|
-
promoted_count = 0
|
|
633
|
-
for cand in clustered:
|
|
634
|
-
if promoted_count >= max_new_nodes:
|
|
635
|
-
skipped.append({"label": cand.label, "reason": "max_new_nodes reached"})
|
|
636
|
-
continue
|
|
637
|
-
decision = should_promote(cand, existing_node_labels=existing_node_labels)
|
|
638
|
-
if decision.promote:
|
|
639
|
-
promotions.append({
|
|
640
|
-
"label": cand.label,
|
|
641
|
-
"importance": decision.importance,
|
|
642
|
-
"aliases": sorted(cand.aliases),
|
|
643
|
-
"sources": cand.sources,
|
|
644
|
-
})
|
|
645
|
-
promoted_count += 1
|
|
646
|
-
else:
|
|
647
|
-
skipped.append({"label": cand.label, "reason": decision.reason})
|
|
648
|
-
|
|
649
|
-
return {
|
|
650
|
-
"promotions": promotions,
|
|
651
|
-
"skipped": skipped,
|
|
652
|
-
"candidates_total": len(candidates),
|
|
653
|
-
"clustered_total": len(clustered),
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
__all__ = [
|
|
658
|
-
"RELATION_VERB_GROUPS",
|
|
659
|
-
"build_relation_verb_index",
|
|
660
|
-
"normalize_relation_verb",
|
|
661
|
-
"plan_relation_normalization",
|
|
662
|
-
"plan_concept_noise_reduction",
|
|
663
|
-
"TopicCandidate",
|
|
664
|
-
"PromotionDecision",
|
|
665
|
-
"ThreadEdge",
|
|
666
|
-
"contains_secret",
|
|
667
|
-
"mask_secrets",
|
|
668
|
-
"extract_topic_candidates",
|
|
669
|
-
"cluster_candidates",
|
|
670
|
-
"should_promote",
|
|
671
|
-
"derive_thread_story",
|
|
672
|
-
"curate_nodes",
|
|
673
|
-
"auto_build_graph_overlay",
|
|
674
|
-
"build_alias_index",
|
|
675
|
-
"DEFAULT_ALIAS_GROUPS",
|
|
676
|
-
]
|