ltcai 9.0.0 → 9.2.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 +88 -46
- package/auto_setup.py +7 -853
- package/desktop/electron/README.md +9 -0
- package/desktop/electron/main.cjs +5 -3
- package/docs/CHANGELOG.md +178 -2
- package/docs/COMMUNITY_AND_PLUGINS.md +4 -2
- package/docs/DEVELOPMENT.md +53 -14
- package/docs/LEGACY_COMPATIBILITY.md +4 -4
- package/docs/ONBOARDING.md +10 -2
- package/docs/OPERATIONS.md +8 -4
- package/docs/PRODUCT_DIRECTION_REVIEW.md +4 -0
- package/docs/TRUST_MODEL.md +5 -2
- package/docs/WHY_LATTICE.md +15 -10
- package/docs/WORKFLOW_DESIGNER.md +22 -0
- package/docs/kg-schema.md +13 -2
- package/docs/mcp-tools.md +17 -6
- package/docs/privacy.md +19 -3
- package/docs/public-deploy.md +32 -3
- package/docs/security-model.md +46 -11
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/archive.py +1 -6
- package/lattice_brain/context.py +66 -9
- package/lattice_brain/graph/_kg_fsutil.py +1 -5
- package/lattice_brain/graph/discovery_index.py +174 -20
- package/lattice_brain/graph/documents.py +44 -9
- package/lattice_brain/graph/ingest.py +47 -20
- package/lattice_brain/graph/provenance.py +13 -6
- package/lattice_brain/graph/retrieval.py +140 -39
- package/lattice_brain/graph/retrieval_docgen.py +37 -4
- package/lattice_brain/ingestion.py +4 -7
- package/lattice_brain/portability.py +28 -14
- package/lattice_brain/runtime/agent_runtime.py +5 -9
- package/lattice_brain/runtime/hooks.py +30 -13
- package/lattice_brain/runtime/multi_agent.py +27 -8
- package/lattice_brain/runtime/statuses.py +10 -0
- package/lattice_brain/utils.py +20 -2
- package/lattice_brain/workflow.py +1 -5
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +1 -1
- package/latticeai/api/agent_registry.py +10 -8
- package/latticeai/api/agents.py +22 -3
- package/latticeai/api/auth.py +78 -16
- package/latticeai/api/browser.py +303 -34
- package/latticeai/api/chat.py +320 -850
- package/latticeai/api/chat_agent_http.py +356 -0
- package/latticeai/api/chat_contracts.py +54 -0
- package/latticeai/api/chat_documents.py +256 -0
- package/latticeai/api/chat_helpers.py +28 -1
- package/latticeai/api/chat_history.py +99 -0
- package/latticeai/api/chat_intents.py +316 -0
- package/latticeai/api/chat_stream.py +115 -0
- package/latticeai/api/computer_use.py +62 -9
- package/latticeai/api/health.py +9 -3
- package/latticeai/api/hooks.py +17 -6
- package/latticeai/api/knowledge_graph.py +78 -14
- package/latticeai/api/local_files.py +7 -2
- package/latticeai/api/mcp.py +102 -23
- package/latticeai/api/models.py +72 -33
- package/latticeai/api/network.py +5 -5
- package/latticeai/api/permissions.py +67 -26
- package/latticeai/api/plugins.py +21 -7
- package/latticeai/api/portability.py +5 -5
- package/latticeai/api/realtime.py +33 -5
- package/latticeai/api/setup.py +2 -2
- package/latticeai/api/static_routes.py +123 -24
- package/latticeai/api/tools.py +96 -14
- package/latticeai/api/workflow_designer.py +37 -0
- package/latticeai/api/workspace.py +2 -1
- package/latticeai/app_factory.py +110 -52
- package/latticeai/core/agent.py +50 -13
- package/latticeai/core/agent_prompts.py +5 -0
- package/latticeai/core/agent_registry.py +1 -5
- package/latticeai/core/config.py +23 -3
- package/latticeai/core/context_builder.py +21 -3
- package/latticeai/core/file_generation.py +451 -0
- package/latticeai/core/invitations.py +1 -4
- package/latticeai/core/io_utils.py +9 -1
- package/latticeai/core/legacy_compatibility.py +24 -3
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/plugins.py +15 -0
- package/latticeai/core/policy.py +1 -1
- package/latticeai/core/realtime.py +38 -15
- package/latticeai/core/sessions.py +7 -2
- package/latticeai/core/timeutil.py +10 -0
- package/latticeai/core/tool_registry.py +90 -18
- package/latticeai/core/users.py +1 -5
- package/latticeai/core/workspace_graph_trace.py +31 -5
- package/latticeai/core/workspace_memory.py +3 -1
- package/latticeai/core/workspace_os.py +18 -7
- package/latticeai/core/workspace_os_utils.py +0 -5
- package/latticeai/core/workspace_permissions.py +2 -1
- package/latticeai/core/workspace_plugins.py +1 -1
- package/latticeai/core/workspace_runs.py +14 -5
- package/latticeai/core/workspace_skills.py +1 -1
- package/latticeai/core/workspace_snapshots.py +2 -1
- package/latticeai/core/workspace_timeline.py +2 -1
- package/latticeai/integrations/telegram_bot.py +94 -43
- package/latticeai/models/router.py +130 -36
- package/latticeai/runtime/access_runtime.py +62 -4
- package/latticeai/runtime/automation_runtime.py +13 -7
- package/latticeai/runtime/brain_runtime.py +19 -7
- package/latticeai/runtime/chat_wiring.py +2 -0
- package/latticeai/runtime/config_runtime.py +58 -26
- package/latticeai/runtime/context_runtime.py +16 -4
- package/latticeai/runtime/hooks_runtime.py +1 -1
- package/latticeai/runtime/model_wiring.py +33 -5
- package/latticeai/runtime/namespace_runtime.py +62 -72
- package/latticeai/runtime/platform_runtime_wiring.py +4 -3
- package/latticeai/runtime/review_wiring.py +1 -1
- package/latticeai/runtime/router_registration.py +34 -1
- package/latticeai/runtime/security_runtime.py +110 -13
- package/latticeai/runtime/stages.py +27 -0
- package/latticeai/server_app.py +5 -1
- package/latticeai/services/architecture_readiness.py +74 -5
- package/latticeai/services/brain_automation.py +3 -26
- package/latticeai/services/chat_service.py +205 -15
- package/latticeai/services/local_knowledge.py +423 -0
- package/latticeai/services/memory_service.py +55 -21
- package/latticeai/services/model_engines.py +48 -33
- package/latticeai/services/model_errors.py +17 -0
- package/latticeai/services/model_loading.py +28 -25
- package/latticeai/services/model_runtime.py +228 -162
- package/latticeai/services/p_reinforce.py +22 -3
- package/latticeai/services/platform_runtime.py +83 -23
- package/latticeai/services/product_readiness.py +19 -17
- package/latticeai/services/review_queue.py +12 -0
- package/latticeai/services/router_context.py +1 -0
- package/latticeai/services/run_executor.py +4 -9
- package/latticeai/services/search_service.py +203 -28
- package/latticeai/services/tool_dispatch.py +28 -5
- package/latticeai/services/triggers.py +53 -4
- package/latticeai/services/upload_service.py +13 -2
- package/latticeai/setup/__init__.py +25 -0
- package/latticeai/setup/auto_setup.py +857 -0
- package/latticeai/setup/wizard.py +1264 -0
- package/latticeai/tools/__init__.py +278 -0
- package/{tools → latticeai/tools}/commands.py +67 -7
- package/{tools → latticeai/tools}/computer.py +1 -1
- package/{tools → latticeai/tools}/documents.py +1 -1
- package/{tools → latticeai/tools}/filesystem.py +3 -3
- package/latticeai/tools/knowledge.py +178 -0
- package/{tools → latticeai/tools}/local_files.py +1 -1
- package/{tools → latticeai/tools}/network.py +0 -1
- package/local_knowledge_api.py +4 -342
- package/package.json +22 -2
- package/scripts/brain_quality_eval.py +3 -1
- package/scripts/bump_version.py +3 -0
- package/scripts/capture_release_evidence.mjs +180 -0
- package/scripts/check_current_release_docs.mjs +142 -0
- package/scripts/check_i18n_literals.mjs +107 -31
- package/scripts/check_openapi_drift.mjs +56 -0
- package/scripts/export_openapi.py +67 -7
- package/scripts/i18n_literal_allowlist.json +22 -24
- package/scripts/run_integration_tests.mjs +72 -10
- package/scripts/validate_release_artifacts.py +49 -7
- package/scripts/wheel_smoke.py +5 -0
- package/setup_wizard.py +3 -1260
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +11 -11
- package/static/app/assets/Act-C3dBrWE-.js +1 -0
- package/static/app/assets/Brain-DBYgdcjt.js +321 -0
- package/static/app/assets/Capture-Cf3hqRtN.js +1 -0
- package/static/app/assets/Library-CFfkNn3s.js +1 -0
- package/static/app/assets/System-BOurbT-v.js +1 -0
- package/static/app/assets/index-A3M9sElj.js +17 -0
- package/static/app/assets/index-Bmx9rzTc.css +2 -0
- package/static/app/assets/primitives-DcUUmhdC.js +1 -0
- package/static/app/assets/textarea-BklR6zN4.js +1 -0
- package/static/app/index.html +2 -2
- package/static/sw.js +1 -1
- package/tools/__init__.py +21 -269
- package/docs/CODE_REVIEW_2026-07-06.md +0 -764
- package/latticeai/runtime/app_context_runtime.py +0 -13
- package/latticeai/runtime/tail_wiring.py +0 -21
- package/scripts/com.pts.claudecode.discord.plist +0 -31
- package/scripts/pts-claudecode-discord-bridge.mjs +0 -207
- package/scripts/start-pts-claudecode-discord.sh +0 -51
- package/static/app/assets/Act-21lIXx2E.js +0 -1
- package/static/app/assets/Brain-BqUd5UJJ.js +0 -321
- package/static/app/assets/Capture-BA7Z2Q1u.js +0 -1
- package/static/app/assets/Library-bFMtyni3.js +0 -1
- package/static/app/assets/System-K6krGCqn.js +0 -1
- package/static/app/assets/index-C4R3ws30.js +0 -17
- package/static/app/assets/index-ChSeOB02.css +0 -2
- package/static/app/assets/primitives-sQU3it5I.js +0 -1
- package/static/app/assets/textarea-DK3Fd_lR.js +0 -1
- package/tools/knowledge.py +0 -95
|
@@ -5,6 +5,15 @@ from __future__ import annotations
|
|
|
5
5
|
from ._kg_common import * # noqa: F403,F401
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
def _local_scoped_slug(prefix: str, value: str, workspace_id: Optional[str]) -> str:
|
|
9
|
+
"""Preserve legacy IDs while isolating newly workspace-scoped nodes."""
|
|
10
|
+
slug = _slug(value)
|
|
11
|
+
if not workspace_id:
|
|
12
|
+
return f"{prefix}:{slug}"
|
|
13
|
+
scope = _sha256_text(str(workspace_id))[:12]
|
|
14
|
+
return f"{prefix}:{scope}:{slug}"
|
|
15
|
+
|
|
16
|
+
|
|
8
17
|
class KnowledgeGraphLocalIndexMixin:
|
|
9
18
|
"""Local file → graph indexing (text extraction, node/index upserts,
|
|
10
19
|
graph-node deletion, orphan cleanup, and the index_local_folder driver),
|
|
@@ -136,27 +145,56 @@ class KnowledgeGraphLocalIndexMixin:
|
|
|
136
145
|
file_path: Path,
|
|
137
146
|
os_type: str,
|
|
138
147
|
drive_id: str,
|
|
148
|
+
user_email: Optional[str] = None,
|
|
149
|
+
workspace_id: Optional[str] = None,
|
|
139
150
|
) -> str:
|
|
140
151
|
computer_label = platform.node() or "내 컴퓨터"
|
|
141
|
-
computer_id =
|
|
142
|
-
|
|
152
|
+
computer_id = _local_scoped_slug("computer", computer_label, workspace_id)
|
|
153
|
+
drive_identity = f"{workspace_id}|{os_type}:{drive_id}" if workspace_id else f"{os_type}:{drive_id}"
|
|
154
|
+
drive_node_id = f"drive:{_sha256_text(drive_identity)[:24]}"
|
|
143
155
|
root_folder_id = f"folder:{_sha256_text(f'{source_id}:root')[:24]}"
|
|
144
156
|
self._upsert_node(
|
|
145
|
-
conn,
|
|
157
|
+
conn,
|
|
158
|
+
computer_id,
|
|
159
|
+
"Computer",
|
|
160
|
+
computer_label,
|
|
161
|
+
metadata={"os_type": os_type, "workspace_id": workspace_id},
|
|
162
|
+
owner=user_email,
|
|
163
|
+
workspace_id=workspace_id,
|
|
146
164
|
)
|
|
147
165
|
self._upsert_node(
|
|
148
166
|
conn,
|
|
149
167
|
drive_node_id,
|
|
150
168
|
"Drive",
|
|
151
169
|
drive_id,
|
|
152
|
-
metadata={"os_type": os_type, "drive_id": drive_id},
|
|
170
|
+
metadata={"os_type": os_type, "drive_id": drive_id, "workspace_id": workspace_id},
|
|
171
|
+
owner=user_email,
|
|
172
|
+
workspace_id=workspace_id,
|
|
153
173
|
)
|
|
174
|
+
stale_parents = conn.execute(
|
|
175
|
+
"""
|
|
176
|
+
SELECT e.from_node
|
|
177
|
+
FROM edges e
|
|
178
|
+
JOIN nodes n ON n.id=e.from_node
|
|
179
|
+
WHERE e.to_node=? AND n.type='Drive' AND e.from_node<>?
|
|
180
|
+
""",
|
|
181
|
+
(root_folder_id, drive_node_id),
|
|
182
|
+
).fetchall()
|
|
183
|
+
for row in stale_parents:
|
|
184
|
+
conn.execute(
|
|
185
|
+
"DELETE FROM edges WHERE from_node=? AND to_node=?",
|
|
186
|
+
(row["from_node"], root_folder_id),
|
|
187
|
+
)
|
|
188
|
+
conn.execute(
|
|
189
|
+
"DELETE FROM edges_v2 WHERE source=? AND target=?",
|
|
190
|
+
(row["from_node"], root_folder_id),
|
|
191
|
+
)
|
|
154
192
|
self._upsert_edge(
|
|
155
193
|
conn,
|
|
156
194
|
computer_id,
|
|
157
195
|
drive_node_id,
|
|
158
196
|
"포함함",
|
|
159
|
-
metadata={"source": "local_scan"},
|
|
197
|
+
metadata={"source": "local_scan", "workspace_id": workspace_id},
|
|
160
198
|
)
|
|
161
199
|
self._upsert_node(
|
|
162
200
|
conn,
|
|
@@ -164,14 +202,16 @@ class KnowledgeGraphLocalIndexMixin:
|
|
|
164
202
|
"Folder",
|
|
165
203
|
root.name or str(root),
|
|
166
204
|
summary=str(root),
|
|
167
|
-
metadata={"source_id": source_id, "path": str(root), "root": True},
|
|
205
|
+
metadata={"source_id": source_id, "path": str(root), "root": True, "workspace_id": workspace_id},
|
|
206
|
+
owner=user_email,
|
|
207
|
+
workspace_id=workspace_id,
|
|
168
208
|
)
|
|
169
209
|
self._upsert_edge(
|
|
170
210
|
conn,
|
|
171
211
|
drive_node_id,
|
|
172
212
|
root_folder_id,
|
|
173
213
|
"포함함",
|
|
174
|
-
metadata={"source": "local_scan"},
|
|
214
|
+
metadata={"source": "local_scan", "workspace_id": workspace_id},
|
|
175
215
|
)
|
|
176
216
|
|
|
177
217
|
try:
|
|
@@ -195,10 +235,17 @@ class KnowledgeGraphLocalIndexMixin:
|
|
|
195
235
|
"source_id": source_id,
|
|
196
236
|
"path": str(current_path),
|
|
197
237
|
"root": False,
|
|
238
|
+
"workspace_id": workspace_id,
|
|
198
239
|
},
|
|
240
|
+
owner=user_email,
|
|
241
|
+
workspace_id=workspace_id,
|
|
199
242
|
)
|
|
200
243
|
self._upsert_edge(
|
|
201
|
-
conn,
|
|
244
|
+
conn,
|
|
245
|
+
parent_id,
|
|
246
|
+
folder_id,
|
|
247
|
+
"포함함",
|
|
248
|
+
metadata={"source": "local_scan", "workspace_id": workspace_id},
|
|
202
249
|
)
|
|
203
250
|
parent_id = folder_id
|
|
204
251
|
return parent_id
|
|
@@ -296,6 +343,8 @@ class KnowledgeGraphLocalIndexMixin:
|
|
|
296
343
|
parser_type: str,
|
|
297
344
|
text: str,
|
|
298
345
|
parser_meta: Dict[str, Any],
|
|
346
|
+
user_email: Optional[str] = None,
|
|
347
|
+
workspace_id: Optional[str] = None,
|
|
299
348
|
) -> str:
|
|
300
349
|
text = _clean_text(text)
|
|
301
350
|
if not text:
|
|
@@ -312,17 +361,26 @@ class KnowledgeGraphLocalIndexMixin:
|
|
|
312
361
|
file_path=file_path,
|
|
313
362
|
os_type=os_type,
|
|
314
363
|
drive_id=drive_id,
|
|
364
|
+
user_email=user_email,
|
|
365
|
+
workspace_id=workspace_id,
|
|
315
366
|
)
|
|
316
|
-
|
|
367
|
+
linked_rows = conn.execute(
|
|
317
368
|
"""
|
|
318
|
-
SELECT e.to_node AS id
|
|
369
|
+
SELECT e.to_node AS id, n.type, n.metadata_json
|
|
319
370
|
FROM edges e
|
|
320
371
|
JOIN nodes n ON n.id=e.to_node
|
|
321
|
-
WHERE e.from_node=?
|
|
372
|
+
WHERE e.from_node=?
|
|
322
373
|
""",
|
|
323
374
|
(file_node_id,),
|
|
324
375
|
).fetchall()
|
|
325
|
-
child_ids = [
|
|
376
|
+
child_ids = []
|
|
377
|
+
auto_candidate_ids = set()
|
|
378
|
+
for row in linked_rows:
|
|
379
|
+
linked_metadata = _safe_loads(row["metadata_json"])
|
|
380
|
+
if row["type"] in {"Chunk", "ImageText", "Section"} or linked_metadata.get("source_node") == file_node_id:
|
|
381
|
+
child_ids.append(row["id"])
|
|
382
|
+
elif linked_metadata.get("auto_extracted") and linked_metadata.get("source") == "local_folder":
|
|
383
|
+
auto_candidate_ids.add(row["id"])
|
|
326
384
|
conn.execute("DELETE FROM chunks WHERE source_node=?", (file_node_id,))
|
|
327
385
|
if child_ids:
|
|
328
386
|
placeholders = ",".join("?" * len(child_ids))
|
|
@@ -330,6 +388,27 @@ class KnowledgeGraphLocalIndexMixin:
|
|
|
330
388
|
self._v2_delete_nodes(conn, child_ids)
|
|
331
389
|
conn.execute("DELETE FROM edges WHERE from_node=?", (file_node_id,))
|
|
332
390
|
self._v2_delete_edges_from(conn, file_node_id)
|
|
391
|
+
removable_auto_ids = set()
|
|
392
|
+
for node_id in auto_candidate_ids:
|
|
393
|
+
remaining_edges = conn.execute(
|
|
394
|
+
"SELECT from_node, to_node FROM edges WHERE from_node=? OR to_node=?",
|
|
395
|
+
(node_id, node_id),
|
|
396
|
+
).fetchall()
|
|
397
|
+
if all(
|
|
398
|
+
row["from_node"] in auto_candidate_ids
|
|
399
|
+
and row["to_node"] in auto_candidate_ids
|
|
400
|
+
for row in remaining_edges
|
|
401
|
+
):
|
|
402
|
+
removable_auto_ids.add(node_id)
|
|
403
|
+
if removable_auto_ids:
|
|
404
|
+
placeholders = ",".join("?" * len(removable_auto_ids))
|
|
405
|
+
params = list(removable_auto_ids)
|
|
406
|
+
conn.execute(
|
|
407
|
+
f"DELETE FROM edges WHERE from_node IN ({placeholders}) OR to_node IN ({placeholders})",
|
|
408
|
+
params * 2,
|
|
409
|
+
)
|
|
410
|
+
conn.execute(f"DELETE FROM nodes WHERE id IN ({placeholders})", params)
|
|
411
|
+
self._v2_delete_nodes(conn, params)
|
|
333
412
|
|
|
334
413
|
metadata = {
|
|
335
414
|
"source": "local_folder",
|
|
@@ -345,6 +424,7 @@ class KnowledgeGraphLocalIndexMixin:
|
|
|
345
424
|
"modified_at": _safe_iso_from_stat_mtime(stat.st_mtime),
|
|
346
425
|
"sha256": sha256,
|
|
347
426
|
"parser": parser_meta,
|
|
427
|
+
"workspace_id": workspace_id,
|
|
348
428
|
}
|
|
349
429
|
self._upsert_node(
|
|
350
430
|
conn,
|
|
@@ -354,6 +434,8 @@ class KnowledgeGraphLocalIndexMixin:
|
|
|
354
434
|
summary=text[:700],
|
|
355
435
|
metadata=metadata,
|
|
356
436
|
raw=metadata,
|
|
437
|
+
owner=user_email,
|
|
438
|
+
workspace_id=workspace_id,
|
|
357
439
|
)
|
|
358
440
|
self._upsert_edge(
|
|
359
441
|
conn,
|
|
@@ -361,8 +443,9 @@ class KnowledgeGraphLocalIndexMixin:
|
|
|
361
443
|
file_node_id,
|
|
362
444
|
"포함함",
|
|
363
445
|
weight=1.0,
|
|
364
|
-
metadata={"source": "local_scan"},
|
|
446
|
+
metadata={"source": "local_scan", "workspace_id": workspace_id},
|
|
365
447
|
)
|
|
448
|
+
self._cleanup_local_graph_orphans(conn, source_id)
|
|
366
449
|
|
|
367
450
|
target_for_concepts = text
|
|
368
451
|
if category == "image" and text:
|
|
@@ -377,7 +460,10 @@ class KnowledgeGraphLocalIndexMixin:
|
|
|
377
460
|
"source_node": file_node_id,
|
|
378
461
|
"source_id": source_id,
|
|
379
462
|
"chars": len(text),
|
|
463
|
+
"workspace_id": workspace_id,
|
|
380
464
|
},
|
|
465
|
+
owner=user_email,
|
|
466
|
+
workspace_id=workspace_id,
|
|
381
467
|
)
|
|
382
468
|
self._upsert_edge(
|
|
383
469
|
conn,
|
|
@@ -385,7 +471,7 @@ class KnowledgeGraphLocalIndexMixin:
|
|
|
385
471
|
image_text_id,
|
|
386
472
|
"포함함",
|
|
387
473
|
weight=0.8,
|
|
388
|
-
metadata={"source": "ocr"},
|
|
474
|
+
metadata={"source": "ocr", "workspace_id": workspace_id},
|
|
389
475
|
)
|
|
390
476
|
|
|
391
477
|
for index, chunk in enumerate(_chunks(text)):
|
|
@@ -400,7 +486,10 @@ class KnowledgeGraphLocalIndexMixin:
|
|
|
400
486
|
"index": index,
|
|
401
487
|
"source_node": file_node_id,
|
|
402
488
|
"source_id": source_id,
|
|
489
|
+
"workspace_id": workspace_id,
|
|
403
490
|
},
|
|
491
|
+
owner=user_email,
|
|
492
|
+
workspace_id=workspace_id,
|
|
404
493
|
)
|
|
405
494
|
self._upsert_chunk(
|
|
406
495
|
conn,
|
|
@@ -411,6 +500,7 @@ class KnowledgeGraphLocalIndexMixin:
|
|
|
411
500
|
"index": index,
|
|
412
501
|
"source_node": file_node_id,
|
|
413
502
|
"source_id": source_id,
|
|
503
|
+
"workspace_id": workspace_id,
|
|
414
504
|
},
|
|
415
505
|
)
|
|
416
506
|
self._upsert_edge(
|
|
@@ -419,14 +509,14 @@ class KnowledgeGraphLocalIndexMixin:
|
|
|
419
509
|
chunk_id,
|
|
420
510
|
"포함함",
|
|
421
511
|
weight=0.7,
|
|
422
|
-
metadata={"source": "local_scan"},
|
|
512
|
+
metadata={"source": "local_scan", "workspace_id": workspace_id},
|
|
423
513
|
)
|
|
424
514
|
|
|
425
515
|
concepts = _extract_concepts(target_for_concepts, limit=18)
|
|
426
516
|
concept_ids: Dict[str, str] = {}
|
|
427
517
|
for concept in concepts:
|
|
428
518
|
node_t = _classify_node_type(concept, target_for_concepts)
|
|
429
|
-
concept_id =
|
|
519
|
+
concept_id = _local_scoped_slug(node_t.lower(), concept, workspace_id)
|
|
430
520
|
concept_ids[concept.lower()] = concept_id
|
|
431
521
|
self._upsert_node(
|
|
432
522
|
conn,
|
|
@@ -437,7 +527,10 @@ class KnowledgeGraphLocalIndexMixin:
|
|
|
437
527
|
"auto_extracted": True,
|
|
438
528
|
"source": "local_folder",
|
|
439
529
|
"source_id": source_id,
|
|
530
|
+
"workspace_id": workspace_id,
|
|
440
531
|
},
|
|
532
|
+
owner=user_email,
|
|
533
|
+
workspace_id=workspace_id,
|
|
441
534
|
)
|
|
442
535
|
self._upsert_edge(
|
|
443
536
|
conn,
|
|
@@ -445,7 +538,7 @@ class KnowledgeGraphLocalIndexMixin:
|
|
|
445
538
|
concept_id,
|
|
446
539
|
"언급함",
|
|
447
540
|
weight=0.75,
|
|
448
|
-
metadata={"source": "local_scan"},
|
|
541
|
+
metadata={"source": "local_scan", "workspace_id": workspace_id},
|
|
449
542
|
)
|
|
450
543
|
|
|
451
544
|
for triple in _extract_triples(target_for_concepts, concepts, limit=20):
|
|
@@ -461,6 +554,7 @@ class KnowledgeGraphLocalIndexMixin:
|
|
|
461
554
|
metadata={
|
|
462
555
|
"context": triple.get("context", "")[:240],
|
|
463
556
|
"source_id": source_id,
|
|
557
|
+
"workspace_id": workspace_id,
|
|
464
558
|
},
|
|
465
559
|
)
|
|
466
560
|
|
|
@@ -478,10 +572,20 @@ class KnowledgeGraphLocalIndexMixin:
|
|
|
478
572
|
"auto_extracted": True,
|
|
479
573
|
"source_node": file_node_id,
|
|
480
574
|
"filename": file_path.name,
|
|
575
|
+
"workspace_id": workspace_id,
|
|
481
576
|
},
|
|
482
577
|
raw=item,
|
|
578
|
+
owner=user_email,
|
|
579
|
+
workspace_id=workspace_id,
|
|
580
|
+
)
|
|
581
|
+
self._upsert_edge(
|
|
582
|
+
conn,
|
|
583
|
+
file_node_id,
|
|
584
|
+
sem_id,
|
|
585
|
+
"포함함",
|
|
586
|
+
weight=0.9,
|
|
587
|
+
metadata={"source": "local_scan", "workspace_id": workspace_id},
|
|
483
588
|
)
|
|
484
|
-
self._upsert_edge(conn, file_node_id, sem_id, "포함함", weight=0.9)
|
|
485
589
|
|
|
486
590
|
return file_node_id
|
|
487
591
|
|
|
@@ -627,6 +731,21 @@ class KnowledgeGraphLocalIndexMixin:
|
|
|
627
731
|
except (TypeError, ValueError):
|
|
628
732
|
return False
|
|
629
733
|
|
|
734
|
+
@staticmethod
|
|
735
|
+
def _node_matches_workspace(
|
|
736
|
+
conn: sqlite3.Connection,
|
|
737
|
+
node_id: Optional[str],
|
|
738
|
+
workspace_id: Optional[str],
|
|
739
|
+
) -> bool:
|
|
740
|
+
"""Return true only when the projected node has the expected scope."""
|
|
741
|
+
if not node_id:
|
|
742
|
+
return False
|
|
743
|
+
row = conn.execute(
|
|
744
|
+
"SELECT workspace_id FROM nodes_v2 WHERE id=?",
|
|
745
|
+
(node_id,),
|
|
746
|
+
).fetchone()
|
|
747
|
+
return bool(row is not None and row["workspace_id"] == workspace_id)
|
|
748
|
+
|
|
630
749
|
def index_local_folder(
|
|
631
750
|
self,
|
|
632
751
|
path: Path,
|
|
@@ -634,8 +753,10 @@ class KnowledgeGraphLocalIndexMixin:
|
|
|
634
753
|
include_ocr: bool = False,
|
|
635
754
|
watch_enabled: bool = False,
|
|
636
755
|
user_email: Optional[str] = None,
|
|
756
|
+
workspace_id: Optional[str] = None,
|
|
637
757
|
consent: Optional[Dict[str, Any]] = None,
|
|
638
758
|
max_files: int = 5_000,
|
|
759
|
+
source_id_override: Optional[str] = None,
|
|
639
760
|
) -> Dict[str, Any]:
|
|
640
761
|
"""Read approved files from a local folder and connect them to Graph RAG."""
|
|
641
762
|
root = Path(path).expanduser().resolve()
|
|
@@ -646,17 +767,25 @@ class KnowledgeGraphLocalIndexMixin:
|
|
|
646
767
|
|
|
647
768
|
os_type = _current_os_type()
|
|
648
769
|
drive_id = _drive_id_for_path(root)
|
|
649
|
-
|
|
770
|
+
path_fingerprint = _path_fingerprint(root)
|
|
771
|
+
source_id = str(source_id_override or "").strip()
|
|
772
|
+
if not source_id:
|
|
773
|
+
source_id = (
|
|
774
|
+
f"source:{_sha256_text(f'{workspace_id}|{path_fingerprint}')[:24]}"
|
|
775
|
+
if workspace_id
|
|
776
|
+
else f"source:{path_fingerprint}"
|
|
777
|
+
)
|
|
650
778
|
now = _now()
|
|
651
779
|
max_files = max(1, min(int(max_files or 5_000), 50_000))
|
|
652
780
|
consent_payload = {
|
|
653
781
|
"approved_at": now,
|
|
654
|
-
"approved_by": user_email,
|
|
655
782
|
"knowledge_source": True,
|
|
656
783
|
"include_ocr": bool(include_ocr),
|
|
657
784
|
"watch_enabled": bool(watch_enabled),
|
|
658
785
|
"sensitive_files_default_excluded": True,
|
|
659
786
|
**(consent or {}),
|
|
787
|
+
"approved_by": user_email or (consent or {}).get("approved_by"),
|
|
788
|
+
"workspace_id": workspace_id or (consent or {}).get("workspace_id"),
|
|
660
789
|
}
|
|
661
790
|
counts: Counter = Counter()
|
|
662
791
|
seen_relative_paths: set = set()
|
|
@@ -665,6 +794,23 @@ class KnowledgeGraphLocalIndexMixin:
|
|
|
665
794
|
limit_reached = False
|
|
666
795
|
|
|
667
796
|
with self._connect() as conn:
|
|
797
|
+
existing_source = conn.execute(
|
|
798
|
+
"SELECT id, consent_json FROM knowledge_sources WHERE root_path=?",
|
|
799
|
+
(str(root),),
|
|
800
|
+
).fetchone()
|
|
801
|
+
if existing_source is not None:
|
|
802
|
+
existing_consent = _safe_loads(existing_source["consent_json"])
|
|
803
|
+
existing_scope = existing_consent.get("workspace_id") or "personal"
|
|
804
|
+
requested_scope = workspace_id or consent_payload.get("workspace_id") or "personal"
|
|
805
|
+
if existing_scope != requested_scope:
|
|
806
|
+
raise ValueError(
|
|
807
|
+
"This folder is already connected to another workspace. "
|
|
808
|
+
"Disconnect it there before assigning it to a different Brain."
|
|
809
|
+
)
|
|
810
|
+
if existing_source["id"] != source_id:
|
|
811
|
+
# Reuse the legacy source identity so a personal source is
|
|
812
|
+
# reprojected in place instead of duplicated during upgrade.
|
|
813
|
+
source_id = existing_source["id"]
|
|
668
814
|
conn.execute(
|
|
669
815
|
"""
|
|
670
816
|
INSERT INTO knowledge_sources(
|
|
@@ -765,6 +911,9 @@ class KnowledgeGraphLocalIndexMixin:
|
|
|
765
911
|
and existing["status"] == "indexed"
|
|
766
912
|
and existing["graph_node_id"]
|
|
767
913
|
and self._local_file_index_has_extracted_text(existing)
|
|
914
|
+
and self._node_matches_workspace(
|
|
915
|
+
conn, existing["graph_node_id"], workspace_id
|
|
916
|
+
)
|
|
768
917
|
and existing["size_bytes"] == stat.st_size
|
|
769
918
|
and existing["modified_at"] == modified_at
|
|
770
919
|
):
|
|
@@ -817,6 +966,9 @@ class KnowledgeGraphLocalIndexMixin:
|
|
|
817
966
|
and existing["sha256"] == digest
|
|
818
967
|
and existing["graph_node_id"]
|
|
819
968
|
and self._local_file_index_has_extracted_text(existing)
|
|
969
|
+
and self._node_matches_workspace(
|
|
970
|
+
conn, existing["graph_node_id"], workspace_id
|
|
971
|
+
)
|
|
820
972
|
):
|
|
821
973
|
counts["skipped_unchanged"] += 1
|
|
822
974
|
self._upsert_local_file_index(
|
|
@@ -884,6 +1036,8 @@ class KnowledgeGraphLocalIndexMixin:
|
|
|
884
1036
|
parser_type=parser_type,
|
|
885
1037
|
text=text,
|
|
886
1038
|
parser_meta=parser_meta,
|
|
1039
|
+
user_email=user_email,
|
|
1040
|
+
workspace_id=workspace_id,
|
|
887
1041
|
)
|
|
888
1042
|
self._upsert_local_file_index(
|
|
889
1043
|
conn,
|
|
@@ -12,6 +12,9 @@ class KnowledgeGraphDocumentsMixin:
|
|
|
12
12
|
file_id: str,
|
|
13
13
|
filename: str,
|
|
14
14
|
structure: Dict[str, Any],
|
|
15
|
+
*,
|
|
16
|
+
owner: Optional[str] = None,
|
|
17
|
+
workspace_id: Optional[str] = None,
|
|
15
18
|
) -> None:
|
|
16
19
|
for slide in structure.get("slides") or []:
|
|
17
20
|
index = slide.get("index")
|
|
@@ -19,18 +22,28 @@ class KnowledgeGraphDocumentsMixin:
|
|
|
19
22
|
title = f"{filename} slide {index}"
|
|
20
23
|
summary = "\n".join(slide.get("texts") or [])[:800]
|
|
21
24
|
self._upsert_node(
|
|
22
|
-
conn,
|
|
25
|
+
conn,
|
|
26
|
+
slide_id,
|
|
27
|
+
"Slide",
|
|
28
|
+
title,
|
|
29
|
+
summary=summary,
|
|
30
|
+
metadata={**slide, "workspace_id": workspace_id},
|
|
31
|
+
owner=owner,
|
|
32
|
+
workspace_id=workspace_id,
|
|
23
33
|
)
|
|
24
34
|
self._upsert_edge(conn, file_id, slide_id, "has_slide")
|
|
25
35
|
for text in slide.get("texts") or []:
|
|
26
36
|
for topic in _topic_candidates(text, limit=4):
|
|
27
|
-
|
|
37
|
+
topic_key = f"{workspace_id}|{topic}" if workspace_id else topic
|
|
38
|
+
topic_id = f"topic:{_sha256_text(topic_key)[:24]}" if workspace_id else f"topic:{_slug(topic)}"
|
|
28
39
|
self._upsert_node(
|
|
29
40
|
conn,
|
|
30
41
|
topic_id,
|
|
31
42
|
"Topic",
|
|
32
43
|
topic,
|
|
33
|
-
metadata={"auto_extracted": True},
|
|
44
|
+
metadata={"auto_extracted": True, "workspace_id": workspace_id},
|
|
45
|
+
owner=owner,
|
|
46
|
+
workspace_id=workspace_id,
|
|
34
47
|
)
|
|
35
48
|
self._upsert_edge(conn, slide_id, topic_id, "discusses", weight=0.6)
|
|
36
49
|
|
|
@@ -44,13 +57,22 @@ class KnowledgeGraphDocumentsMixin:
|
|
|
44
57
|
"Page",
|
|
45
58
|
title,
|
|
46
59
|
summary=page.get("preview") or "",
|
|
47
|
-
metadata=page,
|
|
60
|
+
metadata={**page, "workspace_id": workspace_id},
|
|
61
|
+
owner=owner,
|
|
62
|
+
workspace_id=workspace_id,
|
|
48
63
|
)
|
|
49
64
|
self._upsert_edge(conn, file_id, page_id, "has_page")
|
|
50
65
|
for topic in _topic_candidates(page.get("preview") or "", limit=4):
|
|
51
|
-
|
|
66
|
+
topic_key = f"{workspace_id}|{topic}" if workspace_id else topic
|
|
67
|
+
topic_id = f"topic:{_sha256_text(topic_key)[:24]}" if workspace_id else f"topic:{_slug(topic)}"
|
|
52
68
|
self._upsert_node(
|
|
53
|
-
conn,
|
|
69
|
+
conn,
|
|
70
|
+
topic_id,
|
|
71
|
+
"Topic",
|
|
72
|
+
topic,
|
|
73
|
+
metadata={"auto_extracted": True, "workspace_id": workspace_id},
|
|
74
|
+
owner=owner,
|
|
75
|
+
workspace_id=workspace_id,
|
|
54
76
|
)
|
|
55
77
|
self._upsert_edge(conn, page_id, topic_id, "discusses", weight=0.6)
|
|
56
78
|
|
|
@@ -58,7 +80,13 @@ class KnowledgeGraphDocumentsMixin:
|
|
|
58
80
|
sheet_title = sheet.get("title")
|
|
59
81
|
sheet_id = f"sheet:{_sha256_text(f'{file_id}:sheet:{sheet_title}')[:24]}"
|
|
60
82
|
self._upsert_node(
|
|
61
|
-
conn,
|
|
83
|
+
conn,
|
|
84
|
+
sheet_id,
|
|
85
|
+
"Sheet",
|
|
86
|
+
f"{filename} / {sheet_title}",
|
|
87
|
+
metadata={**sheet, "workspace_id": workspace_id},
|
|
88
|
+
owner=owner,
|
|
89
|
+
workspace_id=workspace_id,
|
|
62
90
|
)
|
|
63
91
|
self._upsert_edge(conn, file_id, sheet_id, "has_sheet")
|
|
64
92
|
|
|
@@ -66,14 +94,21 @@ class KnowledgeGraphDocumentsMixin:
|
|
|
66
94
|
image_key = image.get("sha256") or _sha256_text(
|
|
67
95
|
json.dumps(image, ensure_ascii=False, sort_keys=True)
|
|
68
96
|
)
|
|
69
|
-
|
|
97
|
+
scoped_image_key = f"{workspace_id}|{image_key}" if workspace_id else str(image_key)
|
|
98
|
+
image_id = f"image:{_sha256_text(scoped_image_key)[:24]}" if workspace_id else f"image:{str(image_key)[:24]}"
|
|
70
99
|
title_parts = [filename, "image"]
|
|
71
100
|
if image.get("page"):
|
|
72
101
|
title_parts.append(f"page {image.get('page')}")
|
|
73
102
|
if image.get("name"):
|
|
74
103
|
title_parts.append(str(image.get("name")).split("/")[-1])
|
|
75
104
|
self._upsert_node(
|
|
76
|
-
conn,
|
|
105
|
+
conn,
|
|
106
|
+
image_id,
|
|
107
|
+
"Image",
|
|
108
|
+
" / ".join(title_parts),
|
|
109
|
+
metadata={**image, "workspace_id": workspace_id},
|
|
110
|
+
owner=owner,
|
|
111
|
+
workspace_id=workspace_id,
|
|
77
112
|
)
|
|
78
113
|
self._upsert_edge(conn, file_id, image_id, "contains_image")
|
|
79
114
|
|