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 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, or vLLM serve.
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.1** — [Changelog](docs/CHANGELOG.md)
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 "qwen3-vl-8b" in lower:
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,), (NodeType.CHUNK,)),
180
- EdgeType.MENTIONS: ((NodeType.MESSAGE, NodeType.FILE, NodeType.CHUNK),
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 TEXT PRIMARY KEY,
349
- type TEXT NOT NULL,
350
- label TEXT NOT NULL,
351
- attrs TEXT NOT NULL DEFAULT '{}',
352
- embedding BLOB,
353
- owner_id TEXT,
354
- visibility TEXT NOT NULL DEFAULT 'private',
355
- created_at TEXT NOT NULL,
356
- updated_at TEXT NOT NULL
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
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
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