ltcai 0.2.1 → 0.3.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.
- package/README.md +8 -2
- package/auto_setup.py +15 -1
- package/docs/CHANGELOG.md +67 -0
- package/kg_schema.py +64 -15
- package/knowledge_graph.py +499 -31
- package/latticeai/core/__init__.py +1 -1
- package/latticeai/core/context_builder.py +191 -0
- package/latticeai/core/document_generator.py +103 -0
- package/llm_router.py +148 -1
- package/package.json +2 -2
- package/server.py +207 -27
- package/static/css/tokens.css +26 -0
- package/static/lattice-reference.css +390 -375
- package/latticeai/__pycache__/__init__.cpython-314.pyc +0 -0
- package/latticeai/api/__pycache__/admin.cpython-314.pyc +0 -0
- package/latticeai/api/__pycache__/auth.cpython-314.pyc +0 -0
- package/latticeai/core/__pycache__/__init__.cpython-314.pyc +0 -0
- package/latticeai/core/__pycache__/audit.cpython-314.pyc +0 -0
- package/latticeai/core/__pycache__/security.cpython-314.pyc +0 -0
- package/latticeai/core/__pycache__/sessions.cpython-314.pyc +0 -0
package/README.md
CHANGED
|
@@ -154,14 +154,17 @@ Based on public product behavior as of 2026-05.
|
|
|
154
154
|
|-------|----------|------|---------|
|
|
155
155
|
| Qwen3-VL 4B | Multimodal / low spec | ~2.7 GB | 8 GB |
|
|
156
156
|
| Qwen3-VL 8B | Multimodal / balanced | ~4.8 GB | 16 GB |
|
|
157
|
+
| GPT-OSS 20B | Reasoning / open-weight | ~12.1 GB | 32 GB |
|
|
157
158
|
| Gemma 4 26B | Multimodal / large | ~15.6 GB | 32 GB |
|
|
159
|
+
| Gemma 4 31B | Multimodal / latest Gemma 4 | ~18.4 GB | 48 GB |
|
|
158
160
|
| Qwen3-VL 30B A3B | Multimodal / top | ~18 GB | 48 GB |
|
|
161
|
+
| GPT-OSS 120B | Reasoning / top open-weight | ~62.3 GB | 128 GB |
|
|
159
162
|
| Phi 4 Mini | Coding (fast) | ~2.2 GB | 8 GB |
|
|
160
163
|
| Llama 3.1 8B | General | ~4.7 GB | 8 GB |
|
|
161
164
|
| Mistral 7B v0.3 | General / Apache | ~4.1 GB | 8 GB |
|
|
162
165
|
|
|
163
166
|
**Cross-platform (Ollama / LM Studio / vLLM / llama.cpp):**
|
|
164
|
-
Same models via Ollama pull, LM Studio download,
|
|
167
|
+
Same models via Ollama pull, LM Studio download, vLLM serve, or llama.cpp GGUF where available.
|
|
165
168
|
|
|
166
169
|
**Cloud (any platform):**
|
|
167
170
|
OpenAI GPT-5.5 · Claude Opus 4.7 / Sonnet 4.6 / Haiku 4.5 via OpenRouter · Groq · Together · xAI · any OpenAI-compatible endpoint
|
|
@@ -360,7 +363,7 @@ Full reference: [docs/mcp-tools.md](docs/mcp-tools.md)
|
|
|
360
363
|
| VS Code Marketplace | [marketplace.visualstudio.com](https://marketplace.visualstudio.com/items?itemName=parktaesoo.ltcai) |
|
|
361
364
|
| Open VSX | [open-vsx.org](https://open-vsx.org/extension/parktaesoo/ltcai) |
|
|
362
365
|
|
|
363
|
-
Current version: **0.2.
|
|
366
|
+
Current version: **0.2.2** — [Changelog](docs/CHANGELOG.md)
|
|
364
367
|
|
|
365
368
|
---
|
|
366
369
|
|
|
@@ -416,8 +419,11 @@ LTCAI --tunnel # + Cloudflare 공개 URL 자동 발급
|
|
|
416
419
|
|------|------|------|----------|
|
|
417
420
|
| Qwen3-VL 4B | 멀티모달 / 저사양 | ~2.7GB | 8GB |
|
|
418
421
|
| Qwen3-VL 8B | 멀티모달 / 균형 추천 | ~4.8GB | 16GB |
|
|
422
|
+
| GPT-OSS 20B | 추론 / 오픈가중치 | ~12.1GB | 32GB |
|
|
419
423
|
| Gemma 4 26B | 멀티모달 / 대형 | ~15.6GB | 32GB |
|
|
424
|
+
| Gemma 4 31B | 멀티모달 / 최신 Gemma 4 | ~18.4GB | 48GB |
|
|
420
425
|
| Qwen3-VL 30B A3B | 멀티모달 / 최고급 | ~18GB | 48GB |
|
|
426
|
+
| GPT-OSS 120B | 추론 / 최고급 오픈가중치 | ~62.3GB | 128GB |
|
|
421
427
|
|
|
422
428
|
자세한 내용: [docs/CHANGELOG.md](docs/CHANGELOG.md) · [보안](SECURITY.md) · [기여](CONTRIBUTING.md)
|
|
423
429
|
|
package/auto_setup.py
CHANGED
|
@@ -443,8 +443,16 @@ class Recommendation:
|
|
|
443
443
|
_MODEL_CATALOG: List[Dict[str, Any]] = [
|
|
444
444
|
# (min_ram_mb, min_vram_mb, model_id, quant, runtime_preference)
|
|
445
445
|
# OS 오버헤드(~4-6 GB) + KV 캐시 여유를 감안한 보수적 RAM 임계값
|
|
446
|
+
{"ram": 128 * 1024, "vram": 48 * 1024,
|
|
447
|
+
"id": "mlx-community/gpt-oss-120b-MXFP4-Q4", "q": "mxfp4", "multimodal": False},
|
|
448
|
+
{"ram": 64 * 1024, "vram": 32 * 1024,
|
|
449
|
+
"id": "mlx-community/gemma-4-31b-it-4bit", "q": "4bit", "multimodal": True},
|
|
446
450
|
{"ram": 64 * 1024, "vram": 32 * 1024,
|
|
447
451
|
"id": "Qwen/Qwen3-VL-30B-A3B-Instruct", "q": "q4_K_M", "multimodal": True},
|
|
452
|
+
{"ram": 48 * 1024, "vram": 24 * 1024,
|
|
453
|
+
"id": "mlx-community/gemma-4-31b-it-4bit", "q": "4bit", "multimodal": True},
|
|
454
|
+
{"ram": 32 * 1024, "vram": 16 * 1024,
|
|
455
|
+
"id": "mlx-community/gpt-oss-20b-MXFP4-Q8", "q": "mxfp4", "multimodal": False},
|
|
448
456
|
{"ram": 48 * 1024, "vram": 24 * 1024,
|
|
449
457
|
"id": "Qwen/Qwen3-VL-30B-A3B-Instruct", "q": "q4_K_M", "multimodal": True},
|
|
450
458
|
{"ram": 32 * 1024, "vram": 16 * 1024,
|
|
@@ -630,7 +638,13 @@ def plan(profile: SystemProfile, rec: Recommendation) -> InstallPlan:
|
|
|
630
638
|
model_command = ["huggingface-cli", "download", rec.model_id, "--quiet"]
|
|
631
639
|
if rec.runtime == "ollama":
|
|
632
640
|
lower = rec.model_id.lower()
|
|
633
|
-
if "
|
|
641
|
+
if "gpt-oss-120b" in lower:
|
|
642
|
+
model_command = ["ollama", "pull", "gpt-oss:120b"]
|
|
643
|
+
elif "gpt-oss-20b" in lower:
|
|
644
|
+
model_command = ["ollama", "pull", "gpt-oss:20b"]
|
|
645
|
+
elif "gemma-4-31b" in lower:
|
|
646
|
+
model_command = ["ollama", "pull", "hf.co/ggml-org/gemma-4-31B-it-GGUF:Q4_K_M"]
|
|
647
|
+
elif "qwen3-vl-8b" in lower:
|
|
634
648
|
model_command = ["ollama", "pull", "qwen3-vl:8b"]
|
|
635
649
|
elif "qwen3-vl-4b" in lower:
|
|
636
650
|
model_command = ["ollama", "pull", "qwen3-vl:4b"]
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,72 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2026-05-27
|
|
4
|
+
|
|
5
|
+
### Knowledge Graph — LLM Structured Output Extraction
|
|
6
|
+
|
|
7
|
+
- `_extract_concepts()` / `_extract_triples()`를 LLM 기반으로 전환 (rule-based 폴백 유지)
|
|
8
|
+
- LLM Router 참조를 knowledge_graph에 주입하는 `set_llm_router()` 함수 추가
|
|
9
|
+
- `LATTICEAI_LLM_EXTRACTION` 환경변수로 LLM extraction on/off 제어
|
|
10
|
+
|
|
11
|
+
### Knowledge Graph — Hybrid Retrieval & Document Generation
|
|
12
|
+
|
|
13
|
+
- `search_for_document_generation()` 추가 — Hybrid Score (0.5×text + 0.3×graph + 0.2×recency) 기반 검색
|
|
14
|
+
- `multi_hop_context()` 추가 — Seed nodes에서 N-hop 그래프 탐색
|
|
15
|
+
- `DOCUMENT` NodeType, `USED_IN` / `INSPIRED_BY` / `CONTRADICTS` / `EVOLVES_FROM` EdgeType 추가
|
|
16
|
+
- Node에 `style`, `tone`, `importance_score`, `last_used` 필드 추가 (SQLite v2 스키마 반영)
|
|
17
|
+
|
|
18
|
+
### 문서 자동 생성 파이프라인
|
|
19
|
+
|
|
20
|
+
- `latticeai/core/context_builder.py` 신규 — Knowledge Graph → 구조화 Markdown Context 변환
|
|
21
|
+
- `latticeai/core/document_generator.py` 신규 — Intent detection + 전용 System Prompt + Session 관리
|
|
22
|
+
- `llm_router.py`에 `generate_document()` / `stream_generate_document()` 추가
|
|
23
|
+
- `/chat` 엔드포인트에서 "보고서 작성해줘" 같은 문서 생성 의도 자동 감지 → 전용 파이프라인 활성화
|
|
24
|
+
- 생성 문서에 참조 Knowledge Graph 노드 각주 자동 첨부
|
|
25
|
+
- 대화별 `DocumentGenerationSession`으로 반복 수정("이 부분 더 수정해") 지원
|
|
26
|
+
|
|
27
|
+
### UI/UX — 디자인 통일
|
|
28
|
+
|
|
29
|
+
- Account/Chat/Graph/Admin 전체 페이지를 통일된 lavender purple 테마로 전환
|
|
30
|
+
- 다크 모드 base 스타일 완전 제거 (`.app-layout` Obsidian dark, account dark base 등)
|
|
31
|
+
- 초록 테마(`#22d3a0`) 60+ 인스턴스를 보라(`#6f42e8`) 계열로 교체
|
|
32
|
+
- 메시지 버블: 다크 green → 보라 gradient(user), 밝은 lavender glass(AI)
|
|
33
|
+
- 사이드바, 입력창, 버튼, 모달 오버레이 모두 라이트 lavender로 통일
|
|
34
|
+
- 카드/패널에 hover lift 효과, 커스텀 스크롤바, focus ring, selection 색상 추가
|
|
35
|
+
- tokens.css에 글로벌 polish (scrollbar, selection, focus-visible) 추가
|
|
36
|
+
|
|
37
|
+
### 테스트
|
|
38
|
+
|
|
39
|
+
- `test_document_generation.py` 33개 테스트 추가 (intent detection, session, extraction, hybrid retrieval, context builder, schema v2)
|
|
40
|
+
|
|
41
|
+
### Release
|
|
42
|
+
|
|
43
|
+
- 배포 버전을 `0.3.0`으로 상향
|
|
44
|
+
- 대상 채널: `npm` · `PyPI` · `VS Code Marketplace` · `Open VSX`
|
|
45
|
+
|
|
46
|
+
## [0.2.2] - 2026-05-26
|
|
47
|
+
|
|
48
|
+
### 모델 카탈로그
|
|
49
|
+
|
|
50
|
+
- `GPT-OSS 20B`, `GPT-OSS 120B`, `Gemma 4 31B 4-bit`를 MLX/Ollama/vLLM/LM Studio/llama.cpp 모델 선택 및 다운로드/로드 흐름에 추가
|
|
51
|
+
- 엔진별 모델 목록에서 같은 패밀리의 최신 major/minor 버전이 있으면 낮은 버전 항목을 숨기도록 정리
|
|
52
|
+
- 설정 마법사 추천표와 RAM 티어에 새 모델을 반영
|
|
53
|
+
|
|
54
|
+
### 지식 그래프
|
|
55
|
+
|
|
56
|
+
- 로컬 폴더 스캔 시 PDF, Word, PowerPoint, Excel, CSV, 텍스트/코드, OCR 이미지 등 지원 파일은 실제 본문 텍스트가 추출된 경우에만 그래프 노드로 생성
|
|
57
|
+
- 빈 PDF/Word/PowerPoint/Excel 파일이나 OCR이 비어 있는 파일은 `skipped_empty_text`로 기록하고 그래프에는 표시하지 않도록 변경
|
|
58
|
+
- 기존 버전에서 파일명/상대경로만으로 만들어진 로컬 파일 노드는 다음 스캔에서 재추출 검증 후 자동 정리
|
|
59
|
+
- Word 표 셀, PowerPoint 슬라이드 텍스트, Excel 실제 셀 값 추출을 보강하고 파일명 기반 개념 추출을 제거
|
|
60
|
+
|
|
61
|
+
### UX
|
|
62
|
+
|
|
63
|
+
- 지식 그래프 오른쪽 사이드바의 하단 잘림 문제를 수정하고 데스크톱/모바일에서 패널, 메타데이터, 긴 경로가 자연스럽게 스크롤/줄바꿈되도록 조정
|
|
64
|
+
|
|
65
|
+
### Release
|
|
66
|
+
|
|
67
|
+
- 배포 버전을 `0.2.2`로 상향
|
|
68
|
+
- 대상 채널: `npm` · `PyPI` · `VS Code Marketplace` · `Open VSX`
|
|
69
|
+
|
|
3
70
|
## [0.2.1] - 2026-05-25
|
|
4
71
|
|
|
5
72
|
### 버그 수정
|
package/kg_schema.py
CHANGED
|
@@ -81,6 +81,7 @@ class NodeType(str, Enum):
|
|
|
81
81
|
CONVERSATION = "CONVERSATION" # 대화 세션 전체
|
|
82
82
|
MESSAGE = "MESSAGE" # 단일 발화
|
|
83
83
|
FILE = "FILE" # 업로드/연결된 파일
|
|
84
|
+
DOCUMENT = "DOCUMENT" # 생성/관리되는 문서 (보고서, 계획서 등)
|
|
84
85
|
CHUNK = "CHUNK" # 파일의 분할 청크
|
|
85
86
|
CODE_SYMBOL = "CODE_SYMBOL" # 함수·클래스·모듈
|
|
86
87
|
CONCEPT = "CONCEPT" # 추출된 개념 / 태그
|
|
@@ -110,6 +111,10 @@ class EdgeType(str, Enum):
|
|
|
110
111
|
TAGGED_AS = "TAGGED_AS" # ANY → CONCEPT
|
|
111
112
|
VERSION_OF = "VERSION_OF" # FILE → FILE (히스토리)
|
|
112
113
|
GRANTS_ACCESS = "GRANTS_ACCESS" # PERSON → RESOURCE
|
|
114
|
+
USED_IN = "USED_IN" # CONCEPT → DOCUMENT (문서에 활용됨)
|
|
115
|
+
INSPIRED_BY = "INSPIRED_BY" # DOCUMENT → DOCUMENT (영감/참조 관계)
|
|
116
|
+
CONTRADICTS = "CONTRADICTS" # DOCUMENT ↔ DOCUMENT (상충 관계)
|
|
117
|
+
EVOLVES_FROM = "EVOLVES_FROM" # DOCUMENT → DOCUMENT (발전/개정 관계)
|
|
113
118
|
|
|
114
119
|
@classmethod
|
|
115
120
|
def from_legacy(cls, label: str) -> "EdgeType":
|
|
@@ -140,6 +145,13 @@ _LEGACY_NODE_MAP: Dict[str, NodeType] = {
|
|
|
140
145
|
"mcp": NodeType.TOOL,
|
|
141
146
|
"project": NodeType.PROJECT,
|
|
142
147
|
"workspace": NodeType.PROJECT,
|
|
148
|
+
"document": NodeType.DOCUMENT,
|
|
149
|
+
"report": NodeType.DOCUMENT,
|
|
150
|
+
"plan": NodeType.DOCUMENT,
|
|
151
|
+
"proposal": NodeType.DOCUMENT,
|
|
152
|
+
"보고서": NodeType.DOCUMENT,
|
|
153
|
+
"계획서": NodeType.DOCUMENT,
|
|
154
|
+
"기획서": NodeType.DOCUMENT,
|
|
143
155
|
}
|
|
144
156
|
|
|
145
157
|
_LEGACY_EDGE_MAP: Dict[str, EdgeType] = {
|
|
@@ -171,18 +183,27 @@ _LEGACY_EDGE_MAP: Dict[str, EdgeType] = {
|
|
|
171
183
|
"tagged_as": EdgeType.TAGGED_AS,
|
|
172
184
|
"version_of": EdgeType.VERSION_OF,
|
|
173
185
|
"grants_access": EdgeType.GRANTS_ACCESS,
|
|
186
|
+
"used_in": EdgeType.USED_IN,
|
|
187
|
+
"inspired_by": EdgeType.INSPIRED_BY,
|
|
188
|
+
"contradicts": EdgeType.CONTRADICTS,
|
|
189
|
+
"evolves_from": EdgeType.EVOLVES_FROM,
|
|
190
|
+
"활용됨": EdgeType.USED_IN,
|
|
191
|
+
"영감받음": EdgeType.INSPIRED_BY,
|
|
192
|
+
"상충함": EdgeType.CONTRADICTS,
|
|
193
|
+
"발전함": EdgeType.EVOLVES_FROM,
|
|
174
194
|
}
|
|
175
195
|
|
|
176
196
|
# 노드 타입별로 허용되는 source / target 조합 (PPT 카탈로그 그대로)
|
|
177
197
|
# None == 모든 타입 허용
|
|
178
198
|
EDGE_ENDPOINT_RULES: Dict[EdgeType, Tuple[Optional[Sequence[NodeType]], Optional[Sequence[NodeType]]]] = {
|
|
179
|
-
EdgeType.CONTAINS: ((NodeType.FILE,
|
|
180
|
-
|
|
199
|
+
EdgeType.CONTAINS: ((NodeType.FILE, NodeType.DOCUMENT),
|
|
200
|
+
(NodeType.CHUNK,)),
|
|
201
|
+
EdgeType.MENTIONS: ((NodeType.MESSAGE, NodeType.FILE, NodeType.CHUNK, NodeType.DOCUMENT),
|
|
181
202
|
(NodeType.CONCEPT, NodeType.PERSON, NodeType.MODEL, NodeType.TOOL)),
|
|
182
203
|
EdgeType.REFERENCES: ((NodeType.FILE, NodeType.MESSAGE, NodeType.CHUNK),
|
|
183
204
|
(NodeType.FILE, NodeType.MESSAGE, NodeType.CHUNK)),
|
|
184
205
|
EdgeType.REPLIES_TO: ((NodeType.MESSAGE,), (NodeType.MESSAGE,)),
|
|
185
|
-
EdgeType.AUTHORED_BY: ((NodeType.FILE, NodeType.MESSAGE, NodeType.CONVERSATION),
|
|
206
|
+
EdgeType.AUTHORED_BY: ((NodeType.FILE, NodeType.MESSAGE, NodeType.CONVERSATION, NodeType.DOCUMENT),
|
|
186
207
|
(NodeType.PERSON,)),
|
|
187
208
|
EdgeType.USES: ((NodeType.PROJECT, NodeType.CONVERSATION),
|
|
188
209
|
(NodeType.TOOL, NodeType.MODEL)),
|
|
@@ -194,6 +215,14 @@ EDGE_ENDPOINT_RULES: Dict[EdgeType, Tuple[Optional[Sequence[NodeType]], Optional
|
|
|
194
215
|
EdgeType.VERSION_OF: ((NodeType.FILE,), (NodeType.FILE,)),
|
|
195
216
|
EdgeType.GRANTS_ACCESS: ((NodeType.PERSON,),
|
|
196
217
|
(NodeType.FILE, NodeType.CONVERSATION, NodeType.PROJECT)),
|
|
218
|
+
EdgeType.USED_IN: ((NodeType.CONCEPT,),
|
|
219
|
+
(NodeType.DOCUMENT, NodeType.FILE)),
|
|
220
|
+
EdgeType.INSPIRED_BY: ((NodeType.DOCUMENT, NodeType.FILE),
|
|
221
|
+
(NodeType.DOCUMENT, NodeType.FILE)),
|
|
222
|
+
EdgeType.CONTRADICTS: ((NodeType.DOCUMENT, NodeType.FILE),
|
|
223
|
+
(NodeType.DOCUMENT, NodeType.FILE)),
|
|
224
|
+
EdgeType.EVOLVES_FROM: ((NodeType.DOCUMENT, NodeType.FILE),
|
|
225
|
+
(NodeType.DOCUMENT, NodeType.FILE)),
|
|
197
226
|
}
|
|
198
227
|
|
|
199
228
|
|
|
@@ -262,6 +291,10 @@ class Node:
|
|
|
262
291
|
visibility: Visibility = Visibility.PRIVATE
|
|
263
292
|
created_at: str = field(default_factory=_now_iso)
|
|
264
293
|
updated_at: str = field(default_factory=_now_iso)
|
|
294
|
+
style: Optional[str] = None
|
|
295
|
+
tone: Optional[str] = None
|
|
296
|
+
importance_score: float = 0.0
|
|
297
|
+
last_used: Optional[str] = None
|
|
265
298
|
|
|
266
299
|
def validate(self) -> None:
|
|
267
300
|
if not isinstance(self.type, NodeType):
|
|
@@ -345,15 +378,19 @@ CREATE TABLE IF NOT EXISTS kg_meta (
|
|
|
345
378
|
);
|
|
346
379
|
|
|
347
380
|
CREATE TABLE IF NOT EXISTS nodes_v2 (
|
|
348
|
-
id
|
|
349
|
-
type
|
|
350
|
-
label
|
|
351
|
-
attrs
|
|
352
|
-
embedding
|
|
353
|
-
owner_id
|
|
354
|
-
visibility
|
|
355
|
-
created_at
|
|
356
|
-
updated_at
|
|
381
|
+
id TEXT PRIMARY KEY,
|
|
382
|
+
type TEXT NOT NULL,
|
|
383
|
+
label TEXT NOT NULL,
|
|
384
|
+
attrs TEXT NOT NULL DEFAULT '{}',
|
|
385
|
+
embedding BLOB,
|
|
386
|
+
owner_id TEXT,
|
|
387
|
+
visibility TEXT NOT NULL DEFAULT 'private',
|
|
388
|
+
created_at TEXT NOT NULL,
|
|
389
|
+
updated_at TEXT NOT NULL,
|
|
390
|
+
style TEXT,
|
|
391
|
+
tone TEXT,
|
|
392
|
+
importance_score REAL NOT NULL DEFAULT 0.0,
|
|
393
|
+
last_used TEXT
|
|
357
394
|
);
|
|
358
395
|
|
|
359
396
|
CREATE TABLE IF NOT EXISTS edges_v2 (
|
|
@@ -418,8 +455,9 @@ class KGStoreV2:
|
|
|
418
455
|
conn.execute(
|
|
419
456
|
"""
|
|
420
457
|
INSERT INTO nodes_v2(id, type, label, attrs, embedding,
|
|
421
|
-
owner_id, visibility, created_at, updated_at
|
|
422
|
-
|
|
458
|
+
owner_id, visibility, created_at, updated_at,
|
|
459
|
+
style, tone, importance_score, last_used)
|
|
460
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
423
461
|
ON CONFLICT(id) DO UPDATE SET
|
|
424
462
|
type=excluded.type,
|
|
425
463
|
label=excluded.label,
|
|
@@ -427,7 +465,11 @@ class KGStoreV2:
|
|
|
427
465
|
embedding=COALESCE(excluded.embedding, nodes_v2.embedding),
|
|
428
466
|
owner_id=excluded.owner_id,
|
|
429
467
|
visibility=excluded.visibility,
|
|
430
|
-
updated_at=excluded.updated_at
|
|
468
|
+
updated_at=excluded.updated_at,
|
|
469
|
+
style=COALESCE(excluded.style, nodes_v2.style),
|
|
470
|
+
tone=COALESCE(excluded.tone, nodes_v2.tone),
|
|
471
|
+
importance_score=MAX(excluded.importance_score, nodes_v2.importance_score),
|
|
472
|
+
last_used=COALESCE(excluded.last_used, nodes_v2.last_used)
|
|
431
473
|
""",
|
|
432
474
|
(
|
|
433
475
|
node.id, node.type.value, node.label,
|
|
@@ -435,6 +477,8 @@ class KGStoreV2:
|
|
|
435
477
|
encode_embedding(node.embedding),
|
|
436
478
|
node.owner_id, node.visibility.value,
|
|
437
479
|
node.created_at, node.updated_at,
|
|
480
|
+
node.style, node.tone,
|
|
481
|
+
float(node.importance_score), node.last_used,
|
|
438
482
|
),
|
|
439
483
|
)
|
|
440
484
|
return node.id
|
|
@@ -575,6 +619,7 @@ class KGStoreV2:
|
|
|
575
619
|
|
|
576
620
|
# ── Row → model helpers ────────────────────────────────────────────────────
|
|
577
621
|
def _row_to_node(row: sqlite3.Row) -> Node:
|
|
622
|
+
keys = row.keys() if hasattr(row, "keys") else []
|
|
578
623
|
return Node(
|
|
579
624
|
id=row["id"],
|
|
580
625
|
type=NodeType(row["type"]),
|
|
@@ -585,6 +630,10 @@ def _row_to_node(row: sqlite3.Row) -> Node:
|
|
|
585
630
|
visibility=Visibility(row["visibility"]),
|
|
586
631
|
created_at=row["created_at"],
|
|
587
632
|
updated_at=row["updated_at"],
|
|
633
|
+
style=row["style"] if "style" in keys else None,
|
|
634
|
+
tone=row["tone"] if "tone" in keys else None,
|
|
635
|
+
importance_score=float(row["importance_score"]) if "importance_score" in keys else 0.0,
|
|
636
|
+
last_used=row["last_used"] if "last_used" in keys else None,
|
|
588
637
|
)
|
|
589
638
|
|
|
590
639
|
|