ltcai 10.0.0 → 10.3.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 +43 -32
- package/docs/CHANGELOG.md +204 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/HYBRID_CLOUD_KG_STREAMING.md +101 -0
- package/docs/MYPY_BACKLOG.md +98 -0
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +1 -1
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/kg-schema.md +1 -1
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/archive.py +5 -4
- package/lattice_brain/conversations.py +14 -3
- package/lattice_brain/embeddings.py +12 -2
- package/lattice_brain/graph/_kg_common.py +5 -5
- package/lattice_brain/graph/_kg_fsutil.py +5 -4
- package/lattice_brain/graph/discovery.py +4 -3
- package/lattice_brain/graph/discovery_index.py +0 -1
- package/lattice_brain/graph/documents.py +3 -2
- package/lattice_brain/graph/fusion.py +4 -0
- package/lattice_brain/graph/ingest.py +12 -2
- package/lattice_brain/graph/projection.py +3 -2
- package/lattice_brain/graph/provenance.py +5 -3
- package/lattice_brain/graph/rerank.py +4 -1
- package/lattice_brain/graph/retrieval.py +0 -1
- package/lattice_brain/graph/retrieval_docgen.py +0 -1
- package/lattice_brain/graph/retrieval_reads.py +0 -1
- package/lattice_brain/graph/retrieval_vector.py +0 -1
- package/lattice_brain/graph/schema.py +4 -3
- package/lattice_brain/graph/store.py +18 -4
- package/lattice_brain/graph/write_master.py +46 -1
- package/lattice_brain/ingestion.py +4 -1
- package/lattice_brain/portability.py +5 -2
- package/lattice_brain/quality.py +12 -5
- package/lattice_brain/quiet.py +43 -0
- package/lattice_brain/runtime/agent_runtime.py +19 -10
- package/lattice_brain/runtime/hooks.py +3 -2
- package/lattice_brain/runtime/multi_agent.py +2 -3
- package/lattice_brain/sensitivity.py +94 -0
- package/lattice_brain/storage/base.py +30 -2
- package/lattice_brain/storage/migration.py +3 -2
- package/lattice_brain/storage/postgres.py +2 -2
- package/lattice_brain/storage/sqlite.py +6 -4
- package/lattice_brain/workflow.py +4 -2
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +1 -1
- package/latticeai/api/agents.py +4 -2
- package/latticeai/api/auth.py +5 -1
- package/latticeai/api/automation_intelligence.py +2 -1
- package/latticeai/api/browser.py +3 -2
- package/latticeai/api/chat.py +28 -17
- package/latticeai/api/chat_agent_http.py +22 -8
- package/latticeai/api/chat_contracts.py +4 -0
- package/latticeai/api/chat_documents.py +6 -2
- package/latticeai/api/chat_hybrid.py +82 -0
- package/latticeai/api/computer_use.py +8 -3
- package/latticeai/api/knowledge_graph.py +1 -1
- package/latticeai/api/mcp.py +4 -4
- package/latticeai/api/models.py +5 -2
- package/latticeai/api/network_boundary.py +220 -0
- package/latticeai/api/permissions.py +0 -1
- package/latticeai/api/realtime.py +1 -1
- package/latticeai/api/security_dashboard.py +1 -1
- package/latticeai/api/setup.py +16 -3
- package/latticeai/api/static_routes.py +2 -1
- package/latticeai/api/tools.py +12 -8
- package/latticeai/api/voice_capture.py +3 -1
- package/latticeai/api/workflow_designer.py +1 -1
- package/latticeai/api/workspace.py +1 -2
- package/latticeai/app_factory.py +174 -145
- package/latticeai/cli/entrypoint.py +6 -4
- package/latticeai/core/agent.py +55 -495
- package/latticeai/core/agent_eval.py +2 -2
- package/latticeai/core/agent_helpers.py +493 -0
- package/latticeai/core/agent_prompts.py +0 -1
- package/latticeai/core/agent_registry.py +3 -1
- package/latticeai/core/agent_state.py +41 -0
- package/latticeai/core/audit.py +1 -1
- package/latticeai/core/builtin_hooks.py +2 -1
- package/latticeai/core/config.py +0 -1
- package/latticeai/core/embedding_providers.py +12 -1
- package/latticeai/core/file_generation.py +3 -0
- package/latticeai/core/invitations.py +4 -1
- package/latticeai/core/io_utils.py +3 -1
- package/latticeai/core/legacy_compatibility.py +1 -2
- package/latticeai/core/local_embeddings.py +12 -1
- package/latticeai/core/marketplace.py +1 -2
- package/latticeai/core/mcp_registry.py +0 -1
- package/latticeai/core/model_compat.py +1 -1
- package/latticeai/core/model_resolution.py +1 -1
- package/latticeai/core/network_boundary.py +168 -0
- package/latticeai/core/oidc.py +3 -0
- package/latticeai/core/permission_mode.py +6 -6
- package/latticeai/core/plugins.py +3 -2
- package/latticeai/core/policy.py +0 -1
- package/latticeai/core/quiet.py +84 -0
- package/latticeai/core/realtime.py +3 -2
- package/latticeai/core/run_store.py +4 -2
- package/latticeai/core/security.py +4 -0
- package/latticeai/core/users.py +5 -3
- package/latticeai/core/workspace_graph_trace.py +3 -0
- package/latticeai/core/workspace_os.py +65 -273
- package/latticeai/core/workspace_os_constants.py +126 -0
- package/latticeai/core/workspace_os_state.py +180 -0
- package/latticeai/core/workspace_os_utils.py +3 -1
- package/latticeai/core/workspace_permissions.py +1 -0
- package/latticeai/core/workspace_snapshots.py +3 -1
- package/latticeai/core/workspace_timeline.py +3 -1
- package/latticeai/integrations/telegram_bot.py +25 -16
- package/latticeai/models/router.py +6 -3
- package/latticeai/runtime/access_runtime.py +3 -1
- package/latticeai/runtime/audit_runtime.py +3 -2
- package/latticeai/runtime/chat_wiring.py +4 -1
- package/latticeai/runtime/history_runtime.py +1 -1
- package/latticeai/runtime/history_writer.py +138 -0
- package/latticeai/runtime/lifespan_runtime.py +3 -1
- package/latticeai/runtime/network_boundary_wiring.py +124 -0
- package/latticeai/runtime/persistence_runtime.py +3 -1
- package/latticeai/runtime/router_registration.py +11 -1
- package/latticeai/services/architecture_readiness.py +1 -2
- package/latticeai/services/change_proposals.py +2 -1
- package/latticeai/services/cloud_egress_audit.py +85 -0
- package/latticeai/services/cloud_extraction.py +129 -0
- package/latticeai/services/cloud_streaming.py +266 -0
- package/latticeai/services/cloud_token_guard.py +84 -0
- package/latticeai/services/command_center.py +2 -1
- package/latticeai/services/folder_watch.py +3 -0
- package/latticeai/services/funnel_metrics.py +3 -1
- package/latticeai/services/hybrid_chat.py +265 -0
- package/latticeai/services/hybrid_context.py +228 -0
- package/latticeai/services/hybrid_policy.py +178 -0
- package/latticeai/services/memory_service.py +1 -1
- package/latticeai/services/model_catalog.py +10 -1
- package/latticeai/services/model_engines.py +35 -14
- package/latticeai/services/model_loading.py +3 -2
- package/latticeai/services/model_runtime.py +68 -17
- package/latticeai/services/multimodal_streaming.py +123 -0
- package/latticeai/services/network_boundary_service.py +154 -0
- package/latticeai/services/openai_compatible_adapter.py +100 -0
- package/latticeai/services/p_reinforce.py +4 -0
- package/latticeai/services/platform_runtime.py +9 -4
- package/latticeai/services/process_audit.py +0 -1
- package/latticeai/services/product_readiness.py +1 -1
- package/latticeai/services/run_executor.py +3 -2
- package/latticeai/services/search_service.py +1 -4
- package/latticeai/services/setup_detection.py +2 -1
- package/latticeai/services/tool_dispatch.py +7 -2
- package/latticeai/services/upload_service.py +2 -1
- package/latticeai/setup/auto_setup.py +10 -7
- package/latticeai/setup/wizard.py +15 -11
- package/latticeai/tools/__init__.py +3 -3
- package/latticeai/tools/commands.py +7 -7
- package/latticeai/tools/computer.py +3 -2
- package/latticeai/tools/documents.py +10 -9
- package/latticeai/tools/filesystem.py +7 -4
- package/latticeai/tools/knowledge.py +2 -0
- package/latticeai/tools/local_files.py +1 -1
- package/latticeai/tools/network.py +2 -1
- package/package.json +7 -3
- package/scripts/bench_agent_smoke.py +0 -1
- package/scripts/bench_models.py +0 -1
- package/scripts/brain_quality_eval.py +7 -2
- package/scripts/bump_version.py +2 -1
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/migrate_brain_storage.py +5 -1
- package/scripts/profile_kg.py +2 -7
- package/scripts/verify_hf_model_registry.py +2 -2
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +37 -37
- package/static/app/assets/{Act-BtCREeN1.js → Act-DrKIGZeJ.js} +1 -1
- package/static/app/assets/{AdminConsole-TPeeN18T.js → AdminConsole-CEV1VefA.js} +1 -1
- package/static/app/assets/{Brain-BKs6JAp0.js → Brain-BZSXnwWU.js} +1 -1
- package/static/app/assets/{BrainHome-BPGOvSd6.js → BrainHome-CEBd86k6.js} +1 -1
- package/static/app/assets/{BrainSignals-CtzQZ15J.js → BrainSignals-O4s6R7av.js} +1 -1
- package/static/app/assets/{Capture-1_NaHWqB.js → Capture-AGbOxsg-.js} +1 -1
- package/static/app/assets/{CommandPalette-pqvQOXe4.js → CommandPalette-CeqBjCn2.js} +1 -1
- package/static/app/assets/Library-CXnbNiQW.js +1 -0
- package/static/app/assets/{LivingBrain-DlQ20Q75.js → LivingBrain-D9Iei2ZJ.js} +1 -1
- package/static/app/assets/{ProductFlow-BZvGDRi_.js → ProductFlow-JlXiDfMM.js} +1 -1
- package/static/app/assets/{ReviewCard-BWgI0D2s.js → ReviewCard-QQly2bTV.js} +1 -1
- package/static/app/assets/System-Bp9ZY4ER.js +1 -0
- package/static/app/assets/{activity-Dlfk8YC7.js → activity-CxRPOQ2F.js} +1 -1
- package/static/app/assets/{bot-CDvUB76P.js → bot-1bcsWB2s.js} +1 -1
- package/static/app/assets/{brain-xczrohrt.js → brain-CxaeKCRM.js} +1 -1
- package/static/app/assets/{button-SOdH3Oyf.js → button-ZYFqGan7.js} +1 -1
- package/static/app/assets/{circle-pause-CG1ythH4.js → circle-pause-B5NOIqvk.js} +1 -1
- package/static/app/assets/{circle-play-HXwvjS6W.js → circle-play-BiJVFXf7.js} +1 -1
- package/static/app/assets/{cpu-B0d-rGyk.js → cpu-Vvq0OKTr.js} +1 -1
- package/static/app/assets/{download-BGIkTQL6.js → download-SEcXzkx1.js} +1 -1
- package/static/app/assets/{folder-open-Dst_Z0_K.js → folder-open-Dw1ff_Qa.js} +1 -1
- package/static/app/assets/{hard-drive-D53MsWkV.js → hard-drive-B00fR7_r.js} +1 -1
- package/static/app/assets/index-CEkL4_M-.css +2 -0
- package/static/app/assets/{index-C_IrlQMV.js → index-Dm9VXl1o.js} +3 -3
- package/static/app/assets/{input-C5m0riF6.js → input-DtdWYQEa.js} +1 -1
- package/static/app/assets/{network-C5a-E5iS.js → network-CpjBu0kI.js} +1 -1
- package/static/app/assets/{primitives-vNXYf58F.js → primitives-CwsudBwc.js} +1 -1
- package/static/app/assets/search-CwEJWBEA.js +1 -0
- package/static/app/assets/{shield-alert-Cc-WVXqN.js → shield-alert-CpjBc-gQ.js} +1 -1
- package/static/app/assets/{textarea-BkZ0EqVO.js → textarea-DUPyjHQs.js} +1 -1
- package/static/app/assets/{useFocusTrap-CTtKbAOU.js → useFocusTrap-BWcMtCWH.js} +1 -1
- package/static/app/assets/{useQuery-Dx1fi4Wu.js → useQuery-Dg5fPqTS.js} +1 -1
- package/static/app/assets/{users-BFpQXtEF.js → users-BgcdTQGM.js} +1 -1
- package/static/app/assets/{utils-BA_lmW3J.js → utils-a3YXFtEO.js} +2 -2
- package/static/app/assets/workspace-BMUEiXUm.js +1 -0
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/static/app/assets/Library-DhvoPvC7.js +0 -1
- package/static/app/assets/System-CSMdYLMy.js +0 -1
- package/static/app/assets/index-FxDusbr0.css +0 -2
- package/static/app/assets/search-DhbSgW6m.js +0 -1
- package/static/app/assets/workspace-DBPB0jkX.js +0 -1
|
@@ -20,7 +20,11 @@ from latticeai.core.context_builder import (
|
|
|
20
20
|
format_sources_footnote,
|
|
21
21
|
retrieve_context_for_generation,
|
|
22
22
|
)
|
|
23
|
-
from latticeai.core.document_generator import
|
|
23
|
+
from latticeai.core.document_generator import (
|
|
24
|
+
DocumentGenerationSession,
|
|
25
|
+
detect_document_intent,
|
|
26
|
+
)
|
|
27
|
+
from latticeai.core.quiet import quiet
|
|
24
28
|
|
|
25
29
|
|
|
26
30
|
def extract_screenshot_context(image_data: Optional[str]) -> str:
|
|
@@ -77,7 +81,7 @@ def extract_screenshot_context(image_data: Optional[str]) -> str:
|
|
|
77
81
|
try:
|
|
78
82
|
Path(temp_path).unlink()
|
|
79
83
|
except OSError:
|
|
80
|
-
|
|
84
|
+
quiet()
|
|
81
85
|
return "\n".join(lines)
|
|
82
86
|
|
|
83
87
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Chat-path helpers for hybrid cloud branching (Phase 2).
|
|
2
|
+
|
|
3
|
+
Keeps the main chat router thin: resolve network mode, decide whether to
|
|
4
|
+
enter the hybrid stream, and build the StreamingResponse.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
|
|
11
|
+
from fastapi.responses import StreamingResponse
|
|
12
|
+
|
|
13
|
+
from latticeai.core.network_boundary import (
|
|
14
|
+
NetworkBoundaryMode,
|
|
15
|
+
normalize_network_mode,
|
|
16
|
+
)
|
|
17
|
+
from latticeai.runtime.network_boundary_wiring import resolve_active_network_mode
|
|
18
|
+
from latticeai.services.hybrid_chat import stream_hybrid_cloud_turn
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def resolve_request_network_mode(
|
|
22
|
+
*,
|
|
23
|
+
request_mode: Optional[str],
|
|
24
|
+
user_email: Optional[str],
|
|
25
|
+
workspace_id: Optional[str],
|
|
26
|
+
) -> NetworkBoundaryMode:
|
|
27
|
+
"""Per-request override wins; otherwise use the persisted dial."""
|
|
28
|
+
if request_mode:
|
|
29
|
+
return normalize_network_mode(request_mode)
|
|
30
|
+
return normalize_network_mode(
|
|
31
|
+
resolve_active_network_mode(user_email=user_email, workspace_id=workspace_id)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def maybe_hybrid_stream_response(
|
|
36
|
+
*,
|
|
37
|
+
req: Any,
|
|
38
|
+
mode: NetworkBoundaryMode,
|
|
39
|
+
knowledge_graph: Any,
|
|
40
|
+
enable_graph: bool,
|
|
41
|
+
effective_email: Optional[str],
|
|
42
|
+
workspace_id: Optional[str],
|
|
43
|
+
history_meta: Dict[str, Any],
|
|
44
|
+
history_user: Dict[str, Any],
|
|
45
|
+
chat_service: Any,
|
|
46
|
+
notify: Any,
|
|
47
|
+
model_id: Optional[str],
|
|
48
|
+
) -> Optional[StreamingResponse]:
|
|
49
|
+
"""Return a StreamingResponse when cloud path should run; else None."""
|
|
50
|
+
if mode != NetworkBoundaryMode.CLOUD_ALLOWED:
|
|
51
|
+
return None
|
|
52
|
+
if not (enable_graph and knowledge_graph is not None):
|
|
53
|
+
# Without a graph there is nothing minimal to send; fall back to local.
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
return StreamingResponse(
|
|
57
|
+
stream_hybrid_cloud_turn(
|
|
58
|
+
user_message=req.message,
|
|
59
|
+
knowledge_graph=knowledge_graph,
|
|
60
|
+
mode=mode,
|
|
61
|
+
workspace_id=workspace_id,
|
|
62
|
+
user_email=effective_email,
|
|
63
|
+
model=model_id,
|
|
64
|
+
chat_service=chat_service,
|
|
65
|
+
history_meta=history_meta,
|
|
66
|
+
history_user=history_user,
|
|
67
|
+
notify=notify,
|
|
68
|
+
source=req.source,
|
|
69
|
+
),
|
|
70
|
+
media_type="text/event-stream",
|
|
71
|
+
headers={
|
|
72
|
+
"X-Model": model_id or "cloud",
|
|
73
|
+
"X-Network-Mode": mode.value,
|
|
74
|
+
"X-Hybrid": "1",
|
|
75
|
+
},
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
__all__ = [
|
|
80
|
+
"resolve_request_network_mode",
|
|
81
|
+
"maybe_hybrid_stream_response",
|
|
82
|
+
]
|
|
@@ -10,8 +10,8 @@ from fastapi import APIRouter, HTTPException, Request
|
|
|
10
10
|
from fastapi.responses import StreamingResponse
|
|
11
11
|
from pydantic import BaseModel
|
|
12
12
|
|
|
13
|
-
from latticeai.core.agent import extract_action as _extract_agent_action
|
|
14
13
|
from lattice_brain.runtime.hooks import dispatch_tool
|
|
14
|
+
from latticeai.core.agent import extract_action as _extract_agent_action
|
|
15
15
|
from latticeai.services.tool_dispatch import enforce_tool_policy
|
|
16
16
|
from latticeai.tools import (
|
|
17
17
|
AGENT_ROOT,
|
|
@@ -30,7 +30,6 @@ from latticeai.tools import (
|
|
|
30
30
|
execute_tool,
|
|
31
31
|
)
|
|
32
32
|
|
|
33
|
-
|
|
34
33
|
CU_SYSTEM_PROMPT = """You are Lattice AI desktop-control agent. You control the Mac desktop using tools.
|
|
35
34
|
Prefer non-visual direct actions when possible. Use screenshots only when you must inspect visible UI state or choose screen coordinates.
|
|
36
35
|
|
|
@@ -438,7 +437,13 @@ def create_computer_use_router(
|
|
|
438
437
|
|
|
439
438
|
yield _send("action", {"step": step + 1, "action": name, "args": args})
|
|
440
439
|
try:
|
|
441
|
-
result = _dispatch(
|
|
440
|
+
result = _dispatch(
|
|
441
|
+
name,
|
|
442
|
+
args,
|
|
443
|
+
# Bound as defaults; see chat_agent_http for why.
|
|
444
|
+
lambda name=name, args=args: execute_tool(name, args),
|
|
445
|
+
current_user=current_user,
|
|
446
|
+
)
|
|
442
447
|
if name == "computer_screenshot" and "screenshot_b64" in result:
|
|
443
448
|
last_screenshot_b64 = result["screenshot_b64"]
|
|
444
449
|
result_summary = {k: v for k, v in result.items() if k != "screenshot_b64"}
|
|
@@ -12,8 +12,8 @@ from typing import Any, Callable, Dict, List, Optional
|
|
|
12
12
|
from fastapi import APIRouter, HTTPException, Request
|
|
13
13
|
from pydantic import BaseModel
|
|
14
14
|
|
|
15
|
-
from latticeai.api.ui_redirects import app_redirect
|
|
16
15
|
from lattice_brain.ingestion import IngestionItem
|
|
16
|
+
from latticeai.api.ui_redirects import app_redirect
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class KnowledgeGraphIngestRequest(BaseModel):
|
package/latticeai/api/mcp.py
CHANGED
|
@@ -17,17 +17,17 @@ from datetime import datetime
|
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
from typing import Any, Callable, Dict, List, Optional
|
|
19
19
|
|
|
20
|
-
from lattice_brain.ingestion import IngestionItem
|
|
21
20
|
from fastapi import APIRouter, HTTPException, Request
|
|
22
21
|
from pydantic import BaseModel
|
|
23
22
|
|
|
24
23
|
import latticeai.core.mcp_registry as mcp_registry
|
|
24
|
+
from lattice_brain.ingestion import IngestionItem
|
|
25
25
|
from latticeai.core.mcp_registry import (
|
|
26
|
-
|
|
27
|
-
_fetch_skills_marketplace,
|
|
26
|
+
SKILLS_DIR,
|
|
28
27
|
_fetch_plugin_directory,
|
|
28
|
+
_fetch_skills_marketplace,
|
|
29
|
+
_get_combined_registry,
|
|
29
30
|
install_skill,
|
|
30
|
-
SKILLS_DIR,
|
|
31
31
|
)
|
|
32
32
|
from latticeai.core.tool_registry import (
|
|
33
33
|
KNOWLEDGE_WRITE_TOOLS,
|
package/latticeai/api/models.py
CHANGED
|
@@ -494,7 +494,10 @@ def create_models_router(
|
|
|
494
494
|
async def load_model(req: LoadModelRequest, request: Request):
|
|
495
495
|
current_user = _authorize_model_admin(request, req.user_email)
|
|
496
496
|
try:
|
|
497
|
-
from latticeai.core.model_compat import
|
|
497
|
+
from latticeai.core.model_compat import (
|
|
498
|
+
friendly_model_runtime_error,
|
|
499
|
+
model_runtime_compatibility,
|
|
500
|
+
)
|
|
498
501
|
|
|
499
502
|
model_id = req.model_id
|
|
500
503
|
requested_engine = req.engine or (model_id.split(":", 1)[0] if ":" in model_id else "local_mlx")
|
|
@@ -552,8 +555,8 @@ def create_models_router(
|
|
|
552
555
|
hardware, load_strategy, license, safety_notes) from the structured registry.
|
|
553
556
|
"""
|
|
554
557
|
require_user(request)
|
|
555
|
-
from latticeai.setup.auto_setup import probe as auto_setup_probe
|
|
556
558
|
from latticeai.services.model_recommendation import recommend_catalog
|
|
559
|
+
from latticeai.setup.auto_setup import probe as auto_setup_probe
|
|
557
560
|
|
|
558
561
|
profile = await asyncio.to_thread(lambda: auto_setup_probe().to_json())
|
|
559
562
|
catalog = recommend_catalog(profile, engine=engine)
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Network boundary API — local_only / cloud_allowed dial + Phase 3 policy/UI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable, Dict, Optional
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
from latticeai.core.network_boundary import network_mode_catalog, normalize_network_mode
|
|
11
|
+
from latticeai.services.cloud_egress_audit import record_cloud_egress
|
|
12
|
+
from latticeai.services.cloud_token_guard import budget_for
|
|
13
|
+
from latticeai.services.hybrid_context import build_minimal_context
|
|
14
|
+
from latticeai.services.hybrid_policy import HybridPolicyService
|
|
15
|
+
from latticeai.services.network_boundary_service import NetworkBoundaryService
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SetNetworkBoundaryRequest(BaseModel):
|
|
19
|
+
mode: str = Field(..., description="local_only | cloud_allowed")
|
|
20
|
+
workspace_id: Optional[str] = None
|
|
21
|
+
acknowledge_risk: bool = False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PreviewRequest(BaseModel):
|
|
25
|
+
message: str
|
|
26
|
+
workspace_id: Optional[str] = None
|
|
27
|
+
top_k: int = 6
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SetNodeSensitivityRequest(BaseModel):
|
|
31
|
+
node_id: str
|
|
32
|
+
local_only: bool = True
|
|
33
|
+
reason: Optional[str] = None
|
|
34
|
+
workspace_id: Optional[str] = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SetHybridPolicyRequest(BaseModel):
|
|
38
|
+
workspace_id: Optional[str] = None
|
|
39
|
+
blocked_node_types: Optional[list[str]] = None
|
|
40
|
+
blocked_metadata_flags: Optional[list[str]] = None
|
|
41
|
+
auto_commit: Optional[bool] = None
|
|
42
|
+
allow_multimodal: Optional[bool] = None
|
|
43
|
+
min_extraction_confidence: Optional[float] = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def create_network_boundary_router(
|
|
47
|
+
*,
|
|
48
|
+
service: NetworkBoundaryService,
|
|
49
|
+
require_user: Callable[..., str],
|
|
50
|
+
knowledge_graph: Any = None,
|
|
51
|
+
policy_service: Optional[HybridPolicyService] = None,
|
|
52
|
+
) -> APIRouter:
|
|
53
|
+
router = APIRouter(tags=["network-boundary"])
|
|
54
|
+
|
|
55
|
+
def _scope(request: Request, workspace_id: Optional[str] = None) -> tuple[str, Optional[str]]:
|
|
56
|
+
user = require_user(request)
|
|
57
|
+
header_ws = request.headers.get("X-Workspace-Id")
|
|
58
|
+
scope = workspace_id or (header_ws.strip() if header_ws else None)
|
|
59
|
+
return user, scope
|
|
60
|
+
|
|
61
|
+
@router.get("/api/network-boundary")
|
|
62
|
+
async def get_network_boundary(
|
|
63
|
+
request: Request,
|
|
64
|
+
workspace_id: Optional[str] = None,
|
|
65
|
+
):
|
|
66
|
+
user, scope = _scope(request, workspace_id)
|
|
67
|
+
payload = service.get(user_email=user, workspace_id=scope)
|
|
68
|
+
scope_key = f"{user or 'anon'}|{scope or 'global'}"
|
|
69
|
+
payload["token_budget"] = budget_for(scope_key).snapshot()
|
|
70
|
+
if policy_service is not None:
|
|
71
|
+
payload["policy"] = policy_service.resolve(
|
|
72
|
+
user_email=user, workspace_id=scope
|
|
73
|
+
)
|
|
74
|
+
return payload
|
|
75
|
+
|
|
76
|
+
@router.get("/api/network-boundary/catalog")
|
|
77
|
+
async def network_boundary_catalog(request: Request):
|
|
78
|
+
require_user(request)
|
|
79
|
+
return {"modes": network_mode_catalog()}
|
|
80
|
+
|
|
81
|
+
@router.get("/api/network-boundary/ui-state")
|
|
82
|
+
async def network_boundary_ui_state(
|
|
83
|
+
request: Request,
|
|
84
|
+
workspace_id: Optional[str] = None,
|
|
85
|
+
):
|
|
86
|
+
"""Compact payload for the progressive-enhancement toggle panel."""
|
|
87
|
+
user, scope = _scope(request, workspace_id)
|
|
88
|
+
mode_payload = service.get(user_email=user, workspace_id=scope)
|
|
89
|
+
policy = (
|
|
90
|
+
policy_service.resolve(user_email=user, workspace_id=scope)
|
|
91
|
+
if policy_service is not None
|
|
92
|
+
else {}
|
|
93
|
+
)
|
|
94
|
+
scope_key = f"{user or 'anon'}|{scope or 'global'}"
|
|
95
|
+
return {
|
|
96
|
+
"mode": mode_payload.get("mode"),
|
|
97
|
+
"label": mode_payload.get("label"),
|
|
98
|
+
"label_ko": mode_payload.get("label_ko"),
|
|
99
|
+
"allows_cloud": mode_payload.get("allows_cloud"),
|
|
100
|
+
"requires_ack": mode_payload.get("requires_ack"),
|
|
101
|
+
"warning_ko": next(
|
|
102
|
+
(
|
|
103
|
+
m.get("warning_ko")
|
|
104
|
+
for m in (mode_payload.get("catalog") or [])
|
|
105
|
+
if m.get("id") == mode_payload.get("mode")
|
|
106
|
+
),
|
|
107
|
+
None,
|
|
108
|
+
),
|
|
109
|
+
"policy": policy,
|
|
110
|
+
"token_budget": budget_for(scope_key).snapshot(),
|
|
111
|
+
"catalog": network_mode_catalog(),
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@router.post("/api/network-boundary")
|
|
115
|
+
async def set_network_boundary(body: SetNetworkBoundaryRequest, request: Request):
|
|
116
|
+
user, scope = _scope(request, body.workspace_id)
|
|
117
|
+
try:
|
|
118
|
+
return service.set_mode(
|
|
119
|
+
normalize_network_mode(body.mode),
|
|
120
|
+
user_email=user,
|
|
121
|
+
workspace_id=scope,
|
|
122
|
+
acknowledge_risk=body.acknowledge_risk,
|
|
123
|
+
source="api",
|
|
124
|
+
)
|
|
125
|
+
except PermissionError as exc:
|
|
126
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
127
|
+
|
|
128
|
+
@router.post("/api/network-boundary/preview")
|
|
129
|
+
async def preview_cloud_context(body: PreviewRequest, request: Request):
|
|
130
|
+
user, scope = _scope(request, body.workspace_id)
|
|
131
|
+
mode = service.resolve(user_email=user, workspace_id=scope)
|
|
132
|
+
minimal = build_minimal_context(
|
|
133
|
+
body.message,
|
|
134
|
+
store=knowledge_graph,
|
|
135
|
+
mode=mode,
|
|
136
|
+
top_k=max(1, min(int(body.top_k or 6), 12)),
|
|
137
|
+
allowed_workspaces={scope} if scope else None,
|
|
138
|
+
)
|
|
139
|
+
scope_key = f"{user or 'anon'}|{scope or 'global'}"
|
|
140
|
+
budget = budget_for(scope_key)
|
|
141
|
+
refusal = budget.check_turn(minimal.token_estimate)
|
|
142
|
+
return {
|
|
143
|
+
"mode": mode.value,
|
|
144
|
+
"allows_cloud": mode.value == "cloud_allowed",
|
|
145
|
+
"node_ids": minimal.node_ids,
|
|
146
|
+
"keywords": minimal.keywords,
|
|
147
|
+
"titles": [str(n.get("title") or n.get("id") or "") for n in minimal.nodes],
|
|
148
|
+
"types": [str(n.get("type") or "") for n in minimal.nodes],
|
|
149
|
+
"token_estimate": minimal.token_estimate,
|
|
150
|
+
"quality": minimal.quality,
|
|
151
|
+
"compact_preview": minimal.compact_text[:1200],
|
|
152
|
+
"token_budget": budget.snapshot(),
|
|
153
|
+
"would_block": refusal,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@router.post("/api/network-boundary/node-sensitivity")
|
|
157
|
+
async def set_node_sensitivity(body: SetNodeSensitivityRequest, request: Request):
|
|
158
|
+
"""Mark one memory as never-leaving (or clear the mark).
|
|
159
|
+
|
|
160
|
+
The cloud filter has always looked for this flag. Until 10.2.0 nothing
|
|
161
|
+
in the product could set it, so the guard could not fire. Ingestion
|
|
162
|
+
stamps secret-bearing paths automatically; this covers what a path
|
|
163
|
+
cannot tell you — a note whose *content* is private.
|
|
164
|
+
"""
|
|
165
|
+
user, scope = _scope(request, body.workspace_id)
|
|
166
|
+
if knowledge_graph is None or not hasattr(knowledge_graph, "set_node_sensitivity"):
|
|
167
|
+
raise HTTPException(status_code=501, detail="knowledge graph not available")
|
|
168
|
+
result = knowledge_graph.set_node_sensitivity(
|
|
169
|
+
body.node_id, local_only=bool(body.local_only), reason=body.reason
|
|
170
|
+
)
|
|
171
|
+
if not result.get("ok"):
|
|
172
|
+
raise HTTPException(status_code=404, detail=result.get("reason") or "node not found")
|
|
173
|
+
record_cloud_egress(
|
|
174
|
+
node_ids=[body.node_id], token_estimate=0, mode="(policy)",
|
|
175
|
+
provider="(local)", user_email=user, workspace_id=scope,
|
|
176
|
+
outcome="marked_local_only" if body.local_only else "cleared_local_only",
|
|
177
|
+
detail=body.reason,
|
|
178
|
+
)
|
|
179
|
+
return result
|
|
180
|
+
|
|
181
|
+
@router.get("/api/network-boundary/policy")
|
|
182
|
+
async def get_hybrid_policy(
|
|
183
|
+
request: Request,
|
|
184
|
+
workspace_id: Optional[str] = None,
|
|
185
|
+
):
|
|
186
|
+
if policy_service is None:
|
|
187
|
+
raise HTTPException(status_code=501, detail="hybrid policy service not configured")
|
|
188
|
+
user, scope = _scope(request, workspace_id)
|
|
189
|
+
return policy_service.resolve(user_email=user, workspace_id=scope)
|
|
190
|
+
|
|
191
|
+
@router.post("/api/network-boundary/policy")
|
|
192
|
+
async def set_hybrid_policy(body: SetHybridPolicyRequest, request: Request):
|
|
193
|
+
if policy_service is None:
|
|
194
|
+
raise HTTPException(status_code=501, detail="hybrid policy service not configured")
|
|
195
|
+
user, scope = _scope(request, body.workspace_id)
|
|
196
|
+
patch: Dict[str, Any] = {}
|
|
197
|
+
for key in (
|
|
198
|
+
"blocked_node_types",
|
|
199
|
+
"blocked_metadata_flags",
|
|
200
|
+
"auto_commit",
|
|
201
|
+
"allow_multimodal",
|
|
202
|
+
"min_extraction_confidence",
|
|
203
|
+
):
|
|
204
|
+
val = getattr(body, key, None)
|
|
205
|
+
if val is not None:
|
|
206
|
+
patch[key] = val
|
|
207
|
+
return policy_service.set_policy(
|
|
208
|
+
patch, user_email=user, workspace_id=scope, source="api"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
return router
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
__all__ = [
|
|
215
|
+
"create_network_boundary_router",
|
|
216
|
+
"SetNetworkBoundaryRequest",
|
|
217
|
+
"PreviewRequest",
|
|
218
|
+
"SetHybridPolicyRequest",
|
|
219
|
+
"SetNodeSensitivityRequest",
|
|
220
|
+
]
|
|
@@ -16,8 +16,8 @@ from fastapi import APIRouter, HTTPException, Request
|
|
|
16
16
|
from fastapi.responses import StreamingResponse
|
|
17
17
|
from pydantic import BaseModel
|
|
18
18
|
|
|
19
|
-
from latticeai.api.ui_redirects import app_redirect
|
|
20
19
|
from lattice_brain.runtime.contracts import contract_views
|
|
20
|
+
from latticeai.api.ui_redirects import app_redirect
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class PresenceRequest(BaseModel):
|
|
@@ -195,7 +195,7 @@ def _pdf_report(title: str, rows: List[Dict[str, Any]], overview: Dict[str, Any]
|
|
|
195
195
|
try:
|
|
196
196
|
from reportlab.lib.pagesizes import A4
|
|
197
197
|
from reportlab.lib.styles import getSampleStyleSheet
|
|
198
|
-
from reportlab.platypus import
|
|
198
|
+
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table
|
|
199
199
|
except Exception: # pragma: no cover
|
|
200
200
|
return ("PDF library not available\n" + json.dumps(overview, ensure_ascii=False, indent=2)).encode("utf-8")
|
|
201
201
|
|
package/latticeai/api/setup.py
CHANGED
|
@@ -8,15 +8,24 @@ from fastapi import APIRouter, HTTPException, Request
|
|
|
8
8
|
from fastapi.responses import StreamingResponse
|
|
9
9
|
from pydantic import BaseModel
|
|
10
10
|
|
|
11
|
+
from lattice_brain.ingestion import IngestionItem
|
|
12
|
+
from latticeai.models.router import parse_model_ref
|
|
11
13
|
from latticeai.services.process_audit import command_plan
|
|
12
14
|
from latticeai.setup.auto_setup import (
|
|
13
15
|
plan as auto_setup_plan,
|
|
16
|
+
)
|
|
17
|
+
from latticeai.setup.auto_setup import (
|
|
14
18
|
preset as auto_setup_preset,
|
|
19
|
+
)
|
|
20
|
+
from latticeai.setup.auto_setup import (
|
|
15
21
|
probe as auto_setup_probe,
|
|
22
|
+
)
|
|
23
|
+
from latticeai.setup.auto_setup import (
|
|
16
24
|
recommend as auto_setup_recommend,
|
|
25
|
+
)
|
|
26
|
+
from latticeai.setup.auto_setup import (
|
|
17
27
|
verify as auto_setup_verify,
|
|
18
28
|
)
|
|
19
|
-
from latticeai.models.router import parse_model_ref
|
|
20
29
|
from latticeai.setup.demo_corpus import (
|
|
21
30
|
DEMO_DOCUMENTS,
|
|
22
31
|
DEMO_METADATA_FLAG,
|
|
@@ -24,8 +33,12 @@ from latticeai.setup.demo_corpus import (
|
|
|
24
33
|
demo_source_uri,
|
|
25
34
|
suggested_questions,
|
|
26
35
|
)
|
|
27
|
-
from latticeai.setup.wizard import
|
|
28
|
-
|
|
36
|
+
from latticeai.setup.wizard import (
|
|
37
|
+
get_recommendations,
|
|
38
|
+
install_stream,
|
|
39
|
+
open_url,
|
|
40
|
+
scan_environment,
|
|
41
|
+
)
|
|
29
42
|
|
|
30
43
|
|
|
31
44
|
class SetupInstallRequest(BaseModel):
|
|
@@ -15,6 +15,7 @@ from fastapi import APIRouter, Cookie, HTTPException, Request
|
|
|
15
15
|
from fastapi.responses import FileResponse, HTMLResponse
|
|
16
16
|
|
|
17
17
|
from latticeai.api.ui_redirects import app_redirect
|
|
18
|
+
from latticeai.core.quiet import quiet
|
|
18
19
|
|
|
19
20
|
PRODUCTION_CSP = (
|
|
20
21
|
"default-src 'self'; "
|
|
@@ -282,7 +283,7 @@ def create_static_routes_router(
|
|
|
282
283
|
result["gpu_mem_gb"] = round(gpu_bytes / (1024 ** 3), 2)
|
|
283
284
|
result["gpu_mem_pct"] = round(gpu_bytes / total_bytes * 100, 1) if total_bytes else 0.0
|
|
284
285
|
except Exception:
|
|
285
|
-
|
|
286
|
+
quiet()
|
|
286
287
|
except Exception as e:
|
|
287
288
|
result["error"] = str(e)
|
|
288
289
|
return result
|
package/latticeai/api/tools.py
CHANGED
|
@@ -14,23 +14,27 @@ from fastapi import APIRouter, File, HTTPException, Request, UploadFile
|
|
|
14
14
|
from fastapi.responses import FileResponse, Response
|
|
15
15
|
from pydantic import BaseModel
|
|
16
16
|
|
|
17
|
+
from lattice_brain.runtime.hooks import dispatch_tool
|
|
17
18
|
from latticeai.api.computer_use import create_computer_use_router
|
|
18
19
|
from latticeai.api.local_files import create_local_files_router
|
|
19
|
-
from lattice_brain.runtime.hooks import dispatch_tool
|
|
20
20
|
from latticeai.api.mcp import create_mcp_router
|
|
21
21
|
from latticeai.api.permissions import create_permissions_router
|
|
22
|
-
from latticeai.services.
|
|
22
|
+
from latticeai.services.router_context import ToolRouterContext
|
|
23
23
|
from latticeai.services.tool_dispatch import (
|
|
24
24
|
TOOL_GOVERNANCE,
|
|
25
|
-
TOOL_GOVERNANCE_DEFAULT as _TOOL_GOVERNANCE_DEFAULT,
|
|
26
|
-
check_tool_role as _check_tool_role,
|
|
27
|
-
get_tool_permission,
|
|
28
25
|
enforce_tool_policy,
|
|
26
|
+
get_tool_permission,
|
|
29
27
|
list_tool_permissions,
|
|
30
28
|
tool_registry_diagnostics,
|
|
31
29
|
tool_registry_manifest,
|
|
32
30
|
)
|
|
33
|
-
from latticeai.services.
|
|
31
|
+
from latticeai.services.tool_dispatch import (
|
|
32
|
+
TOOL_GOVERNANCE_DEFAULT as _TOOL_GOVERNANCE_DEFAULT,
|
|
33
|
+
)
|
|
34
|
+
from latticeai.services.tool_dispatch import (
|
|
35
|
+
check_tool_role as _check_tool_role,
|
|
36
|
+
)
|
|
37
|
+
from latticeai.services.upload_service import process_uploaded_document
|
|
34
38
|
from latticeai.tools import (
|
|
35
39
|
AGENT_ROOT,
|
|
36
40
|
ToolError,
|
|
@@ -39,7 +43,6 @@ from latticeai.tools import (
|
|
|
39
43
|
create_pdf,
|
|
40
44
|
create_pptx,
|
|
41
45
|
create_xlsx,
|
|
42
|
-
read_document,
|
|
43
46
|
deploy_project,
|
|
44
47
|
edit_file,
|
|
45
48
|
git_diff,
|
|
@@ -49,8 +52,8 @@ from latticeai.tools import (
|
|
|
49
52
|
grep,
|
|
50
53
|
inspect_html,
|
|
51
54
|
knowledge_save,
|
|
52
|
-
knowledge_search,
|
|
53
55
|
knowledge_scope_root,
|
|
56
|
+
knowledge_search,
|
|
54
57
|
knowledge_tree,
|
|
55
58
|
list_dir,
|
|
56
59
|
network_status,
|
|
@@ -58,6 +61,7 @@ from latticeai.tools import (
|
|
|
58
61
|
obsidian_search,
|
|
59
62
|
obsidian_tree,
|
|
60
63
|
preview_url,
|
|
64
|
+
read_document,
|
|
61
65
|
read_file,
|
|
62
66
|
run_command,
|
|
63
67
|
search_files,
|
|
@@ -19,6 +19,8 @@ from typing import Any, Callable, Optional
|
|
|
19
19
|
|
|
20
20
|
from fastapi import APIRouter, File, Form, HTTPException, Request, UploadFile
|
|
21
21
|
|
|
22
|
+
from latticeai.core.quiet import quiet
|
|
23
|
+
|
|
22
24
|
LOGGER = logging.getLogger(__name__)
|
|
23
25
|
|
|
24
26
|
|
|
@@ -70,7 +72,7 @@ def create_voice_capture_router(
|
|
|
70
72
|
try:
|
|
71
73
|
tmp_path.unlink(missing_ok=True)
|
|
72
74
|
except OSError:
|
|
73
|
-
|
|
75
|
+
quiet()
|
|
74
76
|
if append_audit_event is not None:
|
|
75
77
|
try:
|
|
76
78
|
append_audit_event(
|
|
@@ -75,10 +75,10 @@ def create_workflow_designer_router(
|
|
|
75
75
|
from lattice_brain.workflow import (
|
|
76
76
|
WorkflowEngine,
|
|
77
77
|
WorkflowError,
|
|
78
|
-
validate_definition,
|
|
79
78
|
export_workflow,
|
|
80
79
|
import_workflow,
|
|
81
80
|
legacy_steps_from_nodes,
|
|
81
|
+
validate_definition,
|
|
82
82
|
)
|
|
83
83
|
|
|
84
84
|
router = APIRouter()
|
|
@@ -23,7 +23,6 @@ from pydantic import BaseModel
|
|
|
23
23
|
from latticeai.api.ui_redirects import app_redirect
|
|
24
24
|
from latticeai.services.app_context import AppContext
|
|
25
25
|
|
|
26
|
-
|
|
27
26
|
# ── Request models (workspace-only; moved verbatim from server_app) ──────────
|
|
28
27
|
|
|
29
28
|
class WorkspaceOnboardingStepRequest(BaseModel):
|
|
@@ -290,8 +289,8 @@ def create_workspace_router(context: AppContext) -> APIRouter:
|
|
|
290
289
|
# not_recommended) for this machine, used by the onboarding model step.
|
|
291
290
|
catalog = None
|
|
292
291
|
try:
|
|
293
|
-
from latticeai.setup.auto_setup import probe as auto_setup_probe
|
|
294
292
|
from latticeai.services.model_recommendation import recommend_catalog
|
|
293
|
+
from latticeai.setup.auto_setup import probe as auto_setup_probe
|
|
295
294
|
profile = await asyncio.to_thread(lambda: auto_setup_probe().to_json())
|
|
296
295
|
catalog = recommend_catalog(profile, engine="local_mlx")
|
|
297
296
|
except Exception as exc: # pragma: no cover - recommendation is best-effort
|