ltcai 3.6.0 → 4.0.1

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 (238) hide show
  1. package/README.md +39 -31
  2. package/docs/CHANGELOG.md +64 -0
  3. package/docs/REALTIME_COLLABORATION.md +3 -3
  4. package/docs/V3_FRONTEND.md +9 -8
  5. package/docs/V4_BRAIN_ARCHITECTURE.md +322 -0
  6. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +552 -0
  7. package/docs/V4_IMPLEMENTATION_PLAN.md +470 -0
  8. package/docs/kg-schema.md +51 -53
  9. package/docs/spec-vs-impl.md +10 -10
  10. package/kg_schema.py +2 -520
  11. package/knowledge_graph.py +37 -4629
  12. package/knowledge_graph_api.py +11 -127
  13. package/latticeai/__init__.py +1 -1
  14. package/latticeai/api/admin.py +16 -17
  15. package/latticeai/api/agents.py +20 -7
  16. package/latticeai/api/auth.py +46 -15
  17. package/latticeai/api/chat.py +112 -76
  18. package/latticeai/api/health.py +1 -1
  19. package/latticeai/api/hooks.py +1 -1
  20. package/latticeai/api/invitations.py +100 -0
  21. package/latticeai/api/knowledge_graph.py +139 -0
  22. package/latticeai/api/local_files.py +1 -1
  23. package/latticeai/api/mcp.py +23 -11
  24. package/latticeai/api/memory.py +1 -1
  25. package/latticeai/api/models.py +1 -1
  26. package/latticeai/api/network.py +81 -0
  27. package/latticeai/api/plugins.py +3 -6
  28. package/latticeai/api/realtime.py +5 -8
  29. package/latticeai/api/search.py +26 -2
  30. package/latticeai/api/security_dashboard.py +2 -3
  31. package/latticeai/api/setup.py +2 -2
  32. package/latticeai/api/static_routes.py +11 -16
  33. package/latticeai/api/tools.py +3 -0
  34. package/latticeai/api/ui_redirects.py +26 -0
  35. package/latticeai/api/workflow_designer.py +85 -6
  36. package/latticeai/api/workspace.py +93 -57
  37. package/latticeai/app_factory.py +1781 -0
  38. package/latticeai/brain/__init__.py +18 -0
  39. package/latticeai/brain/_kg_common.py +1123 -0
  40. package/latticeai/brain/context.py +213 -0
  41. package/latticeai/brain/conversations.py +236 -0
  42. package/latticeai/brain/discovery.py +1455 -0
  43. package/latticeai/brain/documents.py +218 -0
  44. package/latticeai/brain/identity.py +175 -0
  45. package/latticeai/brain/ingest.py +644 -0
  46. package/latticeai/brain/memory.py +102 -0
  47. package/latticeai/brain/network.py +205 -0
  48. package/latticeai/brain/projection.py +561 -0
  49. package/latticeai/brain/provenance.py +401 -0
  50. package/latticeai/brain/retrieval.py +1316 -0
  51. package/latticeai/brain/schema.py +640 -0
  52. package/latticeai/brain/store.py +216 -0
  53. package/latticeai/brain/write_master.py +225 -0
  54. package/latticeai/core/agent.py +31 -7
  55. package/latticeai/core/audit.py +0 -7
  56. package/latticeai/core/config.py +1 -1
  57. package/latticeai/core/context_builder.py +1 -2
  58. package/latticeai/core/enterprise.py +1 -1
  59. package/latticeai/core/graph_curator.py +2 -2
  60. package/latticeai/core/invitations.py +131 -0
  61. package/latticeai/core/marketplace.py +1 -1
  62. package/latticeai/core/mcp_registry.py +791 -0
  63. package/latticeai/core/model_compat.py +1 -1
  64. package/latticeai/core/model_resolution.py +0 -1
  65. package/latticeai/core/multi_agent.py +238 -4
  66. package/latticeai/core/policy.py +54 -0
  67. package/latticeai/core/realtime.py +65 -44
  68. package/latticeai/core/security.py +1 -1
  69. package/latticeai/core/sessions.py +66 -10
  70. package/latticeai/core/users.py +147 -0
  71. package/latticeai/core/workflow_engine.py +114 -2
  72. package/latticeai/core/workspace_os.py +477 -29
  73. package/latticeai/models/__init__.py +7 -0
  74. package/latticeai/models/router.py +779 -0
  75. package/latticeai/server_app.py +29 -1536
  76. package/latticeai/services/agent_runtime.py +243 -4
  77. package/latticeai/services/app_context.py +75 -14
  78. package/latticeai/services/ingestion.py +47 -0
  79. package/latticeai/services/kg_portability.py +33 -3
  80. package/latticeai/services/memory_service.py +39 -11
  81. package/latticeai/services/model_runtime.py +2 -5
  82. package/latticeai/services/platform_runtime.py +100 -23
  83. package/latticeai/services/run_executor.py +328 -0
  84. package/latticeai/services/search_service.py +17 -8
  85. package/latticeai/services/tool_dispatch.py +12 -2
  86. package/latticeai/services/triggers.py +241 -0
  87. package/latticeai/services/upload_service.py +37 -12
  88. package/latticeai/services/workspace_service.py +55 -16
  89. package/llm_router.py +29 -772
  90. package/ltcai_cli.py +1 -2
  91. package/mcp_registry.py +25 -788
  92. package/p_reinforce.py +124 -14
  93. package/package.json +10 -20
  94. package/scripts/bump_version.py +99 -0
  95. package/scripts/generate_diagrams.py +0 -1
  96. package/scripts/lint_v3.mjs +105 -18
  97. package/scripts/validate_release_artifacts.py +0 -1
  98. package/scripts/wheel_smoke.py +142 -0
  99. package/server.py +11 -7
  100. package/setup_wizard.py +1142 -0
  101. package/static/sw.js +81 -52
  102. package/static/v3/asset-manifest.json +33 -25
  103. package/static/v3/css/{lattice.base.e4cdd05d.css → lattice.base.49deefb5.css} +1 -1
  104. package/static/v3/css/lattice.base.css +1 -1
  105. package/static/v3/css/{lattice.components.9b49d614.css → lattice.components.cde18231.css} +1 -1
  106. package/static/v3/css/lattice.components.css +1 -1
  107. package/static/v3/css/{lattice.shell.8fcc9d33.css → lattice.shell.29d36d85.css} +1 -1
  108. package/static/v3/css/lattice.shell.css +1 -1
  109. package/static/v3/css/{lattice.tokens.e7018963.css → lattice.tokens.304cbc40.css} +3 -0
  110. package/static/v3/css/lattice.tokens.css +3 -0
  111. package/static/v3/css/{lattice.views.22f69117.css → lattice.views.0a18b6c5.css} +2 -2
  112. package/static/v3/css/lattice.views.css +2 -2
  113. package/static/v3/index.html +3 -4
  114. package/static/v3/js/{app.c541f955.js → app.c5c80c46.js} +1 -1
  115. package/static/v3/js/core/{api.33d6320e.js → api.ba0fbf14.js} +58 -1
  116. package/static/v3/js/core/api.js +57 -0
  117. package/static/v3/js/core/i18n.880e1fec.js +575 -0
  118. package/static/v3/js/core/i18n.js +575 -0
  119. package/static/v3/js/core/routes.37522821.js +101 -0
  120. package/static/v3/js/core/routes.js +71 -63
  121. package/static/v3/js/core/{shell.8c163e0e.js → shell.e3f6bbfa.js} +68 -39
  122. package/static/v3/js/core/shell.js +66 -37
  123. package/static/v3/js/core/{store.34ebd5e6.js → store.7b2aa044.js} +11 -1
  124. package/static/v3/js/core/store.js +11 -1
  125. package/static/v3/js/views/account.eff40715.js +143 -0
  126. package/static/v3/js/views/account.js +143 -0
  127. package/static/v3/js/views/activity.0d271ef9.js +67 -0
  128. package/static/v3/js/views/activity.js +67 -0
  129. package/static/v3/js/views/{admin-users.03bac88c.js → admin-users.f7ac7b43.js} +4 -6
  130. package/static/v3/js/views/admin-users.js +4 -6
  131. package/static/v3/js/views/{agents.014d0b74.js → agents.17c5288d.js} +35 -12
  132. package/static/v3/js/views/agents.js +35 -12
  133. package/static/v3/js/views/{chat.e6dd7dd0.js → chat.e250e2cc.js} +23 -0
  134. package/static/v3/js/views/chat.js +23 -0
  135. package/static/v3/js/views/graph-canvas.17c15d65.js +509 -0
  136. package/static/v3/js/views/graph-canvas.js +509 -0
  137. package/static/v3/js/views/{hybrid-search.b22b97e0.js → hybrid-search.2fb63ed9.js} +1 -2
  138. package/static/v3/js/views/hybrid-search.js +1 -2
  139. package/static/v3/js/views/{knowledge-graph.a96040a5.js → knowledge-graph.4d09c537.js} +60 -44
  140. package/static/v3/js/views/knowledge-graph.js +60 -44
  141. package/static/v3/js/views/network.52a4f181.js +97 -0
  142. package/static/v3/js/views/network.js +97 -0
  143. package/static/v3/js/views/{planning.9ac3e313.js → planning.4876fd77.js} +26 -5
  144. package/static/v3/js/views/planning.js +26 -5
  145. package/static/v3/js/views/runs.b63b2afa.js +144 -0
  146. package/static/v3/js/views/runs.js +144 -0
  147. package/static/v3/js/views/{settings.8631fa5e.js → settings.b7140634.js} +7 -8
  148. package/static/v3/js/views/settings.js +7 -8
  149. package/static/v3/js/views/snapshots.6f5db095.js +135 -0
  150. package/static/v3/js/views/snapshots.js +135 -0
  151. package/static/v3/js/views/{workflows.26c57290.js → workflows.7752225a.js} +87 -2
  152. package/static/v3/js/views/workflows.js +87 -2
  153. package/static/v3/js/views/workspace-admin.c466029b.js +156 -0
  154. package/static/v3/js/views/workspace-admin.js +156 -0
  155. package/static/vendor/chart.umd.min.js +20 -0
  156. package/static/vendor/fonts/inter-latin-300-normal.woff2 +0 -0
  157. package/static/vendor/fonts/inter-latin-400-normal.woff2 +0 -0
  158. package/static/vendor/fonts/inter-latin-500-normal.woff2 +0 -0
  159. package/static/vendor/fonts/inter-latin-600-normal.woff2 +0 -0
  160. package/static/vendor/fonts/inter-latin-700-normal.woff2 +0 -0
  161. package/static/vendor/fonts/inter-latin-800-normal.woff2 +0 -0
  162. package/static/vendor/fonts/inter.css +44 -0
  163. package/static/vendor/icons/tabler-icons.min.css +4 -0
  164. package/static/vendor/icons/tabler-icons.woff2 +0 -0
  165. package/static/vendor/marked.min.js +69 -0
  166. package/telegram_bot.py +1 -2
  167. package/tools/commands.py +4 -2
  168. package/tools/computer.py +1 -1
  169. package/tools/documents.py +1 -3
  170. package/tools/filesystem.py +0 -4
  171. package/tools/knowledge.py +1 -3
  172. package/tools/network.py +1 -3
  173. package/codex_telegram_bot.py +0 -195
  174. package/docs/assets/v3.4.0/agent-run.png +0 -0
  175. package/docs/assets/v3.4.0/agents.png +0 -0
  176. package/docs/assets/v3.4.0/before/chat-before.png +0 -0
  177. package/docs/assets/v3.4.0/before/files-before.png +0 -0
  178. package/docs/assets/v3.4.0/chat.png +0 -0
  179. package/docs/assets/v3.4.0/connect-folder.png +0 -0
  180. package/docs/assets/v3.4.0/files.png +0 -0
  181. package/docs/assets/v3.4.0/home.png +0 -0
  182. package/docs/assets/v3.4.0/hooks-dispatch.png +0 -0
  183. package/docs/assets/v3.4.0/knowledge-graph.png +0 -0
  184. package/docs/assets/v3.4.0/local-agent.png +0 -0
  185. package/docs/assets/v3.4.0/memory.png +0 -0
  186. package/docs/assets/v3.4.0/settings.png +0 -0
  187. package/docs/assets/v3.4.0/vision-input.png +0 -0
  188. package/docs/assets/v3.4.0/workflows.png +0 -0
  189. package/docs/assets/v3.4.1/e2e_runtime_log.txt +0 -42
  190. package/docs/assets/v3.4.1/hooks-dispatch.png +0 -0
  191. package/docs/assets/v3.4.1/local-agent.png +0 -0
  192. package/docs/images/admin-dashboard.png +0 -0
  193. package/docs/images/architecture.png +0 -0
  194. package/docs/images/enterprise.png +0 -0
  195. package/docs/images/graph.png +0 -0
  196. package/docs/images/hero.gif +0 -0
  197. package/docs/images/knowledge-graph.png +0 -0
  198. package/docs/images/lattice-ai-demo.gif +0 -0
  199. package/docs/images/lattice-ai-hero.png +0 -0
  200. package/docs/images/logo.svg +0 -33
  201. package/docs/images/mobile-responsive.png +0 -0
  202. package/docs/images/model-recommendation.png +0 -0
  203. package/docs/images/onboarding.png +0 -0
  204. package/docs/images/organization.png +0 -0
  205. package/docs/images/pipeline.png +0 -0
  206. package/docs/images/screenshot-admin.png +0 -0
  207. package/docs/images/screenshot-chat.png +0 -0
  208. package/docs/images/screenshot-graph.png +0 -0
  209. package/docs/images/skills.png +0 -0
  210. package/docs/images/workspace-dark.png +0 -0
  211. package/docs/images/workspace-light.png +0 -0
  212. package/docs/images/workspace.png +0 -0
  213. package/requirements.txt +0 -16
  214. package/static/account.html +0 -115
  215. package/static/activity.html +0 -73
  216. package/static/admin.html +0 -488
  217. package/static/agents.html +0 -139
  218. package/static/chat.html +0 -844
  219. package/static/css/reference/account.css +0 -439
  220. package/static/css/reference/admin.css +0 -610
  221. package/static/css/reference/base.css +0 -1661
  222. package/static/css/reference/chat.css +0 -4623
  223. package/static/css/reference/graph.css +0 -1016
  224. package/static/css/responsive.css +0 -861
  225. package/static/graph.html +0 -124
  226. package/static/platform.css +0 -104
  227. package/static/plugins.html +0 -136
  228. package/static/scripts/account.js +0 -238
  229. package/static/scripts/admin.js +0 -1614
  230. package/static/scripts/chat.js +0 -5081
  231. package/static/scripts/graph.js +0 -1804
  232. package/static/scripts/platform.js +0 -64
  233. package/static/scripts/ux.js +0 -167
  234. package/static/scripts/workspace.js +0 -948
  235. package/static/v3/js/core/routes.2ce3815a.js +0 -93
  236. package/static/workflows.html +0 -146
  237. package/static/workspace.css +0 -1121
  238. package/static/workspace.html +0 -357
@@ -9,7 +9,7 @@
9
9
 
10
10
  | 약속 | 목표 (PPT) | 현재 (repo) | 갭 |
11
11
  |------|-----------|-------------|----|
12
- | Cross-Platform Parity | Win·macOS·Linux·iOS·Android, 같은 디자인 토큰·컴포넌트 | Web(static) + VSCode ext + Telegram (브라우저 어디서나) | 네이티브 데스크탑/모바일 셸 없음 (PWA 부분 지원) |
12
+ | Cross-Platform Parity | Win·macOS·Linux·iOS·Android, 같은 디자인 토큰·컴포넌트 | `/app` SPA + VSCode ext + Telegram (브라우저 어디서나) | 네이티브 데스크탑/모바일 셸 없음 (PWA 부분 지원) |
13
13
  | Zero-Config Auto Setup | PROBE → RECOMMEND → INSTALL → VERIFY → PRESET, 90초 내 | `LTCAI doctor` (의존성만 체크 = PROBE의 일부) | GPU/RAM 프로빙, 추천, 설치, 벤치마크, 프리셋 미구현 |
14
14
  | Everything is a Graph | 10 노드 타입 / 12 엣지 타입 / 임베딩 / 신뢰도+증거 | nodes·edges·chunks 테이블 + 한글 동사 엣지(EDGE_VERB) | 명시 enum·embedding·confidence/evidence·owner 결손 |
15
15
 
@@ -20,7 +20,7 @@
20
20
  PPT 명세는 "한 코드·다섯 화면" — Shared Core(Design Tokens, UI Components, Business Logic, AI/Graph Core) 위에서 Tauri(데스크탑) / Capacitor·RN(모바일) 렌더러가 같은 결과를 낸다.
21
21
 
22
22
  **현재 구현**
23
- - `static/chat.html`, `static/graph.html`, `static/admin.html`, `static/account.html` 4개 HTML `static/css/tokens.css` 단일 토큰을 공유 (v2.2.1)
23
+ - `static/v3/` `/app` SPA 단일 토큰과 컴포넌트를 공유하며 legacy HTML 페이지는 v4에서 삭제됨
24
24
  - `vscode-extension/` — TypeScript VSCode 통합
25
25
  - `static/manifest.json` + `static/sw.js` — PWA 부분 지원 (iOS/Android 홈 화면 추가는 됨)
26
26
  - `telegram_bot.py` — Telegram 미러
@@ -28,11 +28,11 @@ PPT 명세는 "한 코드·다섯 화면" — Shared Core(Design Tokens, UI Comp
28
28
  **갭**
29
29
  - 데스크탑 네이티브 셸 (Tauri) 미구현
30
30
  - 모바일 네이티브 (Capacitor / RN) 미구현
31
- - 다국어(i18n) 시스템화 (HTML에 한글 하드코딩)
31
+ - `/app` SPA i18n(en/ko) 런타임은 구현됨; 네이티브 데스크탑/모바일 셸은 아직 없음
32
32
 
33
33
  **보강 결과물 (완료, v2.2.1)**
34
- - `static/css/tokens.css` — 4개 HTML이 공유하는 단일 진실 토큰 (`:root` = 라이트, `[data-lt-theme="dark"]` = 다크)
35
- - `static/css/responsive.css` + `static/scripts/ux.js` — 반응형 레이아웃 / 라이트·다크 토글 + OS 감지 + 지속화
34
+ - `static/css/tokens.css` + `static/v3/css/` `/app` SPA가 공유하는 단일 진실 토큰
35
+ - `static/v3/js/core/store.js` + `shell.js` — 반응형 레이아웃 / 라이트·다크 토글 + OS 감지 + 지속화
36
36
  - 로드맵: `apps/desktop/` (Tauri 셸) · `apps/mobile/` (Capacitor 셸) 차후 단계
37
37
 
38
38
  ---
@@ -103,9 +103,9 @@ chunks ( id, source_node, text, metadata_json, created_at )
103
103
  | **PPT 명세** | `#FFFFFF` 또는 `#0B0B16` | `#6E4AE6` Lattice 보라 |
104
104
 
105
105
  **보강 결과물 (완료, v2.2.1)**
106
- - `static/css/tokens.css` — 단일 토큰을 모든 화면이 공유. `:root` 가 라이트 값,
106
+ - `static/css/tokens.css` — 단일 토큰을 `/app` 화면이 공유. `:root` 가 라이트 값,
107
107
  `[data-lt-theme="dark"]` 가 다크 값을 정의하는 단일 진실 소스
108
- - 라이트/다크 토글 + OS 다크모드 감지 + 지속화는 `static/scripts/ux.js` 가 담당
108
+ - 라이트/다크 토글 + OS 다크모드 감지 + 지속화는 `static/v3/js/core/store.js` 와 `shell.js` 가 담당
109
109
 
110
110
  ---
111
111
 
@@ -115,10 +115,10 @@ PPT 화면 1, 13 (login, security) 에 한국어 / Microsoft Entra ID / Okta SSO
115
115
 
116
116
  **현재**
117
117
  - `server.py` 의 `/auth/sso` 엔드포인트 존재 (architecture.md 언급) — Entra/Okta 둘 다 명시되어 있는지 확인 필요
118
- - 다국어 — HTML 하드코딩 (`lang="ko"`)
118
+ - 다국어 — `/app` SPA en/ko 런타임(`static/v3/js/core/i18n.js`)과 언어 선택이 구현됨
119
119
 
120
120
  **갭 / 다음 단계**
121
- - i18n 사전 (`static/i18n/{ko,en,ja}.json`) 추출 PPT 명세 그대로 토큰화
121
+ - i18n 사전 런타임은 `static/v3/js/core/i18n.js` 구현됨; 추가 언어는 사전에 확장
122
122
 
123
123
  ---
124
124
 
@@ -127,7 +127,7 @@ PPT 화면 1, 13 (login, security) 에 한국어 / Microsoft Entra ID / Okta SSO
127
127
  | 순위 | 파일 | 무엇 |
128
128
  |------|------|------|
129
129
  | 1 | `docs/kg-schema.md`, `kg_schema.py` | KG 스키마 정식화 (10 노드 · 12 엣지 · embedding · confidence) |
130
- | 2 | `static/css/tokens.css` (+ `responsive.css`, `ux.js`) | 디자인 토큰 통합 + 라이트/다크 v2.2.1 완료 |
130
+ | 2 | `static/css/tokens.css` + `static/v3/` | 디자인 토큰 통합 + 라이트/다크 + en/ko i18n |
131
131
  | 3 | `auto_setup.py` | OS 프로빙 + 모델 추천 + 설치 어댑터 |
132
132
  | 4 | `docs/architecture.md` 보강 | 위 변경 반영 |
133
133
  | 5 | (차후) `apps/desktop`, `apps/mobile` 스캐폴딩 | Tauri/Capacitor |
package/kg_schema.py CHANGED
@@ -1,521 +1,3 @@
1
- """
2
- Lattice AI — Knowledge Graph v2 schema (PPT spec aligned)
3
- =========================================================
1
+ """Compatibility shim for the v4 Knowledge Graph schema."""
4
2
 
5
- 명세: ``lattice_ai_full_spec.pptx`` 슬라이드 20~22 (Node / Edge / Data Model)
6
-
7
- 목적
8
- ----
9
- 기존 ``knowledge_graph.py`` 의 자유 문자열 노드/엣지 타입을 **명시 enum + SQLite v2
10
- 스키마** 로 정식화한다. 이 모듈은 **스키마/초기화/프로젝션 지원** 역할만 담당한다:
11
- ``NodeType``/``EdgeType`` taxonomy + legacy 정규화 매핑, ``nodes_v2``/``edges_v2``
12
- DDL(``SCHEMA_SQL``), 그리고 ``KGStoreV2``(스키마 init·heal·stats).
13
-
14
- 실제 데이터 read/write 는 ``knowledge_graph.py`` 의 ``KnowledgeGraphStore`` 가
15
- legacy 테이블에 대한 dual-write 프로젝션(raw SQL) + ``kgv2_*`` 재구성 뷰로 수행한다.
16
- (과거의 native ``Node``/``Edge`` 모델과 ``KGStoreV2.upsert_*``/``get_node``/
17
- ``search_*`` API 는 production 에서 쓰이지 않아 제거되었다.)
18
-
19
- 설계 원칙
20
- ---------
21
- 1. **기존 코드를 깨지 않는다**: 새 테이블 이름은 ``nodes_v2`` / ``edges_v2``
22
- 로 분리. 기존 ``nodes`` / ``edges`` 와 공존한다. legacy → v2 reprojection 은
23
- ``knowledge_graph.py`` 의 버전 게이트 백필 한 곳에서만 수행한다.
24
- 2. **정규화 + 무손실**: legacy 자유 문자열 타입은 ``NodeType``/``EdgeType``
25
- superset 으로 정규화해 ``type`` 칼럼에 저장하고, 원본 문자열은 ``legacy_type``
26
- 칼럼에 그대로 보존한다. summary 와 metadata 는 ``attrs._kg`` 패스스루 blob 이
27
- 아니라 전용 ``summary`` 칼럼 / ``attrs``·``metadata`` 칼럼에 1급으로 저장한다.
28
- 3. **표준 라이브러리만 사용**: 외부 의존성 없이 ``sqlite3`` 만으로 동작한다.
29
- 4. **정규화 매핑은 명시적**: 한글 동사/legacy 라벨 → 영문 enum 표가 코드 안에
30
- 들어 있어서 어떤 옛 라벨이 어디로 매핑되는지 한눈에 보인다.
31
-
32
- 사용 예
33
- -------
34
- ```python
35
- from kg_schema import KGStoreV2
36
-
37
- store = KGStoreV2("/Users/me/.ltcai/kg_v2.db")
38
- store.init_schema() # nodes_v2 / edges_v2 생성 + 컬럼 drift self-heal
39
- print(store.stats()) # {"nodes": ..., "by_node_type": {...}, ...}
40
- ```
41
- """
42
-
43
- from __future__ import annotations
44
-
45
- import json
46
- import os
47
- import logging
48
- import sqlite3
49
- from contextlib import contextmanager
50
- from enum import Enum
51
- from typing import Any, Dict, Optional
52
-
53
-
54
- # ── Schema version ──────────────────────────────────────────────────────────
55
- KG_SCHEMA_V2_VERSION = 2
56
- EMBED_DIM = int(os.getenv("LATTICEAI_EMBED_DIM", "1024"))
57
-
58
-
59
- # ── Node / Edge taxonomy (PPT 슬라이드 20·21) ──────────────────────────────
60
- class NodeType(str, Enum):
61
- """워크스페이스의 모든 ‘명사’.
62
-
63
- PPT 슬라이드 20 카탈로그(상단 그룹)에 더해, ``knowledge_graph.py`` 가 실제로
64
- 써오던 legacy 자유 문자열 타입을 **무손실 superset**(하단 그룹)으로 1급 enum 화
65
- 한다. 덕분에 ``from_legacy`` 정규화가 의미를 잃지 않고(예: ``Computer`` →
66
- ``COMPUTER``), 알 수 없는/동적(이벤트) 타입만 ``CONCEPT`` 로 폴백한다.
67
- 원본 문자열은 ``nodes_v2.legacy_type`` 에 그대로 보존되므로 정규화는 항상 무손실.
68
- """
69
- # PPT 슬라이드 20 정식 카탈로그
70
- CONVERSATION = "CONVERSATION" # 대화 세션 전체
71
- MESSAGE = "MESSAGE" # 단일 발화
72
- FILE = "FILE" # 업로드/연결된 파일
73
- DOCUMENT = "DOCUMENT" # 생성/관리되는 문서 (보고서, 계획서 등)
74
- CHUNK = "CHUNK" # 파일의 분할 청크
75
- CODE_SYMBOL = "CODE_SYMBOL" # 함수·클래스·모듈
76
- CONCEPT = "CONCEPT" # 추출된 개념 / 태그
77
- PERSON = "PERSON" # 사용자·협업자
78
- MODEL = "MODEL" # 로컬/원격 LLM
79
- TOOL = "TOOL" # MCP 서버·외부 도구
80
- PROJECT = "PROJECT" # 주제별 작업 공간
81
- # legacy superset — knowledge_graph.py 가 실제로 생성하던 노드 타입들
82
- COMPUTER = "COMPUTER" # 내 컴퓨터 (로컬 스캔 루트)
83
- DRIVE = "DRIVE" # 드라이브 / 볼륨
84
- FOLDER = "FOLDER" # 폴더
85
- CODE_FILE = "CODE_FILE" # 코드 파일 (.py/.ts 등)
86
- SPREADSHEET = "SPREADSHEET" # 엑셀 / CSV
87
- SLIDE_DECK = "SLIDE_DECK" # 프레젠테이션
88
- IMAGE = "IMAGE" # 이미지 파일
89
- IMAGE_TEXT = "IMAGE_TEXT" # OCR 텍스트
90
- SLIDE = "SLIDE" # 슬라이드 (덱의 한 장)
91
- PAGE = "PAGE" # 페이지 (문서의 한 면)
92
- SHEET = "SHEET" # 시트 (스프레드시트의 한 탭)
93
- SECTION = "SECTION" # 문서 섹션
94
- CHAT = "CHAT" # 대화 세션(채팅 UI)
95
- AI_RESPONSE = "AI_RESPONSE" # 어시스턴트 발화
96
- TOPIC = "TOPIC" # 주제 / 토픽
97
- FEATURE = "FEATURE" # 소프트웨어 기능
98
- TASK = "TASK" # 할 일
99
- DECISION = "DECISION" # 결정 사항
100
- ERROR = "ERROR" # 오류 / 버그
101
- EVENT = "EVENT" # 분석/시스템 이벤트(동적 타입 폴백)
102
- # v3.6.0 Knowledge Graph First — 모든 데이터 소스가 그래프로 수렴하기 위한
103
- # 1급 엔티티. 추가형(additive)·확장 가능(extensible): 새 도메인 엔티티는
104
- # 여기에 enum 멤버를 추가하고 _LEGACY_NODE_MAP 에 별칭만 등록하면 된다.
105
- SOURCE = "SOURCE" # 수집 출처(파일/URL/브라우저 탭/git 등)의 출처 노드
106
- REPOSITORY = "REPOSITORY" # git 저장소
107
- MEETING = "MEETING" # 회의 / 미팅
108
- ORGANIZATION = "ORGANIZATION" # 조직 / 회사 / 팀
109
- WORKFLOW = "WORKFLOW" # 워크플로우 정의/실행
110
- AGENT = "AGENT" # 에이전트(역할/실행 주체)
111
-
112
- @classmethod
113
- def from_legacy(cls, label: str) -> "NodeType":
114
- """legacy ``knowledge_graph.py`` 의 자유 문자열을 정식 enum 으로 정규화.
115
-
116
- 매핑이 없는(동적 이벤트 등) 타입은 ``CONCEPT`` 로 폴백하지만, 호출부는
117
- 원본 문자열을 ``legacy_type`` 칼럼에 별도 보존하므로 정보 손실은 없다.
118
- """
119
- m = (label or "").strip().lower()
120
- return _LEGACY_NODE_MAP.get(m, cls.CONCEPT)
121
-
122
-
123
- class EdgeType(str, Enum):
124
- """노드 사이의 ‘방향성 있고 타입이 명시된’ 관계. PPT 슬라이드 21."""
125
- CONTAINS = "CONTAINS" # FILE → CHUNK
126
- MENTIONS = "MENTIONS" # MESSAGE → CONCEPT
127
- REFERENCES = "REFERENCES" # FILE → FILE / URL
128
- REPLIES_TO = "REPLIES_TO" # MESSAGE → MESSAGE
129
- AUTHORED_BY = "AUTHORED_BY" # FILE → PERSON
130
- USES = "USES" # PROJECT → TOOL / MODEL
131
- DERIVED_FROM = "DERIVED_FROM" # CHUNK → CHUNK (요약 등)
132
- SIMILAR_TO = "SIMILAR_TO" # ANY ↔ ANY (의미 유사도)
133
- DEPENDS_ON = "DEPENDS_ON" # CODE_SYMBOL → CODE_SYMBOL
134
- TAGGED_AS = "TAGGED_AS" # ANY → CONCEPT
135
- VERSION_OF = "VERSION_OF" # FILE → FILE (히스토리)
136
- GRANTS_ACCESS = "GRANTS_ACCESS" # PERSON → RESOURCE
137
- USED_IN = "USED_IN" # CONCEPT → DOCUMENT (문서에 활용됨)
138
- INSPIRED_BY = "INSPIRED_BY" # DOCUMENT → DOCUMENT (영감/참조 관계)
139
- CONTRADICTS = "CONTRADICTS" # DOCUMENT ↔ DOCUMENT (상충 관계)
140
- EVOLVES_FROM = "EVOLVES_FROM" # DOCUMENT → DOCUMENT (발전/개정 관계)
141
- # legacy superset — knowledge_graph.py 가 실제로 생성하던 엣지 타입들
142
- UPLOADED_BY = "UPLOADED_BY" # PERSON → FILE (업로드함)
143
- WROTE = "WROTE" # PERSON → CONVERSATION (작성함)
144
- HAS_EVENT = "HAS_EVENT" # CONVERSATION → EVENT (has_event)
145
- TRIGGERED = "TRIGGERED" # PERSON → EVENT (triggered)
146
- HAS_SLIDE = "HAS_SLIDE" # SLIDE_DECK → SLIDE (has_slide)
147
- HAS_PAGE = "HAS_PAGE" # DOCUMENT → PAGE (has_page)
148
- HAS_SHEET = "HAS_SHEET" # SPREADSHEET → SHEET (has_sheet)
149
- HAS_CHUNK = "HAS_CHUNK" # FILE → CHUNK (has_chunk)
150
- CONTAINS_IMAGE = "CONTAINS_IMAGE" # FILE → IMAGE (contains_image)
151
- CONTAINS_SIGNAL = "CONTAINS_SIGNAL" # NODE → CONCEPT (contains_signal)
152
- DISCUSSES = "DISCUSSES" # SLIDE/PAGE → TOPIC (discusses)
153
- IMPLIES = "IMPLIES" # NODE → NODE (implies)
154
- RELATED_TO = "RELATED_TO" # ANY ↔ ANY (related_to)
155
- # v3.6.0 Knowledge Graph First — 출처/소유/구성/결정 관계를 1급 엣지로 승격.
156
- # 추가형: 새 관계는 enum 멤버 추가 + _LEGACY_EDGE_MAP 별칭 등록만으로 확장된다.
157
- INDEXED_FROM = "INDEXED_FROM" # NODE → SOURCE (어떤 출처에서 색인됐는가)
158
- MODIFIED_BY = "MODIFIED_BY" # NODE → PERSON (마지막 수정자)
159
- BELONGS_TO_PROJECT = "BELONGS_TO_PROJECT" # NODE → PROJECT
160
- PART_OF = "PART_OF" # NODE → NODE (구성요소 관계)
161
- DISCUSSED_IN = "DISCUSSED_IN" # CONCEPT/DECISION → MEETING/CHAT
162
- DECIDED_BY = "DECIDED_BY" # DECISION → PERSON
163
- GENERATED_BY = "GENERATED_BY" # NODE → AGENT/MODEL/WORKFLOW
164
- USED_BY_AGENT = "USED_BY_AGENT" # NODE → AGENT (에이전트가 사용함)
165
-
166
- @classmethod
167
- def from_legacy(cls, label: str) -> "EdgeType":
168
- """legacy 자유 문자열/한글 동사를 정식 enum 으로 정규화.
169
-
170
- 매핑이 없는 동적 타입은 ``MENTIONS`` 로 폴백하지만, 호출부는 원본 문자열을
171
- ``edges_v2.legacy_type`` 에 보존하므로 정보 손실은 없다.
172
- """
173
- m = (label or "").strip().lower()
174
- return _LEGACY_EDGE_MAP.get(m, cls.MENTIONS)
175
-
176
-
177
- # legacy(자유 문자열 / 한글 동사) → enum 매핑 표.
178
- # superset 정규화: 알려진 legacy 타입은 1:1 의미 보존 매핑, 미지/동적 타입만 폴백.
179
- _LEGACY_NODE_MAP: Dict[str, NodeType] = {
180
- "conversation": NodeType.CONVERSATION,
181
- "chat": NodeType.CHAT,
182
- "message": NodeType.MESSAGE,
183
- "airesponse": NodeType.AI_RESPONSE,
184
- "file": NodeType.FILE,
185
- "codefile": NodeType.CODE_FILE,
186
- "spreadsheet": NodeType.SPREADSHEET,
187
- "slidedeck": NodeType.SLIDE_DECK,
188
- "image": NodeType.IMAGE,
189
- "imagetext": NodeType.IMAGE_TEXT,
190
- "computer": NodeType.COMPUTER,
191
- "drive": NodeType.DRIVE,
192
- "folder": NodeType.FOLDER,
193
- "page": NodeType.PAGE,
194
- "sheet": NodeType.SHEET,
195
- "slide": NodeType.SLIDE,
196
- "section": NodeType.SECTION,
197
- "chunk": NodeType.CHUNK,
198
- "code": NodeType.CODE_SYMBOL,
199
- "concept": NodeType.CONCEPT,
200
- "topic": NodeType.TOPIC,
201
- "feature": NodeType.FEATURE,
202
- "task": NodeType.TASK,
203
- "decision": NodeType.DECISION,
204
- "error": NodeType.ERROR,
205
- "event": NodeType.EVENT,
206
- "tag": NodeType.CONCEPT,
207
- "person": NodeType.PERSON,
208
- "user": NodeType.PERSON,
209
- "model": NodeType.MODEL,
210
- "tool": NodeType.TOOL,
211
- "mcp": NodeType.TOOL,
212
- "project": NodeType.PROJECT,
213
- "workspace": NodeType.PROJECT,
214
- "document": NodeType.DOCUMENT,
215
- "report": NodeType.DOCUMENT,
216
- "plan": NodeType.DOCUMENT,
217
- "proposal": NodeType.DOCUMENT,
218
- "보고서": NodeType.DOCUMENT,
219
- "계획서": NodeType.DOCUMENT,
220
- "기획서": NodeType.DOCUMENT,
221
- # v3.6.0 Knowledge Graph First 엔티티
222
- "source": NodeType.SOURCE,
223
- "ingestionsource": NodeType.SOURCE,
224
- "repository": NodeType.REPOSITORY,
225
- "repo": NodeType.REPOSITORY,
226
- "gitrepo": NodeType.REPOSITORY,
227
- "meeting": NodeType.MEETING,
228
- "organization": NodeType.ORGANIZATION,
229
- "org": NodeType.ORGANIZATION,
230
- "company": NodeType.ORGANIZATION,
231
- "team": NodeType.ORGANIZATION,
232
- "workflow": NodeType.WORKFLOW,
233
- "agent": NodeType.AGENT,
234
- }
235
-
236
- _LEGACY_EDGE_MAP: Dict[str, EdgeType] = {
237
- # 한글 동사 (knowledge_graph.py 의 EDGE_VERB)
238
- "언급함": EdgeType.MENTIONS,
239
- "포함함": EdgeType.CONTAINS,
240
- "해결함": EdgeType.REFERENCES,
241
- "의존함": EdgeType.DEPENDS_ON,
242
- "설명함": EdgeType.MENTIONS,
243
- "비교함": EdgeType.SIMILAR_TO,
244
- "사용함": EdgeType.USES,
245
- "연결함": EdgeType.REFERENCES,
246
- "확장함": EdgeType.DERIVED_FROM,
247
- "생성함": EdgeType.AUTHORED_BY,
248
- "작성함": EdgeType.WROTE,
249
- "업로드함": EdgeType.UPLOADED_BY,
250
- "대체함": EdgeType.VERSION_OF,
251
- "지원함": EdgeType.USES,
252
- "발생함": EdgeType.REFERENCES,
253
- "관련됨": EdgeType.MENTIONS,
254
- # 영문 별칭
255
- "mentions": EdgeType.MENTIONS,
256
- "contains": EdgeType.CONTAINS,
257
- "references": EdgeType.REFERENCES,
258
- "replies_to": EdgeType.REPLIES_TO,
259
- "authored_by": EdgeType.AUTHORED_BY,
260
- "uses": EdgeType.USES,
261
- "derived_from": EdgeType.DERIVED_FROM,
262
- "similar_to": EdgeType.SIMILAR_TO,
263
- "depends_on": EdgeType.DEPENDS_ON,
264
- "tagged_as": EdgeType.TAGGED_AS,
265
- "version_of": EdgeType.VERSION_OF,
266
- "grants_access": EdgeType.GRANTS_ACCESS,
267
- "used_in": EdgeType.USED_IN,
268
- "inspired_by": EdgeType.INSPIRED_BY,
269
- "contradicts": EdgeType.CONTRADICTS,
270
- "evolves_from": EdgeType.EVOLVES_FROM,
271
- # legacy superset 별칭 (knowledge_graph.py 가 실제로 쓰던 엣지 타입)
272
- "uploaded_by": EdgeType.UPLOADED_BY,
273
- "wrote": EdgeType.WROTE,
274
- "has_event": EdgeType.HAS_EVENT,
275
- "triggered": EdgeType.TRIGGERED,
276
- "has_slide": EdgeType.HAS_SLIDE,
277
- "has_page": EdgeType.HAS_PAGE,
278
- "has_sheet": EdgeType.HAS_SHEET,
279
- "has_chunk": EdgeType.HAS_CHUNK,
280
- "contains_image": EdgeType.CONTAINS_IMAGE,
281
- "contains_signal": EdgeType.CONTAINS_SIGNAL,
282
- "discusses": EdgeType.DISCUSSES,
283
- "implies": EdgeType.IMPLIES,
284
- "related_to": EdgeType.RELATED_TO,
285
- "활용됨": EdgeType.USED_IN,
286
- "영감받음": EdgeType.INSPIRED_BY,
287
- "상충함": EdgeType.CONTRADICTS,
288
- "발전함": EdgeType.EVOLVES_FROM,
289
- # v3.6.0 Knowledge Graph First 관계
290
- "indexed_from": EdgeType.INDEXED_FROM,
291
- "modified_by": EdgeType.MODIFIED_BY,
292
- "belongs_to_project": EdgeType.BELONGS_TO_PROJECT,
293
- "belongs_to": EdgeType.BELONGS_TO_PROJECT,
294
- "part_of": EdgeType.PART_OF,
295
- "discussed_in": EdgeType.DISCUSSED_IN,
296
- "decided_by": EdgeType.DECIDED_BY,
297
- "generated_by": EdgeType.GENERATED_BY,
298
- "used_by_agent": EdgeType.USED_BY_AGENT,
299
- "색인됨": EdgeType.INDEXED_FROM,
300
- "수정함": EdgeType.MODIFIED_BY,
301
- "결정함": EdgeType.DECIDED_BY,
302
- "구성요소": EdgeType.PART_OF,
303
- }
304
-
305
- # ── SQLite v2 store ─────────────────────────────────────────────────────────
306
- SCHEMA_SQL = """
307
- CREATE TABLE IF NOT EXISTS kg_meta (
308
- key TEXT PRIMARY KEY,
309
- value TEXT NOT NULL
310
- );
311
-
312
- CREATE TABLE IF NOT EXISTS nodes_v2 (
313
- id TEXT PRIMARY KEY,
314
- type TEXT NOT NULL,
315
- legacy_type TEXT,
316
- label TEXT NOT NULL,
317
- summary TEXT,
318
- attrs TEXT NOT NULL DEFAULT '{}',
319
- embedding BLOB,
320
- owner_id TEXT,
321
- visibility TEXT NOT NULL DEFAULT 'private',
322
- created_at TEXT NOT NULL,
323
- updated_at TEXT NOT NULL,
324
- style TEXT,
325
- tone TEXT,
326
- importance_score REAL NOT NULL DEFAULT 0.0,
327
- last_used TEXT
328
- );
329
-
330
- CREATE TABLE IF NOT EXISTS edges_v2 (
331
- id TEXT PRIMARY KEY,
332
- source TEXT NOT NULL,
333
- target TEXT NOT NULL,
334
- type TEXT NOT NULL,
335
- legacy_type TEXT NOT NULL DEFAULT '',
336
- weight REAL NOT NULL DEFAULT 1.0,
337
- confidence REAL NOT NULL DEFAULT 1.0,
338
- evidence TEXT NOT NULL DEFAULT '[]',
339
- metadata TEXT NOT NULL DEFAULT '{}',
340
- created_by TEXT NOT NULL DEFAULT 'user',
341
- created_at TEXT NOT NULL,
342
- -- Edge identity follows the *raw* legacy type, not the normalized type:
343
- -- two distinct legacy types between the same pair (e.g. "mentions" and
344
- -- "관련됨") must stay distinct edges even though both normalize to MENTIONS.
345
- UNIQUE(source, target, legacy_type),
346
- FOREIGN KEY(source) REFERENCES nodes_v2(id) ON DELETE CASCADE,
347
- FOREIGN KEY(target) REFERENCES nodes_v2(id) ON DELETE CASCADE
348
- );
349
-
350
- CREATE INDEX IF NOT EXISTS idx_nodes_v2_type ON nodes_v2(type);
351
- CREATE INDEX IF NOT EXISTS idx_nodes_v2_legacy ON nodes_v2(legacy_type);
352
- CREATE INDEX IF NOT EXISTS idx_nodes_v2_owner ON nodes_v2(owner_id);
353
- CREATE INDEX IF NOT EXISTS idx_edges_v2_source ON edges_v2(source);
354
- CREATE INDEX IF NOT EXISTS idx_edges_v2_target ON edges_v2(target);
355
- CREATE INDEX IF NOT EXISTS idx_edges_v2_type ON edges_v2(type);
356
- CREATE INDEX IF NOT EXISTS idx_edges_v2_legacy ON edges_v2(legacy_type);
357
- """
358
-
359
-
360
- def _exec_script(conn: sqlite3.Connection, script: str) -> None:
361
- """Run a multi-statement SQL script on ``conn`` statement-by-statement.
362
-
363
- Unlike ``sqlite3.Connection.executescript``, this does NOT issue an implicit
364
- COMMIT before running, so the statements join the caller's open transaction.
365
- Safe for our schema/view DDL (no ``;`` inside string literals).
366
- """
367
- for stmt in script.split(";"):
368
- s = stmt.strip()
369
- if s:
370
- conn.execute(s)
371
-
372
-
373
- class KGStoreV2:
374
- """가벼운 SQLite 기반 v2 스토어 — **스키마/초기화 지원 전용**.
375
-
376
- ``init_schema`` 으로 ``nodes_v2``/``edges_v2`` 를 생성·heal 하고 ``stats`` 로
377
- 집계를 노출한다. 데이터 read/write 는 ``knowledge_graph.KnowledgeGraphStore``
378
- 프로젝션이 담당하므로 native upsert/get/search API 는 두지 않는다.
379
- """
380
-
381
- def __init__(self, db_path: str):
382
- self.db_path = db_path
383
-
384
- @contextmanager
385
- def _conn(self):
386
- conn = sqlite3.connect(self.db_path)
387
- conn.row_factory = sqlite3.Row
388
- conn.execute("PRAGMA foreign_keys = ON")
389
- try:
390
- yield conn
391
- conn.commit()
392
- finally:
393
- conn.close()
394
-
395
- # Columns the current code writes; used to detect schema-evolution drift in
396
- # v2 tables that an older ``CREATE TABLE IF NOT EXISTS`` left behind.
397
- _V2_EXPECTED_COLUMNS = {
398
- "edges_v2": {"id", "source", "target", "type", "legacy_type", "weight",
399
- "confidence", "evidence", "metadata", "created_by", "created_at"},
400
- "nodes_v2": {"id", "type", "legacy_type", "label", "summary", "attrs",
401
- "embedding", "owner_id", "visibility", "created_at",
402
- "updated_at", "style", "tone", "importance_score", "last_used"},
403
- }
404
-
405
- def _drop_stale_empty_v2_tables(self, conn: sqlite3.Connection) -> None:
406
- """Drop v2 tables that predate a schema change — but only when empty.
407
-
408
- ``CREATE TABLE IF NOT EXISTS`` never upgrades an existing table, so a
409
- v2 table created by an older version keeps its old columns and breaks
410
- inserts. Recreating is safe precisely because these tables have never
411
- held data (the v2 read-path isn't wired yet); we refuse to drop any
412
- table that contains rows.
413
- """
414
- # edges_v2 first (it has FKs into nodes_v2)
415
- for table in ("edges_v2", "nodes_v2"):
416
- exists = conn.execute(
417
- "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (table,)
418
- ).fetchone()
419
- if not exists:
420
- continue
421
- cols = {r[1] for r in conn.execute(f"PRAGMA table_info({table})").fetchall()}
422
- missing = self._V2_EXPECTED_COLUMNS[table] - cols
423
- if not missing:
424
- continue
425
- count = conn.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]
426
- if count == 0:
427
- conn.execute(f"DROP TABLE {table}")
428
- else:
429
- logging.warning(
430
- "kg_schema: %s is missing columns %s but holds %d rows — "
431
- "leaving it untouched (manual migration required).",
432
- table, sorted(missing), count,
433
- )
434
-
435
- def init_schema(self, conn: Optional[sqlite3.Connection] = None) -> None:
436
- """Create the v2 schema and record metadata.
437
-
438
- Pass ``conn`` to run inside the caller's open transaction (used by the
439
- atomic knowledge_graph migration); otherwise a private connection is
440
- opened and committed. Uses ``_exec_script`` rather than
441
- ``executescript`` so it never force-commits the caller's transaction.
442
- """
443
- if conn is not None:
444
- self._init_schema_on(conn)
445
- return
446
- with self._conn() as own:
447
- self._init_schema_on(own)
448
-
449
- def _init_schema_on(self, conn: sqlite3.Connection) -> None:
450
- self._drop_stale_empty_v2_tables(conn)
451
- _exec_script(conn, SCHEMA_SQL)
452
- conn.execute(
453
- "INSERT OR REPLACE INTO kg_meta(key, value) VALUES (?, ?)",
454
- ("schema_version", str(KG_SCHEMA_V2_VERSION)),
455
- )
456
- conn.execute(
457
- "INSERT OR REPLACE INTO kg_meta(key, value) VALUES (?, ?)",
458
- ("embed_dim", str(EMBED_DIM)),
459
- )
460
-
461
- # ── Maintenance ──────────────────────────────────────────
462
- def stats(self) -> Dict[str, Any]:
463
- with self._conn() as conn:
464
- n_nodes = conn.execute("SELECT COUNT(*) FROM nodes_v2").fetchone()[0]
465
- n_edges = conn.execute("SELECT COUNT(*) FROM edges_v2").fetchone()[0]
466
- per_type = {
467
- r["type"]: r["c"]
468
- for r in conn.execute(
469
- "SELECT type, COUNT(*) AS c FROM nodes_v2 GROUP BY type"
470
- ).fetchall()
471
- }
472
- per_edge = {
473
- r["type"]: r["c"]
474
- for r in conn.execute(
475
- "SELECT type, COUNT(*) AS c FROM edges_v2 GROUP BY type"
476
- ).fetchall()
477
- }
478
- return {
479
- "schema_version": KG_SCHEMA_V2_VERSION,
480
- "embed_dim": EMBED_DIM,
481
- "nodes": n_nodes,
482
- "edges": n_edges,
483
- "by_node_type": per_type,
484
- "by_edge_type": per_edge,
485
- }
486
-
487
-
488
- # NOTE: legacy → v2 reprojection lives in ``knowledge_graph.py``
489
- # (``KnowledgeGraphStore._backfill_v2_if_needed`` / ``_v2_project_node``/_edge),
490
- # which is the single live, version-gated migration path. The old standalone
491
- # ``migrate_legacy_to_v2()`` helper + CLI ``migrate`` subcommand were removed as
492
- # dead code (no callers); the normalized projection now writes the first-class
493
- # ``legacy_type``/``summary``/``metadata`` columns directly.
494
-
495
-
496
- # ── CLI ────────────────────────────────────────────────────────────────────
497
- def _cli() -> int:
498
- import argparse
499
- p = argparse.ArgumentParser(prog="kg_schema",
500
- description="Lattice AI KG v2 utilities")
501
- sub = p.add_subparsers(dest="cmd", required=True)
502
-
503
- sub_init = sub.add_parser("init", help="initialize v2 schema in a DB")
504
- sub_init.add_argument("db", help="path to sqlite db")
505
-
506
- sub_stats = sub.add_parser("stats", help="print store statistics")
507
- sub_stats.add_argument("db", help="path to sqlite db")
508
-
509
- args = p.parse_args()
510
- if args.cmd == "init":
511
- KGStoreV2(args.db).init_schema()
512
- print(f"initialized v2 schema in {args.db}")
513
- return 0
514
- if args.cmd == "stats":
515
- print(json.dumps(KGStoreV2(args.db).stats(), indent=2, ensure_ascii=False))
516
- return 0
517
- return 2
518
-
519
-
520
- if __name__ == "__main__":
521
- raise SystemExit(_cli())
3
+ from latticeai.brain.schema import * # noqa: F403,F401