ltcai 11.9.0 → 12.0.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 +80 -59
- package/docs/CHANGELOG.md +92 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +267 -119
- package/docs/ENTERPRISE.md +1 -1
- package/docs/MULTI_AGENT_RUNTIME.md +4 -4
- package/docs/ONBOARDING.md +13 -3
- package/docs/OPERATIONS.md +13 -4
- package/docs/REALTIME_COLLABORATION.md +1 -1
- package/docs/ROADMAP.md +113 -0
- package/docs/TRUST_MODEL.md +16 -2
- package/docs/WHY_LATTICE.md +8 -2
- package/docs/WORKFLOW_DESIGNER.md +2 -2
- package/docs/kg-schema.md +51 -5
- package/docs/mcp-tools.md +17 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/graph/_kg_common/extraction.py +459 -105
- package/lattice_brain/graph/_kg_common/normalize.py +305 -0
- package/lattice_brain/graph/_kg_common/patterns.py +275 -0
- package/lattice_brain/graph/_kg_common/relations.py +12 -3
- package/lattice_brain/graph/_kg_common/sections.py +107 -0
- package/lattice_brain/graph/_kg_constants.py +7 -0
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/agent_worker_seam.py +32 -0
- package/latticeai/api/worker_compute.py +78 -3
- package/latticeai/core/embedding_providers/__init__.py +16 -0
- package/latticeai/core/embedding_providers/autodetect.py +302 -0
- package/latticeai/core/embedding_providers/base.py +25 -0
- package/latticeai/core/embedding_providers/profiles.py +44 -0
- package/latticeai/core/embedding_providers/text.py +74 -8
- package/latticeai/core/vector_index/__init__.py +61 -0
- package/latticeai/core/vector_index/hnsw.py +383 -0
- package/latticeai/core/vector_index/sidecar.py +329 -0
- package/latticeai/models/router/generation.py +101 -22
- package/latticeai/models/router/loading.py +109 -4
- package/latticeai/runtime/brain_runtime.py +43 -9
- package/latticeai/runtime/build_phases/worker_profile.py +13 -4
- package/latticeai/services/architecture_readiness.py +2 -2
- package/latticeai/services/product_readiness.py +10 -5
- package/latticeai/services/search_service.py +7 -0
- package/latticeai/tools/__init__.py +6 -1
- package/latticeai/tools/documents.py +12 -0
- package/latticeai/tools/markup.py +152 -0
- package/package.json +2 -1
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/compose_openapi.py +2 -0
- package/scripts/openapi_route_families.json +7 -3
- package/scripts/publish_release.mjs +157 -0
- package/scripts/release_screen_claims.json +12 -0
- package/src-tauri/Cargo.lock +44 -10
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +47 -41
- package/static/app/assets/Act-Cf1L2709.js +2 -0
- package/static/app/assets/AdminConsole-DPAbLTYV.js +1 -0
- package/static/app/assets/Brain-DqamGrj-.js +2 -0
- package/static/app/assets/BrainHome-MHe2_RYs.js +2 -0
- package/static/app/assets/BrainSignals-CQPPfyyH.js +1 -0
- package/static/app/assets/Capture-DGdIH_Zc.js +1 -0
- package/static/app/assets/Chronicle-C-UlCJoJ.js +1 -0
- package/static/app/assets/CommandPalette-WNT4EqUX.js +1 -0
- package/static/app/assets/DigitalBrainExplorer-CEBH5Cwc.js +321 -0
- package/static/app/assets/Library-C6xd1dlf.js +1 -0
- package/static/app/assets/LivingBrain-BEk-0ohw.js +1 -0
- package/static/app/assets/ProductFlow-CZLm5iXh.js +1 -0
- package/static/app/assets/QueryClientProvider-B3OjqSyJ.js +1 -0
- package/static/app/assets/ReviewCard-CEHG6evf.js +3 -0
- package/static/app/assets/RunsListPanel-CLtEJSRW.js +1 -0
- package/static/app/assets/System-CAxwBUXw.js +1 -0
- package/static/app/assets/WorkflowGraph-Dj10RuGE.js +1 -0
- package/static/app/assets/WorkflowsPanel-Kyeh_LIT.js +2 -0
- package/static/app/assets/actHelpers-CtSmK9Dw.js +1 -0
- package/static/app/assets/arrow-left-CRl5EO4D.js +1 -0
- package/static/app/assets/{bot-Bc3Q27YR.js → bot-DhUGRel2.js} +1 -1
- package/static/app/assets/brain-CLkhHsHF.js +1 -0
- package/static/app/assets/button-CmaEqG1T.js +1 -0
- package/static/app/assets/circle-check-CFgejkOS.js +1 -0
- package/static/app/assets/{circle-pause-BGMiV8UU.js → circle-pause-l96izbxj.js} +1 -1
- package/static/app/assets/{circle-play-DoanLHnd.js → circle-play-CrZa25_q.js} +1 -1
- package/static/app/assets/{cpu-DwzNf82m.js → cpu-BaXudqwl.js} +1 -1
- package/static/app/assets/{download-Ddw49yCV.js → download-hCVFPiyc.js} +1 -1
- package/static/app/assets/{folder-open-Brd6Kvto.js → folder-open-CHL82Yp7.js} +1 -1
- package/static/app/assets/{hard-drive-Bu-DTJdB.js → hard-drive-DDzET7lk.js} +1 -1
- package/static/app/assets/{index-CGdg_aq9.css → index-CB93CZWW.css} +1 -1
- package/static/app/assets/index-D2H-wSl6.js +13 -0
- package/static/app/assets/input-Df1CAY_I.js +1 -0
- package/static/app/assets/jsx-runtime-bzQ4Vb5N.js +1 -0
- package/static/app/assets/{link-2-DZ4OA5tJ.js → link-2-xNnTIX1_.js} +1 -1
- package/static/app/assets/{permissionCopy-D9TR0F8b.js → permissionCopy-D3aWHco-.js} +1 -1
- package/static/app/assets/primitives-BioD2slS.js +1 -0
- package/static/app/assets/search-BzBw8YcW.js +1 -0
- package/static/app/assets/{share-2-BC5FirFv.js → share-2-FkzGf8Df.js} +1 -1
- package/static/app/assets/{shield-alert-DUbR2W2s.js → shield-alert-B3dwzik4.js} +1 -1
- package/static/app/assets/sourceMeta-DQSY_tah.js +1 -0
- package/static/app/assets/textarea-P8o6pvOP.js +1 -0
- package/static/app/assets/useFocusTrap-hswOIkXE.js +1 -0
- package/static/app/assets/useMutation-OJLrYSRA.js +1 -0
- package/static/app/assets/workspace-BCuk3Ku9.js +1 -0
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/static/app/assets/Act-B4WT81kh.js +0 -1
- package/static/app/assets/AdminConsole--Wf71m-o.js +0 -1
- package/static/app/assets/Brain-CtYaa26c.js +0 -321
- package/static/app/assets/BrainHome-Be2VPJEc.js +0 -2
- package/static/app/assets/BrainSignals-C0__xgpG.js +0 -1
- package/static/app/assets/Capture-DdNi5Peb.js +0 -1
- package/static/app/assets/Chronicle-B3hNveeI.js +0 -1
- package/static/app/assets/CommandPalette-CIsnSsFL.js +0 -1
- package/static/app/assets/Library-Bl5XClFV.js +0 -1
- package/static/app/assets/LivingBrain-DjkTB_gh.js +0 -1
- package/static/app/assets/ProductFlow-DCUNRNHs.js +0 -1
- package/static/app/assets/ReviewCard-CD3yWvUB.js +0 -3
- package/static/app/assets/System-BJ6jQ_SL.js +0 -1
- package/static/app/assets/arrow-left-6_28Z0qH.js +0 -1
- package/static/app/assets/brain-B9BDrMTe.js +0 -1
- package/static/app/assets/button-D6JcpYcf.js +0 -1
- package/static/app/assets/circle-check-BFu9lD-3.js +0 -1
- package/static/app/assets/index-CWKRRsLW.js +0 -10
- package/static/app/assets/input-CEqsxtil.js +0 -1
- package/static/app/assets/primitives-DORg7Z_7.js +0 -1
- package/static/app/assets/search-0NQ21wXe.js +0 -1
- package/static/app/assets/textarea-jtQcRSXo.js +0 -1
- package/static/app/assets/useFocusTrap-HRemcWId.js +0 -1
- package/static/app/assets/useMutation-DqlFE-Bw.js +0 -1
- package/static/app/assets/useQuery-BizqBNGw.js +0 -1
- package/static/app/assets/utils-WgW4V69R.js +0 -4
- package/static/app/assets/workspace-DSek3jCY.js +0 -1
|
@@ -13,7 +13,16 @@ was requested.
|
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
-
from typing import Any, Callable, Mapping
|
|
16
|
+
from typing import Any, Callable, Mapping, Optional, Tuple
|
|
17
|
+
|
|
18
|
+
from latticeai.core.embedding_providers.autodetect import (
|
|
19
|
+
AUTO_PROVIDER,
|
|
20
|
+
Detection,
|
|
21
|
+
detect_embedder,
|
|
22
|
+
resolve_auto_provider,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
_FALLBACK_NAMES = {"", "hash", "local", "fallback"}
|
|
17
26
|
|
|
18
27
|
|
|
19
28
|
def build_embedder_runtime(
|
|
@@ -21,16 +30,26 @@ def build_embedder_runtime(
|
|
|
21
30
|
config: Any,
|
|
22
31
|
profile: Mapping[str, Any],
|
|
23
32
|
resolve_embedder: Callable[..., Any],
|
|
33
|
+
detect: Optional[Callable[..., Detection]] = None,
|
|
24
34
|
) -> Any:
|
|
25
|
-
"""Resolve the configured embedding provider once.
|
|
35
|
+
"""Resolve the configured embedding provider once.
|
|
26
36
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
The resolution order is the product's, unchanged, with one addition at the
|
|
38
|
+
end: detection. What is on this machine is *always* looked up (so
|
|
39
|
+
``GET /api/embeddings/status`` can say "a real embedder is right there"),
|
|
40
|
+
and it changes the resolution only when the operator asked for it with
|
|
41
|
+
``LATTICEAI_EMBEDDING_PROVIDER=auto``. The finding rides back on the
|
|
42
|
+
resolved object as ``detected`` — an added attribute, so every existing
|
|
43
|
+
reader of ``ResolvedEmbedder`` is untouched.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
provider, model, dim = _configured(config, profile)
|
|
47
|
+
detection = (detect or detect_embedder)(
|
|
48
|
+
configured_provider=provider, configured_model=model
|
|
49
|
+
)
|
|
50
|
+
provider, model, dim = resolve_auto_provider(provider, model, dim, detection)
|
|
32
51
|
|
|
33
|
-
|
|
52
|
+
resolved = resolve_embedder(
|
|
34
53
|
provider,
|
|
35
54
|
model=model,
|
|
36
55
|
base_url=config.embedding_base_url,
|
|
@@ -38,8 +57,23 @@ def build_embedder_runtime(
|
|
|
38
57
|
dim=dim,
|
|
39
58
|
timeout=config.embedding_timeout,
|
|
40
59
|
extra={"target": config.embedding_custom_target},
|
|
41
|
-
probe=provider not in
|
|
60
|
+
probe=provider not in _FALLBACK_NAMES,
|
|
42
61
|
)
|
|
62
|
+
try:
|
|
63
|
+
resolved.detected = detection
|
|
64
|
+
except AttributeError: # pragma: no cover - a stand-in without __dict__
|
|
65
|
+
pass
|
|
66
|
+
return resolved
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _configured(config: Any, profile: Mapping[str, Any]) -> Tuple[str, str, int]:
|
|
70
|
+
"""``(provider, model, dim)`` from configuration and the named profile."""
|
|
71
|
+
provider = config.embedding_provider
|
|
72
|
+
model = config.embedding_model or str(profile.get("model") or "")
|
|
73
|
+
dim = config.embedding_dim or int(profile.get("dimensions") or 0)
|
|
74
|
+
if config.embedding_profile and provider in _FALLBACK_NAMES | {AUTO_PROVIDER}:
|
|
75
|
+
provider = str(profile.get("provider") or provider)
|
|
76
|
+
return provider, model, dim
|
|
43
77
|
|
|
44
78
|
|
|
45
79
|
__all__ = ["build_embedder_runtime"]
|
|
@@ -86,10 +86,11 @@ WORKER_SEAM_ROUTES: Tuple[Tuple[str, str], ...] = (
|
|
|
86
86
|
#: The pure-compute seams of Wave 2.5 §W2 (``latticeai/api/worker_compute.py``).
|
|
87
87
|
#: Kept apart from :data:`WORKER_SEAM_ROUTES` because they age in the opposite
|
|
88
88
|
#: direction: the state seams above are retired once §W3 lands the native write
|
|
89
|
-
#: engine, while these
|
|
90
|
-
#: write. Mounted here and nowhere else. ``/worker/multimodal/describe`` was
|
|
91
|
-
#:
|
|
92
|
-
#: was never built
|
|
89
|
+
#: engine, while these nine are what the worker is *for* once Rust owns every
|
|
90
|
+
#: write. Mounted here and nowhere else. ``/worker/multimodal/describe`` was
|
|
91
|
+
#: here until v11.8.0 — it described an image for a native image ingest that
|
|
92
|
+
#: was never built. ``/worker/vector/query`` is the HNSW sidecar door
|
|
93
|
+
#: (v12.0.0): approximate candidates, exact rescore in lattice-retrieval.
|
|
93
94
|
WORKER_COMPUTE_ROUTES: Tuple[Tuple[str, str], ...] = (
|
|
94
95
|
("POST", "/worker/embed"),
|
|
95
96
|
("POST", "/worker/parse"),
|
|
@@ -99,6 +100,7 @@ WORKER_COMPUTE_ROUTES: Tuple[Tuple[str, str], ...] = (
|
|
|
99
100
|
("POST", "/worker/render/pdf"),
|
|
100
101
|
("POST", "/worker/asr"),
|
|
101
102
|
("POST", "/worker/extract"),
|
|
103
|
+
("POST", "/worker/vector/query"),
|
|
102
104
|
)
|
|
103
105
|
|
|
104
106
|
|
|
@@ -147,12 +149,19 @@ def phase_worker_routes(ctx: Any) -> None:
|
|
|
147
149
|
# ``getattr`` because a worker built without a Brain still boots — each seam
|
|
148
150
|
# reports the absence rather than failing construction.
|
|
149
151
|
ports = getattr(ctx, "MULTIMODAL_PORTS", None)
|
|
152
|
+
data_dir = getattr(ctx, "DATA_DIR", None)
|
|
153
|
+
db_path = None
|
|
154
|
+
if data_dir is not None:
|
|
155
|
+
from pathlib import Path
|
|
156
|
+
|
|
157
|
+
db_path = Path(data_dir) / "knowledge_graph.sqlite"
|
|
150
158
|
app.include_router(
|
|
151
159
|
create_worker_compute_router(
|
|
152
160
|
embedder=getattr(ctx, "EMBEDDER", None),
|
|
153
161
|
transcriber=getattr(ports, "transcriber", None),
|
|
154
162
|
require_user=ctx.require_user,
|
|
155
163
|
enforce_rate_limit=ctx.enforce_rate_limit,
|
|
164
|
+
db_path=db_path,
|
|
156
165
|
)
|
|
157
166
|
)
|
|
158
167
|
apply_worker_route_filter(app)
|
|
@@ -19,7 +19,7 @@ from dataclasses import dataclass
|
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
from typing import Any, Dict, List
|
|
21
21
|
|
|
22
|
-
ARCHITECTURE_VERSION_TARGET = "
|
|
22
|
+
ARCHITECTURE_VERSION_TARGET = "12.0.0"
|
|
23
23
|
|
|
24
24
|
PREFERRED_REFACTORING_ORDER = [
|
|
25
25
|
"agent-runtime",
|
|
@@ -177,7 +177,7 @@ def architecture_readiness(root: Path | None = None) -> Dict[str, Any]:
|
|
|
177
177
|
and not agent_alias_forbidden
|
|
178
178
|
) else "incomplete",
|
|
179
179
|
evidence=[
|
|
180
|
-
"rust/lattice-agent/src/permission.rs block_reason_for_tool (native owner)",
|
|
180
|
+
"rust/lattice-agent/src/kernel/permission.rs block_reason_for_tool (native owner)",
|
|
181
181
|
"latticeai.api.agent_worker_seam.create_agent_worker_seam_router",
|
|
182
182
|
"tests/unit/test_agent_worker_seam.py",
|
|
183
183
|
],
|
|
@@ -13,6 +13,11 @@ generator whose subject is now native: they point at the cargo test or the
|
|
|
13
13
|
committed golden that actually guards the behaviour. The assertions themselves
|
|
14
14
|
were not deleted — an evidence path is repointed at the owner or it is a gate
|
|
15
15
|
that has stopped meaning anything.
|
|
16
|
+
|
|
17
|
+
12.0.0 regrouped the Rust crates into domain directories, so four needles that
|
|
18
|
+
named a top-level file in ``rust/lattice-agent/src/`` now name the same symbol
|
|
19
|
+
in ``src/kernel/``. The needle text is unchanged — only the path moved, and
|
|
20
|
+
each one was re-checked against the file it now names rather than assumed.
|
|
16
21
|
"""
|
|
17
22
|
|
|
18
23
|
from __future__ import annotations
|
|
@@ -23,7 +28,7 @@ from typing import Any, Dict, List
|
|
|
23
28
|
|
|
24
29
|
from latticeai.services.architecture_readiness import architecture_readiness
|
|
25
30
|
|
|
26
|
-
PRODUCT_VERSION_TARGET = "
|
|
31
|
+
PRODUCT_VERSION_TARGET = "12.0.0"
|
|
27
32
|
|
|
28
33
|
|
|
29
34
|
@dataclass(frozen=True)
|
|
@@ -74,7 +79,7 @@ PRODUCT_GATES: List[ProductGate] = [
|
|
|
74
79
|
"latticeai/tools/documents.py::read_document",
|
|
75
80
|
"latticeai/tools/filesystem.py::read_file",
|
|
76
81
|
"latticeai/core/tool_governor.py::classify_tool_call",
|
|
77
|
-
"rust/lattice-agent/src/permission.rs::pub fn block_reason_for_tool",
|
|
82
|
+
"rust/lattice-agent/src/kernel/permission.rs::pub fn block_reason_for_tool",
|
|
78
83
|
"tests/unit/test_worker_compute.py::test_the_docx_bytes_open_as_a_document_with_the_blocks_that_were_sent",
|
|
79
84
|
"tests/unit/test_tool_registry.py::test_execute_tool_uses_registry",
|
|
80
85
|
],
|
|
@@ -119,10 +124,10 @@ PRODUCT_GATES: List[ProductGate] = [
|
|
|
119
124
|
f"docs/CHANGELOG.md::## [{PRODUCT_VERSION_TARGET}]",
|
|
120
125
|
"FEATURE_STATUS.md",
|
|
121
126
|
f"RELEASE_NOTES_v{PRODUCT_VERSION_TARGET}.md",
|
|
122
|
-
"rust/lattice-agent/src/permission.rs::pub fn block_reason_for_tool",
|
|
123
|
-
"rust/lattice-agent/src/
|
|
127
|
+
"rust/lattice-agent/src/kernel/permission.rs::pub fn block_reason_for_tool",
|
|
128
|
+
"rust/lattice-agent/src/kernel/mod.rs::pub mod agentloop",
|
|
124
129
|
"latticeai/api/agent_worker_seam.py::create_agent_worker_seam_router",
|
|
125
|
-
"rust/lattice-agent/src/permission.rs::pub fn non_auto_plan_steps",
|
|
130
|
+
"rust/lattice-agent/src/kernel/permission.rs::pub fn non_auto_plan_steps",
|
|
126
131
|
"latticeai/worker_app.py::create_worker_app",
|
|
127
132
|
"latticeai/worker_app.py::def create_worker_app",
|
|
128
133
|
"latticeai/services/architecture_readiness.py::lattice-architecture-contract/v1",
|
|
@@ -84,6 +84,13 @@ class SearchService:
|
|
|
84
84
|
"state": state,
|
|
85
85
|
"fell_back": fell_back,
|
|
86
86
|
"health": health,
|
|
87
|
+
# v12.0.0, additive: is what this worker embeds with *semantic*, and
|
|
88
|
+
# if not, is a real embedder already sitting on the disk? The hash
|
|
89
|
+
# fallback is honest about being a fallback but said nothing about
|
|
90
|
+
# what it could have been, so nobody ever switched.
|
|
91
|
+
"semantic": grade != "fallback",
|
|
92
|
+
"detected": dict(resolved.get("detected") or {}),
|
|
93
|
+
"identity": f"{meta.get('model_id')}:{meta.get('dim')}",
|
|
87
94
|
"detail": resolved.get("detail", ""),
|
|
88
95
|
# Index completeness is native (lattice-jobs). Present and empty:
|
|
89
96
|
# a reader learns this answer does not speak for the index, which
|
|
@@ -116,7 +116,12 @@ _CJK_FONT_CANDIDATES = [
|
|
|
116
116
|
"/usr/share/fonts/truetype/nanum/NanumGothic.ttf",
|
|
117
117
|
]
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
# `.html`/`.htm` joined in v12.0.0: they were classified as *code* and read
|
|
120
|
+
# verbatim, so the graph stored markup instead of what the page says. See
|
|
121
|
+
# `latticeai.tools.markup.html_to_text`.
|
|
122
|
+
_SUPPORTED_READ_EXTENSIONS = {
|
|
123
|
+
".pdf", ".docx", ".xlsx", ".pptx", ".txt", ".md", ".csv", ".html", ".htm",
|
|
124
|
+
}
|
|
120
125
|
DOCUMENT_MAX_READ_BYTES = 10_000_000 # 10 MB
|
|
121
126
|
|
|
122
127
|
|
|
@@ -128,6 +128,18 @@ def read_document(path: str) -> Dict[str, Any]:
|
|
|
128
128
|
except Exception as exc:
|
|
129
129
|
raise ToolError(f"PPTX 읽기 실패: {exc}") from exc
|
|
130
130
|
|
|
131
|
+
elif ext in {".html", ".htm"}:
|
|
132
|
+
# A web page read verbatim is markup, not text: the graph filled up
|
|
133
|
+
# with `<div class=…>` and the contents of every `<script>`. The
|
|
134
|
+
# conversion keeps the page's headings as `#` lines so a fact
|
|
135
|
+
# extracted from it can still say which section it came from.
|
|
136
|
+
try:
|
|
137
|
+
from latticeai.tools.markup import html_to_text
|
|
138
|
+
|
|
139
|
+
text = html_to_text(target.read_text(encoding="utf-8", errors="replace"))
|
|
140
|
+
except Exception as exc:
|
|
141
|
+
raise ToolError(f"HTML 읽기 실패: {exc}") from exc
|
|
142
|
+
|
|
131
143
|
elif ext in {".txt", ".md", ".csv"}:
|
|
132
144
|
try:
|
|
133
145
|
text = target.read_text(encoding="utf-8", errors="replace")
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""HTML → readable text, with the document's own structure kept.
|
|
2
|
+
|
|
3
|
+
Until v12.0.0 an ``.html`` file was classified as *code* and read verbatim, so
|
|
4
|
+
what reached the graph was `<div class="wrapper">` and the contents of every
|
|
5
|
+
`<script>` tag. Chunks were markup, concepts were attribute names, and a search
|
|
6
|
+
for a sentence on the page could not find the page.
|
|
7
|
+
|
|
8
|
+
The conversion is deliberately small and dependency-free (``html.parser`` and
|
|
9
|
+
``html.unescape`` are stdlib; adding BeautifulSoup for this would be a new
|
|
10
|
+
runtime dependency for a hundred lines of tag handling):
|
|
11
|
+
|
|
12
|
+
* ``<script>``, ``<style>``, ``<noscript>``, ``<template>`` and everything in
|
|
13
|
+
``<head>`` except ``<title>`` are dropped — they are not what the page says;
|
|
14
|
+
* ``<h1>``–``<h6>`` become markdown headings, so the section machinery in
|
|
15
|
+
:mod:`lattice_brain.graph._kg_common.sections` gives every extracted fact the
|
|
16
|
+
heading it lived under, exactly as it does for a ``.md`` file;
|
|
17
|
+
* ``<li>`` becomes ``- ``, table cells are tab-separated, and block elements
|
|
18
|
+
end a line, so paragraph and list boundaries survive into chunking;
|
|
19
|
+
* entities are unescaped once, at the end of each text run.
|
|
20
|
+
|
|
21
|
+
The output is plain text that happens to carry markdown headings. It is *not*
|
|
22
|
+
a markdown converter and does not try to be: bold, links and images are read
|
|
23
|
+
for their text, because a knowledge graph wants the sentence, not the styling.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import logging
|
|
29
|
+
import re
|
|
30
|
+
from html import unescape
|
|
31
|
+
from html.parser import HTMLParser
|
|
32
|
+
from typing import List, Set
|
|
33
|
+
|
|
34
|
+
#: Elements whose *content* is never page text.
|
|
35
|
+
SKIPPED_ELEMENTS: Set[str] = {"script", "style", "noscript", "template", "svg"}
|
|
36
|
+
#: Elements that end the current line.
|
|
37
|
+
BLOCK_ELEMENTS: Set[str] = {
|
|
38
|
+
"p", "div", "section", "article", "header", "footer", "nav", "aside",
|
|
39
|
+
"main", "ul", "ol", "li", "table", "tr", "blockquote", "pre", "form",
|
|
40
|
+
"figure", "figcaption", "dl", "dt", "dd", "hr", "br", "h1", "h2", "h3",
|
|
41
|
+
"h4", "h5", "h6", "title",
|
|
42
|
+
}
|
|
43
|
+
#: Elements that separate cells with a tab rather than a newline.
|
|
44
|
+
CELL_ELEMENTS: Set[str] = {"td", "th"}
|
|
45
|
+
|
|
46
|
+
_HEADINGS = {f"h{level}": level for level in range(1, 7)}
|
|
47
|
+
_BLANK_RUN = re.compile(r"\n{3,}")
|
|
48
|
+
_TRAILING_SPACE = re.compile(r"[ \t]+\n")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class _TextExtractor(HTMLParser):
|
|
52
|
+
"""Collect a page's readable text, marking its headings."""
|
|
53
|
+
|
|
54
|
+
def __init__(self) -> None:
|
|
55
|
+
super().__init__(convert_charrefs=False)
|
|
56
|
+
self.parts: List[str] = []
|
|
57
|
+
self._skip_depth = 0
|
|
58
|
+
self._heading: int = 0
|
|
59
|
+
self._in_head = False
|
|
60
|
+
|
|
61
|
+
# ── structure ────────────────────────────────────────────────────────
|
|
62
|
+
def handle_starttag(self, tag: str, attrs: object) -> None:
|
|
63
|
+
if tag in SKIPPED_ELEMENTS:
|
|
64
|
+
self._skip_depth += 1
|
|
65
|
+
return
|
|
66
|
+
if tag == "head":
|
|
67
|
+
self._in_head = True
|
|
68
|
+
return
|
|
69
|
+
if self._skip_depth:
|
|
70
|
+
return
|
|
71
|
+
level = _HEADINGS.get(tag)
|
|
72
|
+
# `<title>` is the page's name; treat it as the top heading so the
|
|
73
|
+
# section path of a page with no `<h1>` is still its own title.
|
|
74
|
+
if tag == "title":
|
|
75
|
+
level = 1
|
|
76
|
+
if level:
|
|
77
|
+
self._heading = level
|
|
78
|
+
self.parts.append("\n\n" + "#" * level + " ")
|
|
79
|
+
return
|
|
80
|
+
if tag == "li":
|
|
81
|
+
self.parts.append("\n- ")
|
|
82
|
+
elif tag in CELL_ELEMENTS:
|
|
83
|
+
self.parts.append("\t")
|
|
84
|
+
elif tag in BLOCK_ELEMENTS:
|
|
85
|
+
self.parts.append("\n")
|
|
86
|
+
|
|
87
|
+
def handle_endtag(self, tag: str) -> None:
|
|
88
|
+
if tag in SKIPPED_ELEMENTS:
|
|
89
|
+
self._skip_depth = max(0, self._skip_depth - 1)
|
|
90
|
+
return
|
|
91
|
+
if tag == "head":
|
|
92
|
+
self._in_head = False
|
|
93
|
+
return
|
|
94
|
+
if self._skip_depth:
|
|
95
|
+
return
|
|
96
|
+
if tag in _HEADINGS or tag == "title":
|
|
97
|
+
self._heading = 0
|
|
98
|
+
self.parts.append("\n\n")
|
|
99
|
+
elif tag == "li":
|
|
100
|
+
# `<li>` already opens its own line; closing one too would put a
|
|
101
|
+
# blank line between every bullet and turn a list into a page.
|
|
102
|
+
return
|
|
103
|
+
elif tag in BLOCK_ELEMENTS:
|
|
104
|
+
self.parts.append("\n")
|
|
105
|
+
|
|
106
|
+
# ── text ─────────────────────────────────────────────────────────────
|
|
107
|
+
def handle_data(self, data: str) -> None:
|
|
108
|
+
if self._skip_depth:
|
|
109
|
+
return
|
|
110
|
+
# Inside `<head>`, only `<title>` text is page content; `<meta>` and
|
|
111
|
+
# friends carry no data, but stray whitespace and CDATA do.
|
|
112
|
+
if self._in_head and not self._heading:
|
|
113
|
+
return
|
|
114
|
+
text = unescape(data)
|
|
115
|
+
if self._heading:
|
|
116
|
+
# A heading must stay on one line or it stops being a heading.
|
|
117
|
+
text = " ".join(text.split())
|
|
118
|
+
if text:
|
|
119
|
+
self.parts.append(text)
|
|
120
|
+
return
|
|
121
|
+
cleaned = re.sub(r"[ \t\r\f\v]+", " ", text.replace("\n", " "))
|
|
122
|
+
if cleaned.strip():
|
|
123
|
+
self.parts.append(cleaned)
|
|
124
|
+
elif cleaned:
|
|
125
|
+
self.parts.append(" ")
|
|
126
|
+
|
|
127
|
+
def handle_entityref(self, name: str) -> None:
|
|
128
|
+
self.handle_data(f"&{name};")
|
|
129
|
+
|
|
130
|
+
def handle_charref(self, name: str) -> None:
|
|
131
|
+
self.handle_data(f"&#{name};")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def html_to_text(markup: str) -> str:
|
|
135
|
+
"""Readable text for ``markup``, with headings kept as ``#`` lines.
|
|
136
|
+
|
|
137
|
+
Never raises: a truncated or malformed page yields whatever text was
|
|
138
|
+
recoverable, because half a document in the graph beats a failed ingest.
|
|
139
|
+
"""
|
|
140
|
+
extractor = _TextExtractor()
|
|
141
|
+
try:
|
|
142
|
+
extractor.feed(str(markup or ""))
|
|
143
|
+
extractor.close()
|
|
144
|
+
except Exception as exc: # pragma: no cover - html.parser is very tolerant
|
|
145
|
+
logging.debug("HTML parse stopped early; keeping what was read: %s", exc)
|
|
146
|
+
text = "".join(extractor.parts)
|
|
147
|
+
text = _TRAILING_SPACE.sub("\n", text)
|
|
148
|
+
text = _BLANK_RUN.sub("\n\n", text)
|
|
149
|
+
return "\n".join(line.rstrip() for line in text.split("\n")).strip()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
__all__ = ["BLOCK_ELEMENTS", "CELL_ELEMENTS", "SKIPPED_ELEMENTS", "html_to_text"]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ltcai",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "12.0.0",
|
|
4
4
|
"description": "Lattice AI — local-first Digital Brain that keeps your knowledge durable across any AI model.",
|
|
5
5
|
"homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
|
|
6
6
|
"repository": {
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"release:artifacts": "node scripts/clean_release_artifacts.mjs $npm_package_version && npm run build:assets && npm run build:python && npm pack && npm run package:vsix && npm run desktop:tauri:build",
|
|
58
58
|
"release:validate": "node scripts/run_python.mjs scripts/validate_release_artifacts.py $npm_package_version --require-vsix --require-tgz --require-dmg",
|
|
59
59
|
"release:smoke": "node scripts/run_python.mjs scripts/release_smoke.py $npm_package_version",
|
|
60
|
+
"publish:release": "node scripts/publish_release.mjs",
|
|
60
61
|
"publish:npm": "npm pack && npm publish ltcai-$npm_package_version.tgz --access public",
|
|
61
62
|
"publish:pypi": "npm run build:python && node scripts/run_python.mjs -m twine upload --skip-existing dist/ltcai-$npm_package_version.tar.gz dist/ltcai-$npm_package_version-py3-none-any.whl",
|
|
62
63
|
"publish:vscode": "cd vscode-extension && npm run package:vsix && npm run publish:vscode",
|
|
@@ -6,7 +6,7 @@ const root = process.cwd();
|
|
|
6
6
|
const pkg = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
|
|
7
7
|
const version = pkg.version;
|
|
8
8
|
const releaseDir = `output/release/v${version}`;
|
|
9
|
-
const releaseTheme = "
|
|
9
|
+
const releaseTheme = "Open House";
|
|
10
10
|
const title = `${version} — ${releaseTheme}`;
|
|
11
11
|
const escapedVersion = version.replaceAll(".", "\\.");
|
|
12
12
|
|
|
@@ -122,6 +122,8 @@ INTERNAL_WORKER_PATHS = frozenset(
|
|
|
122
122
|
# ``/worker/multimodal/describe`` was here until v11.8.0 deleted the
|
|
123
123
|
# seam: it described an image for a native ingest nothing ever built.
|
|
124
124
|
"/worker/extract",
|
|
125
|
+
# v12.0.0 — HNSW sidecar query. Same loopback-only reasoning.
|
|
126
|
+
"/worker/vector/query",
|
|
125
127
|
}
|
|
126
128
|
)
|
|
127
129
|
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"source": {
|
|
19
19
|
"spec": "frontend/openapi.json",
|
|
20
20
|
"spec_version": "11.8.0",
|
|
21
|
-
"paths":
|
|
22
|
-
"operations":
|
|
21
|
+
"paths": 415,
|
|
22
|
+
"operations": 457,
|
|
23
23
|
"classification": "scout_routes.md (v11.6.0 route inventory) + docs/v11.6.0_ONE_DOOR_PLAN.md",
|
|
24
24
|
"module_attribution": "FastAPI route.endpoint.__module__ of the app built by scripts/export_openapi.py"
|
|
25
25
|
},
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"wp": "R8",
|
|
83
83
|
"target": "lattice-platform / lattice-agent",
|
|
84
84
|
"summary": "MCP, marketplace, plugin SDK, agent registry, multi-agent runtime and the direct tool surface.",
|
|
85
|
-
"operations":
|
|
85
|
+
"operations": 87
|
|
86
86
|
},
|
|
87
87
|
"portability_network": {
|
|
88
88
|
"wp": "R9",
|
|
@@ -1746,6 +1746,10 @@
|
|
|
1746
1746
|
"module": "latticeai.api.marketplace",
|
|
1747
1747
|
"family": "mcp_market"
|
|
1748
1748
|
},
|
|
1749
|
+
"POST /mcp": {
|
|
1750
|
+
"module": "latticeai.api.mcp",
|
|
1751
|
+
"family": "mcp_market"
|
|
1752
|
+
},
|
|
1749
1753
|
"POST /mcp/call": {
|
|
1750
1754
|
"module": "latticeai.api.mcp",
|
|
1751
1755
|
"family": "mcp_market"
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// One-command publish for a built release: npm, PyPI, VS Code Marketplace, OpenVSX.
|
|
3
|
+
//
|
|
4
|
+
// node scripts/publish_release.mjs # publish every target not yet at this version
|
|
5
|
+
// node scripts/publish_release.mjs --dry-run # report what would happen, publish nothing
|
|
6
|
+
//
|
|
7
|
+
// Run it from anywhere — the script cd's to the repo root itself. It publishes the
|
|
8
|
+
// artifacts `npm run release:artifacts` already built and never rebuilds them.
|
|
9
|
+
// Each registry is checked first, so re-running after a partial failure only
|
|
10
|
+
// retries what is actually missing.
|
|
11
|
+
//
|
|
12
|
+
// Credentials (never passed as argv — argv leaks into shell history and npm logs):
|
|
13
|
+
// npm — `npm login` session;每 publish마다 브라우저 2FA 승인이 뜬다.
|
|
14
|
+
// PyPI — ~/.pypirc, TWINE_USERNAME/TWINE_PASSWORD, or interactive prompt.
|
|
15
|
+
// Marketplace — VSCE_PAT environment variable.
|
|
16
|
+
// OpenVSX — OVSX_PAT environment variable.
|
|
17
|
+
import { spawnSync } from "node:child_process";
|
|
18
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
|
|
22
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
23
|
+
process.chdir(ROOT);
|
|
24
|
+
|
|
25
|
+
const DRY = process.argv.includes("--dry-run");
|
|
26
|
+
const pkg = JSON.parse(readFileSync(path.join(ROOT, "package.json"), "utf8"));
|
|
27
|
+
const V = pkg.version;
|
|
28
|
+
const PUBLISHER = JSON.parse(
|
|
29
|
+
readFileSync(path.join(ROOT, "vscode-extension", "package.json"), "utf8"),
|
|
30
|
+
).publisher;
|
|
31
|
+
|
|
32
|
+
const TGZ = path.join(ROOT, `ltcai-${V}.tgz`);
|
|
33
|
+
const WHEEL = path.join(ROOT, "dist", `ltcai-${V}-py3-none-any.whl`);
|
|
34
|
+
const SDIST = path.join(ROOT, "dist", `ltcai-${V}.tar.gz`);
|
|
35
|
+
const VSIX = path.join(ROOT, "dist", `ltcai-${V}.vsix`);
|
|
36
|
+
|
|
37
|
+
function run(cmd, args, env = {}) {
|
|
38
|
+
const r = spawnSync(cmd, args, {
|
|
39
|
+
stdio: "inherit",
|
|
40
|
+
cwd: ROOT,
|
|
41
|
+
env: { ...process.env, ...env },
|
|
42
|
+
});
|
|
43
|
+
return r.status === 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function json(url, init) {
|
|
47
|
+
try {
|
|
48
|
+
const res = await fetch(url, init);
|
|
49
|
+
if (!res.ok) return null;
|
|
50
|
+
return await res.json();
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const checks = {
|
|
57
|
+
async npm() {
|
|
58
|
+
const d = await json(`https://registry.npmjs.org/ltcai/${V}`);
|
|
59
|
+
return d?.version === V;
|
|
60
|
+
},
|
|
61
|
+
async pypi() {
|
|
62
|
+
return (await json(`https://pypi.org/pypi/ltcai/${V}/json`)) !== null;
|
|
63
|
+
},
|
|
64
|
+
async marketplace() {
|
|
65
|
+
const d = await json(
|
|
66
|
+
"https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery",
|
|
67
|
+
{
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: {
|
|
70
|
+
"Content-Type": "application/json",
|
|
71
|
+
Accept: "application/json;api-version=3.0-preview.1",
|
|
72
|
+
},
|
|
73
|
+
body: JSON.stringify({
|
|
74
|
+
filters: [{ criteria: [{ filterType: 7, value: `${PUBLISHER}.ltcai` }] }],
|
|
75
|
+
flags: 1,
|
|
76
|
+
}),
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
const ext = d?.results?.[0]?.extensions?.[0];
|
|
80
|
+
return ext?.versions?.some((x) => x.version === V) ?? false;
|
|
81
|
+
},
|
|
82
|
+
async openvsx() {
|
|
83
|
+
return (await json(`https://open-vsx.org/api/${PUBLISHER}/ltcai/${V}`)) !== null;
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const targets = [
|
|
88
|
+
{
|
|
89
|
+
name: "npm",
|
|
90
|
+
artifacts: [TGZ],
|
|
91
|
+
credsMissing: () => null, // npm prompts its own browser 2FA
|
|
92
|
+
publish: () => run("npm", ["publish", TGZ]),
|
|
93
|
+
note: "브라우저 2FA 승인 창이 뜨면 승인해야 완료됩니다.",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "pypi",
|
|
97
|
+
artifacts: [SDIST, WHEEL],
|
|
98
|
+
credsMissing: () => null, // twine falls back to ~/.pypirc or an interactive prompt
|
|
99
|
+
publish: () =>
|
|
100
|
+
run("node", [
|
|
101
|
+
"scripts/run_python.mjs",
|
|
102
|
+
"-m",
|
|
103
|
+
"twine",
|
|
104
|
+
"upload",
|
|
105
|
+
"--skip-existing",
|
|
106
|
+
SDIST,
|
|
107
|
+
WHEEL,
|
|
108
|
+
]),
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "marketplace",
|
|
112
|
+
artifacts: [VSIX],
|
|
113
|
+
credsMissing: () => (process.env.VSCE_PAT ? null : "VSCE_PAT 환경변수가 필요합니다"),
|
|
114
|
+
publish: () => run("npx", ["--yes", "@vscode/vsce", "publish", "--packagePath", VSIX]),
|
|
115
|
+
note: "게시 후 마켓플레이스 검증에 몇 분 걸릴 수 있습니다.",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "openvsx",
|
|
119
|
+
artifacts: [VSIX],
|
|
120
|
+
credsMissing: () => (process.env.OVSX_PAT ? null : "OVSX_PAT 환경변수가 필요합니다"),
|
|
121
|
+
publish: () => run("npx", ["--yes", "ovsx", "publish", VSIX]),
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
const results = [];
|
|
126
|
+
for (const t of targets) {
|
|
127
|
+
const already = await checks[t.name]();
|
|
128
|
+
if (already) {
|
|
129
|
+
results.push([t.name, "already-published"]);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const missing = t.artifacts.filter((a) => !existsSync(a));
|
|
133
|
+
if (missing.length) {
|
|
134
|
+
results.push([t.name, `artifact missing: ${missing.map((m) => path.basename(m)).join(", ")}`]);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const creds = t.credsMissing();
|
|
138
|
+
if (creds) {
|
|
139
|
+
results.push([t.name, `skipped: ${creds}`]);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (DRY) {
|
|
143
|
+
results.push([t.name, "would publish"]);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (t.note) console.log(`\n[${t.name}] ${t.note}`);
|
|
147
|
+
console.log(`[${t.name}] publishing v${V}…`);
|
|
148
|
+
results.push([t.name, t.publish() ? "published" : "FAILED"]);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log(`\n=== publish report — ltcai v${V} ===`);
|
|
152
|
+
for (const [name, status] of results) console.log(` ${name.padEnd(12)} ${status}`);
|
|
153
|
+
|
|
154
|
+
const bad = results.filter(
|
|
155
|
+
([, s]) => s === "FAILED" || s.startsWith("artifact missing") || s.startsWith("skipped"),
|
|
156
|
+
);
|
|
157
|
+
process.exit(bad.length ? 1 : 0);
|
|
@@ -20,6 +20,18 @@
|
|
|
20
20
|
"whole-image delta: naming where the change is makes the claim more",
|
|
21
21
|
"specific, not less."
|
|
22
22
|
],
|
|
23
|
+
"12.0.0": {
|
|
24
|
+
"_why": [
|
|
25
|
+
"Open House brings component and resilience updates across the frontend:",
|
|
26
|
+
"FolderMemoryHealth gained the 「삭제된 파일 정리 (N)」 button + preview dialog,",
|
|
27
|
+
"error boundaries added around views, preview pattern extended, and code",
|
|
28
|
+
"splitting (lazy view chunks).",
|
|
29
|
+
"",
|
|
30
|
+
"Brain/Chat home is unchanged since 11.8.0. Screens not touched are claimed",
|
|
31
|
+
"unchanged. The prune button renders conditionally and preview is user-triggered,",
|
|
32
|
+
"so static baseline captures remain unchanged. Empty claim set on purpose."
|
|
33
|
+
]
|
|
34
|
+
},
|
|
23
35
|
"11.9.0": {
|
|
24
36
|
"_why": [
|
|
25
37
|
"Working Order is a backend-truth release. The product work is live",
|