ltcai 4.0.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.
- package/README.md +37 -33
- package/docs/CHANGELOG.md +64 -0
- package/docs/REALTIME_COLLABORATION.md +3 -3
- package/docs/V3_FRONTEND.md +9 -8
- package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +86 -43
- package/docs/kg-schema.md +6 -2
- package/docs/spec-vs-impl.md +10 -10
- package/kg_schema.py +2 -603
- package/knowledge_graph.py +37 -4958
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +15 -16
- package/latticeai/api/agents.py +13 -6
- package/latticeai/api/auth.py +19 -11
- package/latticeai/api/invitations.py +100 -0
- package/latticeai/api/knowledge_graph.py +4 -11
- package/latticeai/api/plugins.py +3 -6
- package/latticeai/api/realtime.py +4 -7
- package/latticeai/api/static_routes.py +9 -12
- package/latticeai/api/ui_redirects.py +26 -0
- package/latticeai/api/workflow_designer.py +39 -6
- package/latticeai/api/workspace.py +24 -10
- package/latticeai/app_factory.py +88 -17
- package/latticeai/brain/_kg_common.py +1123 -0
- package/latticeai/brain/discovery.py +1455 -0
- package/latticeai/brain/documents.py +218 -0
- package/latticeai/brain/ingest.py +644 -0
- package/latticeai/brain/projection.py +561 -0
- package/latticeai/brain/provenance.py +401 -0
- package/latticeai/brain/retrieval.py +1316 -0
- package/latticeai/brain/schema.py +640 -0
- package/latticeai/brain/store.py +216 -0
- package/latticeai/brain/write_master.py +225 -0
- package/latticeai/core/invitations.py +131 -0
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/multi_agent.py +1 -1
- package/latticeai/core/policy.py +54 -0
- package/latticeai/core/realtime.py +65 -44
- package/latticeai/core/sessions.py +31 -5
- package/latticeai/core/users.py +147 -0
- package/latticeai/core/workspace_os.py +420 -20
- package/latticeai/services/agent_runtime.py +242 -4
- package/latticeai/services/run_executor.py +328 -0
- package/latticeai/services/workspace_service.py +27 -19
- package/package.json +2 -14
- package/scripts/lint_v3.mjs +23 -0
- package/static/v3/asset-manifest.json +21 -14
- package/static/v3/js/{app.356e6452.js → app.c5c80c46.js} +1 -1
- package/static/v3/js/core/{api.7a308b89.js → api.ba0fbf14.js} +58 -1
- package/static/v3/js/core/api.js +57 -0
- package/static/v3/js/core/i18n.880e1fec.js +575 -0
- package/static/v3/js/core/i18n.js +575 -0
- package/static/v3/js/core/routes.37522821.js +101 -0
- package/static/v3/js/core/routes.js +71 -63
- package/static/v3/js/core/{shell.a1657f20.js → shell.e3f6bbfa.js} +67 -38
- package/static/v3/js/core/shell.js +65 -36
- package/static/v3/js/core/{store.204a08b2.js → store.7b2aa044.js} +10 -0
- package/static/v3/js/core/store.js +10 -0
- package/static/v3/js/views/account.eff40715.js +143 -0
- package/static/v3/js/views/account.js +143 -0
- package/static/v3/js/views/activity.0d271ef9.js +67 -0
- package/static/v3/js/views/activity.js +67 -0
- package/static/v3/js/views/{admin-users.03bac88c.js → admin-users.f7ac7b43.js} +4 -6
- package/static/v3/js/views/admin-users.js +4 -6
- package/static/v3/js/views/{agents.014d0b74.js → agents.17c5288d.js} +35 -12
- package/static/v3/js/views/agents.js +35 -12
- package/static/v3/js/views/{chat.e6dd7dd0.js → chat.e250e2cc.js} +23 -0
- package/static/v3/js/views/chat.js +23 -0
- package/static/v3/js/views/{knowledge-graph.5e40cbeb.js → knowledge-graph.4d09c537.js} +27 -7
- package/static/v3/js/views/knowledge-graph.js +27 -7
- package/static/v3/js/views/network.52a4f181.js +97 -0
- package/static/v3/js/views/network.js +97 -0
- package/static/v3/js/views/{planning.9ac3e313.js → planning.4876fd77.js} +26 -5
- package/static/v3/js/views/planning.js +26 -5
- package/static/v3/js/views/runs.b63b2afa.js +144 -0
- package/static/v3/js/views/runs.js +144 -0
- package/static/v3/js/views/{settings.8631fa5e.js → settings.b7140634.js} +7 -8
- package/static/v3/js/views/settings.js +7 -8
- package/static/v3/js/views/snapshots.6f5db095.js +135 -0
- package/static/v3/js/views/snapshots.js +135 -0
- package/static/v3/js/views/{workflows.26c57290.js → workflows.7752225a.js} +87 -2
- package/static/v3/js/views/workflows.js +87 -2
- package/static/v3/js/views/workspace-admin.c466029b.js +156 -0
- package/static/v3/js/views/workspace-admin.js +156 -0
- package/static/account.html +0 -113
- package/static/activity.html +0 -73
- package/static/admin.html +0 -486
- package/static/agents.html +0 -139
- package/static/chat.html +0 -841
- package/static/css/reference/account.css +0 -439
- package/static/css/reference/admin.css +0 -610
- package/static/css/reference/base.css +0 -1661
- package/static/css/reference/chat.css +0 -4623
- package/static/css/reference/graph.css +0 -1016
- package/static/css/responsive.css +0 -861
- package/static/graph.html +0 -122
- package/static/platform.css +0 -104
- package/static/plugins.html +0 -136
- package/static/scripts/account.js +0 -238
- package/static/scripts/admin.js +0 -1614
- package/static/scripts/chat.js +0 -5081
- package/static/scripts/graph.js +0 -1804
- package/static/scripts/platform.js +0 -64
- package/static/scripts/ux.js +0 -167
- package/static/scripts/workspace.js +0 -948
- package/static/v3/js/core/routes.7222343d.js +0 -93
- package/static/workflows.html +0 -146
- package/static/workspace.css +0 -1121
- package/static/workspace.html +0 -357
package/kg_schema.py
CHANGED
|
@@ -1,604 +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
|
-
|
|
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()
|
|
120
|
-
# Canonical values round-trip exactly (v4 native writes use them);
|
|
121
|
-
# without this, CODE_FILE/AI_RESPONSE etc. would degrade to CONCEPT.
|
|
122
|
-
try:
|
|
123
|
-
return cls(m.upper())
|
|
124
|
-
except ValueError:
|
|
125
|
-
pass
|
|
126
|
-
return _LEGACY_NODE_MAP.get(m.lower(), cls.CONCEPT)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
class EdgeType(str, Enum):
|
|
130
|
-
"""노드 사이의 ‘방향성 있고 타입이 명시된’ 관계. PPT 슬라이드 21."""
|
|
131
|
-
CONTAINS = "CONTAINS" # FILE → CHUNK
|
|
132
|
-
MENTIONS = "MENTIONS" # MESSAGE → CONCEPT
|
|
133
|
-
REFERENCES = "REFERENCES" # FILE → FILE / URL
|
|
134
|
-
REPLIES_TO = "REPLIES_TO" # MESSAGE → MESSAGE
|
|
135
|
-
AUTHORED_BY = "AUTHORED_BY" # FILE → PERSON
|
|
136
|
-
USES = "USES" # PROJECT → TOOL / MODEL
|
|
137
|
-
DERIVED_FROM = "DERIVED_FROM" # CHUNK → CHUNK (요약 등)
|
|
138
|
-
SIMILAR_TO = "SIMILAR_TO" # ANY ↔ ANY (의미 유사도)
|
|
139
|
-
DEPENDS_ON = "DEPENDS_ON" # CODE_SYMBOL → CODE_SYMBOL
|
|
140
|
-
TAGGED_AS = "TAGGED_AS" # ANY → CONCEPT
|
|
141
|
-
VERSION_OF = "VERSION_OF" # FILE → FILE (히스토리)
|
|
142
|
-
GRANTS_ACCESS = "GRANTS_ACCESS" # PERSON → RESOURCE
|
|
143
|
-
USED_IN = "USED_IN" # CONCEPT → DOCUMENT (문서에 활용됨)
|
|
144
|
-
INSPIRED_BY = "INSPIRED_BY" # DOCUMENT → DOCUMENT (영감/참조 관계)
|
|
145
|
-
CONTRADICTS = "CONTRADICTS" # DOCUMENT ↔ DOCUMENT (상충 관계)
|
|
146
|
-
EVOLVES_FROM = "EVOLVES_FROM" # DOCUMENT → DOCUMENT (발전/개정 관계)
|
|
147
|
-
# legacy superset — knowledge_graph.py 가 실제로 생성하던 엣지 타입들
|
|
148
|
-
UPLOADED_BY = "UPLOADED_BY" # PERSON → FILE (업로드함)
|
|
149
|
-
WROTE = "WROTE" # PERSON → CONVERSATION (작성함)
|
|
150
|
-
HAS_EVENT = "HAS_EVENT" # CONVERSATION → EVENT (has_event)
|
|
151
|
-
TRIGGERED = "TRIGGERED" # PERSON → EVENT (triggered)
|
|
152
|
-
HAS_SLIDE = "HAS_SLIDE" # SLIDE_DECK → SLIDE (has_slide)
|
|
153
|
-
HAS_PAGE = "HAS_PAGE" # DOCUMENT → PAGE (has_page)
|
|
154
|
-
HAS_SHEET = "HAS_SHEET" # SPREADSHEET → SHEET (has_sheet)
|
|
155
|
-
HAS_CHUNK = "HAS_CHUNK" # FILE → CHUNK (has_chunk)
|
|
156
|
-
CONTAINS_IMAGE = "CONTAINS_IMAGE" # FILE → IMAGE (contains_image)
|
|
157
|
-
CONTAINS_SIGNAL = "CONTAINS_SIGNAL" # NODE → CONCEPT (contains_signal)
|
|
158
|
-
DISCUSSES = "DISCUSSES" # SLIDE/PAGE → TOPIC (discusses)
|
|
159
|
-
IMPLIES = "IMPLIES" # NODE → NODE (implies)
|
|
160
|
-
RELATED_TO = "RELATED_TO" # ANY ↔ ANY (related_to)
|
|
161
|
-
# v3.6.0 Knowledge Graph First — 출처/소유/구성/결정 관계를 1급 엣지로 승격.
|
|
162
|
-
# 추가형: 새 관계는 enum 멤버 추가 + _LEGACY_EDGE_MAP 별칭 등록만으로 확장된다.
|
|
163
|
-
INDEXED_FROM = "INDEXED_FROM" # NODE → SOURCE (어떤 출처에서 색인됐는가)
|
|
164
|
-
MODIFIED_BY = "MODIFIED_BY" # NODE → PERSON (마지막 수정자)
|
|
165
|
-
BELONGS_TO_PROJECT = "BELONGS_TO_PROJECT" # NODE → PROJECT
|
|
166
|
-
PART_OF = "PART_OF" # NODE → NODE (구성요소 관계)
|
|
167
|
-
DISCUSSED_IN = "DISCUSSED_IN" # CONCEPT/DECISION → MEETING/CHAT
|
|
168
|
-
DECIDED_BY = "DECIDED_BY" # DECISION → PERSON
|
|
169
|
-
GENERATED_BY = "GENERATED_BY" # NODE → AGENT/MODEL/WORKFLOW
|
|
170
|
-
USED_BY_AGENT = "USED_BY_AGENT" # NODE → AGENT (에이전트가 사용함)
|
|
171
|
-
|
|
172
|
-
@classmethod
|
|
173
|
-
def from_legacy(cls, label: str) -> "EdgeType":
|
|
174
|
-
"""legacy 자유 문자열/한글 동사를 정식 enum 으로 정규화.
|
|
175
|
-
|
|
176
|
-
매핑이 없는 동적 타입은 ``MENTIONS`` 로 폴백하지만, 호출부는 원본 문자열을
|
|
177
|
-
``edges_v2.legacy_type`` 에 보존하므로 정보 손실은 없다.
|
|
178
|
-
"""
|
|
179
|
-
m = (label or "").strip()
|
|
180
|
-
# Canonical values round-trip exactly (v4 native writes use them).
|
|
181
|
-
try:
|
|
182
|
-
return cls(m.upper())
|
|
183
|
-
except ValueError:
|
|
184
|
-
pass
|
|
185
|
-
return _LEGACY_EDGE_MAP.get(m.lower(), cls.MENTIONS)
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
# legacy(자유 문자열 / 한글 동사) → enum 매핑 표.
|
|
189
|
-
# superset 정규화: 알려진 legacy 타입은 1:1 의미 보존 매핑, 미지/동적 타입만 폴백.
|
|
190
|
-
_LEGACY_NODE_MAP: Dict[str, NodeType] = {
|
|
191
|
-
"conversation": NodeType.CONVERSATION,
|
|
192
|
-
"chat": NodeType.CHAT,
|
|
193
|
-
"message": NodeType.MESSAGE,
|
|
194
|
-
"airesponse": NodeType.AI_RESPONSE,
|
|
195
|
-
"file": NodeType.FILE,
|
|
196
|
-
"codefile": NodeType.CODE_FILE,
|
|
197
|
-
"spreadsheet": NodeType.SPREADSHEET,
|
|
198
|
-
"slidedeck": NodeType.SLIDE_DECK,
|
|
199
|
-
"image": NodeType.IMAGE,
|
|
200
|
-
"imagetext": NodeType.IMAGE_TEXT,
|
|
201
|
-
"computer": NodeType.COMPUTER,
|
|
202
|
-
"drive": NodeType.DRIVE,
|
|
203
|
-
"folder": NodeType.FOLDER,
|
|
204
|
-
"page": NodeType.PAGE,
|
|
205
|
-
"sheet": NodeType.SHEET,
|
|
206
|
-
"slide": NodeType.SLIDE,
|
|
207
|
-
"section": NodeType.SECTION,
|
|
208
|
-
"chunk": NodeType.CHUNK,
|
|
209
|
-
"code": NodeType.CODE_SYMBOL,
|
|
210
|
-
"concept": NodeType.CONCEPT,
|
|
211
|
-
"topic": NodeType.TOPIC,
|
|
212
|
-
"feature": NodeType.FEATURE,
|
|
213
|
-
"task": NodeType.TASK,
|
|
214
|
-
"decision": NodeType.DECISION,
|
|
215
|
-
"error": NodeType.ERROR,
|
|
216
|
-
"event": NodeType.EVENT,
|
|
217
|
-
"tag": NodeType.CONCEPT,
|
|
218
|
-
"person": NodeType.PERSON,
|
|
219
|
-
"user": NodeType.PERSON,
|
|
220
|
-
"model": NodeType.MODEL,
|
|
221
|
-
"tool": NodeType.TOOL,
|
|
222
|
-
"mcp": NodeType.TOOL,
|
|
223
|
-
"project": NodeType.PROJECT,
|
|
224
|
-
"workspace": NodeType.PROJECT,
|
|
225
|
-
"document": NodeType.DOCUMENT,
|
|
226
|
-
"report": NodeType.DOCUMENT,
|
|
227
|
-
"plan": NodeType.DOCUMENT,
|
|
228
|
-
"proposal": NodeType.DOCUMENT,
|
|
229
|
-
"보고서": NodeType.DOCUMENT,
|
|
230
|
-
"계획서": NodeType.DOCUMENT,
|
|
231
|
-
"기획서": NodeType.DOCUMENT,
|
|
232
|
-
# v3.6.0 Knowledge Graph First 엔티티
|
|
233
|
-
"source": NodeType.SOURCE,
|
|
234
|
-
"ingestionsource": NodeType.SOURCE,
|
|
235
|
-
"repository": NodeType.REPOSITORY,
|
|
236
|
-
"repo": NodeType.REPOSITORY,
|
|
237
|
-
"gitrepo": NodeType.REPOSITORY,
|
|
238
|
-
"meeting": NodeType.MEETING,
|
|
239
|
-
"organization": NodeType.ORGANIZATION,
|
|
240
|
-
"org": NodeType.ORGANIZATION,
|
|
241
|
-
"company": NodeType.ORGANIZATION,
|
|
242
|
-
"team": NodeType.ORGANIZATION,
|
|
243
|
-
"workflow": NodeType.WORKFLOW,
|
|
244
|
-
"agent": NodeType.AGENT,
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
_LEGACY_EDGE_MAP: Dict[str, EdgeType] = {
|
|
248
|
-
# 한글 동사 (knowledge_graph.py 의 EDGE_VERB)
|
|
249
|
-
"언급함": EdgeType.MENTIONS,
|
|
250
|
-
"포함함": EdgeType.CONTAINS,
|
|
251
|
-
"해결함": EdgeType.REFERENCES,
|
|
252
|
-
"의존함": EdgeType.DEPENDS_ON,
|
|
253
|
-
"설명함": EdgeType.MENTIONS,
|
|
254
|
-
"비교함": EdgeType.SIMILAR_TO,
|
|
255
|
-
"사용함": EdgeType.USES,
|
|
256
|
-
"연결함": EdgeType.REFERENCES,
|
|
257
|
-
"확장함": EdgeType.DERIVED_FROM,
|
|
258
|
-
"생성함": EdgeType.AUTHORED_BY,
|
|
259
|
-
"작성함": EdgeType.WROTE,
|
|
260
|
-
"업로드함": EdgeType.UPLOADED_BY,
|
|
261
|
-
"대체함": EdgeType.VERSION_OF,
|
|
262
|
-
"지원함": EdgeType.USES,
|
|
263
|
-
"발생함": EdgeType.REFERENCES,
|
|
264
|
-
"관련됨": EdgeType.MENTIONS,
|
|
265
|
-
# 영문 별칭
|
|
266
|
-
"mentions": EdgeType.MENTIONS,
|
|
267
|
-
"contains": EdgeType.CONTAINS,
|
|
268
|
-
"references": EdgeType.REFERENCES,
|
|
269
|
-
"replies_to": EdgeType.REPLIES_TO,
|
|
270
|
-
"authored_by": EdgeType.AUTHORED_BY,
|
|
271
|
-
"uses": EdgeType.USES,
|
|
272
|
-
"derived_from": EdgeType.DERIVED_FROM,
|
|
273
|
-
"similar_to": EdgeType.SIMILAR_TO,
|
|
274
|
-
"depends_on": EdgeType.DEPENDS_ON,
|
|
275
|
-
"tagged_as": EdgeType.TAGGED_AS,
|
|
276
|
-
"version_of": EdgeType.VERSION_OF,
|
|
277
|
-
"grants_access": EdgeType.GRANTS_ACCESS,
|
|
278
|
-
"used_in": EdgeType.USED_IN,
|
|
279
|
-
"inspired_by": EdgeType.INSPIRED_BY,
|
|
280
|
-
"contradicts": EdgeType.CONTRADICTS,
|
|
281
|
-
"evolves_from": EdgeType.EVOLVES_FROM,
|
|
282
|
-
# legacy superset 별칭 (knowledge_graph.py 가 실제로 쓰던 엣지 타입)
|
|
283
|
-
"uploaded_by": EdgeType.UPLOADED_BY,
|
|
284
|
-
"wrote": EdgeType.WROTE,
|
|
285
|
-
"has_event": EdgeType.HAS_EVENT,
|
|
286
|
-
"triggered": EdgeType.TRIGGERED,
|
|
287
|
-
"has_slide": EdgeType.HAS_SLIDE,
|
|
288
|
-
"has_page": EdgeType.HAS_PAGE,
|
|
289
|
-
"has_sheet": EdgeType.HAS_SHEET,
|
|
290
|
-
"has_chunk": EdgeType.HAS_CHUNK,
|
|
291
|
-
"contains_image": EdgeType.CONTAINS_IMAGE,
|
|
292
|
-
"contains_signal": EdgeType.CONTAINS_SIGNAL,
|
|
293
|
-
"discusses": EdgeType.DISCUSSES,
|
|
294
|
-
"implies": EdgeType.IMPLIES,
|
|
295
|
-
"related_to": EdgeType.RELATED_TO,
|
|
296
|
-
"활용됨": EdgeType.USED_IN,
|
|
297
|
-
"영감받음": EdgeType.INSPIRED_BY,
|
|
298
|
-
"상충함": EdgeType.CONTRADICTS,
|
|
299
|
-
"발전함": EdgeType.EVOLVES_FROM,
|
|
300
|
-
# v3.6.0 Knowledge Graph First 관계
|
|
301
|
-
"indexed_from": EdgeType.INDEXED_FROM,
|
|
302
|
-
"modified_by": EdgeType.MODIFIED_BY,
|
|
303
|
-
"belongs_to_project": EdgeType.BELONGS_TO_PROJECT,
|
|
304
|
-
"belongs_to": EdgeType.BELONGS_TO_PROJECT,
|
|
305
|
-
"part_of": EdgeType.PART_OF,
|
|
306
|
-
"discussed_in": EdgeType.DISCUSSED_IN,
|
|
307
|
-
"decided_by": EdgeType.DECIDED_BY,
|
|
308
|
-
"generated_by": EdgeType.GENERATED_BY,
|
|
309
|
-
"used_by_agent": EdgeType.USED_BY_AGENT,
|
|
310
|
-
"색인됨": EdgeType.INDEXED_FROM,
|
|
311
|
-
"수정함": EdgeType.MODIFIED_BY,
|
|
312
|
-
"결정함": EdgeType.DECIDED_BY,
|
|
313
|
-
"구성요소": EdgeType.PART_OF,
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
# ── SQLite v2 store ─────────────────────────────────────────────────────────
|
|
317
|
-
SCHEMA_SQL = """
|
|
318
|
-
CREATE TABLE IF NOT EXISTS kg_meta (
|
|
319
|
-
key TEXT PRIMARY KEY,
|
|
320
|
-
value TEXT NOT NULL
|
|
321
|
-
);
|
|
322
|
-
|
|
323
|
-
CREATE TABLE IF NOT EXISTS nodes_v2 (
|
|
324
|
-
id TEXT PRIMARY KEY,
|
|
325
|
-
type TEXT NOT NULL,
|
|
326
|
-
legacy_type TEXT,
|
|
327
|
-
label TEXT NOT NULL,
|
|
328
|
-
summary TEXT,
|
|
329
|
-
attrs TEXT NOT NULL DEFAULT '{}',
|
|
330
|
-
embedding BLOB,
|
|
331
|
-
owner_id TEXT,
|
|
332
|
-
-- NULL workspace_id = legacy-global (pre-scoping rows, readable machine-wide).
|
|
333
|
-
workspace_id TEXT,
|
|
334
|
-
-- 'legacy' marks rows that predate scoping — the 'private' default must not
|
|
335
|
-
-- silently privatize previously machine-shared data (design-review ruling).
|
|
336
|
-
visibility TEXT NOT NULL DEFAULT 'private',
|
|
337
|
-
-- Revision chain: a node replaced by a newer one points at its successor.
|
|
338
|
-
superseded_by TEXT,
|
|
339
|
-
created_at TEXT NOT NULL,
|
|
340
|
-
updated_at TEXT NOT NULL,
|
|
341
|
-
style TEXT,
|
|
342
|
-
tone TEXT,
|
|
343
|
-
importance_score REAL NOT NULL DEFAULT 0.0,
|
|
344
|
-
last_used TEXT
|
|
345
|
-
);
|
|
346
|
-
|
|
347
|
-
CREATE TABLE IF NOT EXISTS edges_v2 (
|
|
348
|
-
id TEXT PRIMARY KEY,
|
|
349
|
-
source TEXT NOT NULL,
|
|
350
|
-
target TEXT NOT NULL,
|
|
351
|
-
type TEXT NOT NULL,
|
|
352
|
-
legacy_type TEXT NOT NULL DEFAULT '',
|
|
353
|
-
weight REAL NOT NULL DEFAULT 1.0,
|
|
354
|
-
confidence REAL NOT NULL DEFAULT 1.0,
|
|
355
|
-
evidence TEXT NOT NULL DEFAULT '[]',
|
|
356
|
-
metadata TEXT NOT NULL DEFAULT '{}',
|
|
357
|
-
created_by TEXT NOT NULL DEFAULT 'user',
|
|
358
|
-
created_at TEXT NOT NULL,
|
|
359
|
-
-- Edge identity (v4): the normalized type AND the raw legacy type.
|
|
360
|
-
-- Migrated rows keep their legacy_type discriminator, so two distinct
|
|
361
|
-
-- legacy strings between one pair (e.g. "mentions" / "관련됨") stay
|
|
362
|
-
-- distinct even though both normalize to MENTIONS. Native canonical
|
|
363
|
-
-- writes carry legacy_type='' so their identity is effectively
|
|
364
|
-
-- (source, target, type) — two canonical types between the same pair
|
|
365
|
-
-- (e.g. MENTIONS + CONTAINS) never collide. The pre-v4
|
|
366
|
-
-- UNIQUE(source, target, legacy_type) would have silently merged them.
|
|
367
|
-
UNIQUE(source, target, type, legacy_type),
|
|
368
|
-
FOREIGN KEY(source) REFERENCES nodes_v2(id) ON DELETE CASCADE,
|
|
369
|
-
FOREIGN KEY(target) REFERENCES nodes_v2(id) ON DELETE CASCADE
|
|
370
|
-
);
|
|
371
|
-
|
|
372
|
-
-- Temporal dimension (v4): every repeated observation of a relationship is
|
|
373
|
-
-- recorded — edges_v2's UNIQUE identity + weight=max would otherwise erase
|
|
374
|
-
-- when something was learned, how often, and whether it still holds.
|
|
375
|
-
CREATE TABLE IF NOT EXISTS edge_occurrences (
|
|
376
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
377
|
-
edge_id TEXT NOT NULL,
|
|
378
|
-
observed_at TEXT NOT NULL,
|
|
379
|
-
weight REAL NOT NULL DEFAULT 1.0,
|
|
380
|
-
source TEXT,
|
|
381
|
-
FOREIGN KEY(edge_id) REFERENCES edges_v2(id) ON DELETE CASCADE
|
|
382
|
-
);
|
|
383
|
-
CREATE INDEX IF NOT EXISTS idx_edge_occurrences_edge ON edge_occurrences(edge_id);
|
|
384
|
-
CREATE INDEX IF NOT EXISTS idx_edge_occurrences_time ON edge_occurrences(observed_at);
|
|
385
|
-
|
|
386
|
-
CREATE INDEX IF NOT EXISTS idx_nodes_v2_type ON nodes_v2(type);
|
|
387
|
-
CREATE INDEX IF NOT EXISTS idx_nodes_v2_legacy ON nodes_v2(legacy_type);
|
|
388
|
-
CREATE INDEX IF NOT EXISTS idx_nodes_v2_owner ON nodes_v2(owner_id);
|
|
389
|
-
CREATE INDEX IF NOT EXISTS idx_edges_v2_source ON edges_v2(source);
|
|
390
|
-
CREATE INDEX IF NOT EXISTS idx_edges_v2_target ON edges_v2(target);
|
|
391
|
-
CREATE INDEX IF NOT EXISTS idx_edges_v2_type ON edges_v2(type);
|
|
392
|
-
CREATE INDEX IF NOT EXISTS idx_edges_v2_legacy ON edges_v2(legacy_type);
|
|
393
|
-
"""
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
def _exec_script(conn: sqlite3.Connection, script: str) -> None:
|
|
397
|
-
"""Run a multi-statement SQL script on ``conn`` statement-by-statement.
|
|
398
|
-
|
|
399
|
-
Unlike ``sqlite3.Connection.executescript``, this does NOT issue an implicit
|
|
400
|
-
COMMIT before running, so the statements join the caller's open transaction.
|
|
401
|
-
Safe for our schema/view DDL (no ``;`` inside string literals).
|
|
402
|
-
"""
|
|
403
|
-
for stmt in script.split(";"):
|
|
404
|
-
s = stmt.strip()
|
|
405
|
-
if s:
|
|
406
|
-
conn.execute(s)
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
class KGStoreV2:
|
|
410
|
-
"""가벼운 SQLite 기반 v2 스토어 — **스키마/초기화 지원 전용**.
|
|
411
|
-
|
|
412
|
-
``init_schema`` 으로 ``nodes_v2``/``edges_v2`` 를 생성·heal 하고 ``stats`` 로
|
|
413
|
-
집계를 노출한다. 데이터 read/write 는 ``knowledge_graph.KnowledgeGraphStore``
|
|
414
|
-
프로젝션이 담당하므로 native upsert/get/search API 는 두지 않는다.
|
|
415
|
-
"""
|
|
416
|
-
|
|
417
|
-
def __init__(self, db_path: str):
|
|
418
|
-
self.db_path = db_path
|
|
419
|
-
|
|
420
|
-
@contextmanager
|
|
421
|
-
def _conn(self):
|
|
422
|
-
conn = sqlite3.connect(self.db_path)
|
|
423
|
-
conn.row_factory = sqlite3.Row
|
|
424
|
-
conn.execute("PRAGMA foreign_keys = ON")
|
|
425
|
-
try:
|
|
426
|
-
yield conn
|
|
427
|
-
conn.commit()
|
|
428
|
-
finally:
|
|
429
|
-
conn.close()
|
|
430
|
-
|
|
431
|
-
# Columns the current code writes; used to detect schema-evolution drift in
|
|
432
|
-
# v2 tables that an older ``CREATE TABLE IF NOT EXISTS`` left behind.
|
|
433
|
-
_V2_EXPECTED_COLUMNS = {
|
|
434
|
-
"edges_v2": {"id", "source", "target", "type", "legacy_type", "weight",
|
|
435
|
-
"confidence", "evidence", "metadata", "created_by", "created_at"},
|
|
436
|
-
"nodes_v2": {"id", "type", "legacy_type", "label", "summary", "attrs",
|
|
437
|
-
"embedding", "owner_id", "workspace_id", "visibility",
|
|
438
|
-
"superseded_by", "created_at", "updated_at", "style",
|
|
439
|
-
"tone", "importance_score", "last_used"},
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
# Columns added after a table's first release that can be healed in place
|
|
443
|
-
# with ALTER TABLE ADD COLUMN (nullable / defaulted only).
|
|
444
|
-
_V2_ADDABLE_COLUMNS = {
|
|
445
|
-
"nodes_v2": {"workspace_id": "TEXT", "superseded_by": "TEXT"},
|
|
446
|
-
"edges_v2": {},
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
def _drop_stale_empty_v2_tables(self, conn: sqlite3.Connection) -> None:
|
|
450
|
-
"""Drop v2 tables that predate a schema change — but only when empty.
|
|
451
|
-
|
|
452
|
-
``CREATE TABLE IF NOT EXISTS`` never upgrades an existing table, so a
|
|
453
|
-
v2 table created by an older version keeps its old columns and breaks
|
|
454
|
-
inserts. Recreating is safe precisely because these tables have never
|
|
455
|
-
held data (the v2 read-path isn't wired yet); we refuse to drop any
|
|
456
|
-
table that contains rows.
|
|
457
|
-
"""
|
|
458
|
-
# edges_v2 first (it has FKs into nodes_v2)
|
|
459
|
-
for table in ("edges_v2", "nodes_v2"):
|
|
460
|
-
exists = conn.execute(
|
|
461
|
-
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (table,)
|
|
462
|
-
).fetchone()
|
|
463
|
-
if not exists:
|
|
464
|
-
continue
|
|
465
|
-
cols = {r[1] for r in conn.execute(f"PRAGMA table_info({table})").fetchall()}
|
|
466
|
-
missing = self._V2_EXPECTED_COLUMNS[table] - cols
|
|
467
|
-
if not missing:
|
|
468
|
-
continue
|
|
469
|
-
# Additive columns heal in place without touching data.
|
|
470
|
-
addable = self._V2_ADDABLE_COLUMNS.get(table, {})
|
|
471
|
-
for col in sorted(missing & set(addable)):
|
|
472
|
-
conn.execute(f"ALTER TABLE {table} ADD COLUMN {col} {addable[col]}")
|
|
473
|
-
missing -= set(addable)
|
|
474
|
-
if not missing:
|
|
475
|
-
continue
|
|
476
|
-
count = conn.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]
|
|
477
|
-
if count == 0:
|
|
478
|
-
conn.execute(f"DROP TABLE {table}")
|
|
479
|
-
else:
|
|
480
|
-
logging.warning(
|
|
481
|
-
"kg_schema: %s is missing columns %s but holds %d rows — "
|
|
482
|
-
"leaving it untouched (manual migration required).",
|
|
483
|
-
table, sorted(missing), count,
|
|
484
|
-
)
|
|
485
|
-
|
|
486
|
-
def init_schema(self, conn: Optional[sqlite3.Connection] = None) -> None:
|
|
487
|
-
"""Create the v2 schema and record metadata.
|
|
488
|
-
|
|
489
|
-
Pass ``conn`` to run inside the caller's open transaction (used by the
|
|
490
|
-
atomic knowledge_graph migration); otherwise a private connection is
|
|
491
|
-
opened and committed. Uses ``_exec_script`` rather than
|
|
492
|
-
``executescript`` so it never force-commits the caller's transaction.
|
|
493
|
-
"""
|
|
494
|
-
if conn is not None:
|
|
495
|
-
self._init_schema_on(conn)
|
|
496
|
-
return
|
|
497
|
-
with self._conn() as own:
|
|
498
|
-
self._init_schema_on(own)
|
|
499
|
-
|
|
500
|
-
def _rebuild_edges_identity(self, conn: sqlite3.Connection) -> None:
|
|
501
|
-
"""Migrate edges_v2 from the pre-v4 UNIQUE(source, target, legacy_type)
|
|
502
|
-
identity to UNIQUE(source, target, type, legacy_type).
|
|
503
|
-
|
|
504
|
-
SQLite cannot alter constraints, so this is a create→copy→swap inside
|
|
505
|
-
the caller's transaction. Data-preserving: every existing row keeps its
|
|
506
|
-
legacy_type discriminator. Re-entrant: keyed on the actual constraint
|
|
507
|
-
in sqlite_master, not a one-time stamp.
|
|
508
|
-
"""
|
|
509
|
-
row = conn.execute(
|
|
510
|
-
"SELECT sql FROM sqlite_master WHERE type='table' AND name='edges_v2'"
|
|
511
|
-
).fetchone()
|
|
512
|
-
if not row or "UNIQUE(source, target, type, legacy_type)" in (row["sql"] or ""):
|
|
513
|
-
return
|
|
514
|
-
conn.execute("ALTER TABLE edges_v2 RENAME TO edges_v2_old")
|
|
515
|
-
# Recreate from the canonical DDL (edges_v2 portion of SCHEMA_SQL).
|
|
516
|
-
start = SCHEMA_SQL.index("CREATE TABLE IF NOT EXISTS edges_v2")
|
|
517
|
-
end = SCHEMA_SQL.index(");", start) + 2
|
|
518
|
-
conn.execute(SCHEMA_SQL[start:end].rstrip(";"))
|
|
519
|
-
conn.execute(
|
|
520
|
-
"""
|
|
521
|
-
INSERT INTO edges_v2 (id, source, target, type, legacy_type, weight,
|
|
522
|
-
confidence, evidence, metadata, created_by, created_at)
|
|
523
|
-
SELECT id, source, target, type, legacy_type, weight,
|
|
524
|
-
confidence, evidence, metadata, created_by, created_at
|
|
525
|
-
FROM edges_v2_old
|
|
526
|
-
"""
|
|
527
|
-
)
|
|
528
|
-
conn.execute("DROP TABLE edges_v2_old")
|
|
529
|
-
logging.info("kg_schema: rebuilt edges_v2 with (source, target, type, legacy_type) identity")
|
|
530
|
-
|
|
531
|
-
def _init_schema_on(self, conn: sqlite3.Connection) -> None:
|
|
532
|
-
self._drop_stale_empty_v2_tables(conn)
|
|
533
|
-
self._rebuild_edges_identity(conn)
|
|
534
|
-
_exec_script(conn, SCHEMA_SQL)
|
|
535
|
-
conn.execute(
|
|
536
|
-
"INSERT OR REPLACE INTO kg_meta(key, value) VALUES (?, ?)",
|
|
537
|
-
("schema_version", str(KG_SCHEMA_V2_VERSION)),
|
|
538
|
-
)
|
|
539
|
-
conn.execute(
|
|
540
|
-
"INSERT OR REPLACE INTO kg_meta(key, value) VALUES (?, ?)",
|
|
541
|
-
("embed_dim", str(EMBED_DIM)),
|
|
542
|
-
)
|
|
543
|
-
|
|
544
|
-
# ── Maintenance ──────────────────────────────────────────
|
|
545
|
-
def stats(self) -> Dict[str, Any]:
|
|
546
|
-
with self._conn() as conn:
|
|
547
|
-
n_nodes = conn.execute("SELECT COUNT(*) FROM nodes_v2").fetchone()[0]
|
|
548
|
-
n_edges = conn.execute("SELECT COUNT(*) FROM edges_v2").fetchone()[0]
|
|
549
|
-
per_type = {
|
|
550
|
-
r["type"]: r["c"]
|
|
551
|
-
for r in conn.execute(
|
|
552
|
-
"SELECT type, COUNT(*) AS c FROM nodes_v2 GROUP BY type"
|
|
553
|
-
).fetchall()
|
|
554
|
-
}
|
|
555
|
-
per_edge = {
|
|
556
|
-
r["type"]: r["c"]
|
|
557
|
-
for r in conn.execute(
|
|
558
|
-
"SELECT type, COUNT(*) AS c FROM edges_v2 GROUP BY type"
|
|
559
|
-
).fetchall()
|
|
560
|
-
}
|
|
561
|
-
return {
|
|
562
|
-
"schema_version": KG_SCHEMA_V2_VERSION,
|
|
563
|
-
"embed_dim": EMBED_DIM,
|
|
564
|
-
"nodes": n_nodes,
|
|
565
|
-
"edges": n_edges,
|
|
566
|
-
"by_node_type": per_type,
|
|
567
|
-
"by_edge_type": per_edge,
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
# NOTE: legacy → v2 reprojection lives in ``knowledge_graph.py``
|
|
572
|
-
# (``KnowledgeGraphStore._backfill_v2_if_needed`` / ``_v2_project_node``/_edge),
|
|
573
|
-
# which is the single live, version-gated migration path. The old standalone
|
|
574
|
-
# ``migrate_legacy_to_v2()`` helper + CLI ``migrate`` subcommand were removed as
|
|
575
|
-
# dead code (no callers); the normalized projection now writes the first-class
|
|
576
|
-
# ``legacy_type``/``summary``/``metadata`` columns directly.
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
# ── CLI ────────────────────────────────────────────────────────────────────
|
|
580
|
-
def _cli() -> int:
|
|
581
|
-
import argparse
|
|
582
|
-
p = argparse.ArgumentParser(prog="kg_schema",
|
|
583
|
-
description="Lattice AI KG v2 utilities")
|
|
584
|
-
sub = p.add_subparsers(dest="cmd", required=True)
|
|
585
|
-
|
|
586
|
-
sub_init = sub.add_parser("init", help="initialize v2 schema in a DB")
|
|
587
|
-
sub_init.add_argument("db", help="path to sqlite db")
|
|
588
|
-
|
|
589
|
-
sub_stats = sub.add_parser("stats", help="print store statistics")
|
|
590
|
-
sub_stats.add_argument("db", help="path to sqlite db")
|
|
591
|
-
|
|
592
|
-
args = p.parse_args()
|
|
593
|
-
if args.cmd == "init":
|
|
594
|
-
KGStoreV2(args.db).init_schema()
|
|
595
|
-
print(f"initialized v2 schema in {args.db}")
|
|
596
|
-
return 0
|
|
597
|
-
if args.cmd == "stats":
|
|
598
|
-
print(json.dumps(KGStoreV2(args.db).stats(), indent=2, ensure_ascii=False))
|
|
599
|
-
return 0
|
|
600
|
-
return 2
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
if __name__ == "__main__":
|
|
604
|
-
raise SystemExit(_cli())
|
|
3
|
+
from latticeai.brain.schema import * # noqa: F403,F401
|