ltcai 11.5.0 → 11.5.2
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 +86 -47
- package/docs/CHANGELOG.md +76 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/HYBRID_CLOUD_KG_STREAMING.md +8 -4
- package/docs/MULTI_AGENT_RUNTIME.md +1 -1
- 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/WORKFLOW_DESIGNER.md +1 -1
- package/docs/architecture.md +4 -4
- package/docs/kg-schema.md +1 -1
- package/docs/v11.5.1_RUST_FULL_LOOP_PLAN.md +85 -0
- package/docs/v11.5.2_TIGHT_SHIP_PLAN.md +144 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/graph/proactive.py +1 -1
- package/lattice_brain/graph/projection/v2_schema.py +0 -28
- package/lattice_brain/graph/vector_index/base.py +0 -3
- package/lattice_brain/graph/vector_index/brute_force.py +0 -4
- package/lattice_brain/graph/vector_index/hnsw.py +0 -3
- package/lattice_brain/graph/vector_index/quantized.py +0 -3
- package/lattice_brain/ingestion_jobs.py +0 -3
- package/lattice_brain/memory.py +0 -27
- package/lattice_brain/portability/sharing.py +0 -4
- package/lattice_brain/quality.py +0 -49
- package/lattice_brain/runtime/agent_runtime.py +1 -1
- package/lattice_brain/runtime/hooks.py +0 -13
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/lattice_brain/sealed_box.py +0 -4
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +12 -12
- package/latticeai/api/agent_worker_seam.py +389 -0
- package/latticeai/api/auth.py +25 -2
- package/latticeai/api/chat.py +3 -3
- package/latticeai/api/chat_agent_http.py +3 -12
- package/latticeai/api/chat_helpers.py +10 -13
- package/latticeai/api/computer_use.py +31 -42
- package/latticeai/api/health.py +21 -1
- package/latticeai/api/local_files.py +14 -0
- package/latticeai/api/permissions.py +38 -12
- package/latticeai/api/search.py +24 -0
- package/latticeai/api/static_routes.py +4 -1
- package/latticeai/cli/runtime.py +7 -3
- package/latticeai/core/config.py +47 -3
- package/latticeai/core/csrf.py +28 -2
- package/latticeai/core/embedding_providers/__init__.py +3 -5
- package/latticeai/core/embedding_providers/base.py +1 -1
- package/latticeai/core/embedding_providers/text.py +1 -1
- package/latticeai/core/enterprise.py +1 -4
- package/latticeai/core/http_origin.py +146 -0
- package/latticeai/core/invitations.py +3 -2
- package/latticeai/core/io_utils.py +2 -11
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/messages.py +40 -0
- package/latticeai/core/model_compat.py +2 -8
- package/latticeai/core/module_probe.py +37 -0
- package/latticeai/core/run_explain.py +5 -2
- package/latticeai/core/run_store.py +2 -2
- package/latticeai/core/security.py +13 -0
- package/latticeai/core/sessions.py +3 -2
- package/latticeai/core/sse.py +25 -0
- package/latticeai/core/users.py +0 -9
- package/latticeai/core/workspace_graph_trace.py +2 -8
- package/latticeai/core/workspace_os.py +0 -10
- package/latticeai/core/workspace_os_constants.py +1 -1
- package/latticeai/core/workspace_os_utils.py +34 -0
- package/latticeai/core/workspace_review_items.py +2 -8
- package/latticeai/core/workspace_runs.py +2 -8
- package/latticeai/core/workspace_skills.py +2 -7
- package/latticeai/models/router/documents.py +0 -35
- package/latticeai/models/router/registry.py +0 -4
- package/latticeai/runtime/access_runtime.py +16 -4
- package/latticeai/runtime/build_phases/features.py +25 -2
- package/latticeai/runtime/build_phases/web.py +2 -0
- package/latticeai/runtime/feature_toggle_wiring.py +12 -18
- package/latticeai/runtime/network_boundary_wiring.py +8 -19
- package/latticeai/runtime/permission_mode_wiring.py +6 -13
- package/latticeai/runtime/router_registration.py +4 -0
- package/latticeai/runtime/runtime_context.py +1 -13
- package/latticeai/runtime/service_singletons.py +55 -0
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/brain_intelligence/proposals.py +0 -5
- package/latticeai/services/change_proposals.py +2 -36
- package/latticeai/services/chronicle.py +4 -6
- package/latticeai/services/command_center.py +4 -4
- package/latticeai/services/evidence_actions.py +5 -2
- package/latticeai/services/hybrid_chat.py +2 -2
- package/latticeai/services/hybrid_policy.py +8 -57
- package/latticeai/services/mode_store.py +132 -0
- package/latticeai/services/model_capability_registry.py +0 -15
- package/latticeai/services/model_catalog.py +0 -7
- package/latticeai/services/model_loading.py +3 -2
- package/latticeai/services/model_runtime/__init__.py +0 -57
- package/latticeai/services/model_runtime/engines.py +27 -128
- package/latticeai/services/model_runtime/loading.py +2 -2
- package/latticeai/services/network_boundary_service.py +7 -44
- package/latticeai/services/permission_mode_service.py +8 -56
- package/latticeai/services/product_readiness.py +1 -1
- package/latticeai/services/setup_detection.py +67 -1
- package/latticeai/services/tool_dispatch.py +0 -4
- package/latticeai/services/upload_service.py +7 -20
- package/latticeai/services/workspace_service.py +0 -6
- package/latticeai/setup/auto_setup.py +15 -26
- package/latticeai/setup/wizard/catalog.py +5 -2
- package/latticeai/setup/wizard/detect.py +6 -27
- package/latticeai/setup/wizard/paths.py +2 -5
- package/latticeai/setup/wizard/plans.py +2 -2
- package/package.json +2 -4
- package/scripts/bump_version.py +14 -1
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/check_legacy_debt.mjs +1 -1
- package/scripts/check_server_i18n.mjs +1 -0
- package/scripts/generate_agent_loop_fixtures.py +994 -0
- package/scripts/generate_rust_parity_fixtures.py +72 -161
- package/scripts/parity_fixture_corpus_context.py +162 -0
- package/scripts/parity_fixture_corpus_docgen.py +341 -0
- package/scripts/release_screen_claims.json +20 -0
- package/src-tauri/Cargo.lock +13 -7
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/backend.rs +76 -2
- package/src-tauri/src/main.rs +13 -3
- package/src-tauri/tauri.conf.json +2 -2
- package/static/app/asset-manifest.json +41 -41
- package/static/app/assets/{Act-CWnxSCgN.js → Act-DcQizkl1.js} +1 -1
- package/static/app/assets/{AdminConsole-BEQYU6kF.js → AdminConsole-cf4npybT.js} +1 -1
- package/static/app/assets/{Brain-DWu1BhFg.js → Brain-3VCSHFcn.js} +1 -1
- package/static/app/assets/{BrainHome-95Hilr9R.js → BrainHome-Qm8eaztx.js} +1 -1
- package/static/app/assets/{BrainSignals-QdeqCpAF.js → BrainSignals-DS9BtKOW.js} +1 -1
- package/static/app/assets/{Capture-BHpCxnzb.js → Capture-DiQ219jW.js} +1 -1
- package/static/app/assets/{Chronicle-B4xYKoed.js → Chronicle-BGvuAchH.js} +1 -1
- package/static/app/assets/{CommandPalette-BVXnttSz.js → CommandPalette-Bqhm0Urn.js} +1 -1
- package/static/app/assets/{Library-DgYcHome.js → Library-BV6NnF0a.js} +1 -1
- package/static/app/assets/{LivingBrain-CrJLDbf7.js → LivingBrain-GzenJchP.js} +1 -1
- package/static/app/assets/{ProductFlow-DFlScKoJ.js → ProductFlow-DEP6-vML.js} +1 -1
- package/static/app/assets/{ReviewCard-Cy5f48Pj.js → ReviewCard-CNZ7XjWG.js} +1 -1
- package/static/app/assets/{System-NF8IfhTa.js → System-CieofHQa.js} +1 -1
- package/static/app/assets/arrow-left-kfsrk0mv.js +1 -0
- package/static/app/assets/{bot-CucuhLhm.js → bot-B_K1Tdmw.js} +1 -1
- package/static/app/assets/{brain-BBnSryW_.js → brain-DWyaV1L1.js} +1 -1
- package/static/app/assets/{button-C2GUj2Ai.js → button-aTn4s84A.js} +1 -1
- package/static/app/assets/circle-check-qqLug9nU.js +1 -0
- package/static/app/assets/{circle-pause-CbkWzBmG.js → circle-pause-xKgeGXkT.js} +1 -1
- package/static/app/assets/{circle-play-7lEaqHdJ.js → circle-play-DkT6tYPX.js} +1 -1
- package/static/app/assets/{cpu-DAlCXlIy.js → cpu-85xYObUC.js} +1 -1
- package/static/app/assets/{download-RNhuuJwh.js → download-B5Fm7YXo.js} +1 -1
- package/static/app/assets/{folder-open-CLW4odzM.js → folder-open-kk2Xa52u.js} +1 -1
- package/static/app/assets/{hard-drive-NKEiDIAJ.js → hard-drive-DkA3zBW_.js} +1 -1
- package/static/app/assets/{index-DMurvUuR.js → index-BMPdTmlY.js} +3 -3
- package/static/app/assets/index-DxmOfNRi.css +2 -0
- package/static/app/assets/{input-D2UhPC1X.js → input-B0nRf2jO.js} +1 -1
- package/static/app/assets/{link-2-6amKbP_P.js → link-2-Dwb4gnTc.js} +1 -1
- package/static/app/assets/{permissionCopy-Cu9TZtdR.js → permissionCopy-CQDUBrOZ.js} +1 -1
- package/static/app/assets/{primitives-gPsccucr.js → primitives-SNp0LRJz.js} +1 -1
- package/static/app/assets/search-BcHqkjoy.js +1 -0
- package/static/app/assets/{share-2-Bau7KkPq.js → share-2-BsrxFglO.js} +1 -1
- package/static/app/assets/{shield-alert-BufNYypi.js → shield-alert-5BStfp2_.js} +1 -1
- package/static/app/assets/{textarea-BQnVWhYs.js → textarea-Cg8IUA-k.js} +1 -1
- package/static/app/assets/{useFocusTrap-B3_w60si.js → useFocusTrap-CYKvE46M.js} +1 -1
- package/static/app/assets/{useMutation-BHhCflT6.js → useMutation-CSn9t1op.js} +1 -1
- package/static/app/assets/{useQuery-rBWfI-5t.js → useQuery-CY2OI2uy.js} +1 -1
- package/static/app/assets/{utils-V_5-wxr5.js → utils-Ddol2RWD.js} +1 -1
- package/static/app/assets/{workspace-K1zjYUHj.js → workspace-BqDwOz_p.js} +1 -1
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/desktop/electron/README.md +0 -9
- package/desktop/electron/main.cjs +0 -58
- package/desktop/electron/preload.cjs +0 -5
- package/latticeai/core/graph_curator.py +0 -11
- package/latticeai/core/hooks.py +0 -11
- package/latticeai/core/local_embeddings.py +0 -104
- package/latticeai/core/multi_agent.py +0 -11
- package/latticeai/core/workflow_engine.py +0 -11
- package/latticeai/services/ingestion.py +0 -11
- package/latticeai/services/kg_portability.py +0 -11
- package/latticeai/services/multimodal_streaming.py +0 -129
- package/scripts/measure_brain_home_fill.mjs +0 -141
- package/static/app/assets/arrow-left-DwkSYrjR.js +0 -1
- package/static/app/assets/circle-check-CxOVPwYq.js +0 -1
- package/static/app/assets/index-BLPb5lmE.css +0 -2
- package/static/app/assets/search-Cj_TKk_2.js +0 -1
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
"""Local deterministic embeddings for Lattice AI search.
|
|
2
|
-
|
|
3
|
-
The v3 backend needs a local-first vector signal without introducing a cloud
|
|
4
|
-
runtime requirement. This module provides a small feature-hashing embedder that
|
|
5
|
-
is deterministic, cheap to run, and good enough for indexing/search tests. A
|
|
6
|
-
future runtime can swap the implementation behind the same interface when a
|
|
7
|
-
local model server is available.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
import hashlib
|
|
13
|
-
import math
|
|
14
|
-
import os
|
|
15
|
-
import re
|
|
16
|
-
import struct
|
|
17
|
-
from dataclasses import dataclass
|
|
18
|
-
from typing import Iterable, List
|
|
19
|
-
|
|
20
|
-
DEFAULT_EMBEDDING_DIM = int(os.getenv("LATTICEAI_VECTOR_DIM", "384"))
|
|
21
|
-
EMBEDDING_MODEL_ID = f"lattice-local-hash-v1:{DEFAULT_EMBEDDING_DIM}"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def embedding_model_id(dim: int) -> str:
|
|
25
|
-
return f"lattice-local-hash-v1:{int(dim)}"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def _tokenize(text: str) -> List[str]:
|
|
29
|
-
raw = str(text or "").lower()
|
|
30
|
-
tokens = re.findall(r"[a-z0-9][a-z0-9_.:/+-]{1,}|[가-힣]{2,}", raw)
|
|
31
|
-
features: List[str] = []
|
|
32
|
-
for token in tokens:
|
|
33
|
-
features.append(f"tok:{token}")
|
|
34
|
-
if len(token) >= 5 and re.search(r"[a-z]", token):
|
|
35
|
-
for i in range(0, len(token) - 2):
|
|
36
|
-
features.append(f"tri:{token[i:i+3]}")
|
|
37
|
-
if re.search(r"[가-힣]", token) and len(token) >= 3:
|
|
38
|
-
for i in range(0, len(token) - 1):
|
|
39
|
-
features.append(f"ko:{token[i:i+2]}")
|
|
40
|
-
return features
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def _hash_to_index(feature: str, dim: int) -> tuple[int, float]:
|
|
44
|
-
digest = hashlib.blake2b(feature.encode("utf-8"), digest_size=8).digest()
|
|
45
|
-
value = int.from_bytes(digest, "big", signed=False)
|
|
46
|
-
sign = 1.0 if (value & 1) == 0 else -1.0
|
|
47
|
-
return value % dim, sign
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
@dataclass(frozen=True)
|
|
51
|
-
class LocalEmbeddingModel:
|
|
52
|
-
"""Deterministic feature-hashing embedder.
|
|
53
|
-
|
|
54
|
-
The output vectors are L2-normalized, so cosine similarity is just a dot
|
|
55
|
-
product. No network access, model download, or global mutable state is
|
|
56
|
-
required.
|
|
57
|
-
"""
|
|
58
|
-
|
|
59
|
-
dim: int = DEFAULT_EMBEDDING_DIM
|
|
60
|
-
model_id: str = EMBEDDING_MODEL_ID
|
|
61
|
-
|
|
62
|
-
def __post_init__(self) -> None:
|
|
63
|
-
if self.model_id == EMBEDDING_MODEL_ID and self.dim != DEFAULT_EMBEDDING_DIM:
|
|
64
|
-
object.__setattr__(self, "model_id", embedding_model_id(self.dim))
|
|
65
|
-
|
|
66
|
-
def embed(self, text: str) -> List[float]:
|
|
67
|
-
vector = [0.0] * self.dim
|
|
68
|
-
features = _tokenize(text)
|
|
69
|
-
if not features:
|
|
70
|
-
return vector
|
|
71
|
-
for feature in features:
|
|
72
|
-
index, sign = _hash_to_index(feature, self.dim)
|
|
73
|
-
vector[index] += sign
|
|
74
|
-
norm = math.sqrt(sum(value * value for value in vector))
|
|
75
|
-
if norm <= 0:
|
|
76
|
-
return vector
|
|
77
|
-
return [value / norm for value in vector]
|
|
78
|
-
|
|
79
|
-
def similarity(self, left: Iterable[float], right: Iterable[float]) -> float:
|
|
80
|
-
# strict=True: a dimension mismatch means the two vectors came from
|
|
81
|
-
# different embedding models. Truncating to the shorter one produces a
|
|
82
|
-
# plausible-looking similarity that is meaningless — exactly the silent
|
|
83
|
-
# wrongness this codebase keeps finding. Callers that can hit a model
|
|
84
|
-
# swap already handle failure and fall back to lexical search.
|
|
85
|
-
left_v, right_v = list(left), list(right)
|
|
86
|
-
if len(left_v) != len(right_v):
|
|
87
|
-
raise ValueError(
|
|
88
|
-
f"embedding dimension mismatch: {len(left_v)} vs {len(right_v)}; "
|
|
89
|
-
"the vector index was built with a different model"
|
|
90
|
-
)
|
|
91
|
-
return float(sum(a * b for a, b in zip(left_v, right_v, strict=True)))
|
|
92
|
-
|
|
93
|
-
def encode(self, vector: Iterable[float]) -> bytes:
|
|
94
|
-
values = list(vector)
|
|
95
|
-
return struct.pack(f"<{len(values)}f", *values)
|
|
96
|
-
|
|
97
|
-
def decode(self, payload: bytes, dim: int | None = None) -> List[float]:
|
|
98
|
-
if not payload:
|
|
99
|
-
return []
|
|
100
|
-
count = int(dim or self.dim)
|
|
101
|
-
expected = count * 4
|
|
102
|
-
if len(payload) != expected:
|
|
103
|
-
count = len(payload) // 4
|
|
104
|
-
return list(struct.unpack(f"<{count}f", payload[: count * 4]))
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
"""Compatibility shim: physically moved to ``lattice_brain.runtime.multi_agent``.
|
|
2
|
-
|
|
3
|
-
Aliases itself to the physical module so identity, module-level state, and
|
|
4
|
-
monkeypatching keep working through the old import path.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import sys
|
|
8
|
-
|
|
9
|
-
import lattice_brain.runtime.multi_agent as _impl
|
|
10
|
-
|
|
11
|
-
sys.modules[__name__] = _impl
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
"""Compatibility shim: physically moved to ``lattice_brain.workflow``.
|
|
2
|
-
|
|
3
|
-
Aliases itself to the physical module so identity, module-level state, and
|
|
4
|
-
monkeypatching keep working through the old import path.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import sys
|
|
8
|
-
|
|
9
|
-
import lattice_brain.workflow as _impl
|
|
10
|
-
|
|
11
|
-
sys.modules[__name__] = _impl
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
"""Compatibility shim: physically moved to ``lattice_brain.ingestion``.
|
|
2
|
-
|
|
3
|
-
Aliases itself to the physical module so identity, module-level state, and
|
|
4
|
-
monkeypatching keep working through the old import path.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import sys
|
|
8
|
-
|
|
9
|
-
import lattice_brain.ingestion as _impl
|
|
10
|
-
|
|
11
|
-
sys.modules[__name__] = _impl
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
"""Compatibility shim: physically moved to ``lattice_brain.portability``.
|
|
2
|
-
|
|
3
|
-
Aliases itself to the physical module so identity, module-level state, and
|
|
4
|
-
monkeypatching keep working through the old import path.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import sys
|
|
8
|
-
|
|
9
|
-
import lattice_brain.portability as _impl
|
|
10
|
-
|
|
11
|
-
sys.modules[__name__] = _impl
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
"""Multimodal / video streaming contracts behind NetworkBoundaryMode (Phase 3).
|
|
2
|
-
|
|
3
|
-
Video generation and other multimodal cloud calls share the same boundary dial
|
|
4
|
-
as text: nothing leaves the machine unless mode is CLOUD_ALLOWED *and* the
|
|
5
|
-
hybrid policy has ``allow_multimodal=True``.
|
|
6
|
-
|
|
7
|
-
This module is intentionally adapter-shaped: concrete providers (Runway, Luma,
|
|
8
|
-
Veo-compatible endpoints, etc.) plug in later without changing the chat path.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
from __future__ import annotations
|
|
12
|
-
|
|
13
|
-
from dataclasses import dataclass, field
|
|
14
|
-
from typing import Any, AsyncIterator, Dict, List, Optional, Protocol
|
|
15
|
-
|
|
16
|
-
from latticeai.core.network_boundary import (
|
|
17
|
-
NetworkBoundaryMode,
|
|
18
|
-
normalize_network_mode,
|
|
19
|
-
)
|
|
20
|
-
from latticeai.services.hybrid_context import MinimalContext
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@dataclass
|
|
24
|
-
class MultimodalTurnResult:
|
|
25
|
-
user_message: str
|
|
26
|
-
media_urls: List[str] = field(default_factory=list)
|
|
27
|
-
media_kind: str = "video" # video | image | audio
|
|
28
|
-
answer_text: str = ""
|
|
29
|
-
sent_node_ids: List[str] = field(default_factory=list)
|
|
30
|
-
provider: str = ""
|
|
31
|
-
model: str = ""
|
|
32
|
-
usage: Dict[str, Any] = field(default_factory=dict)
|
|
33
|
-
|
|
34
|
-
def to_dict(self) -> Dict[str, Any]:
|
|
35
|
-
return {
|
|
36
|
-
"user_message": self.user_message,
|
|
37
|
-
"media_urls": list(self.media_urls),
|
|
38
|
-
"media_kind": self.media_kind,
|
|
39
|
-
"answer_text": self.answer_text,
|
|
40
|
-
"sent_node_ids": list(self.sent_node_ids),
|
|
41
|
-
"provider": self.provider,
|
|
42
|
-
"model": self.model,
|
|
43
|
-
"usage": dict(self.usage),
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class MultimodalAdapter(Protocol):
|
|
48
|
-
# See CloudLLMAdapter.stream: an async generator is declared with `def`.
|
|
49
|
-
def stream_media(
|
|
50
|
-
self,
|
|
51
|
-
*,
|
|
52
|
-
prompt: str,
|
|
53
|
-
context: str,
|
|
54
|
-
model: Optional[str] = None,
|
|
55
|
-
) -> AsyncIterator[Any]:
|
|
56
|
-
"""Yield progress events, then a final event with media_urls.
|
|
57
|
-
|
|
58
|
-
Typed as ``Any`` rather than ``Dict[str, Any]`` because adapters are
|
|
59
|
-
supplied by callers: the bridge validates each event's shape rather
|
|
60
|
-
than trusting the annotation.
|
|
61
|
-
"""
|
|
62
|
-
...
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
class MultimodalStreamingBridge:
|
|
66
|
-
"""Boundary-gated multimodal turn orchestrator."""
|
|
67
|
-
|
|
68
|
-
def __init__(self, adapter: Optional[MultimodalAdapter] = None) -> None:
|
|
69
|
-
self._adapter = adapter
|
|
70
|
-
|
|
71
|
-
async def run_turn(
|
|
72
|
-
self,
|
|
73
|
-
*,
|
|
74
|
-
user_message: str,
|
|
75
|
-
minimal: MinimalContext,
|
|
76
|
-
mode: NetworkBoundaryMode | str,
|
|
77
|
-
allow_multimodal: bool,
|
|
78
|
-
model: Optional[str] = None,
|
|
79
|
-
) -> MultimodalTurnResult:
|
|
80
|
-
mode = normalize_network_mode(mode)
|
|
81
|
-
if mode != NetworkBoundaryMode.CLOUD_ALLOWED:
|
|
82
|
-
raise PermissionError(
|
|
83
|
-
f"multimodal cloud refused under NetworkBoundaryMode={mode.value!r}"
|
|
84
|
-
)
|
|
85
|
-
if not allow_multimodal:
|
|
86
|
-
raise PermissionError(
|
|
87
|
-
"multimodal cloud is disabled by hybrid policy (allow_multimodal=false)"
|
|
88
|
-
)
|
|
89
|
-
if self._adapter is None:
|
|
90
|
-
return MultimodalTurnResult(
|
|
91
|
-
user_message=user_message,
|
|
92
|
-
media_urls=[],
|
|
93
|
-
answer_text=(
|
|
94
|
-
"[multimodal adapter not configured] "
|
|
95
|
-
f"Would stream media grounded on {len(minimal.node_ids)} local node(s)."
|
|
96
|
-
),
|
|
97
|
-
sent_node_ids=list(minimal.node_ids),
|
|
98
|
-
provider="none",
|
|
99
|
-
model=model or "",
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
urls: List[str] = []
|
|
103
|
-
notes: List[str] = []
|
|
104
|
-
async for event in self._adapter.stream_media(
|
|
105
|
-
prompt=user_message,
|
|
106
|
-
context=minimal.compact_text,
|
|
107
|
-
model=model,
|
|
108
|
-
):
|
|
109
|
-
if not isinstance(event, dict):
|
|
110
|
-
continue
|
|
111
|
-
if event.get("media_url"):
|
|
112
|
-
urls.append(str(event["media_url"]))
|
|
113
|
-
if event.get("text"):
|
|
114
|
-
notes.append(str(event["text"]))
|
|
115
|
-
return MultimodalTurnResult(
|
|
116
|
-
user_message=user_message,
|
|
117
|
-
media_urls=urls,
|
|
118
|
-
answer_text="\n".join(notes),
|
|
119
|
-
sent_node_ids=list(minimal.node_ids),
|
|
120
|
-
provider=getattr(self._adapter, "provider_name", "multimodal"),
|
|
121
|
-
model=str(model or getattr(self._adapter, "default_model", "")),
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
__all__ = [
|
|
126
|
-
"MultimodalTurnResult",
|
|
127
|
-
"MultimodalAdapter",
|
|
128
|
-
"MultimodalStreamingBridge",
|
|
129
|
-
]
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Measure Brain chat home (screen 04) content fill against the viewport.
|
|
4
|
-
*
|
|
5
|
-
* The layout rebuild requires the empty home's content bottom to reach at
|
|
6
|
-
* least 90% of the 920px capture viewport — not "looks better" prose.
|
|
7
|
-
*
|
|
8
|
-
* Walks the same onboarding path as capture_release_evidence.mjs so #/brain
|
|
9
|
-
* is the empty first-run home against the visual mock server.
|
|
10
|
-
*
|
|
11
|
-
* Exit 0 when bottom/viewport >= --min-ratio (default 0.90), else 1.
|
|
12
|
-
*/
|
|
13
|
-
import { chromium } from "playwright";
|
|
14
|
-
import { spawn } from "node:child_process";
|
|
15
|
-
|
|
16
|
-
const repoRoot = process.cwd();
|
|
17
|
-
const port = Number(process.env.LTCAI_RELEASE_EVIDENCE_PORT || 4937);
|
|
18
|
-
const baseURL = `http://127.0.0.1:${port}`;
|
|
19
|
-
const minRatio = Number(process.env.LTCAI_HOME_FILL_MIN || "0.90");
|
|
20
|
-
|
|
21
|
-
const server = spawn(process.execPath, ["tests/visual/mock_server.cjs"], {
|
|
22
|
-
cwd: repoRoot,
|
|
23
|
-
env: { ...process.env, LTCAI_VISUAL_PORT: String(port) },
|
|
24
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
async function waitForServer() {
|
|
28
|
-
const deadline = Date.now() + 15000;
|
|
29
|
-
while (Date.now() < deadline) {
|
|
30
|
-
try {
|
|
31
|
-
const res = await fetch(`${baseURL}/health`);
|
|
32
|
-
if (res.ok) return;
|
|
33
|
-
} catch {
|
|
34
|
-
/* retry */
|
|
35
|
-
}
|
|
36
|
-
await new Promise((r) => setTimeout(r, 150));
|
|
37
|
-
}
|
|
38
|
-
throw new Error(`visual server did not start on ${baseURL}`);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function main() {
|
|
42
|
-
await waitForServer();
|
|
43
|
-
const browser = await chromium.launch({ headless: true });
|
|
44
|
-
const context = await browser.newContext({
|
|
45
|
-
viewport: { width: 1440, height: 920 },
|
|
46
|
-
});
|
|
47
|
-
const page = await context.newPage();
|
|
48
|
-
page.setDefaultTimeout(15000);
|
|
49
|
-
|
|
50
|
-
// Same first-run reset as release capture — without this the product flow
|
|
51
|
-
// may already be complete and the wake button never appears.
|
|
52
|
-
await page.addInitScript(() => {
|
|
53
|
-
localStorage.removeItem("lattice.productFlow.complete");
|
|
54
|
-
localStorage.removeItem("lattice.productFlow.user");
|
|
55
|
-
localStorage.setItem("lattice.language", "ko");
|
|
56
|
-
localStorage.setItem("lattice.mode", "basic");
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
await page.goto(`${baseURL}/app`, { waitUntil: "networkidle" });
|
|
60
|
-
const korean = page.getByRole("button", { name: "한국어" });
|
|
61
|
-
if (await korean.count()) {
|
|
62
|
-
await korean.click();
|
|
63
|
-
}
|
|
64
|
-
await page.getByRole("button", { name: "Brain 지금 깨우기" }).click();
|
|
65
|
-
await page.getByText("이 Brain의 주인을 정합니다.").waitFor();
|
|
66
|
-
await page.getByPlaceholder(/You|나/).fill("Codex");
|
|
67
|
-
await page.getByPlaceholder("you@local").fill("codex@local");
|
|
68
|
-
await page.getByPlaceholder("로컬 Brain 비밀번호").fill("Lattice123");
|
|
69
|
-
await page.getByRole("button", { name: "내 Brain 시작하기" }).click();
|
|
70
|
-
await page.getByText("추천대로 시작하세요.").waitFor();
|
|
71
|
-
await page.getByRole("button", { name: "추천으로 바로 시작" }).click();
|
|
72
|
-
await page.getByText("모델을 준비하고 시작합니다.").waitFor();
|
|
73
|
-
await page.getByRole("button", { name: "준비하고 시작하기" }).click();
|
|
74
|
-
await page.waitForTimeout(180);
|
|
75
|
-
await page
|
|
76
|
-
.getByRole("button", { name: /시작|들어가|Brain 열기|Open Brain/i })
|
|
77
|
-
.first()
|
|
78
|
-
.click({ timeout: 5000 })
|
|
79
|
-
.catch(() => {});
|
|
80
|
-
await page.locator("main[aria-label='Lattice Brain']").waitFor({ timeout: 15000 });
|
|
81
|
-
await page.waitForTimeout(250);
|
|
82
|
-
|
|
83
|
-
const metrics = await page.evaluate(() => {
|
|
84
|
-
const main = document.querySelector("main[aria-label='Lattice Brain']");
|
|
85
|
-
const home =
|
|
86
|
-
document.querySelector(".brain-centered-home") ||
|
|
87
|
-
document.querySelector(".brain-home-station") ||
|
|
88
|
-
main;
|
|
89
|
-
const quiet = document.querySelector(
|
|
90
|
-
"footer.brain-home-quiet, .brain-home-quiet, .brain-home-shelves",
|
|
91
|
-
);
|
|
92
|
-
const composer = document.querySelector(
|
|
93
|
-
".brain-composer, [data-testid='brain-composer'], textarea",
|
|
94
|
-
);
|
|
95
|
-
const box = (el) => {
|
|
96
|
-
if (!el) return null;
|
|
97
|
-
const r = el.getBoundingClientRect();
|
|
98
|
-
return { top: r.top, bottom: r.bottom, height: r.height, width: r.width };
|
|
99
|
-
};
|
|
100
|
-
const candidates = [home, quiet, composer, main].filter(Boolean);
|
|
101
|
-
let bottom = 0;
|
|
102
|
-
let top = Infinity;
|
|
103
|
-
for (const el of candidates) {
|
|
104
|
-
const r = el.getBoundingClientRect();
|
|
105
|
-
bottom = Math.max(bottom, r.bottom);
|
|
106
|
-
top = Math.min(top, r.top);
|
|
107
|
-
}
|
|
108
|
-
return {
|
|
109
|
-
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
110
|
-
main: box(main),
|
|
111
|
-
home: box(home),
|
|
112
|
-
quiet: box(quiet),
|
|
113
|
-
composer: box(composer),
|
|
114
|
-
contentTop: top === Infinity ? 0 : top,
|
|
115
|
-
contentBottom: bottom,
|
|
116
|
-
};
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
await browser.close();
|
|
120
|
-
|
|
121
|
-
const ratio = metrics.contentBottom / metrics.viewport.height;
|
|
122
|
-
const pct = (100 * ratio).toFixed(1);
|
|
123
|
-
console.log(JSON.stringify({ ...metrics, fillRatio: ratio, fillPct: `${pct}%`, minRatio }, null, 2));
|
|
124
|
-
console.log(
|
|
125
|
-
`brain home fill: contentBottom=${metrics.contentBottom.toFixed(1)}px / viewport=${metrics.viewport.height}px = ${pct}% (need >= ${(100 * minRatio).toFixed(0)}%)`,
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
if (ratio + 1e-6 < minRatio) {
|
|
129
|
-
console.error(
|
|
130
|
-
`brain home fill FAIL: content only reaches ${pct}% of viewport (required ${(100 * minRatio).toFixed(0)}%).`,
|
|
131
|
-
);
|
|
132
|
-
process.exit(1);
|
|
133
|
-
}
|
|
134
|
-
console.log("brain home fill PASS");
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
try {
|
|
138
|
-
await main();
|
|
139
|
-
} finally {
|
|
140
|
-
server.kill();
|
|
141
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{_ as e}from"./utils-V_5-wxr5.js";var t=e(`arrow-left`,[[`path`,{d:`m12 19-7-7 7-7`,key:`1l729n`}],[`path`,{d:`M19 12H5`,key:`x3x0zl`}]]);export{t};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{_ as e}from"./utils-V_5-wxr5.js";var t=e(`circle-check`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]);export{t};
|