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
package/latticeai/api/browser.py
DELETED
|
@@ -1,493 +0,0 @@
|
|
|
1
|
-
"""Browser & web ingestion — Knowledge Graph inputs, not standalone features.
|
|
2
|
-
|
|
3
|
-
v3.6.0 Knowledge Graph First: a public URL or an open browser tab is just another
|
|
4
|
-
source that converges into the Knowledge Graph through the unified ingestion
|
|
5
|
-
pipeline. Everything runs on the **local runtime** — the server fetches/reads
|
|
6
|
-
locally, stores into local SQLite, and never uploads to a cloud service.
|
|
7
|
-
|
|
8
|
-
Two layers, both feeding ``IngestionPipeline.ingest``:
|
|
9
|
-
|
|
10
|
-
* ``POST /api/browser/read-url`` — the runtime fetches a public URL locally,
|
|
11
|
-
extracts readable text, stores it as ``source_type=web_url``. Fails gracefully
|
|
12
|
-
on blocked / login-required pages.
|
|
13
|
-
* ``POST /api/browser/ingest-current-tab`` — accepts a payload from the local
|
|
14
|
-
browser extension (url/title/text/selected_text/html), sanitizes + size-limits
|
|
15
|
-
it, stores it as ``source_type=browser_tab``.
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
from __future__ import annotations
|
|
19
|
-
|
|
20
|
-
import ipaddress
|
|
21
|
-
import socket
|
|
22
|
-
from html.parser import HTMLParser
|
|
23
|
-
from typing import Any, Callable, Optional, Tuple
|
|
24
|
-
from urllib.parse import SplitResult, urljoin, urlsplit, urlunsplit
|
|
25
|
-
|
|
26
|
-
from fastapi import APIRouter, HTTPException, Request
|
|
27
|
-
from pydantic import BaseModel
|
|
28
|
-
|
|
29
|
-
from lattice_brain.ingestion import IngestionItem, capture_quality_verdict
|
|
30
|
-
from latticeai import __version__
|
|
31
|
-
from latticeai.api.workspace_scope import resolve_workspace_scope
|
|
32
|
-
from latticeai.core.messages import DEFAULT_LANGUAGE, http_error, resolve_language
|
|
33
|
-
from latticeai.core.quiet import quiet
|
|
34
|
-
|
|
35
|
-
MAX_TAB_BYTES = 4 * 1024 * 1024 # 4 MB per captured tab payload
|
|
36
|
-
MAX_URL_FETCH_BYTES = 4 * 1024 * 1024 # 4 MB cap on a fetched page
|
|
37
|
-
URL_FETCH_TIMEOUT = 12.0 # seconds
|
|
38
|
-
MAX_URL_LENGTH = 8192
|
|
39
|
-
MAX_URL_REDIRECTS = 5
|
|
40
|
-
|
|
41
|
-
_REDIRECT_STATUSES = frozenset({301, 302, 303, 307, 308})
|
|
42
|
-
_TEXTUAL_APPLICATION_TYPES = frozenset({
|
|
43
|
-
"application/json",
|
|
44
|
-
"application/ld+json",
|
|
45
|
-
"application/xhtml+xml",
|
|
46
|
-
"application/xml",
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class BrowserFetchError(Exception):
|
|
51
|
-
"""A URL could not be fetched (blocked, login-required, timeout, too big)."""
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
# ── readable-text extraction ─────────────────────────────────────────────────
|
|
55
|
-
_SKIP_TAGS = {"script", "style", "noscript", "template", "svg", "head"}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
class _TextExtractor(HTMLParser):
|
|
59
|
-
def __init__(self) -> None:
|
|
60
|
-
super().__init__(convert_charrefs=True)
|
|
61
|
-
self._skip_depth = 0
|
|
62
|
-
self._chunks: list[str] = []
|
|
63
|
-
self.title: str = ""
|
|
64
|
-
self._in_title = False
|
|
65
|
-
|
|
66
|
-
def handle_starttag(self, tag, attrs):
|
|
67
|
-
if tag in _SKIP_TAGS:
|
|
68
|
-
self._skip_depth += 1
|
|
69
|
-
if tag == "title":
|
|
70
|
-
self._in_title = True
|
|
71
|
-
|
|
72
|
-
def handle_endtag(self, tag):
|
|
73
|
-
if tag in _SKIP_TAGS and self._skip_depth > 0:
|
|
74
|
-
self._skip_depth -= 1
|
|
75
|
-
if tag == "title":
|
|
76
|
-
self._in_title = False
|
|
77
|
-
if tag in {"p", "div", "br", "li", "h1", "h2", "h3", "h4", "section", "article"}:
|
|
78
|
-
self._chunks.append("\n")
|
|
79
|
-
|
|
80
|
-
def handle_data(self, data):
|
|
81
|
-
if self._in_title:
|
|
82
|
-
self.title += data
|
|
83
|
-
if self._skip_depth == 0:
|
|
84
|
-
text = data.strip()
|
|
85
|
-
if text:
|
|
86
|
-
self._chunks.append(text)
|
|
87
|
-
|
|
88
|
-
def text(self) -> str:
|
|
89
|
-
raw = " ".join(self._chunks)
|
|
90
|
-
# collapse runs of whitespace while keeping paragraph breaks
|
|
91
|
-
lines = [ln.strip() for ln in raw.replace("\r", "").split("\n")]
|
|
92
|
-
return "\n".join([ln for ln in lines if ln]).strip()
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def extract_readable_text(html: str) -> Tuple[str, str]:
|
|
96
|
-
"""Return (title, readable_text) from an HTML string. Never raises."""
|
|
97
|
-
parser = _TextExtractor()
|
|
98
|
-
try:
|
|
99
|
-
parser.feed(html or "")
|
|
100
|
-
except Exception: # noqa: BLE001 — malformed HTML must still yield best-effort text
|
|
101
|
-
quiet()
|
|
102
|
-
return parser.title.strip(), parser.text()
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def _parse_http_url(url: str) -> tuple[str, SplitResult, str, int]:
|
|
106
|
-
"""Parse a URL into safe, unambiguous HTTP connection components."""
|
|
107
|
-
cleaned = (url or "").strip()
|
|
108
|
-
if not cleaned:
|
|
109
|
-
raise ValueError("url is required.")
|
|
110
|
-
if len(cleaned) > MAX_URL_LENGTH:
|
|
111
|
-
raise ValueError("URL is too long.")
|
|
112
|
-
if "\\" in cleaned or any(ord(char) < 32 or ord(char) == 127 for char in cleaned):
|
|
113
|
-
raise ValueError("Malformed URL.")
|
|
114
|
-
|
|
115
|
-
try:
|
|
116
|
-
parsed = urlsplit(cleaned)
|
|
117
|
-
hostname = parsed.hostname
|
|
118
|
-
port = parsed.port
|
|
119
|
-
except ValueError as exc:
|
|
120
|
-
raise ValueError("Malformed URL.") from exc
|
|
121
|
-
if parsed.scheme.lower() not in ("http", "https"):
|
|
122
|
-
raise ValueError("Only http(s) URLs are supported.")
|
|
123
|
-
if not parsed.netloc or not hostname:
|
|
124
|
-
raise ValueError("Malformed URL.")
|
|
125
|
-
if parsed.username is not None or parsed.password is not None:
|
|
126
|
-
raise ValueError("URLs containing credentials are not supported.")
|
|
127
|
-
if "%" in hostname:
|
|
128
|
-
# Scoped IPv6 literals are interface-local by definition and also have
|
|
129
|
-
# inconsistent URL parser semantics across HTTP clients.
|
|
130
|
-
raise ValueError("Scoped IP addresses are not supported.")
|
|
131
|
-
|
|
132
|
-
hostname = hostname.rstrip(".")
|
|
133
|
-
if not hostname:
|
|
134
|
-
raise ValueError("Malformed URL.")
|
|
135
|
-
try:
|
|
136
|
-
ascii_hostname = hostname.encode("idna").decode("ascii").lower()
|
|
137
|
-
except UnicodeError as exc:
|
|
138
|
-
raise ValueError("Malformed URL hostname.") from exc
|
|
139
|
-
if len(ascii_hostname) > 253:
|
|
140
|
-
raise ValueError("Malformed URL hostname.")
|
|
141
|
-
|
|
142
|
-
port = port if port is not None else (443 if parsed.scheme.lower() == "https" else 80)
|
|
143
|
-
if port < 1 or port > 65535:
|
|
144
|
-
raise ValueError("Malformed URL port.")
|
|
145
|
-
return cleaned, parsed, ascii_hostname, port
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
def _is_public_ip(value: str) -> bool:
|
|
149
|
-
"""Return whether *value* is globally routable (IPv4 or IPv6)."""
|
|
150
|
-
try:
|
|
151
|
-
address = ipaddress.ip_address(value)
|
|
152
|
-
except ValueError:
|
|
153
|
-
return False
|
|
154
|
-
# Some Python releases report multicast addresses as ``is_global``. Keep
|
|
155
|
-
# the security classes explicit instead of relying on that single flag.
|
|
156
|
-
return address.is_global and not any((
|
|
157
|
-
address.is_loopback,
|
|
158
|
-
address.is_private,
|
|
159
|
-
address.is_link_local,
|
|
160
|
-
address.is_multicast,
|
|
161
|
-
address.is_unspecified,
|
|
162
|
-
address.is_reserved,
|
|
163
|
-
getattr(address, "is_site_local", False),
|
|
164
|
-
))
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def _resolve_public_target(
|
|
168
|
-
url: str,
|
|
169
|
-
*,
|
|
170
|
-
resolver: Optional[Callable[..., Any]] = None,
|
|
171
|
-
) -> tuple[str, SplitResult, str, int, tuple[str, ...]]:
|
|
172
|
-
"""Resolve a URL and reject every non-public address before connecting."""
|
|
173
|
-
try:
|
|
174
|
-
cleaned, parsed, hostname, port = _parse_http_url(url)
|
|
175
|
-
except ValueError as exc:
|
|
176
|
-
raise BrowserFetchError(str(exc)) from exc
|
|
177
|
-
|
|
178
|
-
if hostname == "localhost" or hostname.endswith(".localhost"):
|
|
179
|
-
raise BrowserFetchError("Local and private network URLs are not allowed.")
|
|
180
|
-
|
|
181
|
-
resolve = resolver or socket.getaddrinfo
|
|
182
|
-
try:
|
|
183
|
-
records = resolve(
|
|
184
|
-
hostname,
|
|
185
|
-
port,
|
|
186
|
-
family=socket.AF_UNSPEC,
|
|
187
|
-
type=socket.SOCK_STREAM,
|
|
188
|
-
)
|
|
189
|
-
except (OSError, socket.gaierror) as exc:
|
|
190
|
-
raise BrowserFetchError(f"Could not resolve the page host: {hostname}.") from exc
|
|
191
|
-
|
|
192
|
-
addresses: list[str] = []
|
|
193
|
-
for record in records or ():
|
|
194
|
-
try:
|
|
195
|
-
address = str(record[4][0])
|
|
196
|
-
except (IndexError, TypeError):
|
|
197
|
-
raise BrowserFetchError("The page host returned an invalid DNS record.")
|
|
198
|
-
if not _is_public_ip(address):
|
|
199
|
-
raise BrowserFetchError("Local and private network URLs are not allowed.")
|
|
200
|
-
if address not in addresses:
|
|
201
|
-
addresses.append(address)
|
|
202
|
-
if not addresses:
|
|
203
|
-
raise BrowserFetchError(f"Could not resolve the page host: {hostname}.")
|
|
204
|
-
return cleaned, parsed, hostname, port, tuple(addresses)
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
def _origin_host_header(hostname: str, port: int, scheme: str) -> str:
|
|
208
|
-
try:
|
|
209
|
-
is_ipv6 = ipaddress.ip_address(hostname).version == 6
|
|
210
|
-
except ValueError:
|
|
211
|
-
is_ipv6 = False
|
|
212
|
-
host = f"[{hostname}]" if is_ipv6 else hostname
|
|
213
|
-
default_port = 443 if scheme == "https" else 80
|
|
214
|
-
return host if port == default_port else f"{host}:{port}"
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
def _pinned_request_url(parsed: SplitResult, address: str, port: int) -> str:
|
|
218
|
-
"""Build a URL that connects to a pre-validated DNS result.
|
|
219
|
-
|
|
220
|
-
The request retains the original Host header and TLS SNI separately. This
|
|
221
|
-
closes the DNS-rebinding window between validation and socket connection.
|
|
222
|
-
"""
|
|
223
|
-
ip = ipaddress.ip_address(address)
|
|
224
|
-
host = f"[{ip.compressed}]" if ip.version == 6 else ip.compressed
|
|
225
|
-
default_port = 443 if parsed.scheme.lower() == "https" else 80
|
|
226
|
-
netloc = host if port == default_port else f"{host}:{port}"
|
|
227
|
-
return urlunsplit((parsed.scheme.lower(), netloc, parsed.path or "/", parsed.query, ""))
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
def _read_limited_response(response: Any, max_bytes: int) -> bytes:
|
|
231
|
-
content_length = response.headers.get("content-length")
|
|
232
|
-
if content_length:
|
|
233
|
-
try:
|
|
234
|
-
declared_size = int(content_length)
|
|
235
|
-
except ValueError:
|
|
236
|
-
declared_size = -1
|
|
237
|
-
if declared_size > max_bytes:
|
|
238
|
-
raise BrowserFetchError("The page is too large to ingest.")
|
|
239
|
-
|
|
240
|
-
body = bytearray()
|
|
241
|
-
for chunk in response.iter_bytes():
|
|
242
|
-
if len(body) + len(chunk) > max_bytes:
|
|
243
|
-
raise BrowserFetchError("The page is too large to ingest.")
|
|
244
|
-
body.extend(chunk)
|
|
245
|
-
return bytes(body)
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
def _default_fetch_url(
|
|
249
|
-
url: str,
|
|
250
|
-
*,
|
|
251
|
-
resolver: Optional[Callable[..., Any]] = None,
|
|
252
|
-
transport: Any = None,
|
|
253
|
-
max_bytes: int = MAX_URL_FETCH_BYTES,
|
|
254
|
-
max_redirects: int = MAX_URL_REDIRECTS,
|
|
255
|
-
) -> Tuple[str, str]:
|
|
256
|
-
"""Fetch a public URL on the local runtime and extract readable text.
|
|
257
|
-
|
|
258
|
-
Raises :class:`BrowserFetchError` on any non-success (blocked, login wall,
|
|
259
|
-
timeout, oversized, non-HTML) so the route can fail gracefully.
|
|
260
|
-
"""
|
|
261
|
-
import httpx
|
|
262
|
-
|
|
263
|
-
if max_bytes < 1 or max_redirects < 0:
|
|
264
|
-
raise BrowserFetchError("Invalid URL fetch limits.")
|
|
265
|
-
|
|
266
|
-
client_options: dict[str, Any] = {
|
|
267
|
-
"follow_redirects": False,
|
|
268
|
-
"timeout": URL_FETCH_TIMEOUT,
|
|
269
|
-
"trust_env": False,
|
|
270
|
-
"limits": httpx.Limits(max_keepalive_connections=0),
|
|
271
|
-
}
|
|
272
|
-
if transport is not None:
|
|
273
|
-
client_options["transport"] = transport
|
|
274
|
-
|
|
275
|
-
current_url = url
|
|
276
|
-
try:
|
|
277
|
-
with httpx.Client(**client_options) as client:
|
|
278
|
-
for redirect_count in range(max_redirects + 1):
|
|
279
|
-
current_url, parsed, hostname, port, addresses = _resolve_public_target(
|
|
280
|
-
current_url,
|
|
281
|
-
resolver=resolver,
|
|
282
|
-
)
|
|
283
|
-
next_url: Optional[str] = None
|
|
284
|
-
connect_errors: list[Exception] = []
|
|
285
|
-
|
|
286
|
-
for address in addresses:
|
|
287
|
-
request = client.build_request(
|
|
288
|
-
"GET",
|
|
289
|
-
_pinned_request_url(parsed, address, port),
|
|
290
|
-
headers={
|
|
291
|
-
"Accept": "text/html, text/plain;q=0.9, application/xhtml+xml;q=0.8",
|
|
292
|
-
"Accept-Encoding": "identity",
|
|
293
|
-
"Connection": "close",
|
|
294
|
-
"Host": _origin_host_header(hostname, port, parsed.scheme.lower()),
|
|
295
|
-
"User-Agent": f"LatticeAI-local/{__version__} (+local-first knowledge graph)",
|
|
296
|
-
},
|
|
297
|
-
extensions={"sni_hostname": hostname},
|
|
298
|
-
)
|
|
299
|
-
response = None
|
|
300
|
-
try:
|
|
301
|
-
response = client.send(request, stream=True)
|
|
302
|
-
except (httpx.ConnectError, httpx.ConnectTimeout) as exc:
|
|
303
|
-
connect_errors.append(exc)
|
|
304
|
-
continue
|
|
305
|
-
|
|
306
|
-
try:
|
|
307
|
-
if response.status_code in _REDIRECT_STATUSES:
|
|
308
|
-
location = (response.headers.get("location") or "").strip()
|
|
309
|
-
if not location:
|
|
310
|
-
raise BrowserFetchError("The page returned a redirect without a location.")
|
|
311
|
-
if redirect_count >= max_redirects:
|
|
312
|
-
raise BrowserFetchError("The page redirected too many times.")
|
|
313
|
-
# Join against the original public URL, never the pinned
|
|
314
|
-
# IP URL exposed only to the transport.
|
|
315
|
-
next_url = urljoin(current_url, location)
|
|
316
|
-
break
|
|
317
|
-
if response.status_code in (401, 403):
|
|
318
|
-
raise BrowserFetchError(
|
|
319
|
-
"The page is login-required or blocked "
|
|
320
|
-
f"(HTTP {response.status_code})."
|
|
321
|
-
)
|
|
322
|
-
if response.status_code >= 400:
|
|
323
|
-
raise BrowserFetchError(
|
|
324
|
-
f"The page returned HTTP {response.status_code}."
|
|
325
|
-
)
|
|
326
|
-
|
|
327
|
-
content_type = response.headers.get("content-type", "")
|
|
328
|
-
media_type = content_type.split(";", 1)[0].strip().lower()
|
|
329
|
-
if not (
|
|
330
|
-
media_type.startswith("text/")
|
|
331
|
-
or media_type in _TEXTUAL_APPLICATION_TYPES
|
|
332
|
-
):
|
|
333
|
-
raise BrowserFetchError(
|
|
334
|
-
f"Unsupported content type: {content_type or 'unknown'}."
|
|
335
|
-
)
|
|
336
|
-
|
|
337
|
-
raw_body = _read_limited_response(response, max_bytes)
|
|
338
|
-
try:
|
|
339
|
-
encoding = response.encoding or "utf-8"
|
|
340
|
-
body = raw_body.decode(encoding, "replace")
|
|
341
|
-
except (LookupError, UnicodeError):
|
|
342
|
-
body = raw_body.decode("utf-8", "replace")
|
|
343
|
-
title, text = extract_readable_text(body)
|
|
344
|
-
return (title or current_url, text)
|
|
345
|
-
finally:
|
|
346
|
-
response.close()
|
|
347
|
-
|
|
348
|
-
if next_url is not None:
|
|
349
|
-
current_url = next_url
|
|
350
|
-
continue
|
|
351
|
-
if connect_errors:
|
|
352
|
-
raise BrowserFetchError(
|
|
353
|
-
f"Could not reach the page: {connect_errors[-1]}"
|
|
354
|
-
) from connect_errors[-1]
|
|
355
|
-
raise BrowserFetchError("Could not reach the page.")
|
|
356
|
-
except BrowserFetchError:
|
|
357
|
-
raise
|
|
358
|
-
except httpx.HTTPError as exc:
|
|
359
|
-
raise BrowserFetchError(f"Could not reach the page: {exc}") from exc
|
|
360
|
-
|
|
361
|
-
raise BrowserFetchError("The page redirected too many times.")
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
def _validate_http_url(url: str) -> str:
|
|
365
|
-
try:
|
|
366
|
-
cleaned, _parsed, _hostname, _port = _parse_http_url(url)
|
|
367
|
-
except ValueError as exc:
|
|
368
|
-
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
369
|
-
return cleaned
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
# ── request models ───────────────────────────────────────────────────────────
|
|
373
|
-
class ReadUrlRequest(BaseModel):
|
|
374
|
-
url: str
|
|
375
|
-
workspace_id: Optional[str] = None
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
class IngestTabRequest(BaseModel):
|
|
379
|
-
url: str
|
|
380
|
-
title: Optional[str] = None
|
|
381
|
-
text: Optional[str] = None
|
|
382
|
-
selected_text: Optional[str] = None
|
|
383
|
-
html: Optional[str] = None
|
|
384
|
-
captured_at: Optional[str] = None
|
|
385
|
-
workspace_id: Optional[str] = None
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
def create_browser_router(
|
|
389
|
-
*,
|
|
390
|
-
pipeline: Any,
|
|
391
|
-
require_user: Callable[[Request], str],
|
|
392
|
-
workspace_service: Any = None,
|
|
393
|
-
fetch_url: Optional[Callable[[str], Tuple[str, str]]] = None,
|
|
394
|
-
max_tab_bytes: int = MAX_TAB_BYTES,
|
|
395
|
-
) -> APIRouter:
|
|
396
|
-
router = APIRouter()
|
|
397
|
-
_fetch = fetch_url or _default_fetch_url
|
|
398
|
-
|
|
399
|
-
def _require_pipeline(language: str = DEFAULT_LANGUAGE):
|
|
400
|
-
if pipeline is None or not pipeline.available():
|
|
401
|
-
raise http_error(503, "capture.ingestion_disabled", language)
|
|
402
|
-
|
|
403
|
-
def _write_workspace(request: Request, body_workspace: Optional[str], user: str) -> Optional[str]:
|
|
404
|
-
return resolve_workspace_scope(
|
|
405
|
-
request,
|
|
406
|
-
user=user,
|
|
407
|
-
workspace_service=workspace_service,
|
|
408
|
-
write=True,
|
|
409
|
-
body_workspace=body_workspace,
|
|
410
|
-
)
|
|
411
|
-
|
|
412
|
-
@router.post("/api/browser/read-url")
|
|
413
|
-
async def read_url(req: ReadUrlRequest, request: Request):
|
|
414
|
-
"""Fetch a public URL locally and ingest it as a web_url source."""
|
|
415
|
-
user = require_user(request)
|
|
416
|
-
_require_pipeline(resolve_language(request))
|
|
417
|
-
workspace_id = _write_workspace(request, req.workspace_id, user)
|
|
418
|
-
url = _validate_http_url(req.url)
|
|
419
|
-
try:
|
|
420
|
-
title, text = _fetch(url)
|
|
421
|
-
except BrowserFetchError as exc:
|
|
422
|
-
# Graceful failure — not a 5xx; the page was simply unreadable.
|
|
423
|
-
raise HTTPException(status_code=422, detail=str(exc))
|
|
424
|
-
if not (text or "").strip():
|
|
425
|
-
return {"status": "empty", "source_type": "web_url", "url": url,
|
|
426
|
-
"detail": "No readable text was extracted from the page.",
|
|
427
|
-
# Structured CTA (backlog #9): the extraction produced
|
|
428
|
-
# nothing, tell the UI what the user can do about it.
|
|
429
|
-
"capture_quality": capture_quality_verdict(None, source_type="web_url")}
|
|
430
|
-
res = pipeline.ingest(
|
|
431
|
-
IngestionItem(
|
|
432
|
-
source_type="web_url", title=title, text=text, source_uri=url,
|
|
433
|
-
owner=user, workspace_id=workspace_id,
|
|
434
|
-
),
|
|
435
|
-
user_email=user,
|
|
436
|
-
)
|
|
437
|
-
payload = res.as_dict()
|
|
438
|
-
payload["capture_quality"] = capture_quality_verdict(
|
|
439
|
-
res.extraction_quality, source_type="web_url",
|
|
440
|
-
)
|
|
441
|
-
return payload
|
|
442
|
-
|
|
443
|
-
@router.post("/api/browser/ingest-current-tab")
|
|
444
|
-
async def ingest_current_tab(req: IngestTabRequest, request: Request):
|
|
445
|
-
"""Ingest a payload captured from the local browser extension."""
|
|
446
|
-
user = require_user(request)
|
|
447
|
-
_require_pipeline(resolve_language(request))
|
|
448
|
-
workspace_id = _write_workspace(request, req.workspace_id, user)
|
|
449
|
-
url = _validate_http_url(req.url)
|
|
450
|
-
# Bound the entire capture, not merely each field independently. The
|
|
451
|
-
# extension commonly supplies full text, selection, and HTML together.
|
|
452
|
-
captured_bytes = sum(
|
|
453
|
-
len(value.encode("utf-8", "ignore"))
|
|
454
|
-
for value in (
|
|
455
|
-
req.url,
|
|
456
|
-
req.title,
|
|
457
|
-
req.text,
|
|
458
|
-
req.selected_text,
|
|
459
|
-
req.html,
|
|
460
|
-
req.captured_at,
|
|
461
|
-
workspace_id,
|
|
462
|
-
)
|
|
463
|
-
if value
|
|
464
|
-
)
|
|
465
|
-
if captured_bytes > max_tab_bytes:
|
|
466
|
-
raise http_error(413, "capture.payload_too_large", resolve_language(request))
|
|
467
|
-
text = (req.text or "").strip()
|
|
468
|
-
if not text and req.html:
|
|
469
|
-
_title, text = extract_readable_text(req.html)
|
|
470
|
-
if not text:
|
|
471
|
-
text = (req.selected_text or "").strip()
|
|
472
|
-
if not text:
|
|
473
|
-
raise http_error(400, "capture.nothing_to_capture", resolve_language(request))
|
|
474
|
-
res = pipeline.ingest(
|
|
475
|
-
IngestionItem(
|
|
476
|
-
source_type="browser_tab",
|
|
477
|
-
title=req.title or url,
|
|
478
|
-
text=text,
|
|
479
|
-
source_uri=url,
|
|
480
|
-
captured_at=req.captured_at,
|
|
481
|
-
owner=user,
|
|
482
|
-
workspace_id=workspace_id,
|
|
483
|
-
metadata={"has_selection": bool(req.selected_text)},
|
|
484
|
-
),
|
|
485
|
-
user_email=user,
|
|
486
|
-
)
|
|
487
|
-
payload = res.as_dict()
|
|
488
|
-
payload["capture_quality"] = capture_quality_verdict(
|
|
489
|
-
res.extraction_quality, source_type="browser_tab",
|
|
490
|
-
)
|
|
491
|
-
return payload
|
|
492
|
-
|
|
493
|
-
return router
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
"""Change proposal API router (v9.6.0).
|
|
2
|
-
|
|
3
|
-
Read/approve/reject surface over
|
|
4
|
-
:class:`~latticeai.services.change_proposals.ChangeProposalService`. The
|
|
5
|
-
items live in the shared review queue (source ``change_proposal``), so the
|
|
6
|
-
Act review center shows them too; this router adds the proposal-specific
|
|
7
|
-
semantics: **approve applies the staged content exactly as reviewed**,
|
|
8
|
-
reject discards it, and nothing touches disk while a proposal is pending.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
from __future__ import annotations
|
|
12
|
-
|
|
13
|
-
from typing import Callable, Optional
|
|
14
|
-
|
|
15
|
-
from fastapi import APIRouter, HTTPException, Request
|
|
16
|
-
from pydantic import BaseModel
|
|
17
|
-
|
|
18
|
-
from latticeai.services.change_proposals import ChangeProposalService
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class RejectProposalRequest(BaseModel):
|
|
22
|
-
"""Optional rejection context — kept in the item's provenance."""
|
|
23
|
-
|
|
24
|
-
reason: str = ""
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def create_change_proposals_router(
|
|
28
|
-
*,
|
|
29
|
-
service: ChangeProposalService,
|
|
30
|
-
require_user: Callable[[Request], str],
|
|
31
|
-
gate_read: Callable[[Request], Optional[str]],
|
|
32
|
-
gate_write: Callable[[Request], Optional[str]],
|
|
33
|
-
) -> APIRouter:
|
|
34
|
-
router = APIRouter()
|
|
35
|
-
|
|
36
|
-
@router.get("/api/proposals")
|
|
37
|
-
async def list_proposals(request: Request):
|
|
38
|
-
user = require_user(request)
|
|
39
|
-
scope = gate_read(request)
|
|
40
|
-
return service.pending(user_email=user, workspace_id=scope)
|
|
41
|
-
|
|
42
|
-
# NOTE: declared before /api/proposals/{item_id} so "counts" never
|
|
43
|
-
# resolves as an item id.
|
|
44
|
-
@router.get("/api/proposals/counts")
|
|
45
|
-
async def proposal_counts(request: Request):
|
|
46
|
-
user = require_user(request)
|
|
47
|
-
scope = gate_read(request)
|
|
48
|
-
return service.counts(user_email=user, workspace_id=scope)
|
|
49
|
-
|
|
50
|
-
@router.get("/api/proposals/{item_id}")
|
|
51
|
-
async def get_proposal(item_id: str, request: Request):
|
|
52
|
-
require_user(request)
|
|
53
|
-
scope = gate_read(request)
|
|
54
|
-
try:
|
|
55
|
-
return service.get_proposal(item_id, workspace_id=scope)
|
|
56
|
-
except (KeyError, FileNotFoundError) as exc:
|
|
57
|
-
raise HTTPException(status_code=404, detail=str(exc))
|
|
58
|
-
|
|
59
|
-
@router.post("/api/proposals/{item_id}/approve")
|
|
60
|
-
async def approve_proposal(item_id: str, request: Request):
|
|
61
|
-
user = require_user(request)
|
|
62
|
-
scope = gate_write(request)
|
|
63
|
-
try:
|
|
64
|
-
return service.approve_and_apply(
|
|
65
|
-
item_id, user_email=user, workspace_id=scope
|
|
66
|
-
)
|
|
67
|
-
except (KeyError, FileNotFoundError) as exc:
|
|
68
|
-
raise HTTPException(status_code=404, detail=str(exc))
|
|
69
|
-
except ValueError as exc:
|
|
70
|
-
raise HTTPException(status_code=400, detail=str(exc))
|
|
71
|
-
|
|
72
|
-
@router.post("/api/proposals/{item_id}/reject")
|
|
73
|
-
async def reject_proposal(
|
|
74
|
-
item_id: str, request: Request, req: Optional[RejectProposalRequest] = None
|
|
75
|
-
):
|
|
76
|
-
user = require_user(request)
|
|
77
|
-
scope = gate_write(request)
|
|
78
|
-
try:
|
|
79
|
-
return service.reject(
|
|
80
|
-
item_id, user_email=user, workspace_id=scope,
|
|
81
|
-
reason=(req.reason if req else ""),
|
|
82
|
-
)
|
|
83
|
-
except (KeyError, FileNotFoundError) as exc:
|
|
84
|
-
raise HTTPException(status_code=404, detail=str(exc))
|
|
85
|
-
|
|
86
|
-
return router
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
__all__ = ["create_change_proposals_router", "RejectProposalRequest"]
|