ltcai 6.1.0 → 6.2.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.
Files changed (44) hide show
  1. package/README.md +33 -38
  2. package/docs/CHANGELOG.md +33 -1
  3. package/frontend/src/App.tsx +3 -1286
  4. package/frontend/src/components/LanguageSwitcher.tsx +23 -0
  5. package/frontend/src/components/ProductFlow.tsx +32 -669
  6. package/frontend/src/components/onboarding/ProductFlowScreens.tsx +688 -0
  7. package/frontend/src/features/admin/AdminConsole.tsx +294 -0
  8. package/frontend/src/features/brain/BrainHome.tsx +999 -0
  9. package/frontend/src/features/brain/brainData.ts +98 -0
  10. package/frontend/src/features/brain/graphLayout.ts +26 -0
  11. package/frontend/src/features/brain/types.ts +44 -0
  12. package/frontend/src/i18n.ts +198 -0
  13. package/frontend/src/styles.css +220 -0
  14. package/lattice_brain/__init__.py +1 -1
  15. package/lattice_brain/runtime/multi_agent.py +1 -1
  16. package/latticeai/__init__.py +1 -1
  17. package/latticeai/api/tools.py +50 -23
  18. package/latticeai/app_factory.py +36 -45
  19. package/latticeai/cli/entrypoint.py +283 -0
  20. package/latticeai/core/marketplace.py +1 -1
  21. package/latticeai/core/workspace_os.py +1 -1
  22. package/latticeai/integrations/__init__.py +0 -0
  23. package/latticeai/integrations/telegram_bot.py +1009 -0
  24. package/latticeai/runtime/lifespan_runtime.py +1 -1
  25. package/latticeai/runtime/platform_runtime_wiring.py +87 -0
  26. package/latticeai/runtime/router_registration.py +49 -30
  27. package/latticeai/services/p_reinforce.py +258 -0
  28. package/latticeai/services/router_context.py +52 -0
  29. package/ltcai_cli.py +7 -279
  30. package/p_reinforce.py +4 -255
  31. package/package.json +2 -1
  32. package/scripts/release_smoke.py +133 -0
  33. package/scripts/wheel_smoke.py +4 -0
  34. package/src-tauri/Cargo.lock +1 -1
  35. package/src-tauri/Cargo.toml +1 -1
  36. package/src-tauri/tauri.conf.json +1 -1
  37. package/static/app/asset-manifest.json +5 -5
  38. package/static/app/assets/{index-B744yblP.css → index-B2-1Gm0q.css} +1 -1
  39. package/static/app/assets/index-D91Rz5--.js +16 -0
  40. package/static/app/assets/index-D91Rz5--.js.map +1 -0
  41. package/static/app/index.html +2 -2
  42. package/telegram_bot.py +9 -1002
  43. package/static/app/assets/index-DYaUKNfl.js +0 -16
  44. package/static/app/assets/index-DYaUKNfl.js.map +0 -1
package/ltcai_cli.py CHANGED
@@ -1,283 +1,11 @@
1
- """Command line entrypoint for Lattice AI."""
1
+ """Compatibility shim for the historical root CLI module."""
2
2
 
3
- from __future__ import annotations
4
-
5
- import argparse
6
- import os
7
- import platform
8
- import re
9
- import shutil
10
- import socket
11
- import stat
12
- import subprocess
13
- import sys
14
- import threading
15
- import time
16
- import urllib.request
17
- from pathlib import Path
18
-
19
- from latticeai.cli.runtime import (
20
- _apply_extra_path,
21
- _has_module,
22
- _load_env_file,
23
- )
24
-
25
- def _local_ips() -> list[str]:
26
- ips: list[str] = []
27
- try:
28
- hostname = socket.gethostname()
29
- for info in socket.getaddrinfo(hostname, None):
30
- addr = info[4][0]
31
- if ":" not in addr and not addr.startswith("127."):
32
- if addr not in ips:
33
- ips.append(addr)
34
- except Exception:
35
- pass
36
- if not ips:
37
- try:
38
- s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
39
- s.connect(("8.8.8.8", 80))
40
- ips.append(s.getsockname()[0])
41
- s.close()
42
- except Exception:
43
- pass
44
- return ips
45
-
46
-
47
- def _print_banner(host: str, port: int, tunnel_url: str | None = None) -> None:
48
- local_url = f"http://localhost:{port}"
49
- print()
50
- print("=" * 56)
51
- print(" Lattice AI is running")
52
- print(f" Local: {local_url}")
53
- if host == "0.0.0.0":
54
- for ip in _local_ips():
55
- print(f" Network: http://{ip}:{port}")
56
- print()
57
- print(" Other devices on the same Wi-Fi can open the")
58
- print(" Network URL above in their browser.")
59
- print(" On iPad/Android: browser menu → 'Add to Home Screen'")
60
- if tunnel_url:
61
- print()
62
- print(f" Tunnel: {tunnel_url}")
63
- print(" Anyone on the internet can access via the Tunnel URL.")
64
- print("=" * 56)
65
- print()
66
-
67
-
68
- def doctor() -> int:
69
- checks = [
70
- ("Python 3.11+", sys.version_info >= (3, 11), sys.version.split()[0], True),
71
- ("FastAPI", _has_module("fastapi"), "required server dependency", True),
72
- ("Uvicorn", _has_module("uvicorn"), "required server dependency", True),
73
- ("OpenAI SDK", _has_module("openai"), "required for cloud providers", False),
74
- ("MLX", _has_module("mlx"), "required for Apple Silicon multimodal models", False),
75
- ("MLX-VLM", _has_module("mlx_vlm"), "required for Gemma-4/VLM models", False),
76
- ("Ollama binary", shutil.which("ollama") is not None, "optional local-server engine", False),
77
- ]
78
- data_dir = Path(os.getenv("LATTICEAI_DATA_DIR") or Path.home() / ".ltcai")
79
- static_dir = Path(os.getenv("LATTICEAI_STATIC_DIR") or Path(__file__).resolve().parent / "static")
80
- checks.extend([
81
- ("Data dir", data_dir.exists() or data_dir.parent.exists(), str(data_dir), True),
82
- ("Static UI", static_dir.exists(), str(static_dir), True),
83
- ])
84
-
85
- ok = True
86
- for label, passed, detail, required in checks:
87
- icon = "OK" if passed else ("MISS" if required else "OPTIONAL")
88
- print(f"[{icon}] {label}: {detail}")
89
- ok = ok and (passed or not required)
90
-
91
- cloud_keys = ["OPENAI_API_KEY", "OPENROUTER_API_KEY", "GROQ_API_KEY", "TOGETHER_API_KEY"]
92
- configured = [key for key in cloud_keys if os.getenv(key)]
93
- print(f"[INFO] Cloud keys configured: {', '.join(configured) if configured else 'none'}")
94
- return 0 if ok else 1
95
-
96
-
97
- # ── Cloudflare Tunnel ─────────────────────────────────────────────────────────
98
-
99
- def _cloudflared_url() -> str:
100
- system = sys.platform
101
- machine = platform.machine().lower()
102
- base = "https://github.com/cloudflare/cloudflared/releases/latest/download"
103
- if system == "darwin":
104
- arch = "arm64" if machine in ("arm64", "aarch64") else "amd64"
105
- return f"{base}/cloudflared-darwin-{arch}"
106
- if system == "win32":
107
- return f"{base}/cloudflared-windows-amd64.exe"
108
- arch = "arm64" if machine in ("arm64", "aarch64") else "amd64"
109
- return f"{base}/cloudflared-linux-{arch}"
110
-
111
-
112
- def _cloudflared_bin() -> Path:
113
- suffix = ".exe" if sys.platform == "win32" else ""
114
- return Path.home() / ".latticeai" / "bin" / f"cloudflared{suffix}"
115
-
116
-
117
- def _ensure_cloudflared() -> str:
118
- found = shutil.which("cloudflared")
119
- if found:
120
- return found
121
- dest = _cloudflared_bin()
122
- if dest.exists():
123
- return str(dest)
124
- dest.parent.mkdir(parents=True, exist_ok=True)
125
- url = _cloudflared_url()
126
- print(" cloudflared not found — downloading from GitHub...")
127
- try:
128
- urllib.request.urlretrieve(url, dest)
129
- if sys.platform != "win32":
130
- dest.chmod(dest.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
131
- print(f" cloudflared installed at {dest}")
132
- return str(dest)
133
- except Exception as e:
134
- print(f" cloudflared download failed: {e}")
135
- print(" Install manually: https://developers.cloudflare.com/cloudflared/install")
136
- return ""
137
-
138
-
139
- def _send_telegram(token: str, chat_id: str, text: str) -> None:
140
- try:
141
- data = urllib.parse.urlencode({"chat_id": chat_id, "text": text}).encode()
142
- req = urllib.request.Request(
143
- f"https://api.telegram.org/bot{token}/sendMessage",
144
- data=data,
145
- )
146
- urllib.request.urlopen(req, timeout=10)
147
- except Exception:
148
- pass
149
-
150
-
151
- def _start_tunnel(port: int) -> str | None:
152
-
153
- bin_path = _ensure_cloudflared()
154
- if not bin_path:
155
- return None
156
-
157
- log_path = Path.home() / ".latticeai" / "tunnel.log"
158
- log_path.parent.mkdir(parents=True, exist_ok=True)
159
-
160
- proc = subprocess.Popen(
161
- [bin_path, "tunnel", "--url", f"http://localhost:{port}"],
162
- stdout=open(log_path, "w"),
163
- stderr=subprocess.STDOUT,
164
- )
165
-
166
- # Wait for the public URL (up to 30s)
167
- pattern = re.compile(r"https://[a-z0-9-]+\.trycloudflare\.com")
168
- deadline = time.time() + 30
169
- url: str | None = None
170
- while time.time() < deadline:
171
- time.sleep(0.5)
172
- try:
173
- text = log_path.read_text(errors="replace")
174
- m = pattern.search(text)
175
- if m:
176
- url = m.group(0)
177
- break
178
- except Exception:
179
- pass
180
-
181
- if not url:
182
- return None
183
-
184
- # Telegram notification if configured
185
- token = os.getenv("LATTICEAI_TELEGRAM_BOT_TOKEN", "")
186
- chat_id = os.getenv("LATTICEAI_TELEGRAM_CHAT_ID", "")
187
- if token and chat_id:
188
- msg = (
189
- f"✅ Lattice AI 시작됨\n\n"
190
- f"🌐 외부 URL: {url}\n"
191
- f"🏠 로컬: http://localhost:{port}"
192
- )
193
- threading.Thread(target=_send_telegram, args=(token, chat_id, msg), daemon=True).start()
194
-
195
- return url
196
-
197
-
198
- # ─────────────────────────────────────────────────────────────────────────────
199
-
200
- def main() -> None:
201
- app_dir = Path(__file__).resolve().parent
202
- _load_env_file(app_dir / ".env")
203
- _apply_extra_path()
204
-
205
- parser = argparse.ArgumentParser(prog="LTCAI", description="Run the Lattice AI local server.")
206
- subparsers = parser.add_subparsers(dest="command")
207
- subparsers.add_parser("doctor", help="Check local runtime dependencies and configuration.")
208
- parser.add_argument("--host", default=os.getenv("LATTICEAI_HOST") or "127.0.0.1")
209
- parser.add_argument("--port", type=int, default=int(os.getenv("LATTICEAI_PORT") or "4825"))
210
- parser.add_argument("--reload", action="store_true", help="Enable uvicorn reload for local development.")
211
- parser.add_argument(
212
- "--tunnel",
213
- action="store_true",
214
- help="Open a public Cloudflare tunnel so anyone can access this server from the internet.",
215
- )
216
- args = parser.parse_args()
217
-
218
- if args.command == "doctor":
219
- raise SystemExit(doctor())
220
-
221
- os.chdir(app_dir)
222
-
223
- if not args.tunnel and os.getenv("LATTICEAI_TUNNEL", "").lower() in (
224
- "1",
225
- "true",
226
- "yes",
227
- ):
228
- print(
229
- " LATTICEAI_TUNNEL is ignored during default local startup; "
230
- "restart with --tunnel to expose this server."
231
- )
232
-
233
- # --tunnel forces 0.0.0.0 so cloudflared can reach the server
234
- if args.tunnel and args.host == "127.0.0.1":
235
- args.host = "0.0.0.0"
236
- os.environ.setdefault("LATTICEAI_HOST", "0.0.0.0")
237
- os.environ.setdefault("LATTICEAI_CORS_ALLOW_NETWORK", "true")
238
- os.environ.setdefault("LATTICEAI_REQUIRE_AUTH", "true")
239
-
240
- # Keep the app config in sync with CLI flags. ``Config.from_env`` is the
241
- # source of truth for /mode, /health.features, SSO defaults, and routers.
242
- os.environ["LATTICEAI_HOST"] = str(args.host)
243
- os.environ["LATTICEAI_PORT"] = str(args.port)
244
-
245
- tunnel_url: str | None = None
246
- if args.tunnel:
247
- print()
248
- print(" Starting Cloudflare tunnel...")
249
- tunnel_url = _start_tunnel(args.port)
250
- if not tunnel_url:
251
- print(" ⚠️ Tunnel URL not obtained — server will start without tunnel.")
252
-
253
- _print_banner(args.host, args.port, tunnel_url)
254
-
255
- # Telegram startup notification (local start, tunnel handled separately inside _start_tunnel)
256
- if not args.tunnel:
257
- _tg_enabled = os.getenv("LATTICEAI_ENABLE_TELEGRAM", "").strip().lower() in ("1", "true", "yes", "on")
258
- _tg_token = os.getenv("LATTICEAI_TELEGRAM_BOT_TOKEN", "")
259
- _tg_chat = os.getenv("LATTICEAI_TELEGRAM_CHAT_ID", "")
260
- if _tg_enabled and _tg_token and _tg_chat:
261
- _local_msg = (
262
- f"✅ Lattice AI 시작됨\n\n"
263
- f"🏠 로컬: http://localhost:{args.port}"
264
- )
265
- threading.Thread(
266
- target=_send_telegram,
267
- args=(_tg_token, _tg_chat, _local_msg),
268
- daemon=True,
269
- ).start()
270
-
271
- import uvicorn
272
-
273
- uvicorn.run(
274
- "server:app",
275
- host=args.host,
276
- port=args.port,
277
- reload=args.reload,
278
- log_level="info",
279
- )
3
+ from latticeai.cli import entrypoint as _impl
280
4
 
281
5
 
282
6
  if __name__ == "__main__":
283
- main()
7
+ _impl.main()
8
+ else:
9
+ import sys
10
+
11
+ sys.modules[__name__] = _impl
package/p_reinforce.py CHANGED
@@ -1,258 +1,7 @@
1
- """
2
- P-Reinforce Knowledge Gardener — notes capture with a brain-backed memory.
1
+ """Compatibility shim for the historical root P-Reinforce module."""
3
2
 
4
- v4 (T4.3 garden absorption): the markdown vault is no longer a second brain.
5
- The vault stays as the user-owned, Obsidian-compatible *mirror* (capability
6
- preserved), but the Knowledge Graph is authoritative: notes created through
7
- the API are ingested through the unified pipeline (provenance + hooks), the
8
- existing vault is imported idempotently, and chat context comes from brain
9
- queries instead of an O(n) vault scan per message.
10
- """
3
+ import sys
11
4
 
12
- import logging
13
- import os
14
- import re
15
- import shutil
16
- from datetime import datetime
17
- from pathlib import Path
18
- from typing import Any, Optional
5
+ from latticeai.services import p_reinforce as _impl
19
6
 
20
- BRAIN_DIR = Path(
21
- os.getenv("LATTICEAI_OBSIDIAN_VAULT_DIR")
22
- or os.getenv("LATTICEAI_BRAIN_DIR")
23
- or Path.home() / ".ltcai-brain"
24
- )
25
-
26
- STRUCTURE = {
27
- "10_Wiki": "검증된 지식, 개념 설명, 레퍼런스",
28
- "00_Raw": "정제되지 않은 원시 데이터, 아이디어 메모",
29
- "20_Skills": "재사용 가능한 코드 스니펫, 프롬프트, 워크플로",
30
- "30_Projects": "프로젝트별 컨텍스트, 진행 상황",
31
- "40_Log": "날짜별 작업 로그",
32
- }
33
-
34
-
35
- class PReinforceGardener:
36
- def __init__(self, ingestion_pipeline: Any = None, knowledge_graph: Any = None):
37
- self._pipeline = ingestion_pipeline
38
- self._kg = knowledge_graph
39
- self._ensure_structure()
40
-
41
- def _ensure_structure(self):
42
- for folder in STRUCTURE:
43
- (BRAIN_DIR / folder).mkdir(parents=True, exist_ok=True)
44
- # 인덱스 파일
45
- index_path = BRAIN_DIR / "INDEX.md"
46
- if not index_path.exists():
47
- index_path.write_text(self._render_index())
48
-
49
- def _render_index(self) -> str:
50
- lines = ["# 🧠 Lattice AI Brain — P-Reinforce Index\n"]
51
- lines.append(f"*Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}*\n")
52
- lines.append("\nThis folder is an Obsidian-compatible Markdown vault.\n")
53
- lines.append("\nThe Knowledge Graph is the authoritative store; this vault is the\nuser-owned markdown mirror of garden notes.\n")
54
- for folder, desc in STRUCTURE.items():
55
- lines.append(f"## [{folder}](./{folder}/)\n_{desc}_\n")
56
- lines.append("## Connector Status\n")
57
- lines.append(f"- OCR engine: `{'tesseract' if shutil.which('tesseract') else 'not installed'}`\n")
58
- return "\n".join(lines)
59
-
60
- # ── Classify ──────────────────────────────────────────────────────────────
61
-
62
- def _classify(self, text: str) -> str:
63
- """간단한 규칙 기반 분류 (LLM 없이도 동작)"""
64
- text_lower = text.lower()
65
-
66
- code_signals = ["def ", "class ", "import ", "```", "function ", "const ", "let ", "var "]
67
- if any(s in text for s in code_signals):
68
- return "20_Skills"
69
-
70
- wiki_signals = ["개념", "원리", "이란", "what is", "how does", "definition", "explanation"]
71
- if any(s in text_lower for s in wiki_signals):
72
- return "10_Wiki"
73
-
74
- project_signals = ["project", "프로젝트", "todo", "task", "작업", "기능", "feature"]
75
- if any(s in text_lower for s in project_signals):
76
- return "30_Projects"
77
-
78
- return "00_Raw"
79
-
80
- # ── File Naming ───────────────────────────────────────────────────────────
81
-
82
- def _make_filename(self, text: str, folder: str) -> str:
83
- # 첫 줄을 제목으로
84
- first_line = text.strip().split("\n")[0][:60]
85
- # 파일명 안전하게
86
- safe = re.sub(r"[^\w\s-]", "", first_line).strip()
87
- safe = re.sub(r"\s+", "_", safe)
88
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
89
- return f"{timestamp}_{safe or 'note'}.md"
90
-
91
- # ── Process ───────────────────────────────────────────────────────────────
92
-
93
- async def process(self, raw_data: str, category: Optional[str] = None) -> dict:
94
- folder = category if category in STRUCTURE else self._classify(raw_data)
95
- filename = self._make_filename(raw_data, folder)
96
- filepath = BRAIN_DIR / folder / filename
97
-
98
- # 마크다운 미러 (사용자 소유 Obsidian 호환 아티팩트)
99
- content = self._wrap_markdown(raw_data, folder)
100
- filepath.write_text(content, encoding="utf-8")
101
-
102
- # 오늘 로그에도 기록
103
- self._append_log(raw_data[:200], folder, filename)
104
-
105
- result = {
106
- "status": "saved",
107
- "folder": folder,
108
- "filename": filename,
109
- "path": str(filepath),
110
- "classified_as": folder,
111
- "description": STRUCTURE[folder],
112
- }
113
- # 두뇌(Knowledge Graph)가 정식 저장소: 통합 수집 파이프라인으로 ingest.
114
- result.update(self._ingest_note(raw_data, source_uri=str(filepath), folder=folder))
115
- return result
116
-
117
- def _ingest_note(self, text: str, *, source_uri: str, folder: str, title: Optional[str] = None) -> dict:
118
- if self._pipeline is None:
119
- return {"graph": "unavailable", "graph_detail": "ingestion pipeline not wired"}
120
- try:
121
- from lattice_brain.ingestion import IngestionItem
122
-
123
- ingest = self._pipeline.ingest(
124
- IngestionItem(
125
- source_type="note",
126
- title=title or text.strip().split("\n")[0][:80],
127
- text=text,
128
- source_uri=source_uri,
129
- metadata={"garden_folder": folder, "pipeline": "p-reinforce"},
130
- )
131
- )
132
- if ingest.status != "ok":
133
- return {"graph": ingest.status, "graph_detail": ingest.detail}
134
- return {
135
- "graph": "ok",
136
- "graph_node_id": ingest.node_id,
137
- "provenance_id": ingest.provenance_id,
138
- "duplicate": ingest.duplicate,
139
- }
140
- except Exception as exc:
141
- logging.warning("garden note ingest failed: %s", exc)
142
- return {"graph": "failed", "graph_detail": str(exc)}
143
-
144
- def import_vault(self) -> dict:
145
- """Idempotent import of every existing vault note into the brain.
146
-
147
- Content-hash dedup in the store makes re-runs safe; vault files are
148
- never modified or deleted. INDEX.md and the daily logs are skipped.
149
- """
150
- if self._pipeline is None:
151
- return {"status": "unavailable", "imported": 0}
152
- imported = duplicates = failed = 0
153
- for file_path in sorted(BRAIN_DIR.rglob("*.md")):
154
- if file_path.name == "INDEX.md" or "40_Log" in file_path.parts:
155
- continue
156
- try:
157
- text = file_path.read_text(encoding="utf-8")
158
- except Exception:
159
- failed += 1
160
- continue
161
- folder = file_path.parent.name if file_path.parent != BRAIN_DIR else "00_Raw"
162
- outcome = self._ingest_note(
163
- text, source_uri=str(file_path), folder=folder, title=file_path.stem
164
- )
165
- if outcome.get("graph") == "ok":
166
- if outcome.get("duplicate"):
167
- duplicates += 1
168
- else:
169
- imported += 1
170
- else:
171
- failed += 1
172
- if imported:
173
- logging.info("garden: imported %d vault notes into the brain (%d already known)", imported, duplicates)
174
- return {"status": "ok", "imported": imported, "duplicates": duplicates, "failed": failed}
175
-
176
- def _wrap_markdown(self, raw: str, folder: str) -> str:
177
- now = datetime.now().strftime("%Y-%m-%d %H:%M")
178
- first_line = raw.strip().split("\n")[0][:80]
179
- lines = [
180
- f"# {first_line}",
181
- f"\n> 📁 `{folder}` | 🕐 {now} | Lattice AI MLX\n",
182
- "---\n",
183
- raw,
184
- "\n\n---",
185
- "*Auto-organized by P-Reinforce Gardener*",
186
- ]
187
- return "\n".join(lines)
188
-
189
- def _append_log(self, preview: str, folder: str, filename: str):
190
- today = datetime.now().strftime("%Y-%m-%d")
191
- log_path = BRAIN_DIR / "40_Log" / f"{today}.md"
192
- entry = f"\n- [{datetime.now().strftime('%H:%M')}] → `{folder}/{filename}`\n > {preview[:100]}\n"
193
- with open(log_path, "a", encoding="utf-8") as f:
194
- if log_path.stat().st_size == 0 if log_path.exists() else True:
195
- f.write(f"# 📅 Log — {today}\n")
196
- f.write(entry)
197
-
198
- # ── Tree ──────────────────────────────────────────────────────────────────
199
-
200
- def get_tree(self) -> dict:
201
- """지식 정원 파일트리 (마크다운 미러 기준)."""
202
- folders = []
203
- for folder, desc in STRUCTURE.items():
204
- folder_path = BRAIN_DIR / folder
205
- files = []
206
- if folder_path.exists():
207
- for file_path in sorted(folder_path.glob("*.md")):
208
- try:
209
- stat = file_path.stat()
210
- files.append({
211
- "name": file_path.name,
212
- "size_bytes": stat.st_size,
213
- "modified_at": datetime.fromtimestamp(stat.st_mtime).isoformat(timespec="seconds"),
214
- })
215
- except OSError:
216
- continue
217
- folders.append({"name": folder, "description": desc, "files": files, "count": len(files)})
218
- return {"root": str(BRAIN_DIR), "folders": folders}
219
-
220
- def get_relevant_context(self, query: str, limit: int = 3) -> str:
221
- """질문과 관련된 정원 노트를 두뇌에서 검색해 컨텍스트로 반환.
222
-
223
- v4: 채팅마다 vault 전체를 rglob 하던 O(n) 스캔을 브레인 검색으로
224
- 대체. 그래프가 없으면(비활성) 기존 파일 스캔으로 정직하게 폴백.
225
- """
226
- if self._kg is not None:
227
- try:
228
- matches = self._kg.search(query, max(limit * 4, 8)).get("matches", [])
229
- results = []
230
- for match in matches:
231
- meta = match.get("metadata") or {}
232
- if not (meta.get("garden_folder") or meta.get("pipeline") == "p-reinforce"):
233
- continue
234
- title = match.get("title") or "note"
235
- body = match.get("summary") or ""
236
- results.append(f"--- Document: {title} ---\n{body[:800]}")
237
- if len(results) >= limit:
238
- break
239
- return "\n\n".join(results)
240
- except Exception as exc:
241
- logging.debug("garden brain context failed, falling back to vault scan: %s", exc)
242
- return self._scan_vault_context(query, limit)
243
-
244
- def _scan_vault_context(self, query: str, limit: int = 3) -> str:
245
- results = []
246
- for file_path in BRAIN_DIR.rglob("*.md"):
247
- if file_path.name == "INDEX.md" or "40_Log" in str(file_path):
248
- continue
249
- try:
250
- content = file_path.read_text(encoding="utf-8")
251
- keywords = [k for k in re.split(r"\s+", query) if len(k) > 1]
252
- if any(k.lower() in content.lower() for k in keywords):
253
- results.append(f"--- Document: {file_path.name} ---\n{content[:800]}")
254
- if len(results) >= limit:
255
- break
256
- except Exception:
257
- continue
258
- return "\n\n".join(results)
7
+ sys.modules[__name__] = _impl
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "6.1.0",
3
+ "version": "6.2.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": {
@@ -40,6 +40,7 @@
40
40
  "package:vsix": "node scripts/build_vsix.mjs",
41
41
  "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",
42
42
  "release:validate": "node scripts/run_python.mjs scripts/validate_release_artifacts.py $npm_package_version --require-vsix --require-tgz --require-dmg",
43
+ "release:smoke": "node scripts/run_python.mjs scripts/release_smoke.py $npm_package_version",
43
44
  "publish:npm": "npm pack && npm publish ltcai-$npm_package_version.tgz --access public",
44
45
  "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",
45
46
  "publish:vscode": "cd vscode-extension && npm run package:vsix && npm run publish:vscode",