ltcai 9.9.2 → 9.9.4
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 +44 -39
- package/docs/CHANGELOG.md +109 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +1 -1
- package/docs/SURFACE_PARITY.md +47 -0
- 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/graph/_kg_common.py +335 -0
- package/lattice_brain/graph/curator.py +141 -0
- package/lattice_brain/graph/discovery_index.py +8 -1
- package/lattice_brain/graph/documents.py +111 -0
- package/lattice_brain/graph/fusion.py +179 -0
- package/lattice_brain/graph/ingest.py +43 -9
- package/lattice_brain/graph/projection.py +382 -37
- package/lattice_brain/graph/retrieval.py +71 -4
- package/lattice_brain/graph/retrieval_policy.py +174 -0
- package/lattice_brain/graph/retrieval_vector.py +122 -2
- package/lattice_brain/ingestion.py +66 -0
- package/lattice_brain/portability.py +41 -12
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/automation_intelligence.py +159 -0
- package/latticeai/api/browser.py +15 -4
- package/latticeai/api/chat.py +33 -2
- package/latticeai/api/chat_agent_http.py +363 -6
- package/latticeai/api/chat_contracts.py +7 -1
- package/latticeai/api/chat_helpers.py +147 -0
- package/latticeai/api/chat_intents.py +217 -10
- package/latticeai/api/chat_stream.py +93 -3
- package/latticeai/api/funnel_metrics.py +31 -0
- package/latticeai/api/knowledge_graph.py +65 -1
- package/latticeai/api/local_files.py +90 -0
- package/latticeai/api/setup.py +134 -1
- package/latticeai/api/tools.py +33 -3
- package/latticeai/app_factory.py +14 -0
- package/latticeai/core/agent.py +355 -18
- package/latticeai/core/agent_eval.py +107 -0
- package/latticeai/core/enterprise.py +5 -0
- package/latticeai/core/file_generation.py +404 -2
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/run_store.py +243 -0
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/models/router.py +25 -12
- package/latticeai/runtime/persistence_runtime.py +19 -1
- package/latticeai/runtime/router_registration.py +8 -1
- package/latticeai/services/app_context.py +3 -0
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/automation_execution.py +266 -0
- package/latticeai/services/automation_intelligence.py +7 -0
- package/latticeai/services/command_center.py +110 -2
- package/latticeai/services/folder_watch.py +396 -0
- package/latticeai/services/funnel_metrics.py +210 -0
- package/latticeai/services/product_readiness.py +1 -1
- package/latticeai/services/search_service.py +51 -5
- package/latticeai/setup/demo_corpus.py +98 -0
- package/latticeai/tools/filesystem.py +38 -1
- package/package.json +1 -1
- package/scripts/bench_agent_smoke.py +410 -0
- package/scripts/bench_models.py +234 -5
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/funnel_soft_gate.py +192 -0
- 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 +29 -27
- package/static/app/assets/Act-BCmTU0E2.js +1 -0
- package/static/app/assets/{Brain-BnGbytHa.js → Brain-CTnjox7w.js} +2 -2
- package/static/app/assets/BrainHome-BJ3sFNX0.js +2 -0
- package/static/app/assets/BrainSignals-C52lwZVD.js +1 -0
- package/static/app/assets/{Capture-CAKnLcb0.js → Capture-B6vBhFa3.js} +1 -1
- package/static/app/assets/CommandPalette-90u9FWFH.js +1 -0
- package/static/app/assets/Library-pKCK0_tk.js +1 -0
- package/static/app/assets/{LivingBrain-C3VbogVo.js → LivingBrain-Jlf2wFqI.js} +1 -1
- package/static/app/assets/{ProductFlow-BLau7c7N.js → ProductFlow-yg1fKP1P.js} +1 -1
- package/static/app/assets/ReviewCard-DWvD7n9h.js +3 -0
- package/static/app/assets/{System-DRHc_6Su.js → System-BAEuHqNY.js} +1 -1
- package/static/app/assets/{bot-DZkDdh-A.js → bot-nB_buEZD.js} +1 -1
- package/static/app/assets/circle-pause-DLNw6Ucp.js +1 -0
- package/static/app/assets/circle-play-B-IsFL1y.js +1 -0
- package/static/app/assets/{cpu-DyvsDBbj.js → cpu-CEPBHaBl.js} +1 -1
- package/static/app/assets/{folder-open-CffoIr9t.js → folder-open-hmN0N9cX.js} +1 -1
- package/static/app/assets/{hard-drive-C1oojrHb.js → hard-drive-CBV_B_Yd.js} +1 -1
- package/static/app/assets/index-7FAfYm4v.css +2 -0
- package/static/app/assets/{index-BS_3aN1n.js → index-DrmOCySv.js} +3 -3
- package/static/app/assets/{input-B_U7zvbR.js → input-gtVCg-ll.js} +1 -1
- package/static/app/assets/{navigation-xG-gyjbe.js → navigation-Bot0hvuv.js} +1 -1
- package/static/app/assets/{network-BF13HJJF.js → network-jE42eKfT.js} +1 -1
- package/static/app/assets/{primitives-DsuKh3M8.js → primitives-CX2Komon.js} +1 -1
- package/static/app/assets/shield-alert-BftATuAA.js +1 -0
- package/static/app/assets/{textarea-DsDL4ET8.js → textarea-CiMJfOSI.js} +1 -1
- package/static/app/assets/useFocusTrap-B7RPGfFy.js +1 -0
- package/static/app/assets/utils-SJUNVOj5.js +7 -0
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/static/app/assets/Act-Cx_gZNbE.js +0 -1
- package/static/app/assets/BrainHome-CRUMJL93.js +0 -2
- package/static/app/assets/BrainSignals-BP8-Qh2g.js +0 -1
- package/static/app/assets/CommandPalette-BoUTXBfm.js +0 -1
- package/static/app/assets/Library-PcfVUco6.js +0 -1
- package/static/app/assets/ReviewCard-CX5RqRqx.js +0 -1
- package/static/app/assets/download-C7yxYHvz.js +0 -1
- package/static/app/assets/index-DU78Kogz.css +0 -2
- package/static/app/assets/sunrise-CIP2ELJ1.js +0 -1
- package/static/app/assets/utils-Dh-Dwtkb.js +0 -7
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Query-class aware retrieval fusion weights (backlog #5, review §7.2 E).
|
|
2
|
+
|
|
3
|
+
Hybrid retrieval fuses keyword/lexical, vector, and graph channels. A single
|
|
4
|
+
static weight set treats "이 함수 왜 죽어?" and "어제 회의에서 뭐 결정했지?"
|
|
5
|
+
identically, which is measurably wrong: code questions live on exact
|
|
6
|
+
identifiers (lexical), person questions lean on relationship edges (graph),
|
|
7
|
+
recency questions need fresher lexical hits over semantic neighbors.
|
|
8
|
+
|
|
9
|
+
This module is deliberately tiny and deterministic:
|
|
10
|
+
|
|
11
|
+
* :func:`classify_query` — pure ko/en heuristics → ``fact | code | person |
|
|
12
|
+
recency`` (``fact`` is the fallback class and its weights are identical to
|
|
13
|
+
the historical defaults, so fact-class behavior is byte-compatible).
|
|
14
|
+
* :data:`DEFAULT_FUSION_WEIGHTS` — the per-class weight table. ``keyword`` /
|
|
15
|
+
``vector`` / ``graph`` feed the three-channel service fusion
|
|
16
|
+
(``SearchService.hybrid_search``); ``alpha`` is the vector share for the
|
|
17
|
+
two-channel graph-layer fusion (``KnowledgeGraphStore.hybrid_search``).
|
|
18
|
+
* :func:`fusion_profile` — classify + resolve weights, honoring the
|
|
19
|
+
``LATTICEAI_FUSION_WEIGHTS`` env override (JSON, partial per-class merges,
|
|
20
|
+
e.g. ``{"code": {"alpha": 0.2}}``).
|
|
21
|
+
|
|
22
|
+
Never uses ``or`` on scores (score 0 is falsy but valid) and never raises:
|
|
23
|
+
a malformed override is ignored, an unknown class falls back to ``fact``.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
import re
|
|
31
|
+
from typing import Any, Dict, Mapping, Optional
|
|
32
|
+
|
|
33
|
+
QUERY_CLASSES = ("fact", "code", "person", "recency")
|
|
34
|
+
|
|
35
|
+
FUSION_WEIGHTS_ENV = "LATTICEAI_FUSION_WEIGHTS"
|
|
36
|
+
|
|
37
|
+
# Per-class fusion weights. "fact" mirrors the pre-fusion-gate defaults
|
|
38
|
+
# (SearchService DEFAULT_HYBRID_WEIGHTS + graph-layer alpha=0.6) so the
|
|
39
|
+
# fallback class introduces zero behavioral drift.
|
|
40
|
+
DEFAULT_FUSION_WEIGHTS: Dict[str, Dict[str, float]] = {
|
|
41
|
+
"fact": {"keyword": 0.35, "vector": 0.40, "graph": 0.25, "alpha": 0.60},
|
|
42
|
+
# Code recall lives on exact identifiers/tokens → lexical dominates.
|
|
43
|
+
"code": {"keyword": 0.55, "vector": 0.25, "graph": 0.20, "alpha": 0.35},
|
|
44
|
+
# Person questions are relationship questions → graph channel leads.
|
|
45
|
+
"person": {"keyword": 0.30, "vector": 0.30, "graph": 0.40, "alpha": 0.45},
|
|
46
|
+
# Recency questions want fresh literal hits over semantic neighbors.
|
|
47
|
+
"recency": {"keyword": 0.45, "vector": 0.35, "graph": 0.20, "alpha": 0.50},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# ── query-class heuristics (pure, ko/en) ─────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
_CODE_FENCE_RE = re.compile(r"```|`[^`\n]+`")
|
|
53
|
+
_CODE_IDENT_RE = re.compile(
|
|
54
|
+
# ``\b`` fails between an identifier and an attached Korean particle
|
|
55
|
+
# ("ingest_folder가"), so use explicit ASCII-boundary lookbehinds instead.
|
|
56
|
+
r"[A-Za-z_][A-Za-z0-9_]*\(\)" # call syntax foo()
|
|
57
|
+
r"|(?<![A-Za-z0-9_])[a-z0-9]+_[a-z0-9_]+" # snake_case
|
|
58
|
+
r"|(?<![A-Za-z0-9_])[a-z]+[A-Z][A-Za-z0-9]*" # camelCase
|
|
59
|
+
r"|(?<![A-Za-z0-9_])[\w-]+\.(?:py|js|jsx|ts|tsx|json|yaml|yml|css|html|sql|sh|go|rs|java|rb)(?![A-Za-z0-9])"
|
|
60
|
+
)
|
|
61
|
+
_CODE_WORD_RE = re.compile(
|
|
62
|
+
r"\b(?:def|class|import|function|traceback|exception|stack\s*trace|"
|
|
63
|
+
r"bug|error|null|undefined|api|sql|regex)\b"
|
|
64
|
+
r"|코드|함수|버그|에러|오류|스택|컴파일|빌드\s*실패|구현",
|
|
65
|
+
re.IGNORECASE,
|
|
66
|
+
)
|
|
67
|
+
_PERSON_RE = re.compile(
|
|
68
|
+
r"\b(?:who|whom|whose)\b|누구|어떤\s*사람|담당자|팀원|동료|만난\s*사람"
|
|
69
|
+
r"|[\w.+-]+@[\w-]+\.[\w.]+" # an email address names a person
|
|
70
|
+
r"|[가-힣]{2,4}\s*(?:님|씨)(?:\s|$|[을를이가은는의??.!,])",
|
|
71
|
+
re.IGNORECASE,
|
|
72
|
+
)
|
|
73
|
+
_RECENCY_RE = re.compile(
|
|
74
|
+
r"최근|어제|오늘|그저께|방금|아까|지난\s*주|지난주|지난\s*달|지난달"
|
|
75
|
+
r"|이번\s*주|이번주|이번\s*달|이번달"
|
|
76
|
+
r"|\b(?:recent|recently|yesterday|today|latest|last\s+(?:week|month|night|meeting))\b",
|
|
77
|
+
re.IGNORECASE,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def classify_query(query: Any) -> str:
|
|
82
|
+
"""Classify a retrieval query into fact / code / person / recency.
|
|
83
|
+
|
|
84
|
+
Precedence: code (strongest structural signal) → recency (explicit time
|
|
85
|
+
words) → person → fact. Deterministic, never raises, empty → fact.
|
|
86
|
+
"""
|
|
87
|
+
text = str(query or "").strip()
|
|
88
|
+
if not text:
|
|
89
|
+
return "fact"
|
|
90
|
+
if _CODE_FENCE_RE.search(text) or _CODE_IDENT_RE.search(text) or _CODE_WORD_RE.search(text):
|
|
91
|
+
return "code"
|
|
92
|
+
if _RECENCY_RE.search(text):
|
|
93
|
+
return "recency"
|
|
94
|
+
if _PERSON_RE.search(text):
|
|
95
|
+
return "person"
|
|
96
|
+
return "fact"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _env_overrides() -> Dict[str, Dict[str, float]]:
|
|
100
|
+
"""Parse LATTICEAI_FUSION_WEIGHTS (JSON) → per-class partial overrides."""
|
|
101
|
+
raw = os.getenv(FUSION_WEIGHTS_ENV, "").strip()
|
|
102
|
+
if not raw:
|
|
103
|
+
return {}
|
|
104
|
+
try:
|
|
105
|
+
payload = json.loads(raw)
|
|
106
|
+
except (ValueError, TypeError):
|
|
107
|
+
return {}
|
|
108
|
+
if not isinstance(payload, dict):
|
|
109
|
+
return {}
|
|
110
|
+
overrides: Dict[str, Dict[str, float]] = {}
|
|
111
|
+
for cls, table in payload.items():
|
|
112
|
+
if cls not in DEFAULT_FUSION_WEIGHTS or not isinstance(table, dict):
|
|
113
|
+
continue
|
|
114
|
+
cleaned: Dict[str, float] = {}
|
|
115
|
+
for key, value in table.items():
|
|
116
|
+
if key not in ("keyword", "vector", "graph", "alpha"):
|
|
117
|
+
continue
|
|
118
|
+
try:
|
|
119
|
+
cleaned[key] = max(0.0, min(1.0, float(value)))
|
|
120
|
+
except (TypeError, ValueError):
|
|
121
|
+
continue
|
|
122
|
+
if cleaned:
|
|
123
|
+
overrides[str(cls)] = cleaned
|
|
124
|
+
return overrides
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def fusion_weight_table(
|
|
128
|
+
overrides: Optional[Mapping[str, Mapping[str, float]]] = None,
|
|
129
|
+
) -> Dict[str, Dict[str, float]]:
|
|
130
|
+
"""Full per-class table: defaults ← env override ← caller override."""
|
|
131
|
+
table = {cls: dict(weights) for cls, weights in DEFAULT_FUSION_WEIGHTS.items()}
|
|
132
|
+
for source in (_env_overrides(), overrides or {}):
|
|
133
|
+
for cls, partial in source.items():
|
|
134
|
+
if cls in table and isinstance(partial, Mapping):
|
|
135
|
+
for key, value in partial.items():
|
|
136
|
+
if key in table[cls]:
|
|
137
|
+
try:
|
|
138
|
+
table[cls][key] = max(0.0, min(1.0, float(value)))
|
|
139
|
+
except (TypeError, ValueError):
|
|
140
|
+
continue
|
|
141
|
+
return table
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def fusion_profile(
|
|
145
|
+
query: Any,
|
|
146
|
+
*,
|
|
147
|
+
overrides: Optional[Mapping[str, Mapping[str, float]]] = None,
|
|
148
|
+
) -> Dict[str, Any]:
|
|
149
|
+
"""Classify ``query`` and return its resolved fusion weights.
|
|
150
|
+
|
|
151
|
+
Returns ``{"query_class": str, "weights": {keyword, vector, graph},
|
|
152
|
+
"alpha": float}``. ``weights`` feeds the three-channel service fusion;
|
|
153
|
+
``alpha`` feeds the two-channel graph-layer fusion.
|
|
154
|
+
"""
|
|
155
|
+
query_class = classify_query(query)
|
|
156
|
+
table = fusion_weight_table(overrides)
|
|
157
|
+
resolved = table.get(query_class)
|
|
158
|
+
if resolved is None: # defensive — classify_query only emits known classes
|
|
159
|
+
query_class = "fact"
|
|
160
|
+
resolved = table["fact"]
|
|
161
|
+
return {
|
|
162
|
+
"query_class": query_class,
|
|
163
|
+
"weights": {
|
|
164
|
+
"keyword": resolved["keyword"],
|
|
165
|
+
"vector": resolved["vector"],
|
|
166
|
+
"graph": resolved["graph"],
|
|
167
|
+
},
|
|
168
|
+
"alpha": resolved["alpha"],
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
__all__ = [
|
|
173
|
+
"DEFAULT_FUSION_WEIGHTS",
|
|
174
|
+
"FUSION_WEIGHTS_ENV",
|
|
175
|
+
"QUERY_CLASSES",
|
|
176
|
+
"classify_query",
|
|
177
|
+
"fusion_profile",
|
|
178
|
+
"fusion_weight_table",
|
|
179
|
+
]
|
|
@@ -108,7 +108,11 @@ class KnowledgeGraphIngestMixin:
|
|
|
108
108
|
)
|
|
109
109
|
|
|
110
110
|
# ── 4. RAG chunks (검색용, 그래프에서 숨김) ──────────────────────
|
|
111
|
-
|
|
111
|
+
# Chat text is always the plain strategy: boundaries (and thus
|
|
112
|
+
# chunk ids) stay byte-identical to the legacy _chunks path.
|
|
113
|
+
for index, piece in enumerate(typed_chunks(content, strategy="plain")):
|
|
114
|
+
chunk = piece["text"]
|
|
115
|
+
chunk_fields = typed_chunk_meta_fields(piece)
|
|
112
116
|
chunk_id = f"chunk:{_sha256_text(f'{node_id}:{index}:{chunk}')[:24]}"
|
|
113
117
|
self._upsert_node(
|
|
114
118
|
conn,
|
|
@@ -116,7 +120,7 @@ class KnowledgeGraphIngestMixin:
|
|
|
116
120
|
"Chunk",
|
|
117
121
|
f"chunk {index + 1}",
|
|
118
122
|
summary=chunk[:500],
|
|
119
|
-
metadata={"index": index, "source_node": node_id},
|
|
123
|
+
metadata={"index": index, "source_node": node_id, **chunk_fields},
|
|
120
124
|
owner=user_email,
|
|
121
125
|
workspace_id=workspace_id,
|
|
122
126
|
)
|
|
@@ -125,7 +129,7 @@ class KnowledgeGraphIngestMixin:
|
|
|
125
129
|
chunk_id=chunk_id,
|
|
126
130
|
source_node=node_id,
|
|
127
131
|
text=chunk,
|
|
128
|
-
metadata={"index": index, "source_node": node_id},
|
|
132
|
+
metadata={"index": index, "source_node": node_id, **chunk_fields},
|
|
129
133
|
)
|
|
130
134
|
self._upsert_edge(conn, node_id, chunk_id, "포함함")
|
|
131
135
|
|
|
@@ -322,7 +326,25 @@ class KnowledgeGraphIngestMixin:
|
|
|
322
326
|
self._upsert_edge(conn, conv_id, file_id, "언급함", weight=0.8)
|
|
323
327
|
|
|
324
328
|
# ── RAG chunks (검색용, 그래프 비표시) ────────────────────────────
|
|
325
|
-
|
|
329
|
+
# Strategy from the filename (plain files keep legacy boundaries →
|
|
330
|
+
# identical chunk ids). For PDFs, per-page start offsets in the
|
|
331
|
+
# "\n\n"-joined page text let each chunk carry a 1-based "page" —
|
|
332
|
+
# only when the claimed extent matches the text we actually chunk
|
|
333
|
+
# (honest absence over wrong labels).
|
|
334
|
+
chunk_strategy = chunk_strategy_for(filename, content_type=mime_type or "")
|
|
335
|
+
page_offsets: List[int] = []
|
|
336
|
+
if ext == ".pdf":
|
|
337
|
+
page_offsets = pdf_page_offsets(doc_meta)
|
|
338
|
+
cleaned_len = len(str(text or "").strip())
|
|
339
|
+
if not page_offsets or page_offsets[-1] >= cleaned_len * 1.2:
|
|
340
|
+
page_offsets = []
|
|
341
|
+
for index, piece in enumerate(typed_chunks(text, strategy=chunk_strategy)):
|
|
342
|
+
chunk = piece["text"]
|
|
343
|
+
chunk_fields = typed_chunk_meta_fields(piece)
|
|
344
|
+
if page_offsets:
|
|
345
|
+
page = page_for_offset(page_offsets, chunk_fields["start_char"])
|
|
346
|
+
if page is not None:
|
|
347
|
+
chunk_fields["page"] = page
|
|
326
348
|
chunk_id = f"chunk:{_sha256_text(f'{file_id}:{index}:{chunk}')[:24]}"
|
|
327
349
|
chunk_ids.append(chunk_id)
|
|
328
350
|
self._upsert_node(
|
|
@@ -331,7 +353,7 @@ class KnowledgeGraphIngestMixin:
|
|
|
331
353
|
"Chunk",
|
|
332
354
|
f"{filename} chunk {index + 1}",
|
|
333
355
|
summary=chunk[:500],
|
|
334
|
-
metadata={"index": index, "source_node": file_id, "workspace_id": workspace_id},
|
|
356
|
+
metadata={"index": index, "source_node": file_id, "workspace_id": workspace_id, **chunk_fields},
|
|
335
357
|
owner=owner or uploader,
|
|
336
358
|
workspace_id=workspace_id,
|
|
337
359
|
)
|
|
@@ -340,7 +362,7 @@ class KnowledgeGraphIngestMixin:
|
|
|
340
362
|
chunk_id=chunk_id,
|
|
341
363
|
source_node=file_id,
|
|
342
364
|
text=chunk,
|
|
343
|
-
metadata={"index": index, "source_node": file_id},
|
|
365
|
+
metadata={"index": index, "source_node": file_id, **chunk_fields},
|
|
344
366
|
)
|
|
345
367
|
self._upsert_edge(conn, file_id, chunk_id, "포함함")
|
|
346
368
|
|
|
@@ -651,7 +673,19 @@ class KnowledgeGraphIngestMixin:
|
|
|
651
673
|
)
|
|
652
674
|
self._upsert_edge(conn, conv_id, content_id, "언급함", weight=0.8)
|
|
653
675
|
# ── RAG 청크 ────────────────────────────────────────────────────
|
|
654
|
-
|
|
676
|
+
# Best available filename hint: source URI, then path/filename from
|
|
677
|
+
# caller metadata. Plain content keeps legacy chunk boundaries and
|
|
678
|
+
# therefore identical chunk ids.
|
|
679
|
+
chunk_strategy = chunk_strategy_for(
|
|
680
|
+
source_uri
|
|
681
|
+
or (metadata or {}).get("path")
|
|
682
|
+
or (metadata or {}).get("filename")
|
|
683
|
+
or "",
|
|
684
|
+
content_type=str((metadata or {}).get("mime_type") or ""),
|
|
685
|
+
)
|
|
686
|
+
for index, piece in enumerate(typed_chunks(text, strategy=chunk_strategy)):
|
|
687
|
+
chunk = piece["text"]
|
|
688
|
+
chunk_fields = typed_chunk_meta_fields(piece)
|
|
655
689
|
chunk_id = f"chunk:{_sha256_text(f'{content_id}:{index}:{chunk}')[:24]}"
|
|
656
690
|
chunk_ids.append(chunk_id)
|
|
657
691
|
self._upsert_node(
|
|
@@ -660,7 +694,7 @@ class KnowledgeGraphIngestMixin:
|
|
|
660
694
|
"Chunk",
|
|
661
695
|
f"{title} chunk {index + 1}",
|
|
662
696
|
summary=chunk[:500],
|
|
663
|
-
metadata={"index": index, "source_node": content_id, "workspace_id": workspace_id},
|
|
697
|
+
metadata={"index": index, "source_node": content_id, "workspace_id": workspace_id, **chunk_fields},
|
|
664
698
|
owner=owner,
|
|
665
699
|
workspace_id=workspace_id,
|
|
666
700
|
)
|
|
@@ -669,7 +703,7 @@ class KnowledgeGraphIngestMixin:
|
|
|
669
703
|
chunk_id=chunk_id,
|
|
670
704
|
source_node=content_id,
|
|
671
705
|
text=chunk,
|
|
672
|
-
metadata={"index": index, "source_node": content_id},
|
|
706
|
+
metadata={"index": index, "source_node": content_id, **chunk_fields},
|
|
673
707
|
)
|
|
674
708
|
self._upsert_edge(conn, content_id, chunk_id, "포함함")
|
|
675
709
|
# ── Concept / Feature / Error / Code 노드 + 엣지 ────────────────
|