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.
Files changed (109) hide show
  1. package/README.md +44 -39
  2. package/docs/CHANGELOG.md +109 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/ONBOARDING.md +1 -1
  6. package/docs/OPERATIONS.md +1 -1
  7. package/docs/SURFACE_PARITY.md +47 -0
  8. package/docs/TRUST_MODEL.md +1 -1
  9. package/docs/WHY_LATTICE.md +1 -1
  10. package/docs/kg-schema.md +1 -1
  11. package/lattice_brain/__init__.py +1 -1
  12. package/lattice_brain/graph/_kg_common.py +335 -0
  13. package/lattice_brain/graph/curator.py +141 -0
  14. package/lattice_brain/graph/discovery_index.py +8 -1
  15. package/lattice_brain/graph/documents.py +111 -0
  16. package/lattice_brain/graph/fusion.py +179 -0
  17. package/lattice_brain/graph/ingest.py +43 -9
  18. package/lattice_brain/graph/projection.py +382 -37
  19. package/lattice_brain/graph/retrieval.py +71 -4
  20. package/lattice_brain/graph/retrieval_policy.py +174 -0
  21. package/lattice_brain/graph/retrieval_vector.py +122 -2
  22. package/lattice_brain/ingestion.py +66 -0
  23. package/lattice_brain/portability.py +41 -12
  24. package/lattice_brain/runtime/multi_agent.py +1 -1
  25. package/latticeai/__init__.py +1 -1
  26. package/latticeai/api/automation_intelligence.py +159 -0
  27. package/latticeai/api/browser.py +15 -4
  28. package/latticeai/api/chat.py +33 -2
  29. package/latticeai/api/chat_agent_http.py +363 -6
  30. package/latticeai/api/chat_contracts.py +7 -1
  31. package/latticeai/api/chat_helpers.py +147 -0
  32. package/latticeai/api/chat_intents.py +217 -10
  33. package/latticeai/api/chat_stream.py +93 -3
  34. package/latticeai/api/funnel_metrics.py +31 -0
  35. package/latticeai/api/knowledge_graph.py +65 -1
  36. package/latticeai/api/local_files.py +90 -0
  37. package/latticeai/api/setup.py +134 -1
  38. package/latticeai/api/tools.py +33 -3
  39. package/latticeai/app_factory.py +14 -0
  40. package/latticeai/core/agent.py +355 -18
  41. package/latticeai/core/agent_eval.py +107 -0
  42. package/latticeai/core/enterprise.py +5 -0
  43. package/latticeai/core/file_generation.py +404 -2
  44. package/latticeai/core/legacy_compatibility.py +1 -1
  45. package/latticeai/core/marketplace.py +1 -1
  46. package/latticeai/core/run_store.py +243 -0
  47. package/latticeai/core/workspace_os.py +1 -1
  48. package/latticeai/models/router.py +25 -12
  49. package/latticeai/runtime/persistence_runtime.py +19 -1
  50. package/latticeai/runtime/router_registration.py +8 -1
  51. package/latticeai/services/app_context.py +3 -0
  52. package/latticeai/services/architecture_readiness.py +1 -1
  53. package/latticeai/services/automation_execution.py +266 -0
  54. package/latticeai/services/automation_intelligence.py +7 -0
  55. package/latticeai/services/command_center.py +110 -2
  56. package/latticeai/services/folder_watch.py +396 -0
  57. package/latticeai/services/funnel_metrics.py +210 -0
  58. package/latticeai/services/product_readiness.py +1 -1
  59. package/latticeai/services/search_service.py +51 -5
  60. package/latticeai/setup/demo_corpus.py +98 -0
  61. package/latticeai/tools/filesystem.py +38 -1
  62. package/package.json +1 -1
  63. package/scripts/bench_agent_smoke.py +410 -0
  64. package/scripts/bench_models.py +234 -5
  65. package/scripts/check_current_release_docs.mjs +1 -1
  66. package/scripts/funnel_soft_gate.py +192 -0
  67. package/src-tauri/Cargo.lock +1 -1
  68. package/src-tauri/Cargo.toml +1 -1
  69. package/src-tauri/tauri.conf.json +1 -1
  70. package/static/app/asset-manifest.json +29 -27
  71. package/static/app/assets/Act-BCmTU0E2.js +1 -0
  72. package/static/app/assets/{Brain-BnGbytHa.js → Brain-CTnjox7w.js} +2 -2
  73. package/static/app/assets/BrainHome-BJ3sFNX0.js +2 -0
  74. package/static/app/assets/BrainSignals-C52lwZVD.js +1 -0
  75. package/static/app/assets/{Capture-CAKnLcb0.js → Capture-B6vBhFa3.js} +1 -1
  76. package/static/app/assets/CommandPalette-90u9FWFH.js +1 -0
  77. package/static/app/assets/Library-pKCK0_tk.js +1 -0
  78. package/static/app/assets/{LivingBrain-C3VbogVo.js → LivingBrain-Jlf2wFqI.js} +1 -1
  79. package/static/app/assets/{ProductFlow-BLau7c7N.js → ProductFlow-yg1fKP1P.js} +1 -1
  80. package/static/app/assets/ReviewCard-DWvD7n9h.js +3 -0
  81. package/static/app/assets/{System-DRHc_6Su.js → System-BAEuHqNY.js} +1 -1
  82. package/static/app/assets/{bot-DZkDdh-A.js → bot-nB_buEZD.js} +1 -1
  83. package/static/app/assets/circle-pause-DLNw6Ucp.js +1 -0
  84. package/static/app/assets/circle-play-B-IsFL1y.js +1 -0
  85. package/static/app/assets/{cpu-DyvsDBbj.js → cpu-CEPBHaBl.js} +1 -1
  86. package/static/app/assets/{folder-open-CffoIr9t.js → folder-open-hmN0N9cX.js} +1 -1
  87. package/static/app/assets/{hard-drive-C1oojrHb.js → hard-drive-CBV_B_Yd.js} +1 -1
  88. package/static/app/assets/index-7FAfYm4v.css +2 -0
  89. package/static/app/assets/{index-BS_3aN1n.js → index-DrmOCySv.js} +3 -3
  90. package/static/app/assets/{input-B_U7zvbR.js → input-gtVCg-ll.js} +1 -1
  91. package/static/app/assets/{navigation-xG-gyjbe.js → navigation-Bot0hvuv.js} +1 -1
  92. package/static/app/assets/{network-BF13HJJF.js → network-jE42eKfT.js} +1 -1
  93. package/static/app/assets/{primitives-DsuKh3M8.js → primitives-CX2Komon.js} +1 -1
  94. package/static/app/assets/shield-alert-BftATuAA.js +1 -0
  95. package/static/app/assets/{textarea-DsDL4ET8.js → textarea-CiMJfOSI.js} +1 -1
  96. package/static/app/assets/useFocusTrap-B7RPGfFy.js +1 -0
  97. package/static/app/assets/utils-SJUNVOj5.js +7 -0
  98. package/static/app/index.html +4 -4
  99. package/static/sw.js +1 -1
  100. package/static/app/assets/Act-Cx_gZNbE.js +0 -1
  101. package/static/app/assets/BrainHome-CRUMJL93.js +0 -2
  102. package/static/app/assets/BrainSignals-BP8-Qh2g.js +0 -1
  103. package/static/app/assets/CommandPalette-BoUTXBfm.js +0 -1
  104. package/static/app/assets/Library-PcfVUco6.js +0 -1
  105. package/static/app/assets/ReviewCard-CX5RqRqx.js +0 -1
  106. package/static/app/assets/download-C7yxYHvz.js +0 -1
  107. package/static/app/assets/index-DU78Kogz.css +0 -2
  108. package/static/app/assets/sunrise-CIP2ELJ1.js +0 -1
  109. 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
- for index, chunk in enumerate(_chunks(content)):
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
- for index, chunk in enumerate(_chunks(text)):
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
- for index, chunk in enumerate(_chunks(text)):
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 노드 + 엣지 ────────────────