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,460 +0,0 @@
|
|
|
1
|
-
"""Pure chat helpers: language/intent detection, file-action parsing,
|
|
2
|
-
network-status formatting, and recent-context assembly. Split out of the chat
|
|
3
|
-
router module so create_chat_router keeps only request wiring. chat.py
|
|
4
|
-
re-imports every name, so external import sites (tests, app_factory) are
|
|
5
|
-
unaffected.
|
|
6
|
-
|
|
7
|
-
Workspace-scope extraction used to live here too, as a verbatim copy of the
|
|
8
|
-
header/query derivation. 11.5.2 deleted it: the callers now use
|
|
9
|
-
:func:`latticeai.api.workspace_scope.requested_workspace`, so a request that
|
|
10
|
-
names two different workspaces is refused instead of having one selector
|
|
11
|
-
silently win.
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
from __future__ import annotations
|
|
15
|
-
|
|
16
|
-
import json
|
|
17
|
-
import re
|
|
18
|
-
from typing import Any, AsyncIterator, Dict, List, Optional
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def pair_user_history(history: List[Dict], user_email: str) -> List[Dict]:
|
|
22
|
-
"""Restrict history to one user's exchange.
|
|
23
|
-
|
|
24
|
-
Keeps the user's own messages plus assistant replies that directly follow
|
|
25
|
-
them. A bare role=="assistant" pass would leak every other user's replies
|
|
26
|
-
into this user's prompt context.
|
|
27
|
-
"""
|
|
28
|
-
paired: List[Dict] = []
|
|
29
|
-
include_next_assistant = False
|
|
30
|
-
for item in history:
|
|
31
|
-
if item.get("role") == "assistant":
|
|
32
|
-
if include_next_assistant:
|
|
33
|
-
assistant_user = item.get("user_email")
|
|
34
|
-
if assistant_user and assistant_user != user_email:
|
|
35
|
-
# Concurrent requests can interleave rows in the global
|
|
36
|
-
# history order. A reply explicitly owned by somebody
|
|
37
|
-
# else is never this user's reply; keep waiting for the
|
|
38
|
-
# next correctly-owned (or legacy ownerless) assistant.
|
|
39
|
-
continue
|
|
40
|
-
paired.append(item)
|
|
41
|
-
include_next_assistant = False
|
|
42
|
-
elif item.get("user_email") == user_email:
|
|
43
|
-
paired.append(item)
|
|
44
|
-
include_next_assistant = True
|
|
45
|
-
else:
|
|
46
|
-
include_next_assistant = False
|
|
47
|
-
return paired
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def detect_language(text: str) -> str:
|
|
51
|
-
"""Detect language: 'ko' (Korean) or 'en' (English)."""
|
|
52
|
-
total = max(len(text), 1)
|
|
53
|
-
ko = sum(1 for c in text if '가' <= c <= '힣')
|
|
54
|
-
if ko / total > 0.05:
|
|
55
|
-
return "ko"
|
|
56
|
-
return "en"
|
|
57
|
-
|
|
58
|
-
_LANG_HINT = {
|
|
59
|
-
"ko": "Respond in Korean (한국어로 답변하세요).",
|
|
60
|
-
"en": "Respond in English.",
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
def is_network_status_request(text: str) -> bool:
|
|
64
|
-
"""사용자가 현재 IP/네트워크 정보를 물었는지 감지합니다."""
|
|
65
|
-
t = (text or "").lower()
|
|
66
|
-
explicit_network = any(
|
|
67
|
-
phrase in t
|
|
68
|
-
for phrase in (
|
|
69
|
-
"ipconfig",
|
|
70
|
-
"ifconfig",
|
|
71
|
-
"network status",
|
|
72
|
-
"네트워크 상태",
|
|
73
|
-
"네트워크 확인",
|
|
74
|
-
"현재 네트워크",
|
|
75
|
-
)
|
|
76
|
-
)
|
|
77
|
-
current_ip = bool(
|
|
78
|
-
re.search(
|
|
79
|
-
r"(내|현재|지금|로컬|local|public|공인|외부|내부)\s*(ip|아이피)\s*(주소)?",
|
|
80
|
-
t,
|
|
81
|
-
)
|
|
82
|
-
or re.search(
|
|
83
|
-
r"(ip|아이피)\s*(주소)?\s*(확인|상태|알려줘|보여줘)",
|
|
84
|
-
t,
|
|
85
|
-
)
|
|
86
|
-
)
|
|
87
|
-
return explicit_network or current_ip
|
|
88
|
-
|
|
89
|
-
def is_current_url_request(text: str) -> bool:
|
|
90
|
-
t = (text or "").lower()
|
|
91
|
-
explicit_url = any(phrase in t for phrase in ("현재 url", "current url", "page url", "페이지 url"))
|
|
92
|
-
current_page_link = bool(
|
|
93
|
-
re.search(r"(현재|지금|여기|이\s*페이지|브라우저|접속)\s*(페이지\s*)?(url|링크|주소)", t)
|
|
94
|
-
or re.search(r"(url|링크)\s*(알려줘|보여줘|확인)", t)
|
|
95
|
-
)
|
|
96
|
-
return explicit_url or current_page_link
|
|
97
|
-
|
|
98
|
-
def is_clear_command(text: str) -> bool:
|
|
99
|
-
return (text or "").strip().lower() in {"/clear", "/clear_all"}
|
|
100
|
-
|
|
101
|
-
# Path segments intentionally exclude spaces: allowing spaces let the target
|
|
102
|
-
# match swallow preceding words (e.g. "create a text file report.txt" resolved
|
|
103
|
-
# to the whole phrase as the path). Chat file targets are single tokens.
|
|
104
|
-
_FILE_TARGET_RE = re.compile(
|
|
105
|
-
r"(?<![\w.-])(?:[~./\\]?[\w.@()+-]+[\\/])*[\w.@()+-]+"
|
|
106
|
-
r"\.(?:py|js|jsx|ts|tsx|md|markdown|txt|json|yaml|yml|toml|html|css|csv|xml|pdf|docx|xlsx|pptx|sh|sql)",
|
|
107
|
-
re.IGNORECASE,
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def is_file_action_request(text: str) -> bool:
|
|
112
|
-
"""Return True for chat requests that should execute file tools, not prose.
|
|
113
|
-
|
|
114
|
-
The Brain chat composer posts to ``/chat``. Without this gate, explicit
|
|
115
|
-
side-effect requests such as "create hello.md" are handled as plain model
|
|
116
|
-
generation, which commonly produces a code block instead of a real file.
|
|
117
|
-
Keep the gate narrow so normal Q&A and "how do I create a file?" stay in
|
|
118
|
-
ordinary chat.
|
|
119
|
-
"""
|
|
120
|
-
raw = (text or "").strip()
|
|
121
|
-
if not raw:
|
|
122
|
-
return False
|
|
123
|
-
lower = raw.lower()
|
|
124
|
-
|
|
125
|
-
if any(phrase in lower for phrase in (
|
|
126
|
-
"how to ", "how do i ", "방법", "어떻게", "예시", "sample", "example",
|
|
127
|
-
)) and not any(phrase in lower for phrase in (
|
|
128
|
-
"actually create", "real file", "실제로", "파일로", "저장해", "만들어",
|
|
129
|
-
)):
|
|
130
|
-
return False
|
|
131
|
-
|
|
132
|
-
has_target = bool(_FILE_TARGET_RE.search(raw))
|
|
133
|
-
has_file_word = any(word in lower for word in (
|
|
134
|
-
"file", "파일", "문서", "artifact", "아티팩트", "save as", "저장",
|
|
135
|
-
# Explicit artifact types: users asking for "an html page" expect a
|
|
136
|
-
# real file, not a code block in chat — route them to the file path
|
|
137
|
-
# where the model-agnostic generation pipeline guarantees validity.
|
|
138
|
-
"html", "웹페이지", "웹 페이지", "홈페이지", "webpage", "web page",
|
|
139
|
-
))
|
|
140
|
-
has_action = any(word in lower for word in (
|
|
141
|
-
"create", "make", "write", "save", "generate", "edit", "update",
|
|
142
|
-
"만들", "생성", "작성", "저장", "수정", "써줘", "만들어줘",
|
|
143
|
-
))
|
|
144
|
-
|
|
145
|
-
if not has_action:
|
|
146
|
-
return False
|
|
147
|
-
if has_target and has_action:
|
|
148
|
-
return True
|
|
149
|
-
return has_file_word and has_action
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
def file_action_target(text: str) -> Optional[str]:
|
|
153
|
-
"""Extract the first explicit workspace file target from a request."""
|
|
154
|
-
match = _FILE_TARGET_RE.search(text or "")
|
|
155
|
-
if not match:
|
|
156
|
-
return None
|
|
157
|
-
return match.group(0).strip().strip("`'\".,:;)]}")
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def inline_file_action_content(text: str) -> Optional[str]:
|
|
161
|
-
"""Extract short user-provided content for deterministic file writes."""
|
|
162
|
-
raw = (text or "").strip()
|
|
163
|
-
# Each pattern requires an explicit binder so ambiguous words like "text"
|
|
164
|
-
# ("create a text file report.txt") or a bare "with" ("report.md with a
|
|
165
|
-
# summary of X") do not get captured as literal file content.
|
|
166
|
-
patterns = [
|
|
167
|
-
r"(?:내용|본문)\s*(?:은|는|이에요|입니다)\s*(.+)$",
|
|
168
|
-
r"(?:내용|본문|content|body|text)\s*[:=]\s*(.+)$",
|
|
169
|
-
r"(?:content|body)\s+(?:is|as)\s+(.+)$",
|
|
170
|
-
r"(?:with the content|with content|containing)\s+(.+)$",
|
|
171
|
-
]
|
|
172
|
-
for pattern in patterns:
|
|
173
|
-
match = re.search(pattern, raw, flags=re.IGNORECASE | re.DOTALL)
|
|
174
|
-
if match:
|
|
175
|
-
content = match.group(1).strip()
|
|
176
|
-
return content.strip("`'\"")
|
|
177
|
-
return None
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
def strip_generated_file_content(text: str) -> str:
|
|
181
|
-
"""Remove common chat wrappers when a model is asked for file content only."""
|
|
182
|
-
content = (text or "").strip()
|
|
183
|
-
fenced = re.search(r"```(?:[\w.+-]+)?\s*(.*?)\s*```", content, flags=re.DOTALL)
|
|
184
|
-
if fenced:
|
|
185
|
-
content = fenced.group(1).strip()
|
|
186
|
-
return content
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
def format_network_status(info: Dict) -> str:
|
|
190
|
-
lines = [
|
|
191
|
-
f"내부 IP: {info.get('local_ip') or '확인 안 됨'}",
|
|
192
|
-
f"외부 IP: {info.get('public_ip') or '확인 안 됨'}",
|
|
193
|
-
f"호스트명: {info.get('hostname') or '확인 안 됨'}",
|
|
194
|
-
]
|
|
195
|
-
local_ips = info.get("local_ips") or {}
|
|
196
|
-
if local_ips:
|
|
197
|
-
lines.extend(["", "인터페이스:"])
|
|
198
|
-
lines.extend(f"- {name}: {ip}" for name, ip in local_ips.items())
|
|
199
|
-
note = info.get("note")
|
|
200
|
-
if note:
|
|
201
|
-
lines.extend(["", note])
|
|
202
|
-
return "\n".join(lines)
|
|
203
|
-
|
|
204
|
-
def build_context_quality(
|
|
205
|
-
query: str,
|
|
206
|
-
*,
|
|
207
|
-
knowledge_graph=None,
|
|
208
|
-
allowed_workspaces=None,
|
|
209
|
-
limit: int = 6,
|
|
210
|
-
) -> Dict[str, object]:
|
|
211
|
-
"""Honest RAG context-quality signal for chat responses (v9.8.0, additive).
|
|
212
|
-
|
|
213
|
-
Returns ``{"mode": "hybrid"|"lexical_only"|"none", "nodes": int,
|
|
214
|
-
"limited": bool, "reason": str|None}``. ``limited`` is true when the
|
|
215
|
-
graph produced 0–1 matches, when vector retrieval fell back to
|
|
216
|
-
lexical-only, or when retrieval failed entirely — so the frontend can
|
|
217
|
-
tell the user the answer is weakly grounded instead of implying rich
|
|
218
|
-
graph context. Never raises.
|
|
219
|
-
|
|
220
|
-
A fifth key, ``multimodal``, appears only when the matches really include
|
|
221
|
-
picture nodes (v11.2.0). "6 nodes" reads differently when two of them are
|
|
222
|
-
screenshots whose text came out of OCR, and until this call site passed
|
|
223
|
-
``multimodal=`` the shipped chat surface could not tell the difference the
|
|
224
|
-
signal was built to report.
|
|
225
|
-
"""
|
|
226
|
-
from lattice_brain.graph.retrieval import (
|
|
227
|
-
context_quality_signal,
|
|
228
|
-
multimodal_signal,
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
query = str(query or "").strip()
|
|
232
|
-
if knowledge_graph is None or not query:
|
|
233
|
-
return context_quality_signal(
|
|
234
|
-
"none", 0, reason="지식 그래프 컨텍스트를 사용하지 않았습니다"
|
|
235
|
-
)
|
|
236
|
-
scope_kwargs = (
|
|
237
|
-
{"allowed_workspaces": allowed_workspaces}
|
|
238
|
-
if allowed_workspaces is not None
|
|
239
|
-
else {}
|
|
240
|
-
)
|
|
241
|
-
hybrid_fn = getattr(knowledge_graph, "hybrid_search", None)
|
|
242
|
-
if callable(hybrid_fn):
|
|
243
|
-
try:
|
|
244
|
-
result = hybrid_fn(query, top_k=limit, **scope_kwargs) or {}
|
|
245
|
-
except Exception: # noqa: BLE001 — signal building must never fail chat
|
|
246
|
-
return context_quality_signal("none", 0, reason="그래프 검색에 실패했습니다")
|
|
247
|
-
matches = result.get("matches") or []
|
|
248
|
-
return context_quality_signal(
|
|
249
|
-
str(result.get("mode") or "hybrid"),
|
|
250
|
-
len(matches),
|
|
251
|
-
multimodal=multimodal_signal(matches),
|
|
252
|
-
)
|
|
253
|
-
# Lexical-only stores without the hybrid mixin.
|
|
254
|
-
try:
|
|
255
|
-
matches = (
|
|
256
|
-
knowledge_graph.search(query, limit=limit, **scope_kwargs) or {}
|
|
257
|
-
).get("matches") or []
|
|
258
|
-
except Exception: # noqa: BLE001 — signal building must never fail chat
|
|
259
|
-
return context_quality_signal("none", 0, reason="그래프 검색에 실패했습니다")
|
|
260
|
-
return context_quality_signal(
|
|
261
|
-
"lexical_only", len(matches), multimodal=multimodal_signal(matches)
|
|
262
|
-
)
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
# ── answer-citation binding (backlog #11, review §7.2 E #4) ──────────────────
|
|
266
|
-
_GROUNDING_TOKEN_RE = re.compile(r"[0-9A-Za-z가-힣]{2,}")
|
|
267
|
-
# High-frequency tokens that overlap by accident, not by grounding.
|
|
268
|
-
_GROUNDING_STOP_TOKENS = frozenset({
|
|
269
|
-
"그리고", "그러나", "하지만", "그래서", "있습니다", "입니다", "합니다",
|
|
270
|
-
"있는", "없는", "위해", "통해", "대한", "관련", "경우", "내용", "answer",
|
|
271
|
-
"the", "and", "for", "with", "this", "that", "from", "have", "are",
|
|
272
|
-
"was", "were", "can", "will", "not", "you", "your",
|
|
273
|
-
})
|
|
274
|
-
GROUNDING_MIN_OVERLAP_TOKENS = 2
|
|
275
|
-
GROUNDING_MIN_OVERLAP_RATIO = 0.08
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
def _grounding_tokens(text: str) -> set:
|
|
279
|
-
return {
|
|
280
|
-
token.lower()
|
|
281
|
-
for token in _GROUNDING_TOKEN_RE.findall(str(text or ""))
|
|
282
|
-
if token.lower() not in _GROUNDING_STOP_TOKENS
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
def assess_answer_grounding(
|
|
287
|
-
answer: str,
|
|
288
|
-
*,
|
|
289
|
-
trace: Optional[Dict[str, object]] = None,
|
|
290
|
-
context_quality: Optional[Dict[str, object]] = None,
|
|
291
|
-
) -> Dict[str, object]:
|
|
292
|
-
"""Bind an answer to its retrieved sources — honestly (never blocks).
|
|
293
|
-
|
|
294
|
-
Heuristic: for each retrieved source (the answer trace's ``graph_nodes``
|
|
295
|
-
/ ``source_files``), measure content-token overlap between the answer and
|
|
296
|
-
the source's title+summary, and accept explicit citations (the answer
|
|
297
|
-
names the source title or filename). Returns::
|
|
298
|
-
|
|
299
|
-
{"status": "supported", "label": "근거 있음", "source_ids": [...],
|
|
300
|
-
"cited": [...], "overlap": float}
|
|
301
|
-
{"status": "unsupported", "label": "근거 없음", "source_ids": [], ...}
|
|
302
|
-
{"status": "no_context", "label": "근거 없음", ...} # nothing retrieved
|
|
303
|
-
|
|
304
|
-
``no_context`` (still labeled 근거 없음) lets the UI distinguish "the
|
|
305
|
-
Brain had nothing" from "the Brain had sources but the answer ignored
|
|
306
|
-
them". Annotation only — the answer is never modified or blocked.
|
|
307
|
-
"""
|
|
308
|
-
answer_text = str(answer or "").strip()
|
|
309
|
-
trace_map: Dict[str, Any] = trace if isinstance(trace, dict) else {}
|
|
310
|
-
sources: List[Dict[str, Any]] = []
|
|
311
|
-
seen_ids = set()
|
|
312
|
-
for node in list(trace_map.get("graph_nodes") or []):
|
|
313
|
-
if not isinstance(node, dict):
|
|
314
|
-
continue
|
|
315
|
-
node_id = str(node.get("id") or node.get("node_id") or "")
|
|
316
|
-
if not node_id or node_id in seen_ids:
|
|
317
|
-
continue
|
|
318
|
-
seen_ids.add(node_id)
|
|
319
|
-
raw_meta = node.get("metadata")
|
|
320
|
-
meta: Dict[str, Any] = raw_meta if isinstance(raw_meta, dict) else {}
|
|
321
|
-
sources.append({
|
|
322
|
-
"id": node_id,
|
|
323
|
-
"title": str(node.get("title") or ""),
|
|
324
|
-
"body": " ".join(
|
|
325
|
-
str(part or "")
|
|
326
|
-
for part in (node.get("title"), node.get("summary"), meta.get("filename"))
|
|
327
|
-
),
|
|
328
|
-
})
|
|
329
|
-
for src in list(trace_map.get("source_files") or []):
|
|
330
|
-
if not isinstance(src, dict):
|
|
331
|
-
continue
|
|
332
|
-
node_id = str(src.get("node_id") or src.get("source") or "")
|
|
333
|
-
if not node_id or node_id in seen_ids:
|
|
334
|
-
continue
|
|
335
|
-
seen_ids.add(node_id)
|
|
336
|
-
sources.append({
|
|
337
|
-
"id": node_id,
|
|
338
|
-
"title": str(src.get("node_title") or ""),
|
|
339
|
-
"body": " ".join(
|
|
340
|
-
str(part or "") for part in (src.get("node_title"), src.get("source"))
|
|
341
|
-
),
|
|
342
|
-
})
|
|
343
|
-
|
|
344
|
-
mode = str((context_quality or {}).get("mode") or "none")
|
|
345
|
-
if not sources:
|
|
346
|
-
return {
|
|
347
|
-
"status": "no_context",
|
|
348
|
-
"label": "근거 없음",
|
|
349
|
-
"source_ids": [],
|
|
350
|
-
"cited": [],
|
|
351
|
-
"overlap": 0.0,
|
|
352
|
-
"reason": (
|
|
353
|
-
"검색된 출처가 없습니다"
|
|
354
|
-
if mode == "none"
|
|
355
|
-
else "출처 후보를 답변에 연결하지 못했습니다"
|
|
356
|
-
),
|
|
357
|
-
}
|
|
358
|
-
if not answer_text:
|
|
359
|
-
return {
|
|
360
|
-
"status": "unsupported",
|
|
361
|
-
"label": "근거 없음",
|
|
362
|
-
"source_ids": [],
|
|
363
|
-
"cited": [],
|
|
364
|
-
"overlap": 0.0,
|
|
365
|
-
"reason": "답변이 비어 있습니다",
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
answer_tokens = _grounding_tokens(answer_text)
|
|
369
|
-
answer_lower = answer_text.lower()
|
|
370
|
-
cited: List[Dict[str, Any]] = []
|
|
371
|
-
best_overlap = 0.0
|
|
372
|
-
for source in sources:
|
|
373
|
-
source_tokens = _grounding_tokens(str(source["body"]))
|
|
374
|
-
if not source_tokens:
|
|
375
|
-
continue
|
|
376
|
-
shared = answer_tokens & source_tokens
|
|
377
|
-
denominator = max(1, min(len(source_tokens), 60))
|
|
378
|
-
ratio = len(shared) / denominator
|
|
379
|
-
best_overlap = max(best_overlap, ratio)
|
|
380
|
-
title = str(source.get("title") or "").strip().lower()
|
|
381
|
-
explicit = bool(title) and len(title) >= 4 and title in answer_lower
|
|
382
|
-
# Explicit-citation OR token-overlap binding. Comparisons are explicit
|
|
383
|
-
# (a 0.0 score is falsy but a valid value — never use `or` defaults).
|
|
384
|
-
if explicit or (
|
|
385
|
-
len(shared) >= GROUNDING_MIN_OVERLAP_TOKENS
|
|
386
|
-
and ratio >= GROUNDING_MIN_OVERLAP_RATIO
|
|
387
|
-
):
|
|
388
|
-
cited.append({
|
|
389
|
-
"id": source["id"],
|
|
390
|
-
"title": source.get("title"),
|
|
391
|
-
"overlap": round(ratio, 4),
|
|
392
|
-
"explicit": explicit,
|
|
393
|
-
})
|
|
394
|
-
if cited:
|
|
395
|
-
return {
|
|
396
|
-
"status": "supported",
|
|
397
|
-
"label": "근거 있음",
|
|
398
|
-
"source_ids": [item["id"] for item in cited],
|
|
399
|
-
"cited": cited,
|
|
400
|
-
"overlap": round(best_overlap, 4),
|
|
401
|
-
"reason": None,
|
|
402
|
-
}
|
|
403
|
-
return {
|
|
404
|
-
"status": "unsupported",
|
|
405
|
-
"label": "근거 없음",
|
|
406
|
-
"source_ids": [],
|
|
407
|
-
"cited": [],
|
|
408
|
-
"overlap": round(best_overlap, 4),
|
|
409
|
-
"reason": "답변이 검색된 출처의 내용을 사용하지 않았습니다",
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
async def single_text_stream(text: str, model: str = "system") -> AsyncIterator[str]:
|
|
414
|
-
yield f"data: {json.dumps({'chunk': text, 'model': model}, ensure_ascii=False)}\n\n"
|
|
415
|
-
yield "data: [DONE]\n\n"
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
def build_recent_chat_context(
|
|
419
|
-
*,
|
|
420
|
-
get_history,
|
|
421
|
-
limit: int = 10,
|
|
422
|
-
include_image_missing_replies: bool = True,
|
|
423
|
-
user_email: Optional[str] = None,
|
|
424
|
-
conversation_id: Optional[str] = None,
|
|
425
|
-
workspace_id: Optional[str] = None,
|
|
426
|
-
) -> str:
|
|
427
|
-
if user_email:
|
|
428
|
-
# Apply identity/workspace isolation in the durable query rather than
|
|
429
|
-
# relying only on positional post-filtering. In particular, never
|
|
430
|
-
# admit legacy-global rows into an authenticated model prompt.
|
|
431
|
-
history = get_history(
|
|
432
|
-
user_email=user_email,
|
|
433
|
-
allowed_workspaces={workspace_id} if workspace_id is not None else None,
|
|
434
|
-
include_legacy_global=False,
|
|
435
|
-
)
|
|
436
|
-
else:
|
|
437
|
-
history = get_history()
|
|
438
|
-
if workspace_id is not None:
|
|
439
|
-
history = [
|
|
440
|
-
item for item in history
|
|
441
|
-
if str(item.get("workspace_id") or "personal") == str(workspace_id)
|
|
442
|
-
]
|
|
443
|
-
if conversation_id:
|
|
444
|
-
history = [item for item in history if item.get("conversation_id") == conversation_id]
|
|
445
|
-
if user_email:
|
|
446
|
-
history = pair_user_history(history, user_email)
|
|
447
|
-
history = history[-limit:]
|
|
448
|
-
lines = []
|
|
449
|
-
for item in history:
|
|
450
|
-
role = item.get("role", "user")
|
|
451
|
-
content = item.get("content", "")
|
|
452
|
-
if not include_image_missing_replies and role == "assistant":
|
|
453
|
-
if "이미지" in content and any(word in content for word in ["업로드", "제공", "올려"]):
|
|
454
|
-
continue
|
|
455
|
-
source = item.get("source")
|
|
456
|
-
label = role
|
|
457
|
-
if source:
|
|
458
|
-
label = f"{role} ({source})"
|
|
459
|
-
lines.append(f"{label}: {content}")
|
|
460
|
-
return "\n".join(lines)
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
"""Conversation-history HTTP routes.
|
|
2
|
-
|
|
3
|
-
Persistence and search grouping live in :mod:`latticeai.services.chat_service`;
|
|
4
|
-
this module only authenticates requests and shapes HTTP responses.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
from dataclasses import dataclass
|
|
10
|
-
from typing import Any, Callable, Dict
|
|
11
|
-
|
|
12
|
-
from fastapi import APIRouter, Request
|
|
13
|
-
|
|
14
|
-
from latticeai.core.messages import http_error, resolve_language
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@dataclass(frozen=True)
|
|
18
|
-
class HistoryRouteDependencies:
|
|
19
|
-
chat_service: Any
|
|
20
|
-
require_user: Callable[[Request], str]
|
|
21
|
-
scope_for_user: Callable[[str], Dict[str, Any]]
|
|
22
|
-
group_conversations: Callable[[list], Any]
|
|
23
|
-
get_conversation_messages: Callable[..., list]
|
|
24
|
-
conversation_title: Callable[[Dict[str, Any]], str]
|
|
25
|
-
clear_conversation: Callable[..., Dict[str, Any]]
|
|
26
|
-
clear_history: Callable[..., Dict[str, Any]]
|
|
27
|
-
append_audit_event: Callable[..., None]
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def register_history_routes(router: APIRouter, deps: HistoryRouteDependencies) -> None:
|
|
31
|
-
@router.get("/history")
|
|
32
|
-
async def fetch_history(request: Request):
|
|
33
|
-
"""웹 화면에서 이전 대화를 불러올 수 있도록 히스토리를 반환합니다."""
|
|
34
|
-
current_user = deps.require_user(request)
|
|
35
|
-
return deps.chat_service.history(**deps.scope_for_user(current_user))
|
|
36
|
-
|
|
37
|
-
@router.get("/history/conversations")
|
|
38
|
-
async def fetch_history_conversations(request: Request):
|
|
39
|
-
"""저장된 히스토리를 대화 단위로 묶어 반환합니다."""
|
|
40
|
-
current_user = deps.require_user(request)
|
|
41
|
-
history = deps.chat_service.history(**deps.scope_for_user(current_user))
|
|
42
|
-
return deps.group_conversations(history)
|
|
43
|
-
|
|
44
|
-
@router.get("/history/conversations/{conversation_id:path}")
|
|
45
|
-
async def fetch_history_conversation(conversation_id: str, request: Request):
|
|
46
|
-
"""선택한 대화의 메시지를 반환합니다."""
|
|
47
|
-
current_user = deps.require_user(request)
|
|
48
|
-
messages = deps.get_conversation_messages(
|
|
49
|
-
conversation_id,
|
|
50
|
-
**deps.scope_for_user(current_user),
|
|
51
|
-
)
|
|
52
|
-
if not messages:
|
|
53
|
-
raise http_error(404, "chat.conversation_not_found", resolve_language(request))
|
|
54
|
-
return {"id": conversation_id, "messages": messages}
|
|
55
|
-
|
|
56
|
-
@router.delete("/history/conversations/{conversation_id:path}")
|
|
57
|
-
async def delete_history_conversation(conversation_id: str, request: Request):
|
|
58
|
-
"""선택한 대화방의 메시지만 삭제합니다."""
|
|
59
|
-
email = deps.require_user(request)
|
|
60
|
-
started_at = request.query_params.get("started_at")
|
|
61
|
-
result = deps.clear_conversation(
|
|
62
|
-
conversation_id,
|
|
63
|
-
started_at,
|
|
64
|
-
**deps.scope_for_user(email),
|
|
65
|
-
)
|
|
66
|
-
deps.append_audit_event(
|
|
67
|
-
"conversation_delete",
|
|
68
|
-
user_email=email,
|
|
69
|
-
conversation_id=conversation_id,
|
|
70
|
-
started_at=started_at,
|
|
71
|
-
removed=result.get("removed", 0),
|
|
72
|
-
kept=result.get("kept", 0),
|
|
73
|
-
)
|
|
74
|
-
return result
|
|
75
|
-
|
|
76
|
-
@router.delete("/history")
|
|
77
|
-
async def delete_history(request: Request, keep_last: int = 0):
|
|
78
|
-
email = deps.require_user(request)
|
|
79
|
-
result = deps.clear_history(keep_last, **deps.scope_for_user(email))
|
|
80
|
-
deps.append_audit_event(
|
|
81
|
-
"history_delete",
|
|
82
|
-
user_email=email,
|
|
83
|
-
keep_last=keep_last,
|
|
84
|
-
removed=result.get("removed", 0),
|
|
85
|
-
kept=result.get("kept", 0),
|
|
86
|
-
)
|
|
87
|
-
return result
|
|
88
|
-
|
|
89
|
-
@router.get("/history/search")
|
|
90
|
-
async def search_history(q: str, request: Request):
|
|
91
|
-
"""키워드로 채팅 히스토리를 검색합니다."""
|
|
92
|
-
current_user = deps.require_user(request)
|
|
93
|
-
results = deps.chat_service.search_history(
|
|
94
|
-
q,
|
|
95
|
-
scope=deps.scope_for_user(current_user),
|
|
96
|
-
conversation_title=deps.conversation_title,
|
|
97
|
-
)
|
|
98
|
-
return {"results": results, "query": q}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
__all__ = ["HistoryRouteDependencies", "register_history_routes"]
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
"""Chat-path helpers for hybrid cloud branching (Phase 2).
|
|
2
|
-
|
|
3
|
-
Keeps the main chat router thin: resolve network mode, decide whether to
|
|
4
|
-
enter the hybrid stream, and build the StreamingResponse.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
from typing import Any, Dict, Optional
|
|
10
|
-
|
|
11
|
-
from fastapi.responses import StreamingResponse
|
|
12
|
-
|
|
13
|
-
from latticeai.core.network_boundary import (
|
|
14
|
-
NetworkBoundaryMode,
|
|
15
|
-
normalize_network_mode,
|
|
16
|
-
)
|
|
17
|
-
from latticeai.runtime.network_boundary_wiring import (
|
|
18
|
-
get_hybrid_policy_service,
|
|
19
|
-
resolve_active_network_mode,
|
|
20
|
-
)
|
|
21
|
-
from latticeai.services.hybrid_chat import stream_hybrid_cloud_turn
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def resolve_request_network_mode(
|
|
25
|
-
*,
|
|
26
|
-
request_mode: Optional[str],
|
|
27
|
-
user_email: Optional[str],
|
|
28
|
-
workspace_id: Optional[str],
|
|
29
|
-
) -> NetworkBoundaryMode:
|
|
30
|
-
"""Per-request override wins; otherwise use the persisted dial."""
|
|
31
|
-
if request_mode:
|
|
32
|
-
return normalize_network_mode(request_mode)
|
|
33
|
-
return normalize_network_mode(
|
|
34
|
-
resolve_active_network_mode(user_email=user_email, workspace_id=workspace_id)
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def resolve_hybrid_auto_commit(
|
|
39
|
-
*,
|
|
40
|
-
user_email: Optional[str],
|
|
41
|
-
workspace_id: Optional[str],
|
|
42
|
-
) -> bool:
|
|
43
|
-
"""The scoped hybrid policy's ``auto_commit`` decision for this turn.
|
|
44
|
-
|
|
45
|
-
Sibling of :func:`resolve_request_network_mode`: the dial says whether the
|
|
46
|
-
turn may reach the cloud, this says what may happen to what comes back.
|
|
47
|
-
Default is ``False`` — cloud-derived knowledge waits in the Review Center
|
|
48
|
-
— and a policy that cannot be read is treated as the default rather than
|
|
49
|
-
as permission.
|
|
50
|
-
"""
|
|
51
|
-
try:
|
|
52
|
-
policy = get_hybrid_policy_service().resolve(
|
|
53
|
-
user_email=user_email, workspace_id=workspace_id
|
|
54
|
-
)
|
|
55
|
-
except Exception: # noqa: BLE001 — an unreadable policy never grants a write
|
|
56
|
-
return False
|
|
57
|
-
return bool(policy.get("auto_commit", False))
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def maybe_hybrid_stream_response(
|
|
61
|
-
*,
|
|
62
|
-
req: Any,
|
|
63
|
-
mode: NetworkBoundaryMode,
|
|
64
|
-
knowledge_graph: Any,
|
|
65
|
-
enable_graph: bool,
|
|
66
|
-
effective_email: Optional[str],
|
|
67
|
-
workspace_id: Optional[str],
|
|
68
|
-
history_meta: Dict[str, Any],
|
|
69
|
-
history_user: Dict[str, Any],
|
|
70
|
-
chat_service: Any,
|
|
71
|
-
notify: Any,
|
|
72
|
-
model_id: Optional[str],
|
|
73
|
-
review_queue: Any = None,
|
|
74
|
-
) -> Optional[StreamingResponse]:
|
|
75
|
-
"""Return a StreamingResponse when cloud path should run; else None."""
|
|
76
|
-
if mode != NetworkBoundaryMode.CLOUD_ALLOWED:
|
|
77
|
-
return None
|
|
78
|
-
if not (enable_graph and knowledge_graph is not None):
|
|
79
|
-
# Without a graph there is nothing minimal to send; fall back to local.
|
|
80
|
-
return None
|
|
81
|
-
|
|
82
|
-
return StreamingResponse(
|
|
83
|
-
stream_hybrid_cloud_turn(
|
|
84
|
-
user_message=req.message,
|
|
85
|
-
knowledge_graph=knowledge_graph,
|
|
86
|
-
mode=mode,
|
|
87
|
-
workspace_id=workspace_id,
|
|
88
|
-
user_email=effective_email,
|
|
89
|
-
model=model_id,
|
|
90
|
-
chat_service=chat_service,
|
|
91
|
-
history_meta=history_meta,
|
|
92
|
-
history_user=history_user,
|
|
93
|
-
notify=notify,
|
|
94
|
-
source=req.source,
|
|
95
|
-
review_queue=review_queue,
|
|
96
|
-
auto_commit=resolve_hybrid_auto_commit(
|
|
97
|
-
user_email=effective_email, workspace_id=workspace_id
|
|
98
|
-
),
|
|
99
|
-
),
|
|
100
|
-
media_type="text/event-stream",
|
|
101
|
-
headers={
|
|
102
|
-
"X-Model": model_id or "cloud",
|
|
103
|
-
"X-Network-Mode": mode.value,
|
|
104
|
-
"X-Hybrid": "1",
|
|
105
|
-
},
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
__all__ = [
|
|
110
|
-
"resolve_request_network_mode",
|
|
111
|
-
"resolve_hybrid_auto_commit",
|
|
112
|
-
"maybe_hybrid_stream_response",
|
|
113
|
-
]
|