ltcai 3.5.0 → 4.0.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 +73 -35
- package/docs/CARRYOVER_AUDIT_v3.6.0.md +61 -0
- package/docs/CHANGELOG.md +32 -0
- package/docs/HANDOVER_v3.6.0.md +46 -0
- package/docs/RUNTIME_HOOK_COVERAGE_v3.6.0.md +49 -0
- package/docs/V4_BRAIN_ARCHITECTURE.md +322 -0
- package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +509 -0
- package/docs/V4_IMPLEMENTATION_PLAN.md +470 -0
- package/docs/architecture.md +13 -12
- package/docs/kg-schema.md +102 -53
- package/docs/privacy.md +18 -2
- package/docs/security-model.md +17 -0
- package/kg_schema.py +139 -10
- package/knowledge_graph.py +874 -26
- package/knowledge_graph_api.py +11 -127
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +1 -1
- package/latticeai/api/agents.py +7 -1
- package/latticeai/api/auth.py +27 -4
- package/latticeai/api/browser.py +217 -0
- package/latticeai/api/chat.py +112 -76
- package/latticeai/api/health.py +1 -1
- package/latticeai/api/hooks.py +1 -1
- package/latticeai/api/knowledge_graph.py +146 -0
- package/latticeai/api/local_files.py +1 -1
- package/latticeai/api/mcp.py +23 -11
- package/latticeai/api/memory.py +1 -1
- package/latticeai/api/models.py +1 -1
- package/latticeai/api/network.py +81 -0
- package/latticeai/api/portability.py +93 -0
- package/latticeai/api/realtime.py +1 -1
- package/latticeai/api/search.py +26 -2
- package/latticeai/api/security_dashboard.py +2 -3
- package/latticeai/api/setup.py +2 -2
- package/latticeai/api/static_routes.py +2 -4
- package/latticeai/api/tools.py +3 -0
- package/latticeai/api/workflow_designer.py +46 -0
- package/latticeai/api/workspace.py +71 -49
- package/latticeai/app_factory.py +1710 -0
- package/latticeai/brain/__init__.py +18 -0
- package/latticeai/brain/context.py +213 -0
- package/latticeai/brain/conversations.py +236 -0
- package/latticeai/brain/identity.py +175 -0
- package/latticeai/brain/memory.py +102 -0
- package/latticeai/brain/network.py +205 -0
- package/latticeai/core/agent.py +31 -7
- package/latticeai/core/audit.py +0 -7
- package/latticeai/core/config.py +1 -1
- package/latticeai/core/context_builder.py +1 -2
- package/latticeai/core/enterprise.py +1 -1
- package/latticeai/core/graph_curator.py +2 -2
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/mcp_registry.py +791 -0
- package/latticeai/core/model_compat.py +1 -1
- package/latticeai/core/model_resolution.py +0 -1
- package/latticeai/core/multi_agent.py +238 -4
- package/latticeai/core/security.py +1 -1
- package/latticeai/core/sessions.py +37 -7
- package/latticeai/core/workflow_engine.py +114 -2
- package/latticeai/core/workspace_os.py +58 -10
- package/latticeai/models/__init__.py +7 -0
- package/latticeai/models/router.py +779 -0
- package/latticeai/server_app.py +29 -1504
- package/latticeai/services/agent_runtime.py +1 -0
- package/latticeai/services/app_context.py +75 -14
- package/latticeai/services/ingestion.py +318 -0
- package/latticeai/services/kg_portability.py +207 -0
- package/latticeai/services/memory_service.py +39 -11
- package/latticeai/services/model_runtime.py +2 -5
- package/latticeai/services/platform_runtime.py +100 -23
- package/latticeai/services/search_service.py +17 -8
- package/latticeai/services/tool_dispatch.py +12 -2
- package/latticeai/services/triggers.py +241 -0
- package/latticeai/services/upload_service.py +37 -12
- package/latticeai/services/workspace_service.py +31 -0
- package/llm_router.py +29 -772
- package/ltcai_cli.py +1 -2
- package/mcp_registry.py +25 -788
- package/p_reinforce.py +124 -14
- package/package.json +11 -8
- package/scripts/build_vsix.mjs +72 -0
- package/scripts/bump_version.py +99 -0
- package/scripts/generate_diagrams.py +0 -1
- package/scripts/lint_v3.mjs +82 -18
- package/scripts/validate_release_artifacts.py +0 -1
- package/scripts/wheel_smoke.py +142 -0
- package/server.py +11 -7
- package/setup_wizard.py +1142 -0
- package/static/account.html +2 -4
- package/static/admin.html +3 -5
- package/static/chat.html +3 -6
- package/static/graph.html +2 -4
- package/static/sw.js +81 -52
- package/static/v3/asset-manifest.json +20 -19
- package/static/v3/css/{lattice.base.e4cdd05d.css → lattice.base.49deefb5.css} +1 -1
- package/static/v3/css/lattice.base.css +1 -1
- package/static/v3/css/{lattice.components.9b49d614.css → lattice.components.cde18231.css} +1 -1
- package/static/v3/css/lattice.components.css +1 -1
- package/static/v3/css/{lattice.shell.8fcc9d33.css → lattice.shell.29d36d85.css} +1 -1
- package/static/v3/css/lattice.shell.css +1 -1
- package/static/v3/css/{lattice.tokens.e7018963.css → lattice.tokens.304cbc40.css} +3 -0
- package/static/v3/css/lattice.tokens.css +3 -0
- package/static/v3/css/{lattice.views.22f69117.css → lattice.views.0a18b6c5.css} +2 -2
- package/static/v3/css/lattice.views.css +2 -2
- package/static/v3/index.html +3 -4
- package/static/v3/js/{app.d086489d.js → app.356e6452.js} +1 -1
- package/static/v3/js/core/{api.12b568ad.js → api.7a308b89.js} +39 -1
- package/static/v3/js/core/api.js +38 -0
- package/static/v3/js/core/{routes.d214b399.js → routes.7222343d.js} +22 -22
- package/static/v3/js/core/routes.js +22 -22
- package/static/v3/js/core/{shell.d05266f5.js → shell.a1657f20.js} +4 -4
- package/static/v3/js/core/shell.js +1 -1
- package/static/v3/js/core/{store.34ebd5e6.js → store.204a08b2.js} +1 -1
- package/static/v3/js/core/store.js +1 -1
- package/static/v3/js/views/graph-canvas.17c15d65.js +509 -0
- package/static/v3/js/views/graph-canvas.js +509 -0
- package/static/v3/js/views/{hybrid-search.b22b97e0.js → hybrid-search.2fb63ed9.js} +1 -2
- package/static/v3/js/views/hybrid-search.js +1 -2
- package/static/v3/js/views/knowledge-graph.5e40cbeb.js +509 -0
- package/static/v3/js/views/knowledge-graph.js +326 -54
- package/static/vendor/chart.umd.min.js +20 -0
- package/static/vendor/fonts/inter-latin-300-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-400-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-500-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-600-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-700-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-800-normal.woff2 +0 -0
- package/static/vendor/fonts/inter.css +44 -0
- package/static/vendor/icons/tabler-icons.min.css +4 -0
- package/static/vendor/icons/tabler-icons.woff2 +0 -0
- package/static/vendor/marked.min.js +69 -0
- package/static/workspace.html +2 -2
- package/telegram_bot.py +1 -2
- package/tools/commands.py +4 -2
- package/tools/computer.py +1 -1
- package/tools/documents.py +1 -3
- package/tools/filesystem.py +0 -4
- package/tools/knowledge.py +1 -3
- package/tools/network.py +1 -3
- package/codex_telegram_bot.py +0 -195
- package/docs/assets/v3.4.0/agent-run.png +0 -0
- package/docs/assets/v3.4.0/agents.png +0 -0
- package/docs/assets/v3.4.0/before/chat-before.png +0 -0
- package/docs/assets/v3.4.0/before/files-before.png +0 -0
- package/docs/assets/v3.4.0/chat.png +0 -0
- package/docs/assets/v3.4.0/connect-folder.png +0 -0
- package/docs/assets/v3.4.0/files.png +0 -0
- package/docs/assets/v3.4.0/home.png +0 -0
- package/docs/assets/v3.4.0/hooks-dispatch.png +0 -0
- package/docs/assets/v3.4.0/knowledge-graph.png +0 -0
- package/docs/assets/v3.4.0/local-agent.png +0 -0
- package/docs/assets/v3.4.0/memory.png +0 -0
- package/docs/assets/v3.4.0/settings.png +0 -0
- package/docs/assets/v3.4.0/vision-input.png +0 -0
- package/docs/assets/v3.4.0/workflows.png +0 -0
- package/docs/assets/v3.4.1/e2e_runtime_log.txt +0 -42
- package/docs/assets/v3.4.1/hooks-dispatch.png +0 -0
- package/docs/assets/v3.4.1/local-agent.png +0 -0
- package/docs/images/admin-dashboard.png +0 -0
- package/docs/images/architecture.png +0 -0
- package/docs/images/enterprise.png +0 -0
- package/docs/images/graph.png +0 -0
- package/docs/images/hero.gif +0 -0
- package/docs/images/knowledge-graph.png +0 -0
- package/docs/images/lattice-ai-demo.gif +0 -0
- package/docs/images/lattice-ai-hero.png +0 -0
- package/docs/images/logo.svg +0 -33
- package/docs/images/mobile-responsive.png +0 -0
- package/docs/images/model-recommendation.png +0 -0
- package/docs/images/onboarding.png +0 -0
- package/docs/images/organization.png +0 -0
- package/docs/images/pipeline.png +0 -0
- package/docs/images/screenshot-admin.png +0 -0
- package/docs/images/screenshot-chat.png +0 -0
- package/docs/images/screenshot-graph.png +0 -0
- package/docs/images/skills.png +0 -0
- package/docs/images/workspace-dark.png +0 -0
- package/docs/images/workspace-light.png +0 -0
- package/docs/images/workspace.png +0 -0
- package/requirements.txt +0 -16
- package/static/v3/js/views/knowledge-graph.a14ea7e7.js +0 -237
package/docs/kg-schema.md
CHANGED
|
@@ -78,8 +78,66 @@ Edge {
|
|
|
78
78
|
| `VERSION_OF` | `FILE → FILE` | 버전 히스토리 |
|
|
79
79
|
| `GRANTS_ACCESS` | `PERSON → FILE`·`CONVERSATION`·`PROJECT` | 접근 권한 부여 |
|
|
80
80
|
|
|
81
|
-
**엔드포인트 룰은
|
|
82
|
-
|
|
81
|
+
**엔드포인트 룰은 권고 사항이다 (스키마 문서 기준).** 코드에는 엔드포인트 페어
|
|
82
|
+
검증기가 존재하지 않는다 — `validate_endpoints` 는 구현된 적이 없으며, 쓰기
|
|
83
|
+
경로는 타입 페어를 거부하지 않는다. v4 의 쓰기 정규화는 *타입 어휘* 를
|
|
84
|
+
강제한다: `_upsert_edge` 가 모든 엣지 타입을 canonical `EdgeType` 값으로
|
|
85
|
+
정규화하므로 자유 문자열 타입은 더 이상 생성되지 않는다.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## v3.6.0 — Knowledge Graph First 엔티티/관계
|
|
90
|
+
|
|
91
|
+
v3.6.0 은 "모든 데이터 소스가 Knowledge Graph 로 수렴한다"는 원칙을 1급 스키마로
|
|
92
|
+
승격한다. 아래 타입은 **추가형(additive)**이다 — 기존 enum/legacy 매핑을 깨지 않고
|
|
93
|
+
`from_legacy` 가 무손실로 정규화하며, 알 수 없는 타입은 여전히 `CONCEPT`/`MENTIONS` 로
|
|
94
|
+
폴백한다. 스키마는 **확장 가능**하게 유지한다: 새 도메인 엔티티는 enum 멤버 1개 +
|
|
95
|
+
`_LEGACY_NODE_MAP`/`_LEGACY_EDGE_MAP` 별칭만 추가하면 된다.
|
|
96
|
+
|
|
97
|
+
### 추가 노드 타입
|
|
98
|
+
|
|
99
|
+
| 타입 | 의미 | 대표 `attrs` / 출처 |
|
|
100
|
+
|------|------|--------------------|
|
|
101
|
+
| `SOURCE` | 수집 출처(파일/URL/브라우저 탭/git 등)의 **출처 노드** | `source_type`, `source_uri`, `content_hash`, `captured_at` |
|
|
102
|
+
| `REPOSITORY` | git 저장소 | `remote`, `branch`, `head` |
|
|
103
|
+
| `MEETING` | 회의 / 미팅 | `started_at`, `attendees[]` |
|
|
104
|
+
| `ORGANIZATION` | 조직 / 회사 / 팀 | `domain`, `members[]` |
|
|
105
|
+
| `WORKFLOW` | 워크플로우 정의/실행 | `workflow_id`, `status` |
|
|
106
|
+
| `AGENT` | 에이전트(역할/실행 주체) | `role`, `model_id` |
|
|
107
|
+
|
|
108
|
+
### 추가 엣지 타입
|
|
109
|
+
|
|
110
|
+
| 타입 | 허용 source → target | 의미 |
|
|
111
|
+
|------|---------------------|------|
|
|
112
|
+
| `INDEXED_FROM` | ANY → `SOURCE` | 이 노드가 **어떤 출처에서 색인**됐는가 (provenance) |
|
|
113
|
+
| `MODIFIED_BY` | ANY → `PERSON` | 마지막 수정자 |
|
|
114
|
+
| `BELONGS_TO_PROJECT` | ANY → `PROJECT` | 프로젝트 귀속 |
|
|
115
|
+
| `PART_OF` | ANY → ANY | 구성요소 관계 |
|
|
116
|
+
| `DISCUSSED_IN` | `CONCEPT`·`DECISION` → `MEETING`·`CHAT` | 어디에서 논의됨 |
|
|
117
|
+
| `DECIDED_BY` | `DECISION` → `PERSON` | 결정 주체 |
|
|
118
|
+
| `GENERATED_BY` | ANY → `AGENT`·`MODEL`·`WORKFLOW` | 생성 주체 |
|
|
119
|
+
| `USED_BY_AGENT` | ANY → `AGENT` | 에이전트가 사용함 |
|
|
120
|
+
|
|
121
|
+
### 통합 수집 형태 (Unified Ingestion)
|
|
122
|
+
|
|
123
|
+
모든 출처는 동일한 형태로 그래프에 들어온다:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
SOURCE ──INDEXED_FROM◄── Document/File ──CONTAINS──► Chunk[]
|
|
127
|
+
▲ │
|
|
128
|
+
│ └──(언급/포함)──► Concept / Task / Decision …
|
|
129
|
+
provenance(source_type, source_uri, content_hash, captured_at, modified_at,
|
|
130
|
+
owner, workspace_id, permissions, pipeline, embedded, linked)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
- **콘텐츠 노드**(Document/File/web 노드)는 `content_hash` 로 멱등(idempotent) 처리된다 —
|
|
134
|
+
같은 콘텐츠를 다시 수집하면 새 노드를 만들지 않고 갱신/링크한다.
|
|
135
|
+
- 모든 콘텐츠 노드는 `SOURCE` 노드로 `INDEXED_FROM` 엣지를 가져 **출처를 항상 설명 가능**하다.
|
|
136
|
+
- provenance 는 노드 `metadata.provenance` 에 임베드되며, 동시에 감사 가능한
|
|
137
|
+
`ingestion_provenance` 테이블에 기록된다 (`KnowledgeGraphStore.get_provenance(node_id)`).
|
|
138
|
+
|
|
139
|
+
구현: `latticeai/services/ingestion.py` (`IngestionPipeline`) 가 단일 진입점이며,
|
|
140
|
+
파일/로컬폴더/URL/브라우저 탭/텍스트를 모두 이 형태로 정규화한다.
|
|
83
141
|
|
|
84
142
|
---
|
|
85
143
|
|
|
@@ -143,20 +201,12 @@ Edge {
|
|
|
143
201
|
|
|
144
202
|
### 실행
|
|
145
203
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
# 3) 결과 확인
|
|
154
|
-
python3 kg_schema.py stats ~/.ltcai/knowledge_graph.db
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
마이그레이션은 **기존 `nodes` / `edges` 를 건드리지 않는다.** 신규 `nodes_v2` / `edges_v2`
|
|
158
|
-
테이블에 복사할 뿐이다. 새 코드가 안정화되면 다음 메이저 릴리스에서 legacy 테이블을
|
|
159
|
-
DROP 한다.
|
|
204
|
+
마이그레이션은 별도 CLI 없이 **서버 기동 시 자동으로** 일어난다:
|
|
205
|
+
`knowledge_graph.KnowledgeGraphStore` 가 열릴 때 v2 스키마를 생성/치유하고
|
|
206
|
+
(`kg_schema.KGStoreV2.init_schema` — 추가 컬럼은 `ALTER` 로 in-place 치유,
|
|
207
|
+
edges_v2 식별자 변경은 create→copy→swap 으로 재구축), legacy 데이터를
|
|
208
|
+
v2 로 백필한다. 기존 `nodes` / `edges` 테이블은 건드리지 않는다 — v4 의
|
|
209
|
+
write-mastering 전환(T3d) 전까지 legacy 가 쓰기 마스터다.
|
|
160
210
|
|
|
161
211
|
---
|
|
162
212
|
|
|
@@ -164,47 +214,46 @@ DROP 한다.
|
|
|
164
214
|
|
|
165
215
|
- 차원: 환경 변수 `LATTICEAI_EMBED_DIM` (기본 `1024`)
|
|
166
216
|
- 저장: SQLite `BLOB` 컬럼, `struct.pack('<{n}f', …)` 직렬화
|
|
167
|
-
- 검색: `
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
217
|
+
- 검색: `knowledge_graph.KnowledgeGraphStore.vector_search` — 순수 Python
|
|
218
|
+
코사인 (sqlite-vec/ANN 인덱스는 아직 없음). 기본 임베더는 해시 기반
|
|
219
|
+
폴백(`grade='fallback'`)이며, 실제 임베딩 모델은 setup wizard 를 통해
|
|
220
|
+
사용자 동의 하에 프로비저닝한다.
|
|
221
|
+
- 키워드 검색: v4 부터 FTS5 trigram 인덱스(`node_fts`) 가 LIKE 스캔을
|
|
222
|
+
대체한다 (한국어 부분 문자열 리콜 유지). FTS5/trigram 이 없는 SQLite
|
|
223
|
+
빌드에서는 LIKE 경로가 그대로 동작하며 `index_status().storage.fts_enabled`
|
|
224
|
+
로 정직하게 보고된다.
|
|
172
225
|
|
|
173
226
|
---
|
|
174
227
|
|
|
175
228
|
## 사용 (Python)
|
|
176
229
|
|
|
230
|
+
`KGStoreV2` 는 **스키마/초기화/통계 전용**이다 — 과거 문서에 있던
|
|
231
|
+
`Node`/`Edge` dataclass, `upsert_node`/`upsert_edge`/`neighbors`/
|
|
232
|
+
`search_similar` native API 는 제거되었고 존재하지 않는다. 데이터
|
|
233
|
+
read/write 는 `knowledge_graph.KnowledgeGraphStore` 가 담당한다:
|
|
234
|
+
|
|
177
235
|
```python
|
|
178
|
-
from kg_schema import KGStoreV2,
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
store
|
|
182
|
-
|
|
183
|
-
#
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
)
|
|
191
|
-
store.
|
|
192
|
-
|
|
193
|
-
# 관계 만들기
|
|
194
|
-
store.upsert_edge(Edge(
|
|
195
|
-
source=file_node.id,
|
|
196
|
-
target=concept_node.id,
|
|
197
|
-
type=EdgeType.MENTIONS,
|
|
198
|
-
weight=0.82, confidence=0.91,
|
|
199
|
-
evidence=["chunk:01HX7K…#p3"],
|
|
200
|
-
created_by="extractor:llm-gemma-4-12b",
|
|
201
|
-
))
|
|
202
|
-
|
|
203
|
-
# 이웃 탐색
|
|
204
|
-
for edge, other in store.neighbors(file_node.id, edge_type=EdgeType.MENTIONS):
|
|
205
|
-
print(f"-[{edge.type.value}]-> {other.label}")
|
|
206
|
-
|
|
207
|
-
# 의미 검색
|
|
208
|
-
for node, score in store.search_similar(query_embedding, top_k=8):
|
|
209
|
-
print(f"{score:+.3f} {node.type.value:>12} {node.label}")
|
|
236
|
+
from kg_schema import KGStoreV2, NodeType, EdgeType
|
|
237
|
+
from knowledge_graph import KnowledgeGraphStore
|
|
238
|
+
|
|
239
|
+
store = KnowledgeGraphStore(db_path, blob_dir)
|
|
240
|
+
|
|
241
|
+
# 쓰기: 모든 ingest 경로가 내부적으로 _upsert_node/_upsert_edge 를 통과하며,
|
|
242
|
+
# 엣지 타입은 canonical EdgeType 으로 정규화된다 (자유 문자열 차단).
|
|
243
|
+
store.ingest_message("user", "프로젝트 일정 공유", user_email="me@example.com")
|
|
244
|
+
|
|
245
|
+
# 읽기: search (FTS5/LIKE), vector_search, graph, traverse
|
|
246
|
+
matches = store.search("프로젝트")["matches"]
|
|
247
|
+
|
|
248
|
+
# v2 통계 (정규화된 타입 분포)
|
|
249
|
+
print(KGStoreV2(store.db_path).stats())
|
|
210
250
|
```
|
|
251
|
+
|
|
252
|
+
### v4 컬럼 (T3b/T3c)
|
|
253
|
+
|
|
254
|
+
- `nodes_v2.workspace_id` — `NULL` = legacy-global (스코프 도입 이전 데이터)
|
|
255
|
+
- `nodes_v2.visibility` — 신규 스코프 쓰기는 `workspace`/`private`,
|
|
256
|
+
스코프 없는 쓰기는 `legacy` (기존 공유 데이터를 몰래 private 으로
|
|
257
|
+
만들지 않는다)
|
|
258
|
+
- `nodes_v2.superseded_by` — 개정 체인 (`mark_superseded`)
|
|
259
|
+
- `edge_occurrences` — 관계의 모든 관측 기록 (observed_at/weight/source)
|
package/docs/privacy.md
CHANGED
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
| 사용자 계정 | `~/.ltcai/users.json` | 이름, scrypt 해시 비밀번호, 역할 |
|
|
14
14
|
| 세션 토큰 | `~/.ltcai/sessions.json` | UUID 토큰, 만료시간 |
|
|
15
15
|
| 채팅 히스토리 | `~/.ltcai/chat_history.json` | 사용자-AI 대화 내용 |
|
|
16
|
-
| 지식 그래프 | `~/.ltcai/knowledge_graph.sqlite` |
|
|
17
|
-
|
|
|
16
|
+
| 지식 그래프 | `~/.ltcai/knowledge_graph.sqlite` | 채팅/문서/웹/탭 노드·엣지, 프로비넌스 |
|
|
17
|
+
| 업로드/수집 파일 | `~/.ltcai/knowledge_graph_blobs/` | 원본 PDF/DOCX/웹 텍스트 등 |
|
|
18
|
+
| 수집 출처 기록(프로비넌스) | `~/.ltcai/knowledge_graph.sqlite` (`ingestion_provenance`) | 각 노드의 출처/시각/처리 방식 — 외부 전송 없음 |
|
|
19
|
+
| 그래프 내보내기/백업 | `~/.ltcai/workspace_exports/` | 사용자가 직접 만든 로컬 export/backup 파일 (클라우드 미사용) |
|
|
18
20
|
| 지식 정원 | `~/.ltcai-brain/` | P-Reinforce 분류 저장 |
|
|
19
21
|
| 설정 | `~/.ltcai/config.json` | 모델 설정, API 키 (keyring) |
|
|
20
22
|
|
|
@@ -36,6 +38,20 @@
|
|
|
36
38
|
|
|
37
39
|
Apple Silicon MLX 로컬 모델 사용 시에는 프롬프트가 외부로 전송되지 않습니다.
|
|
38
40
|
|
|
41
|
+
## 웹/브라우저 수집 (v3.6.0)
|
|
42
|
+
|
|
43
|
+
- **URL 읽기**(`/api/browser/read-url`): 사용자가 명시적으로 요청한 URL을 **로컬
|
|
44
|
+
런타임이 직접** 가져와 텍스트만 추출해 그래프에 색인합니다. Lattice AI가 임의로
|
|
45
|
+
크롤링하지 않으며, 가져온 페이지는 외부로 다시 전송되지 않습니다.
|
|
46
|
+
- **브라우저 탭 수집**(`/api/browser/ingest-current-tab`) 및 Manifest V3 확장:
|
|
47
|
+
확장 프로그램은 **오직 `127.0.0.1`(로컬)** 로만 전송합니다. 클라우드 엔드포인트가
|
|
48
|
+
존재하지 않습니다(`browser-extension/` 소스에서 단일 `fetch` 대상 확인 가능).
|
|
49
|
+
|
|
50
|
+
## 그래프 내보내기/백업 (v3.6.0)
|
|
51
|
+
|
|
52
|
+
지식 그래프 export/import 및 백업/복원은 **전적으로 로컬**에서 동작하며 클라우드
|
|
53
|
+
서비스를 요구하지 않습니다. 내보낸 파일의 이동·공유는 사용자 책임입니다.
|
|
54
|
+
|
|
39
55
|
## API 키 보안
|
|
40
56
|
|
|
41
57
|
- API 키는 OS keyring(macOS Keychain, Windows Credential Manager, Linux Secret Service)에 저장됩니다
|
package/docs/security-model.md
CHANGED
|
@@ -78,6 +78,23 @@ MAGIC_NUMBERS = {
|
|
|
78
78
|
- 업로드 시 파일 첫 바이트와 확장자 매핑 검증
|
|
79
79
|
- 불일치 시 400 에러
|
|
80
80
|
|
|
81
|
+
## 수집 & 그래프 포터빌리티 보안 (v3.6.0)
|
|
82
|
+
|
|
83
|
+
- **수집 라이프사이클**: 모든 수집은 `IngestionPipeline.ingest` → `dispatch_tool`
|
|
84
|
+
를 거쳐 `pre_tool`/`post_tool` 훅이 발화됩니다. `pre_tool`이 차단하면 수집은
|
|
85
|
+
정직하게 `status="blocked"`로 거부됩니다(권한 게이트·민감정보 가드 적용).
|
|
86
|
+
- **웹 URL 읽기**(`/api/browser/read-url`): `http(s)` 스킴만 허용(파일/기타 스킴
|
|
87
|
+
거부), 12초 타임아웃, 4MB 응답 상한, HTML/text 컨텐츠 타입만 처리. 차단/로그인
|
|
88
|
+
필요 페이지는 5xx가 아닌 **422로 우아하게 실패**합니다. 로컬 런타임이 사용자가
|
|
89
|
+
명시한 URL만 가져옵니다(자동 크롤링 없음).
|
|
90
|
+
- **브라우저 탭 수집**(`/api/browser/ingest-current-tab`): payload 정화(스크립트/
|
|
91
|
+
스타일 제거) + 페이로드 크기 상한(413). Manifest V3 확장은 **`127.0.0.1`로만**
|
|
92
|
+
전송하며 클라우드 엔드포인트가 없습니다.
|
|
93
|
+
- **포터빌리티 권한**: 그래프 export/status 읽기는 로그인 사용자, **import /
|
|
94
|
+
backup / restore 는 admin 전용**(`require_admin`). 그래프는 머신-전역 자원입니다.
|
|
95
|
+
- **복원 무결성**: 백업 아카이브는 `manifest.json`의 sha256과 대조 검증 후에만
|
|
96
|
+
복원되며, 불일치 시 거부됩니다.
|
|
97
|
+
|
|
81
98
|
## 에이전트 도구 샌드박스
|
|
82
99
|
|
|
83
100
|
### `run_command()` 위험 플래그 차단
|
package/kg_schema.py
CHANGED
|
@@ -99,6 +99,15 @@ class NodeType(str, Enum):
|
|
|
99
99
|
DECISION = "DECISION" # 결정 사항
|
|
100
100
|
ERROR = "ERROR" # 오류 / 버그
|
|
101
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" # 에이전트(역할/실행 주체)
|
|
102
111
|
|
|
103
112
|
@classmethod
|
|
104
113
|
def from_legacy(cls, label: str) -> "NodeType":
|
|
@@ -107,8 +116,14 @@ class NodeType(str, Enum):
|
|
|
107
116
|
매핑이 없는(동적 이벤트 등) 타입은 ``CONCEPT`` 로 폴백하지만, 호출부는
|
|
108
117
|
원본 문자열을 ``legacy_type`` 칼럼에 별도 보존하므로 정보 손실은 없다.
|
|
109
118
|
"""
|
|
110
|
-
m = (label or "").strip()
|
|
111
|
-
|
|
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)
|
|
112
127
|
|
|
113
128
|
|
|
114
129
|
class EdgeType(str, Enum):
|
|
@@ -143,6 +158,16 @@ class EdgeType(str, Enum):
|
|
|
143
158
|
DISCUSSES = "DISCUSSES" # SLIDE/PAGE → TOPIC (discusses)
|
|
144
159
|
IMPLIES = "IMPLIES" # NODE → NODE (implies)
|
|
145
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 (에이전트가 사용함)
|
|
146
171
|
|
|
147
172
|
@classmethod
|
|
148
173
|
def from_legacy(cls, label: str) -> "EdgeType":
|
|
@@ -151,8 +176,13 @@ class EdgeType(str, Enum):
|
|
|
151
176
|
매핑이 없는 동적 타입은 ``MENTIONS`` 로 폴백하지만, 호출부는 원본 문자열을
|
|
152
177
|
``edges_v2.legacy_type`` 에 보존하므로 정보 손실은 없다.
|
|
153
178
|
"""
|
|
154
|
-
m = (label or "").strip()
|
|
155
|
-
|
|
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)
|
|
156
186
|
|
|
157
187
|
|
|
158
188
|
# legacy(자유 문자열 / 한글 동사) → enum 매핑 표.
|
|
@@ -199,6 +229,19 @@ _LEGACY_NODE_MAP: Dict[str, NodeType] = {
|
|
|
199
229
|
"보고서": NodeType.DOCUMENT,
|
|
200
230
|
"계획서": NodeType.DOCUMENT,
|
|
201
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,
|
|
202
245
|
}
|
|
203
246
|
|
|
204
247
|
_LEGACY_EDGE_MAP: Dict[str, EdgeType] = {
|
|
@@ -254,6 +297,20 @@ _LEGACY_EDGE_MAP: Dict[str, EdgeType] = {
|
|
|
254
297
|
"영감받음": EdgeType.INSPIRED_BY,
|
|
255
298
|
"상충함": EdgeType.CONTRADICTS,
|
|
256
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,
|
|
257
314
|
}
|
|
258
315
|
|
|
259
316
|
# ── SQLite v2 store ─────────────────────────────────────────────────────────
|
|
@@ -272,7 +329,13 @@ CREATE TABLE IF NOT EXISTS nodes_v2 (
|
|
|
272
329
|
attrs TEXT NOT NULL DEFAULT '{}',
|
|
273
330
|
embedding BLOB,
|
|
274
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).
|
|
275
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,
|
|
276
339
|
created_at TEXT NOT NULL,
|
|
277
340
|
updated_at TEXT NOT NULL,
|
|
278
341
|
style TEXT,
|
|
@@ -293,14 +356,33 @@ CREATE TABLE IF NOT EXISTS edges_v2 (
|
|
|
293
356
|
metadata TEXT NOT NULL DEFAULT '{}',
|
|
294
357
|
created_by TEXT NOT NULL DEFAULT 'user',
|
|
295
358
|
created_at TEXT NOT NULL,
|
|
296
|
-
-- Edge identity
|
|
297
|
-
--
|
|
298
|
-
--
|
|
299
|
-
|
|
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),
|
|
300
368
|
FOREIGN KEY(source) REFERENCES nodes_v2(id) ON DELETE CASCADE,
|
|
301
369
|
FOREIGN KEY(target) REFERENCES nodes_v2(id) ON DELETE CASCADE
|
|
302
370
|
);
|
|
303
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
|
+
|
|
304
386
|
CREATE INDEX IF NOT EXISTS idx_nodes_v2_type ON nodes_v2(type);
|
|
305
387
|
CREATE INDEX IF NOT EXISTS idx_nodes_v2_legacy ON nodes_v2(legacy_type);
|
|
306
388
|
CREATE INDEX IF NOT EXISTS idx_nodes_v2_owner ON nodes_v2(owner_id);
|
|
@@ -352,8 +434,16 @@ class KGStoreV2:
|
|
|
352
434
|
"edges_v2": {"id", "source", "target", "type", "legacy_type", "weight",
|
|
353
435
|
"confidence", "evidence", "metadata", "created_by", "created_at"},
|
|
354
436
|
"nodes_v2": {"id", "type", "legacy_type", "label", "summary", "attrs",
|
|
355
|
-
"embedding", "owner_id", "
|
|
356
|
-
"
|
|
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": {},
|
|
357
447
|
}
|
|
358
448
|
|
|
359
449
|
def _drop_stale_empty_v2_tables(self, conn: sqlite3.Connection) -> None:
|
|
@@ -374,6 +464,13 @@ class KGStoreV2:
|
|
|
374
464
|
continue
|
|
375
465
|
cols = {r[1] for r in conn.execute(f"PRAGMA table_info({table})").fetchall()}
|
|
376
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)
|
|
377
474
|
if not missing:
|
|
378
475
|
continue
|
|
379
476
|
count = conn.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]
|
|
@@ -400,8 +497,40 @@ class KGStoreV2:
|
|
|
400
497
|
with self._conn() as own:
|
|
401
498
|
self._init_schema_on(own)
|
|
402
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
|
+
|
|
403
531
|
def _init_schema_on(self, conn: sqlite3.Connection) -> None:
|
|
404
532
|
self._drop_stale_empty_v2_tables(conn)
|
|
533
|
+
self._rebuild_edges_identity(conn)
|
|
405
534
|
_exec_script(conn, SCHEMA_SQL)
|
|
406
535
|
conn.execute(
|
|
407
536
|
"INSERT OR REPLACE INTO kg_meta(key, value) VALUES (?, ?)",
|