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,110 +0,0 @@
|
|
|
1
|
-
"""Reads over the real backing stores — the only place the tiers touch them.
|
|
2
|
-
|
|
3
|
-
Six tiers, six backends, one rule: never invent. A workspace/snapshot/
|
|
4
|
-
conversation read that fails raises :class:`MemoryServiceError` so the caller
|
|
5
|
-
learns the tier is unreadable instead of receiving an empty list; a graph or
|
|
6
|
-
vector read that fails degrades to ``None`` because those tiers are optional
|
|
7
|
-
and report themselves as ``unavailable`` upstream.
|
|
8
|
-
|
|
9
|
-
The conversation tier has two backings: the durable SQLite conversation store
|
|
10
|
-
when one is wired (v4), and the legacy JSON history file otherwise.
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
from __future__ import annotations
|
|
14
|
-
|
|
15
|
-
import json
|
|
16
|
-
from typing import Any, Dict, List, Optional
|
|
17
|
-
|
|
18
|
-
from ._contract import MemoryCore as _Core
|
|
19
|
-
from .constants import LOGGER, MemoryServiceError
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class MemoryStoreReadsMixin(_Core):
|
|
23
|
-
"""The store reads. Mixed into ``MemoryService``."""
|
|
24
|
-
|
|
25
|
-
# ── helpers over the underlying stores ────────────────────────────────
|
|
26
|
-
def _workspace_memories(self, *, user_email: Optional[str], workspace_id: Optional[str]) -> List[Dict[str, Any]]:
|
|
27
|
-
try:
|
|
28
|
-
return list(self._store.list_memories(user_email=user_email, workspace_id=workspace_id).get("memories", []))
|
|
29
|
-
except Exception as exc:
|
|
30
|
-
LOGGER.exception("workspace memory read failed")
|
|
31
|
-
raise MemoryServiceError("workspace memory backend unavailable") from exc
|
|
32
|
-
|
|
33
|
-
def _all_memories(self) -> List[Dict[str, Any]]:
|
|
34
|
-
try:
|
|
35
|
-
return list(self._store.list_memories().get("memories", []))
|
|
36
|
-
except Exception as exc:
|
|
37
|
-
LOGGER.exception("global memory read failed")
|
|
38
|
-
raise MemoryServiceError("memory backend unavailable") from exc
|
|
39
|
-
|
|
40
|
-
def _snapshots(self, *, workspace_id: Optional[str]) -> List[Dict[str, Any]]:
|
|
41
|
-
try:
|
|
42
|
-
return list(self._store.list_memory_snapshots(workspace_id=workspace_id, limit=200).get("snapshots", []))
|
|
43
|
-
except Exception as exc:
|
|
44
|
-
LOGGER.exception("memory snapshot read failed")
|
|
45
|
-
raise MemoryServiceError("memory snapshot backend unavailable") from exc
|
|
46
|
-
|
|
47
|
-
def _conversations(self) -> List[Dict[str, Any]]:
|
|
48
|
-
if self._conversation_store is not None:
|
|
49
|
-
try:
|
|
50
|
-
grouped: Dict[str, List[Dict[str, Any]]] = {}
|
|
51
|
-
for item in self._conversation_store.history():
|
|
52
|
-
grouped.setdefault(item.get("conversation_id") or "legacy-previous-history", []).append(item)
|
|
53
|
-
return [{"id": conv_id, "messages": msgs} for conv_id, msgs in grouped.items()]
|
|
54
|
-
except Exception as exc:
|
|
55
|
-
LOGGER.exception("conversation store read failed")
|
|
56
|
-
raise MemoryServiceError("conversation backend unavailable") from exc
|
|
57
|
-
if not self._history_file.exists():
|
|
58
|
-
return []
|
|
59
|
-
try:
|
|
60
|
-
with open(self._history_file, "r", encoding="utf-8") as fh:
|
|
61
|
-
data = json.load(fh)
|
|
62
|
-
except (OSError, json.JSONDecodeError, TypeError, ValueError) as exc:
|
|
63
|
-
LOGGER.exception("legacy conversation history read failed")
|
|
64
|
-
raise MemoryServiceError("conversation history is unreadable") from exc
|
|
65
|
-
if isinstance(data, dict):
|
|
66
|
-
convs = data.get("conversations")
|
|
67
|
-
if isinstance(convs, list):
|
|
68
|
-
return convs
|
|
69
|
-
return [{"id": k, **(v if isinstance(v, dict) else {"messages": v})} for k, v in data.items()]
|
|
70
|
-
if isinstance(data, list):
|
|
71
|
-
return data
|
|
72
|
-
return []
|
|
73
|
-
|
|
74
|
-
def _scoped_conversations(self, *, user_email: Optional[str], workspace_id: Optional[str]) -> List[Dict[str, Any]]:
|
|
75
|
-
if not user_email:
|
|
76
|
-
return self._conversations()
|
|
77
|
-
target_workspace = workspace_id or "personal"
|
|
78
|
-
scoped: List[Dict[str, Any]] = []
|
|
79
|
-
for conversation in self._conversations():
|
|
80
|
-
messages = conversation.get("messages") or []
|
|
81
|
-
if not isinstance(messages, list):
|
|
82
|
-
continue
|
|
83
|
-
kept = [
|
|
84
|
-
message
|
|
85
|
-
for message in messages
|
|
86
|
-
if isinstance(message, dict)
|
|
87
|
-
and message.get("user_email") == user_email
|
|
88
|
-
and (message.get("workspace_id") or "personal") == target_workspace
|
|
89
|
-
]
|
|
90
|
-
if kept:
|
|
91
|
-
scoped.append({**conversation, "messages": kept})
|
|
92
|
-
return scoped
|
|
93
|
-
|
|
94
|
-
def _kg_stats(self) -> Optional[Dict[str, Any]]:
|
|
95
|
-
if not self._enable_graph:
|
|
96
|
-
return None
|
|
97
|
-
try:
|
|
98
|
-
return self._kg.stats()
|
|
99
|
-
except Exception:
|
|
100
|
-
LOGGER.exception("knowledge graph stats read failed")
|
|
101
|
-
return None
|
|
102
|
-
|
|
103
|
-
def _kg_index(self) -> Optional[Dict[str, Any]]:
|
|
104
|
-
if not self._enable_graph:
|
|
105
|
-
return None
|
|
106
|
-
try:
|
|
107
|
-
return self._kg.index_status()
|
|
108
|
-
except Exception:
|
|
109
|
-
LOGGER.exception("knowledge graph index status read failed")
|
|
110
|
-
return None
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
"""The shared storage half of the scoped preference dials.
|
|
2
|
-
|
|
3
|
-
``PermissionModeService``, ``NetworkBoundaryService`` and ``HybridPolicyService``
|
|
4
|
-
are three different policies over one identical store: a JSON file under the
|
|
5
|
-
data dir holding ``{"default": …, "users": {…}, "workspaces": {…}}``, guarded by
|
|
6
|
-
a lock, rebindable after lazy construction, and read defensively enough that a
|
|
7
|
-
truncated or hand-edited file degrades to defaults instead of taking the app
|
|
8
|
-
down.
|
|
9
|
-
|
|
10
|
-
That half was written three times, character for character. Three copies of a
|
|
11
|
-
"corrupt file falls back to defaults" rule is three chances for one of them to
|
|
12
|
-
grow an exception path the others do not have — and the symptom would be one
|
|
13
|
-
dial silently forgetting a user's choice while the other two remember. The
|
|
14
|
-
storage lives here once; each service supplies only what actually differs:
|
|
15
|
-
the file name, what an unset ``default`` means, and how a scope resolves.
|
|
16
|
-
|
|
17
|
-
Deliberately *not* shared: ``set_mode``/``set_policy``. They look similar but
|
|
18
|
-
their write shapes differ (a scalar mode replaces, a policy patch merges) and
|
|
19
|
-
their audit events carry different payloads, so folding them together would
|
|
20
|
-
mean a parameterised method with more branches than the two bodies it replaced.
|
|
21
|
-
"""
|
|
22
|
-
|
|
23
|
-
from __future__ import annotations
|
|
24
|
-
|
|
25
|
-
import json
|
|
26
|
-
import threading
|
|
27
|
-
from pathlib import Path
|
|
28
|
-
from typing import Any, Callable, ClassVar, Dict, Generic, Optional, TypeVar
|
|
29
|
-
|
|
30
|
-
from latticeai.core.io_utils import atomic_write_json
|
|
31
|
-
|
|
32
|
-
#: What ``resolve`` hands back — an enum member for the mode dials, a plain
|
|
33
|
-
#: dict for the policy service.
|
|
34
|
-
ResolvedT = TypeVar("ResolvedT")
|
|
35
|
-
|
|
36
|
-
__all__ = ["JsonBackedModeService"]
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
class JsonBackedModeService(Generic[ResolvedT]):
|
|
40
|
-
"""A lock-guarded, scope-aware JSON preference file.
|
|
41
|
-
|
|
42
|
-
Subclasses set :attr:`FILENAME` and implement :meth:`_default_entry` and
|
|
43
|
-
:meth:`_resolve_from`.
|
|
44
|
-
"""
|
|
45
|
-
|
|
46
|
-
#: File name under the data dir. Subclasses must set this.
|
|
47
|
-
FILENAME: ClassVar[str] = ""
|
|
48
|
-
|
|
49
|
-
def __init__(
|
|
50
|
-
self,
|
|
51
|
-
*,
|
|
52
|
-
data_dir: Path,
|
|
53
|
-
audit: Optional[Callable[..., None]] = None,
|
|
54
|
-
) -> None:
|
|
55
|
-
self._path = Path(data_dir) / self.FILENAME
|
|
56
|
-
self._audit = audit or (lambda *a, **kw: None)
|
|
57
|
-
self._lock = threading.Lock()
|
|
58
|
-
|
|
59
|
-
# ── rebinding ────────────────────────────────────────────────────────────
|
|
60
|
-
def rebind_data_dir(self, data_dir: Path) -> None:
|
|
61
|
-
"""Point the store at the app's real data dir.
|
|
62
|
-
|
|
63
|
-
The wiring may instantiate a service lazily before routers know the
|
|
64
|
-
configured data dir; rebinding keeps one file of record instead of
|
|
65
|
-
stranding writes under the fallback path.
|
|
66
|
-
"""
|
|
67
|
-
with self._lock:
|
|
68
|
-
self._path = Path(data_dir) / self.FILENAME
|
|
69
|
-
|
|
70
|
-
def rebind_audit(self, audit: Callable[..., None]) -> None:
|
|
71
|
-
"""Attach the real audit sink once app wiring provides one."""
|
|
72
|
-
with self._lock:
|
|
73
|
-
self._audit = audit
|
|
74
|
-
|
|
75
|
-
# ── what each subclass must decide ───────────────────────────────────────
|
|
76
|
-
def _default_entry(self) -> Any:
|
|
77
|
-
"""The ``default`` bucket's value when the file does not say.
|
|
78
|
-
|
|
79
|
-
Called fresh at every use because the policy service's answer is a
|
|
80
|
-
mutable dict; a shared instance would let one caller's edit leak into
|
|
81
|
-
the next caller's defaults.
|
|
82
|
-
"""
|
|
83
|
-
raise NotImplementedError
|
|
84
|
-
|
|
85
|
-
def _resolve_from(
|
|
86
|
-
self,
|
|
87
|
-
data: Dict[str, Any],
|
|
88
|
-
*,
|
|
89
|
-
user_email: Optional[str],
|
|
90
|
-
workspace_id: Optional[str],
|
|
91
|
-
) -> ResolvedT:
|
|
92
|
-
"""Apply this dial's scope precedence to already-loaded ``data``.
|
|
93
|
-
|
|
94
|
-
Pure over ``data`` and lock-free, so holders of ``_lock`` can reuse it
|
|
95
|
-
without re-entering a non-reentrant lock.
|
|
96
|
-
"""
|
|
97
|
-
raise NotImplementedError
|
|
98
|
-
|
|
99
|
-
# ── storage ──────────────────────────────────────────────────────────────
|
|
100
|
-
def _empty(self) -> Dict[str, Any]:
|
|
101
|
-
return {"default": self._default_entry(), "users": {}, "workspaces": {}}
|
|
102
|
-
|
|
103
|
-
def _read(self) -> Dict[str, Any]:
|
|
104
|
-
if not self._path.exists():
|
|
105
|
-
return self._empty()
|
|
106
|
-
try:
|
|
107
|
-
data = json.loads(self._path.read_text(encoding="utf-8"))
|
|
108
|
-
except Exception:
|
|
109
|
-
return self._empty()
|
|
110
|
-
if not isinstance(data, dict):
|
|
111
|
-
return self._empty()
|
|
112
|
-
data.setdefault("default", self._default_entry())
|
|
113
|
-
data.setdefault("users", {})
|
|
114
|
-
data.setdefault("workspaces", {})
|
|
115
|
-
return data
|
|
116
|
-
|
|
117
|
-
def _write(self, data: Dict[str, Any]) -> None:
|
|
118
|
-
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
119
|
-
atomic_write_json(self._path, data)
|
|
120
|
-
|
|
121
|
-
# ── reading ──────────────────────────────────────────────────────────────
|
|
122
|
-
def resolve(
|
|
123
|
-
self,
|
|
124
|
-
*,
|
|
125
|
-
user_email: Optional[str] = None,
|
|
126
|
-
workspace_id: Optional[str] = None,
|
|
127
|
-
) -> ResolvedT:
|
|
128
|
-
with self._lock:
|
|
129
|
-
data = self._read()
|
|
130
|
-
return self._resolve_from(
|
|
131
|
-
data, user_email=user_email, workspace_id=workspace_id,
|
|
132
|
-
)
|
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
"""Hardware-aware local model recommendation.
|
|
2
|
-
|
|
3
|
-
Given a detected system profile (from :func:`auto_setup.probe`) this module
|
|
4
|
-
classifies every model in :data:`model_catalog.ENGINE_MODEL_CATALOG` into one of
|
|
5
|
-
three states — **recommended**, **compatible**, or **not_recommended** — and
|
|
6
|
-
groups the result by current model family (Gemma 4, Qwen3.6, Qwen3.5, GPT-OSS,
|
|
7
|
-
LFM2.5).
|
|
8
|
-
|
|
9
|
-
It is intentionally pure and dependency-light: the only input is a plain dict
|
|
10
|
-
describing the machine, so it is fully unit-testable without touching real
|
|
11
|
-
hardware, and it does not import the FastAPI app or the runtime. The setup /
|
|
12
|
-
onboarding routers build the profile via ``auto_setup.probe().to_json()`` and
|
|
13
|
-
hand it here.
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
from __future__ import annotations
|
|
17
|
-
|
|
18
|
-
import re
|
|
19
|
-
from typing import Any, Dict, List, Optional
|
|
20
|
-
|
|
21
|
-
from latticeai.core.model_compat import model_runtime_compatibility
|
|
22
|
-
from latticeai.services.model_catalog import ENGINE_MODEL_CATALOG
|
|
23
|
-
|
|
24
|
-
# ── status vocabulary ─────────────────────────────────────────────────────────
|
|
25
|
-
RECOMMENDED = "recommended"
|
|
26
|
-
COMPATIBLE = "compatible"
|
|
27
|
-
NOT_RECOMMENDED = "not_recommended"
|
|
28
|
-
|
|
29
|
-
# Engines whose models load on any OS (given the engine binary) vs. MLX which is
|
|
30
|
-
# Apple-Silicon only. Used to decide platform availability before sizing.
|
|
31
|
-
_APPLE_ONLY_ENGINES = {"local_mlx"}
|
|
32
|
-
|
|
33
|
-
# Family display order for the grouped view (current generations, largest
|
|
34
|
-
# lineup first). Superseded families are not listed because the capability
|
|
35
|
-
# registry never lets them reach the catalog — they are recognised for loading
|
|
36
|
-
# only. A family missing from this list still renders, it just sorts last.
|
|
37
|
-
_FAMILY_ORDER = [
|
|
38
|
-
"Gemma 4",
|
|
39
|
-
"Qwen3.6",
|
|
40
|
-
"Qwen3.5",
|
|
41
|
-
"GPT-OSS",
|
|
42
|
-
"LFM2.5",
|
|
43
|
-
]
|
|
44
|
-
|
|
45
|
-
# Modalities the recommender will surface. Text-only models earn their place —
|
|
46
|
-
# LFM2.5 is the only thing that runs comfortably on 8GB, and GPT-OSS 20B is the
|
|
47
|
-
# most-downloaded entry in the catalog — so filtering to `multimodal` would have
|
|
48
|
-
# hidden two of the tiers. Each row still reports its own `modality`, so a UI
|
|
49
|
-
# that wants to badge "reads pictures" can, honestly.
|
|
50
|
-
_RECOMMENDABLE_MODALITIES = {"multimodal", "text"}
|
|
51
|
-
|
|
52
|
-
_SIZE_RE = re.compile(r"([\d.]+)\s*(TB|GB|MB)", re.IGNORECASE)
|
|
53
|
-
_UNIT_GB = {"TB": 1024.0, "GB": 1.0, "MB": 1.0 / 1024.0}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def parse_size_gb(size: Any) -> Optional[float]:
|
|
57
|
-
"""Parse a catalog ``size`` string (``"4.7GB"``, ``"963MB"``, ``"40GB+"``).
|
|
58
|
-
|
|
59
|
-
Returns ``None`` when the size is non-numeric (e.g. ``"pull required"`` or
|
|
60
|
-
``"실행 도구에서 관리"``) so callers can treat it as "size unknown".
|
|
61
|
-
"""
|
|
62
|
-
if not isinstance(size, str):
|
|
63
|
-
return None
|
|
64
|
-
match = _SIZE_RE.search(size)
|
|
65
|
-
if not match:
|
|
66
|
-
return None
|
|
67
|
-
value = float(match.group(1))
|
|
68
|
-
return round(value * _UNIT_GB[match.group(2).upper()], 3)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def estimated_ram_gb(size_gb: float) -> float:
|
|
72
|
-
"""Rough RAM needed to run a model: weights + KV cache + OS working set."""
|
|
73
|
-
return round(size_gb * 1.25 + 2.5, 2)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def is_apple_silicon(profile: Dict[str, Any]) -> bool:
|
|
77
|
-
os_name = str(profile.get("os") or "").lower()
|
|
78
|
-
arch = str(profile.get("arch") or "").lower()
|
|
79
|
-
gpu = profile.get("gpu") or {}
|
|
80
|
-
vendor = str(gpu.get("vendor") or "").lower()
|
|
81
|
-
return os_name == "darwin" and (vendor == "apple" or arch in {"arm64", "aarch64"})
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def _ram_gb(profile: Dict[str, Any]) -> float:
|
|
85
|
-
try:
|
|
86
|
-
return max(0.0, float(profile.get("ram_mb") or 0) / 1024.0)
|
|
87
|
-
except (TypeError, ValueError):
|
|
88
|
-
return 0.0
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def _engine_available(engine: str, profile: Dict[str, Any]) -> bool:
|
|
92
|
-
if engine in _APPLE_ONLY_ENGINES:
|
|
93
|
-
return is_apple_silicon(profile)
|
|
94
|
-
# ollama / llamacpp / lmstudio / vllm run cross-platform once installed.
|
|
95
|
-
return True
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def _classify_one(
|
|
99
|
-
model: Dict[str, Any],
|
|
100
|
-
*,
|
|
101
|
-
engine: str,
|
|
102
|
-
engine_available: bool,
|
|
103
|
-
ram_gb: float,
|
|
104
|
-
) -> Dict[str, Any]:
|
|
105
|
-
size_gb = parse_size_gb(model.get("size"))
|
|
106
|
-
need_gb = estimated_ram_gb(size_gb) if size_gb is not None else None
|
|
107
|
-
runtime = model_runtime_compatibility(str(model.get("id") or ""), engine=engine)
|
|
108
|
-
|
|
109
|
-
if not engine_available:
|
|
110
|
-
status, reason = NOT_RECOMMENDED, "Apple Silicon과 MLX-VLM이 필요합니다"
|
|
111
|
-
elif runtime.get("supported") is False:
|
|
112
|
-
status = NOT_RECOMMENDED
|
|
113
|
-
reason = str(runtime.get("user_message") or "이 모델은 현재 설치된 실행 런타임에서 지원되지 않습니다")
|
|
114
|
-
elif need_gb is None:
|
|
115
|
-
# Tool-managed/pull models have no fixed on-disk size, so treat them as
|
|
116
|
-
# compatible and let the execution tool validate the exact model.
|
|
117
|
-
status, reason = COMPATIBLE, "선택한 실행 방식에서 필요할 때 모델을 받습니다"
|
|
118
|
-
elif ram_gb <= 0:
|
|
119
|
-
status, reason = COMPATIBLE, "메모리 정보를 확인하지 못했습니다. 불러오기 전에 검증합니다"
|
|
120
|
-
elif need_gb <= ram_gb * 0.75:
|
|
121
|
-
status, reason = RECOMMENDED, f"현재 메모리에서 안정적으로 사용할 가능성이 높습니다 (~{need_gb:.0f} GB / {ram_gb:.0f} GB)"
|
|
122
|
-
elif need_gb <= ram_gb * 0.9:
|
|
123
|
-
status, reason = COMPATIBLE, f"사용 가능하지만 여유가 적습니다 (~{need_gb:.0f} GB / {ram_gb:.0f} GB)"
|
|
124
|
-
else:
|
|
125
|
-
status, reason = NOT_RECOMMENDED, f"권장 메모리가 부족합니다 (~{need_gb:.0f} GB 필요, 현재 {ram_gb:.0f} GB)"
|
|
126
|
-
|
|
127
|
-
rich = {
|
|
128
|
-
"id": model.get("id"),
|
|
129
|
-
"name": model.get("name"),
|
|
130
|
-
"model_name": model.get("model_name") or model.get("name"),
|
|
131
|
-
"family": model.get("family"),
|
|
132
|
-
"tag": model.get("tag"),
|
|
133
|
-
"modality": model.get("modality") or "multimodal",
|
|
134
|
-
"size": model.get("size"),
|
|
135
|
-
"size_gb": size_gb,
|
|
136
|
-
"required_ram_gb": need_gb,
|
|
137
|
-
"status": status,
|
|
138
|
-
"reason": reason,
|
|
139
|
-
"source_country": model.get("source_country"),
|
|
140
|
-
"source_company": model.get("source_company"),
|
|
141
|
-
"execution_method": model.get("execution_method"),
|
|
142
|
-
"run_location": model.get("run_location"),
|
|
143
|
-
"internet_requirement": model.get("internet_requirement"),
|
|
144
|
-
"source_display_order": model.get("source_display_order"),
|
|
145
|
-
"runtime_compatibility": runtime,
|
|
146
|
-
# 5.2+ user-focused transparency
|
|
147
|
-
"hf_repo_id": model.get("hf_repo_id"),
|
|
148
|
-
"quantization": model.get("quantization"),
|
|
149
|
-
"download_strategy": model.get("download_strategy"),
|
|
150
|
-
"load_strategy": model.get("load_strategy"),
|
|
151
|
-
"hardware": model.get("hardware"),
|
|
152
|
-
"license": model.get("license"),
|
|
153
|
-
"safety_notes": model.get("safety_notes"),
|
|
154
|
-
"verification": model.get("verification"),
|
|
155
|
-
"recommended_default": model.get("recommended_default", False),
|
|
156
|
-
}
|
|
157
|
-
return rich
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def _family_rank(family: str) -> int:
|
|
161
|
-
try:
|
|
162
|
-
return _FAMILY_ORDER.index(family)
|
|
163
|
-
except ValueError:
|
|
164
|
-
return len(_FAMILY_ORDER)
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def recommend_catalog(profile: Dict[str, Any], *, engine: str = "local_mlx") -> Dict[str, Any]:
|
|
168
|
-
"""Classify ``engine``'s catalog for the given machine ``profile``.
|
|
169
|
-
|
|
170
|
-
``profile`` is a dict shaped like ``auto_setup.SystemProfile.to_json()``
|
|
171
|
-
(``os``, ``arch``, ``ram_mb``, ``gpu={vendor,vram_mb}`` …).
|
|
172
|
-
"""
|
|
173
|
-
models = [
|
|
174
|
-
model for model in ENGINE_MODEL_CATALOG.get(engine, [])
|
|
175
|
-
if str(model.get("modality") or "").lower() in _RECOMMENDABLE_MODALITIES
|
|
176
|
-
]
|
|
177
|
-
engine_available = _engine_available(engine, profile)
|
|
178
|
-
ram_gb = _ram_gb(profile)
|
|
179
|
-
|
|
180
|
-
classified = [
|
|
181
|
-
_classify_one(m, engine=engine, engine_available=engine_available, ram_gb=ram_gb)
|
|
182
|
-
for m in models
|
|
183
|
-
]
|
|
184
|
-
|
|
185
|
-
counts = {RECOMMENDED: 0, COMPATIBLE: 0, NOT_RECOMMENDED: 0}
|
|
186
|
-
for item in classified:
|
|
187
|
-
counts[item["status"]] += 1
|
|
188
|
-
|
|
189
|
-
# Group by family, ordered, with the best pick per family surfaced.
|
|
190
|
-
by_family: Dict[str, Dict[str, Any]] = {}
|
|
191
|
-
for item in classified:
|
|
192
|
-
fam = item["family"] or "Other"
|
|
193
|
-
bucket = by_family.setdefault(fam, {"family": fam, "models": [], "best": None})
|
|
194
|
-
bucket["models"].append(item)
|
|
195
|
-
|
|
196
|
-
def _best(models: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
|
197
|
-
# Prefer recommended, then compatible; within a tier prefer the largest
|
|
198
|
-
# model that still fits (more capable).
|
|
199
|
-
for tier in (RECOMMENDED, COMPATIBLE):
|
|
200
|
-
tier_models = [m for m in models if m["status"] == tier]
|
|
201
|
-
if tier_models:
|
|
202
|
-
return max(tier_models, key=lambda m: m["size_gb"] or 0.0)
|
|
203
|
-
return None
|
|
204
|
-
|
|
205
|
-
families = []
|
|
206
|
-
for fam in sorted(by_family, key=_family_rank):
|
|
207
|
-
bucket = by_family[fam]
|
|
208
|
-
bucket["best"] = _best(bucket["models"])
|
|
209
|
-
families.append(bucket)
|
|
210
|
-
|
|
211
|
-
# Overall top pick: the largest recommended model on this machine.
|
|
212
|
-
recommended_models = [m for m in classified if m["status"] == RECOMMENDED]
|
|
213
|
-
top_pick = max(recommended_models, key=lambda m: m["size_gb"] or 0.0) if recommended_models else None
|
|
214
|
-
|
|
215
|
-
return {
|
|
216
|
-
"engine": engine,
|
|
217
|
-
"engine_available": engine_available,
|
|
218
|
-
"apple_silicon": is_apple_silicon(profile),
|
|
219
|
-
"ram_gb": round(ram_gb, 1),
|
|
220
|
-
"counts": counts,
|
|
221
|
-
"top_pick": top_pick,
|
|
222
|
-
"families": families,
|
|
223
|
-
"models": classified,
|
|
224
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
"""Cloud model verification — does this key actually answer for this model?
|
|
2
|
-
|
|
3
|
-
A one-token chat completion per configured cloud model, cached per service
|
|
4
|
-
instance for :data:`CLOUD_VERIFY_TTL_SECONDS`. A model the router already knows
|
|
5
|
-
is unavailable is recorded as such without a network call.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import asyncio
|
|
11
|
-
import os
|
|
12
|
-
import time
|
|
13
|
-
from typing import Any, Dict, Optional
|
|
14
|
-
|
|
15
|
-
from latticeai.models.router import (
|
|
16
|
-
OPENAI_COMPATIBLE_PROVIDERS,
|
|
17
|
-
AsyncOpenAI,
|
|
18
|
-
parse_model_ref,
|
|
19
|
-
)
|
|
20
|
-
from latticeai.services.model_runtime.state import ModelRuntimeState
|
|
21
|
-
|
|
22
|
-
CLOUD_VERIFY_TTL_SECONDS = 600
|
|
23
|
-
|
|
24
|
-
async def _probe_cloud_model(model_ref: str) -> Dict[str, Any]:
|
|
25
|
-
provider, model_name = parse_model_ref(model_ref)
|
|
26
|
-
config = OPENAI_COMPATIBLE_PROVIDERS.get(provider)
|
|
27
|
-
if not config:
|
|
28
|
-
return {"ok": False, "reason": f"Unsupported provider: {provider}"}
|
|
29
|
-
|
|
30
|
-
api_key = os.getenv(config["env_key"]) or config.get("api_key_fallback")
|
|
31
|
-
if not api_key:
|
|
32
|
-
return {"ok": False, "reason": f"Missing API key: {config['env_key']}"}
|
|
33
|
-
|
|
34
|
-
base_url = os.getenv(config.get("base_url_env", "")) if config.get("base_url_env") else None
|
|
35
|
-
base_url = base_url or config.get("base_url")
|
|
36
|
-
try:
|
|
37
|
-
# base_url is passed only when configured: an explicit None is not
|
|
38
|
-
# the same as omitting the argument.
|
|
39
|
-
client = (
|
|
40
|
-
AsyncOpenAI(api_key=api_key, base_url=base_url)
|
|
41
|
-
if base_url
|
|
42
|
-
else AsyncOpenAI(api_key=api_key)
|
|
43
|
-
)
|
|
44
|
-
await asyncio.wait_for(
|
|
45
|
-
client.chat.completions.create(
|
|
46
|
-
model=model_name,
|
|
47
|
-
messages=[{"role": "user", "content": "ping"}],
|
|
48
|
-
max_tokens=1,
|
|
49
|
-
temperature=0,
|
|
50
|
-
),
|
|
51
|
-
timeout=15,
|
|
52
|
-
)
|
|
53
|
-
return {"ok": True, "reason": "ok"}
|
|
54
|
-
except Exception as e:
|
|
55
|
-
return {"ok": False, "reason": str(e)[:220]}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
async def verify_cloud_models(
|
|
59
|
-
force: bool = False,
|
|
60
|
-
provider_filter: Optional[str] = None,
|
|
61
|
-
*,
|
|
62
|
-
state: ModelRuntimeState,
|
|
63
|
-
cache: Dict[str, Dict[str, Any]],
|
|
64
|
-
) -> Dict[str, Dict]:
|
|
65
|
-
now = time.time()
|
|
66
|
-
r = state.router
|
|
67
|
-
cloud_items = [item for item in (r.detected_cloud_models() if r else []) if item.get("tag") == "cloud"]
|
|
68
|
-
if provider_filter:
|
|
69
|
-
cloud_items = [item for item in cloud_items if item.get("provider") == provider_filter]
|
|
70
|
-
|
|
71
|
-
results: Dict[str, Dict] = {}
|
|
72
|
-
for item in cloud_items:
|
|
73
|
-
model_ref = item["id"]
|
|
74
|
-
cached = cache.get(model_ref)
|
|
75
|
-
if not force and cached and (now - cached.get("ts", 0) <= CLOUD_VERIFY_TTL_SECONDS):
|
|
76
|
-
results[model_ref] = cached
|
|
77
|
-
continue
|
|
78
|
-
if item.get("available") is False:
|
|
79
|
-
record = {"ok": False, "reason": item.get("requires") or "API key missing", "ts": now}
|
|
80
|
-
cache[model_ref] = record
|
|
81
|
-
results[model_ref] = record
|
|
82
|
-
continue
|
|
83
|
-
probe = await _probe_cloud_model(model_ref)
|
|
84
|
-
record = {"ok": bool(probe.get("ok")), "reason": probe.get("reason", ""), "ts": now}
|
|
85
|
-
cache[model_ref] = record
|
|
86
|
-
results[model_ref] = record
|
|
87
|
-
return results
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
"""Persisted network-boundary preference for hybrid local + cloud turns.
|
|
2
|
-
|
|
3
|
-
Mirrors PermissionModeService:
|
|
4
|
-
|
|
5
|
-
* process default (env ``LATTICEAI_NETWORK_MODE`` or local_only)
|
|
6
|
-
* per-user override
|
|
7
|
-
* per-workspace override (wins over user)
|
|
8
|
-
|
|
9
|
-
Switching to ``cloud_allowed`` requires ``acknowledge_risk=True`` once; the
|
|
10
|
-
ack is audited. Hard node filters remain in ``latticeai.core.network_boundary``.
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
from __future__ import annotations
|
|
14
|
-
|
|
15
|
-
from pathlib import Path
|
|
16
|
-
from typing import Any, Callable, Dict, Optional
|
|
17
|
-
|
|
18
|
-
from latticeai.core.network_boundary import (
|
|
19
|
-
DEFAULT_NETWORK_MODE,
|
|
20
|
-
NetworkBoundaryMode,
|
|
21
|
-
network_mode_catalog,
|
|
22
|
-
network_mode_contract,
|
|
23
|
-
normalize_network_mode,
|
|
24
|
-
)
|
|
25
|
-
from latticeai.services.mode_store import JsonBackedModeService
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class NetworkBoundaryService(JsonBackedModeService[NetworkBoundaryMode]):
|
|
29
|
-
"""Load / save the network boundary dial."""
|
|
30
|
-
|
|
31
|
-
FILENAME = "network_boundary.json"
|
|
32
|
-
|
|
33
|
-
def __init__(
|
|
34
|
-
self,
|
|
35
|
-
*,
|
|
36
|
-
data_dir: Path,
|
|
37
|
-
default_mode: NetworkBoundaryMode | str = DEFAULT_NETWORK_MODE,
|
|
38
|
-
audit: Optional[Callable[..., None]] = None,
|
|
39
|
-
) -> None:
|
|
40
|
-
super().__init__(data_dir=data_dir, audit=audit)
|
|
41
|
-
self._default = normalize_network_mode(default_mode)
|
|
42
|
-
|
|
43
|
-
def _default_entry(self) -> Any:
|
|
44
|
-
return self._default.value
|
|
45
|
-
|
|
46
|
-
def _resolve_from(
|
|
47
|
-
self,
|
|
48
|
-
data: Dict[str, Any],
|
|
49
|
-
*,
|
|
50
|
-
user_email: Optional[str],
|
|
51
|
-
workspace_id: Optional[str],
|
|
52
|
-
) -> NetworkBoundaryMode:
|
|
53
|
-
if workspace_id:
|
|
54
|
-
ws = (data.get("workspaces") or {}).get(str(workspace_id))
|
|
55
|
-
if ws:
|
|
56
|
-
return normalize_network_mode(ws)
|
|
57
|
-
if user_email:
|
|
58
|
-
user = (data.get("users") or {}).get(str(user_email).lower())
|
|
59
|
-
if user:
|
|
60
|
-
return normalize_network_mode(user)
|
|
61
|
-
return normalize_network_mode(data.get("default") or self._default)
|
|
62
|
-
|
|
63
|
-
def get(
|
|
64
|
-
self,
|
|
65
|
-
*,
|
|
66
|
-
user_email: Optional[str] = None,
|
|
67
|
-
workspace_id: Optional[str] = None,
|
|
68
|
-
) -> Dict[str, Any]:
|
|
69
|
-
mode = self.resolve(user_email=user_email, workspace_id=workspace_id)
|
|
70
|
-
contract = network_mode_contract(mode)
|
|
71
|
-
contract["catalog"] = network_mode_catalog()
|
|
72
|
-
contract["scope"] = {
|
|
73
|
-
"user_email": user_email,
|
|
74
|
-
"workspace_id": workspace_id,
|
|
75
|
-
}
|
|
76
|
-
return contract
|
|
77
|
-
|
|
78
|
-
def set_mode(
|
|
79
|
-
self,
|
|
80
|
-
mode: NetworkBoundaryMode | str,
|
|
81
|
-
*,
|
|
82
|
-
user_email: Optional[str] = None,
|
|
83
|
-
workspace_id: Optional[str] = None,
|
|
84
|
-
acknowledge_risk: bool = False,
|
|
85
|
-
source: str = "api",
|
|
86
|
-
) -> Dict[str, Any]:
|
|
87
|
-
mode = normalize_network_mode(mode)
|
|
88
|
-
if mode == NetworkBoundaryMode.CLOUD_ALLOWED and not acknowledge_risk:
|
|
89
|
-
raise PermissionError(
|
|
90
|
-
"cloud_allowed mode requires acknowledge_risk=true "
|
|
91
|
-
"(minimal related Knowledge Graph nodes may leave this machine)"
|
|
92
|
-
)
|
|
93
|
-
with self._lock:
|
|
94
|
-
data = self._read()
|
|
95
|
-
previous = self._resolve_from(
|
|
96
|
-
data, user_email=user_email, workspace_id=workspace_id,
|
|
97
|
-
)
|
|
98
|
-
if workspace_id:
|
|
99
|
-
data.setdefault("workspaces", {})[str(workspace_id)] = mode.value
|
|
100
|
-
elif user_email:
|
|
101
|
-
data.setdefault("users", {})[str(user_email).lower()] = mode.value
|
|
102
|
-
else:
|
|
103
|
-
data["default"] = mode.value
|
|
104
|
-
self._write(data)
|
|
105
|
-
self._audit(
|
|
106
|
-
"network_boundary_changed",
|
|
107
|
-
user_email=user_email,
|
|
108
|
-
workspace_id=workspace_id,
|
|
109
|
-
previous=previous.value,
|
|
110
|
-
mode=mode.value,
|
|
111
|
-
source=source,
|
|
112
|
-
acknowledge_risk=bool(acknowledge_risk),
|
|
113
|
-
)
|
|
114
|
-
return self.get(user_email=user_email, workspace_id=workspace_id)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
__all__ = ["NetworkBoundaryService"]
|