ltcai 8.8.0 → 9.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.
Files changed (111) hide show
  1. package/README.md +33 -37
  2. package/auto_setup.py +84 -70
  3. package/docs/CHANGELOG.md +113 -237
  4. package/docs/CODE_REVIEW_2026-07-06.md +764 -0
  5. package/docs/COMMUNITY_AND_PLUGINS.md +3 -3
  6. package/docs/DEVELOPMENT.md +10 -10
  7. package/docs/LEGACY_COMPATIBILITY.md +1 -1
  8. package/docs/ONBOARDING.md +2 -2
  9. package/docs/PRODUCT_DIRECTION_REVIEW.md +3 -2
  10. package/docs/ROADMAP_RECOMMENDATIONS.md +61 -36
  11. package/docs/TRUST_MODEL.md +5 -1
  12. package/docs/WHY_LATTICE.md +4 -3
  13. package/docs/architecture.md +4 -0
  14. package/docs/kg-schema.md +1 -1
  15. package/lattice_brain/__init__.py +1 -1
  16. package/lattice_brain/archive.py +4 -9
  17. package/lattice_brain/conversations.py +156 -21
  18. package/lattice_brain/embeddings.py +38 -2
  19. package/lattice_brain/graph/_kg_common.py +39 -496
  20. package/lattice_brain/graph/_kg_constants.py +243 -0
  21. package/lattice_brain/graph/_kg_fsutil.py +297 -0
  22. package/lattice_brain/graph/discovery.py +0 -948
  23. package/lattice_brain/graph/discovery_index.py +972 -0
  24. package/lattice_brain/graph/json_utils.py +25 -0
  25. package/lattice_brain/graph/retrieval.py +66 -597
  26. package/lattice_brain/graph/retrieval_docgen.py +210 -0
  27. package/lattice_brain/graph/retrieval_vector.py +460 -0
  28. package/lattice_brain/graph/runtime.py +16 -0
  29. package/lattice_brain/graph/store.py +6 -0
  30. package/lattice_brain/ingestion.py +68 -0
  31. package/lattice_brain/portability.py +1 -9
  32. package/lattice_brain/quality.py +98 -4
  33. package/lattice_brain/runtime/agent_runtime.py +166 -0
  34. package/lattice_brain/runtime/multi_agent.py +1 -1
  35. package/lattice_brain/utils.py +28 -0
  36. package/latticeai/__init__.py +1 -1
  37. package/latticeai/api/chat.py +368 -418
  38. package/latticeai/api/chat_helpers.py +227 -0
  39. package/latticeai/api/computer_use.py +149 -31
  40. package/latticeai/api/marketplace.py +11 -0
  41. package/latticeai/api/mcp.py +3 -2
  42. package/latticeai/api/models.py +4 -1
  43. package/latticeai/api/permissions.py +72 -33
  44. package/latticeai/api/setup.py +17 -2
  45. package/latticeai/api/tools.py +105 -62
  46. package/latticeai/app_factory.py +101 -296
  47. package/latticeai/core/agent.py +25 -7
  48. package/latticeai/core/io_utils.py +37 -0
  49. package/latticeai/core/legacy_compatibility.py +1 -1
  50. package/latticeai/core/local_embeddings.py +2 -4
  51. package/latticeai/core/marketplace.py +33 -2
  52. package/latticeai/core/mcp_catalog.py +450 -0
  53. package/latticeai/core/mcp_registry.py +2 -441
  54. package/latticeai/core/sessions.py +11 -3
  55. package/latticeai/core/tool_registry.py +15 -4
  56. package/latticeai/core/users.py +4 -9
  57. package/latticeai/core/workspace_os.py +1 -1
  58. package/latticeai/core/workspace_os_utils.py +3 -17
  59. package/latticeai/integrations/telegram_bot.py +7 -2
  60. package/latticeai/models/model_providers.py +111 -0
  61. package/latticeai/models/router.py +58 -136
  62. package/latticeai/runtime/audit_runtime.py +27 -16
  63. package/latticeai/runtime/automation_runtime.py +9 -0
  64. package/latticeai/runtime/bootstrap.py +1 -1
  65. package/latticeai/runtime/history_runtime.py +163 -0
  66. package/latticeai/runtime/namespace_runtime.py +173 -0
  67. package/latticeai/runtime/network_config_runtime.py +56 -0
  68. package/latticeai/runtime/sso_config_runtime.py +128 -0
  69. package/latticeai/runtime/user_key_runtime.py +106 -0
  70. package/latticeai/services/app_context.py +1 -0
  71. package/latticeai/services/architecture_readiness.py +2 -2
  72. package/latticeai/services/memory_service.py +213 -0
  73. package/latticeai/services/model_engines.py +79 -12
  74. package/latticeai/services/model_runtime.py +24 -4
  75. package/latticeai/services/platform_runtime.py +9 -1
  76. package/latticeai/services/process_audit.py +208 -0
  77. package/latticeai/services/product_readiness.py +11 -11
  78. package/latticeai/services/review_queue.py +64 -11
  79. package/latticeai/services/run_executor.py +21 -0
  80. package/latticeai/services/search_service.py +106 -30
  81. package/latticeai/services/setup_detection.py +80 -0
  82. package/latticeai/services/tool_dispatch.py +66 -0
  83. package/latticeai/services/workspace_service.py +15 -0
  84. package/package.json +1 -1
  85. package/scripts/check_i18n_literals.mjs +20 -8
  86. package/scripts/i18n_literal_allowlist.json +34 -0
  87. package/scripts/lint_frontend.mjs +6 -2
  88. package/setup_wizard.py +196 -74
  89. package/src-tauri/Cargo.lock +1 -1
  90. package/src-tauri/Cargo.toml +1 -1
  91. package/src-tauri/tauri.conf.json +1 -1
  92. package/static/app/asset-manifest.json +11 -11
  93. package/static/app/assets/{Act-C7K9wsO9.js → Act-21lIXx2E.js} +1 -1
  94. package/static/app/assets/Brain-BqUd5UJJ.js +321 -0
  95. package/static/app/assets/{Capture-B3V4_5Xp.js → Capture-BA7Z2Q1u.js} +1 -1
  96. package/static/app/assets/{Library-Cgj-EF50.js → Library-bFMtyni3.js} +1 -1
  97. package/static/app/assets/System-K6krGCqn.js +1 -0
  98. package/static/app/assets/index-C4R3ws30.js +17 -0
  99. package/static/app/assets/index-ChSeOB02.css +2 -0
  100. package/static/app/assets/primitives-sQU3it5I.js +1 -0
  101. package/static/app/assets/{textarea-CVQkN2Tk.js → textarea-DK3Fd_lR.js} +1 -1
  102. package/static/app/index.html +2 -2
  103. package/static/css/tokens.css +4 -2
  104. package/static/sw.js +1 -1
  105. package/tools/local_files.py +6 -0
  106. package/latticeai/runtime/sso_runtime.py +0 -52
  107. package/static/app/assets/Brain-I1OSzxJu.js +0 -321
  108. package/static/app/assets/System-D1Lkei3I.js +0 -1
  109. package/static/app/assets/index--P0ksosz.js +0 -17
  110. package/static/app/assets/index-DCh5AoXt.css +0 -2
  111. package/static/app/assets/primitives-BLqaKk5g.js +0 -1
@@ -1,11 +1,11 @@
1
1
  # Community And Plugins
2
2
 
3
- Current release: **8.8.0 — Brain Core Extraction & Recall Proof Hardening**.
3
+ Current release: **9.0.0 — Code Review Closure & Runtime Cleanup**.
4
4
 
5
5
  LatticeAI defines the path from a strong local-first framework (8.4.0
6
6
  action-aware baseline, 8.5.0 registry+DI hardening, 8.6.0 capture/navigation
7
- reliability, 8.7.0 runtime-state hygiene, and 8.8.0 Brain Core extraction
8
- readiness) to a product ecosystem. The
7
+ reliability, 8.7.0 runtime-state hygiene, 8.8.0 Brain Core extraction
8
+ readiness, 8.9.0 scoped Tool API hardening, and 9.0.0 cleanup closure) to a product ecosystem. The
9
9
  immediate goal is small and practical: make it clear how
10
10
  contributors can extend the Brain without weakening local-first trust,
11
11
  workspace scoping, or release quality.
@@ -1,10 +1,10 @@
1
1
  # Lattice AI Development
2
2
 
3
- Current release: **8.8.0 — Brain Core Extraction & Recall Proof Hardening**.
3
+ Current release: **9.0.0 — Code Review Closure & Runtime Cleanup**.
4
4
 
5
5
  This document is for contributors working on the local-first Digital Brain
6
6
  codebase. Product positioning and quick start stay in `README.md`; release
7
- history is intentionally limited to 7.0.0-8.8.0 in `docs/CHANGELOG.md` and
7
+ history is intentionally limited to 8.0.0-9.0.0 in `docs/CHANGELOG.md` and
8
8
  `RELEASE.md`.
9
9
 
10
10
  ## Product Contract
@@ -16,8 +16,8 @@ Engineering work should preserve these boundaries:
16
16
 
17
17
  - the Brain is the durable asset;
18
18
  - models are replaceable voices;
19
- - SQLite is the default local store;
20
- - PostgreSQL, Docker, cloud models, downloads, update checks, Telegram, and
19
+ - SQLite is the live local Brain store;
20
+ - PostgreSQL scale/migration tooling, Docker, cloud models, downloads, update checks, Telegram, and
21
21
  Brain Network are opt-in;
22
22
  - import-only paths must not initialize MLX/GPU, write files, or make network
23
23
  calls;
@@ -114,10 +114,10 @@ For user-facing, API, runtime, release, or packaging changes, check:
114
114
  Release/publish examples must use exact target-version filenames. Do not
115
115
  document wildcard artifact upload commands.
116
116
 
117
- For 8.8.0 release work, exact artifacts are:
117
+ For 9.0.0 release work, exact artifacts are:
118
118
 
119
- - `dist/ltcai-8.8.0-py3-none-any.whl`
120
- - `dist/ltcai-8.8.0.tar.gz`
121
- - `ltcai-8.8.0.tgz`
122
- - `dist/ltcai-8.8.0.vsix`
123
- - `src-tauri/target/release/bundle/dmg/Lattice AI_8.8.0_aarch64.dmg`
119
+ - `dist/ltcai-9.0.0-py3-none-any.whl`
120
+ - `dist/ltcai-9.0.0.tar.gz`
121
+ - `ltcai-9.0.0.tgz`
122
+ - `dist/ltcai-9.0.0.vsix`
123
+ - `src-tauri/target/release/bundle/dmg/Lattice AI_9.0.0_aarch64.dmg`
@@ -1,6 +1,6 @@
1
1
  # Legacy Compatibility Map
2
2
 
3
- Current target: **8.8.0 — Internal shim removal & Brain Core extraction prep**.
3
+ Current target: **9.0.0 — Code Review Closure & Runtime Cleanup**.
4
4
 
5
5
  Lattice AI is moving toward a smaller, modular architecture centered on
6
6
  `lattice_brain`, `latticeai.services`, `latticeai.api`, and `latticeai.runtime`.
@@ -1,6 +1,6 @@
1
1
  # Lattice AI Onboarding
2
2
 
3
- Current release: **8.8.0 — Brain Core Extraction & Recall Proof Hardening**.
3
+ Current release: **9.0.0 — Code Review Closure & Runtime Cleanup**.
4
4
 
5
5
  The first-run goal is a five-minute path from "I opened the app" to "my Brain
6
6
  has a source, a question, and proof." This page is the product contract behind
@@ -32,7 +32,7 @@ read the docs first.
32
32
 
33
33
  ## Release Gate
34
34
 
35
- 8.8.0 treats onboarding as a release gate, not marketing copy. The current
35
+ 9.0.0 treats onboarding as a release gate, not marketing copy. The current
36
36
  machine-checkable product readiness report requires this five-minute contract,
37
37
  the Brain Home surface, setup helpers, graph ingestion tests, and exact release
38
38
  artifact documentation before the release can be called complete.
@@ -23,8 +23,9 @@ Already aligned:
23
23
  - The post-setup home is the living Brain plus conversation.
24
24
  - The graph appears only after progressive depth: Brain -> Memories ->
25
25
  Knowledge -> Relationships -> Graph.
26
- - Brain Core, SQLite default storage, optional PostgreSQL/pgvector scale mode,
27
- backup/restore, and encrypted `.latticebrain` archives support user ownership.
26
+ - Brain Core, SQLite live local storage, optional PostgreSQL/pgvector
27
+ scale/migration tooling, backup/restore, and encrypted `.latticebrain`
28
+ archives support user ownership.
28
29
  - Models are treated as swappable runtime workers rather than the durable asset.
29
30
 
30
31
  Gaps corrected in this review:
@@ -5,43 +5,12 @@ small release-sized work. The operating principle stays unchanged:
5
5
 
6
6
  > Models are temporary. Knowledge is durable. The Brain is the product.
7
7
 
8
- ## 7.3.0 Applied Slice
8
+ ## Applied Release History
9
9
 
10
- 7.3.0 implements the first narrow slice of the roadmap:
11
-
12
- - Runtime evolution: single-agent and multi-agent execution share
13
- `agent-run-contract/v1`, making mode/status/timeline evidence inspectable.
14
- - Hybrid search optimization: the Brain quality gate now includes deterministic
15
- recall and ranking regression thresholds.
16
- - Security and trust: run contracts distinguish runtime type and execution mode
17
- so simulated output is not presented as real execution.
18
-
19
- ## 7.4.0 Applied Slice
20
-
21
- 7.4.0 completes the next roadmap slice without deferring it:
22
-
23
- - Runtime convergence: agent runs, workflow runs, audit events, and realtime
24
- events all expose the `agent-run-contract/v1` family envelope while keeping
25
- legacy top-level fields for compatibility.
26
- - Trust and operations: persisted run rows refresh their contract through
27
- queued, running, terminal, cancelled, and interrupted states; audit events are
28
- contracted only after secret redaction.
29
- - Retrieval quality and scale: the CI quality gate now seeds a real local
30
- Knowledge Graph corpus and scores SearchService hybrid retrieval with judged
31
- queries, recall, precision, NDCG, and must-include hit-rate thresholds.
32
-
33
- ## 7.5.0 Applied Slice
34
-
35
- 7.5.0 burns down the remaining 7.4.0 risk instead of deferring it:
36
-
37
- - Contract consumption: AgentRuntime and realtime feed APIs now emit compact
38
- `contracts` views so UI, replay, admin, and export consumers can depend on the
39
- shared family envelope directly.
40
- - Retrieval scale: the corpus fixture now runs against 250+ local records while
41
- keeping judged queries, graded relevance, and must-include expectations.
42
- - Release trust: stale artifact mixing is handled through clean exact-version
43
- artifact generation, npm audit findings are cleared, and the Tauri 2 stack is
44
- updated past the old `block v0.1.6` future-incompatibility warning.
10
+ Release-specific applied-slice history is now kept only for 8.0.0 and newer in
11
+ [docs/CHANGELOG.md](CHANGELOG.md) and [RELEASE.md](../RELEASE.md). Pre-8.0.0
12
+ release-history notes were removed from the tracked documentation surface during
13
+ the 9.0.0 documentation cleanup.
45
14
 
46
15
  ## Near-Term Tracks
47
16
 
@@ -79,3 +48,59 @@ small release-sized work. The operating principle stays unchanged:
79
48
  - Interoperability with Obsidian, Notion, Email, Calendar, Git, Slack, and Teams.
80
49
  - Encrypted Brain Network sharing.
81
50
  - Plugin marketplace and public benchmarks.
51
+
52
+ ## Large-Scale Work Feasibility Assessment (2026-07-07)
53
+
54
+ 2026-07-07 코드베이스 검사 + 테스트 실행 결과 기반. (app_factory 1214줄, workspace_os 1391줄, 최근 AgentRuntime facade + KG mixin 분해 완료, 833 unit tests passed)
55
+
56
+ ### 1. Knowledge Graph / Retrieval at Large Corpus Scale (가장 추천, 기반 탄탄)
57
+ - Incremental/background ingestion + advanced dedup(콘텐츠해시 이상), conflict resolution, merge.
58
+ - Larger corpus benchmarks (현재 250+ → 수천~수만 아이템), latency budget, channel별 진단.
59
+ - pgvector 풀 프로덕션 경로 + 마이그레이션.
60
+ - **실행 가능성**: IngestionPipeline + provenance + vector mixin + benchmark fixtures 이미 존재. discovery/retrieval 분해 완료. hooks lifecycle 통합됨. 대형 작업으로 적합. 테스트: test_ingestion_pipeline.py 14/14 green.
61
+
62
+ ### 2. Multi-Modal Brain (스키마 준비됨, 구현 공백 큼)
63
+ - 이미지: vision LLM describe + 임베딩, IMAGE/IMAGE_TEXT 노드 + CONTAINS_IMAGE 엣지 완성, UI 증거 표시.
64
+ - 오디오(전사), 비디오(키프레임).
65
+ - **실행 가능성**: schema.py에 IMAGE/IMAGE_TEXT/CONTAINS_IMAGE 정의됨. discovery_index.py에 PIL + pytesseract OCR (include_ocr) 부분 구현. embeddings.py / LLMRouter에 vision 지원 부재. retrieval도 이미지 노드 미처리. ingestion 라우팅 확장 필요. 대형 + 차별화 포인트.
66
+
67
+ ### 3. Server / Runtime Composition 완전화 (구조 부채 해소)
68
+ - app_factory.py 잔여 로직 추가 추출 (더 많은 wiring → runtime/* 전용 모듈).
69
+ - dict(locals()) 레거시 제거, 강력한 RuntimeContext / DI.
70
+ - Config 중앙화 감사 및 강제 (core/config.py 이미 중앙).
71
+ - **실행 가능성**: 최근 review0707 + decomp (1214줄로 감소, runtime/ 디렉토리 풍부) 후에도 여전히 1.2k 라인. AGENTS.md 우선순위 #3,4 정확히 매치. AgentRuntime은 lattice_brain/runtime/agent_runtime.py 로 facade 추출 완료. ToolRegistry도 core/ + services 분리 진행.
72
+
73
+ ### 4. Proactive Synthesis + Temporal / Contradiction (최근 작업 연장)
74
+ - 백그라운드 합성 잡, 그래프 이력 기반 모순 검출, temporal 쿼리.
75
+ - 최근 커밋(Brain synthesis memories, follow-up) 을 스케줄 + 자동화로 확장.
76
+ - **실행 가능성**: MultiAgentOrchestrator + workflow_engine + IngestionPipeline + Brain Brief 이미 wired. hooks/audit 있음. agent synthesis UI 표면 최근 추가. 대형 기능으로 자연스러운 확장.
77
+
78
+ ### 5. Ecosystem / Interop + Plugin Marketplace (외부 연동 대형)
79
+ - Obsidian/Notion/Email/Calendar import bridge (MCP 또는 전용 ingestion).
80
+ - 서명된 플러그인, 원격 카탈로그, marketplace UI.
81
+ - Brain Network (암호화 공유).
82
+ - **실행 가능성**: plugins/ (hello-world, git-insights), mcp_registry, marketplace.py, core/plugins.py 존재. ingestion이 단일 관문. 하지만 실제 외부 브릿지/마켓플레이스 UI/보안 스캔은 대형.
83
+
84
+ 기타: Tauri auto-update + native scale, Workflow Designer 프론트엔드 완성, VSCode 확장 심화 통합 등.
85
+
86
+ **검증 결과 (2026-07-07)**:
87
+ - Core imports (Config, IngestionPipeline, AgentRuntime, ToolRegistry): OK
88
+ - Unit tests: 833 passed (tests/unit/)
89
+ - Ingestion specific: 14 passed
90
+ - Agent runtime tests: 29 passed
91
+ - Frontend lint: typecheck + privacy + OpenAPI 335 paths + lint all pass
92
+ - Git: clean (inspection 시점)
93
+
94
+ **2026-07-07 pts_grok 대형 후보 1~5 슬라이스 실행 결과 (자율 진행)**:
95
+ - 1. KG/Retrieval: background ingestion queue seam, schedule_background, vector index scale diagnostics, backlog reasons/samples, coverage ratio, and rebuild latency budget.
96
+ - 2. Multimodal: offline VisionStub describe/embed path and discovery_index vision_caption fallback so image files have searchable evidence without remote calls.
97
+ - 3. Server decomp: explicit _RUNTIME_BUNDLE in app_factory as the migration contract toward DI while dict(locals()) compatibility remains.
98
+ - 3a. Server decomp follow-up: filtered app_factory runtime namespace so internal scratch imports/runtime dicts no longer leak through lazy server_app compatibility; namespace reduced from 300 to 253 keys while legacy helpers remain, with typed RuntimeBundle added behind legacy _RUNTIME_BUNDLE.
99
+ - 4. Proactive/Temporal: stronger MemoryQualityManager conflict detection, including pairwise opposite-preference and temporal-negation flags.
100
+ - 4a. Proactive UX follow-up: Brain Brief now emits proactive_actions, and BrainHome renders one-click ask/delegate/review/graph action cards.
101
+ - 4b. Proactive UX completion: BrainHome now keeps a local activity trail for suggested actions, showing whether Brain ask, Agent delegation, Review draft, or graph navigation actions are running, completed, or failed.
102
+ - 5. Interop/Marketplace: ingestion_bridge marketplace templates and /marketplace/interop/bridges exposure for future Obsidian/Calendar-style connector imports through unified-ingestion.
103
+ - 총 affected tests: 45 passed in the first targeted gate. Broader lint/static/doc checks are run before commit.
104
+ - 남은: 각 슬라이스는 기반 확장. full background workers, real local VLM/image embeddings, full locals() removal, graph-level temporal queries, and production connector installs remain follow-up work.
105
+
106
+ 이 항목들은 "대형사이즈" 에도 할 만하며, mission (local-first, KG, AgentRuntime, security) 과 AGENTS 우선 리팩토링 순서에 부합. 작은 슬라이스로 시작 추천.
@@ -1,6 +1,6 @@
1
1
  # Lattice AI Trust Model
2
2
 
3
- Current release: **8.8.0 — Brain Core Extraction & Recall Proof Hardening**.
3
+ Current release: **9.0.0 — Code Review Closure & Runtime Cleanup**.
4
4
 
5
5
  Lattice AI is local-first, explicit about external communication, and honest
6
6
  when a capability is unavailable.
@@ -34,6 +34,10 @@ External communication requires configuration plus a user/admin action:
34
34
 
35
35
  Token presence alone must not start external communication.
36
36
 
37
+ Authenticated history, Knowledge Graph reads, and Tool API calls must also stay
38
+ inside the caller's user/workspace scope. Direct HTTP/MCP tool routes run
39
+ ToolRegistry policy before hooks or handlers execute.
40
+
37
41
  ## Consent And Honesty Gates
38
42
 
39
43
  Lattice AI should fail closed or show an unavailable state for:
@@ -1,6 +1,6 @@
1
1
  # Why Lattice AI Exists
2
2
 
3
- Current release: **8.8.0 — Brain Core Extraction & Recall Proof Hardening**.
3
+ Current release: **9.0.0 — Code Review Closure & Runtime Cleanup**.
4
4
 
5
5
  **Lattice AI is a local-first Digital Brain that keeps your knowledge durable
6
6
  across any AI model.**
@@ -32,8 +32,9 @@ The graph matters, but it is not the product identity. The product identity is a
32
32
  local-first Digital Brain: a place where the user can talk, add sources, watch
33
33
  memory grow, and inspect the Knowledge Graph when they need proof.
34
34
 
35
- In 8.8.0 the first screen is intentionally not a dashboard. The living Brain,
36
- conversation composer, evidence-backed Brain Brief, and five-minute onboarding
35
+ In 9.0.0 the first screen is intentionally not a dashboard. The living Brain,
36
+ conversation composer, evidence-backed Brain Brief, scoped memory isolation,
37
+ and five-minute onboarding
37
38
  path appear together so the user immediately knows what matters, what to add,
38
39
  and why the answer is grounded in local memory.
39
40
 
@@ -1,5 +1,9 @@
1
1
  # Lattice AI Architecture
2
2
 
3
+ > Historical subsystem note. The canonical current architecture document is
4
+ > [`ARCHITECTURE.md`](../ARCHITECTURE.md). This file preserves the v3.6.0
5
+ > Knowledge Graph First architecture detail for background.
6
+
3
7
  > v3.6.0 — **Knowledge Graph First.** Every data source converges into the graph
4
8
  > through one unified ingestion pipeline (`latticeai/services/ingestion.py`), with
5
9
  > formalized entities/relationships (`docs/kg-schema.md`), browser/web inputs,
package/docs/kg-schema.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Knowledge Graph Schema
2
2
 
3
- Current release: **8.8.0 — Brain Core Extraction & Recall Proof Hardening**.
3
+ Current release: **9.0.0 — Code Review Closure & Runtime Cleanup**.
4
4
 
5
5
  명세 출처: `lattice_ai_full_spec.pptx` 슬라이드 20·21·22
6
6
  구현: `kg_schema.py`
@@ -26,7 +26,7 @@ from .storage import (
26
26
  storage_from_env,
27
27
  )
28
28
 
29
- __version__ = "8.8.0"
29
+ __version__ = "9.0.0"
30
30
 
31
31
  __all__ = [
32
32
  "AgentRuntime",
@@ -9,7 +9,6 @@ restore on another machine without contacting a service.
9
9
  from __future__ import annotations
10
10
 
11
11
  import base64
12
- import hashlib
13
12
  import io
14
13
  import json
15
14
  import os
@@ -27,6 +26,8 @@ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
27
26
  from cryptography.exceptions import InvalidTag
28
27
  from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
29
28
 
29
+ from .utils import sha256_file as _sha256_file
30
+
30
31
 
31
32
  ARCHIVE_FORMAT = "latticebrain.encrypted"
32
33
  ARCHIVE_VERSION = 2
@@ -68,15 +69,9 @@ def _derive_key(passphrase: str, salt: bytes) -> bytes:
68
69
 
69
70
 
70
71
  def _sha256_bytes(data: bytes) -> str:
71
- return hashlib.sha256(data).hexdigest()
72
+ import hashlib
72
73
 
73
-
74
- def _sha256_file(path: Path) -> str:
75
- h = hashlib.sha256()
76
- with open(path, "rb") as fh:
77
- for block in iter(lambda: fh.read(65536), b""):
78
- h.update(block)
79
- return h.hexdigest()
74
+ return hashlib.sha256(data).hexdigest()
80
75
 
81
76
 
82
77
  def _safe_json(value: Any) -> Any:
@@ -60,27 +60,76 @@ class ConversationStore:
60
60
  user_nickname TEXT,
61
61
  source TEXT,
62
62
  timestamp TEXT NOT NULL,
63
- metadata_json TEXT NOT NULL DEFAULT '{}'
63
+ metadata_json TEXT NOT NULL DEFAULT '{}',
64
+ workspace_id TEXT,
65
+ organization_id TEXT
64
66
  );
65
67
  CREATE INDEX IF NOT EXISTS idx_conv_messages_conv
66
68
  ON conversation_messages(conversation_id);
67
69
  CREATE INDEX IF NOT EXISTS idx_conv_messages_time
68
70
  ON conversation_messages(timestamp);
71
+ CREATE INDEX IF NOT EXISTS idx_conv_messages_user
72
+ ON conversation_messages(user_email);
69
73
  """
70
74
  )
75
+ columns = {row["name"] for row in conn.execute("PRAGMA table_info(conversation_messages)").fetchall()}
76
+ if "workspace_id" not in columns:
77
+ conn.execute("ALTER TABLE conversation_messages ADD COLUMN workspace_id TEXT")
78
+ if "organization_id" not in columns:
79
+ conn.execute("ALTER TABLE conversation_messages ADD COLUMN organization_id TEXT")
80
+ conn.execute(
81
+ """
82
+ CREATE INDEX IF NOT EXISTS idx_conv_messages_workspace
83
+ ON conversation_messages(workspace_id)
84
+ """
85
+ )
86
+ rows = conn.execute(
87
+ """
88
+ SELECT id, metadata_json FROM conversation_messages
89
+ WHERE workspace_id IS NULL OR organization_id IS NULL
90
+ """
91
+ ).fetchall()
92
+ for row in rows:
93
+ try:
94
+ meta = json.loads(row["metadata_json"] or "{}")
95
+ except Exception:
96
+ meta = {}
97
+ workspace_id = meta.get("workspace_id")
98
+ organization_id = meta.get("organization_id")
99
+ if workspace_id or organization_id:
100
+ conn.execute(
101
+ """
102
+ UPDATE conversation_messages
103
+ SET workspace_id=COALESCE(workspace_id, ?),
104
+ organization_id=COALESCE(organization_id, ?)
105
+ WHERE id=?
106
+ """,
107
+ (workspace_id, organization_id, row["id"]),
108
+ )
71
109
 
72
110
  # ── writes ────────────────────────────────────────────────────────────
73
111
  def append(self, item: Dict[str, Any]) -> Dict[str, Any]:
74
112
  """Persist one chat item (the legacy chat_history.json entry shape)."""
75
- known = {"role", "content", "timestamp", "user_email", "user_nickname", "source", "conversation_id"}
113
+ known = {
114
+ "role",
115
+ "content",
116
+ "timestamp",
117
+ "user_email",
118
+ "user_nickname",
119
+ "source",
120
+ "conversation_id",
121
+ "workspace_id",
122
+ "organization_id",
123
+ }
76
124
  extra = {k: v for k, v in item.items() if k not in known}
77
125
  with self._lock, self._connect() as conn:
78
126
  conn.execute(
79
127
  """
80
128
  INSERT OR IGNORE INTO conversation_messages
81
129
  (message_hash, conversation_id, role, content, user_email,
82
- user_nickname, source, timestamp, metadata_json)
83
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
130
+ user_nickname, source, timestamp, metadata_json, workspace_id,
131
+ organization_id)
132
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
84
133
  """,
85
134
  (
86
135
  _message_hash(item),
@@ -92,6 +141,8 @@ class ConversationStore:
92
141
  item.get("source"),
93
142
  str(item.get("timestamp") or ""),
94
143
  json.dumps(extra, ensure_ascii=False) if extra else "{}",
144
+ item.get("workspace_id"),
145
+ item.get("organization_id"),
95
146
  ),
96
147
  )
97
148
  return item
@@ -122,8 +173,9 @@ class ConversationStore:
122
173
  """
123
174
  INSERT OR IGNORE INTO conversation_messages
124
175
  (message_hash, conversation_id, role, content, user_email,
125
- user_nickname, source, timestamp, metadata_json)
126
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, '{}')
176
+ user_nickname, source, timestamp, metadata_json, workspace_id,
177
+ organization_id)
178
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, '{}', ?, ?)
127
179
  """,
128
180
  (
129
181
  _message_hash(item),
@@ -134,6 +186,8 @@ class ConversationStore:
134
186
  item.get("user_nickname"),
135
187
  item.get("source"),
136
188
  str(item.get("timestamp") or ""),
189
+ item.get("workspace_id"),
190
+ item.get("organization_id"),
137
191
  ),
138
192
  )
139
193
  imported += cur.rowcount if cur.rowcount > 0 else 0
@@ -149,7 +203,7 @@ class ConversationStore:
149
203
  "content": row["content"],
150
204
  "timestamp": row["timestamp"],
151
205
  }
152
- for key in ("user_email", "user_nickname", "source", "conversation_id"):
206
+ for key in ("user_email", "user_nickname", "source", "conversation_id", "workspace_id", "organization_id"):
153
207
  if row[key]:
154
208
  item[key] = row[key]
155
209
  try:
@@ -159,13 +213,62 @@ class ConversationStore:
159
213
  item.update(extra)
160
214
  return item
161
215
 
162
- def history(self, *, conversation_id: Optional[str] = None, limit: Optional[int] = None) -> List[Dict[str, Any]]:
216
+ @staticmethod
217
+ def _scope_sql(
218
+ *,
219
+ user_email: Optional[str] = None,
220
+ allowed_workspaces: Optional[Any] = None,
221
+ include_legacy_global: bool = True,
222
+ ) -> tuple[str, List[Any]]:
223
+ clauses: List[str] = []
224
+ params: List[Any] = []
225
+ if user_email:
226
+ if include_legacy_global:
227
+ clauses.append("(user_email = ? OR user_email IS NULL OR user_email = '')")
228
+ else:
229
+ clauses.append("user_email = ?")
230
+ params.append(user_email)
231
+ if allowed_workspaces is not None:
232
+ allowed = [str(item) for item in allowed_workspaces if item]
233
+ if allowed:
234
+ placeholders = ",".join("?" for _ in allowed)
235
+ if include_legacy_global:
236
+ clauses.append(f"(workspace_id IN ({placeholders}) OR workspace_id IS NULL OR workspace_id = '')")
237
+ else:
238
+ clauses.append(f"workspace_id IN ({placeholders})")
239
+ params.extend(allowed)
240
+ elif include_legacy_global:
241
+ clauses.append("(workspace_id IS NULL OR workspace_id = '')")
242
+ else:
243
+ clauses.append("1=0")
244
+ return " AND ".join(clauses), params
245
+
246
+ def history(
247
+ self,
248
+ *,
249
+ conversation_id: Optional[str] = None,
250
+ limit: Optional[int] = None,
251
+ user_email: Optional[str] = None,
252
+ allowed_workspaces: Optional[Any] = None,
253
+ include_legacy_global: bool = True,
254
+ ) -> List[Dict[str, Any]]:
163
255
  """Chronological items; the unbounded successor of get_history()."""
164
256
  query = "SELECT * FROM conversation_messages"
165
257
  params: List[Any] = []
258
+ where: List[str] = []
166
259
  if conversation_id is not None:
167
- query += " WHERE conversation_id IS ?" if conversation_id == "" else " WHERE conversation_id = ?"
260
+ where.append("conversation_id IS ?" if conversation_id == "" else "conversation_id = ?")
168
261
  params.append(None if conversation_id == "" else conversation_id)
262
+ scope_sql, scope_params = self._scope_sql(
263
+ user_email=user_email,
264
+ allowed_workspaces=allowed_workspaces,
265
+ include_legacy_global=include_legacy_global,
266
+ )
267
+ if scope_sql:
268
+ where.append(scope_sql)
269
+ params.extend(scope_params)
270
+ if where:
271
+ query += " WHERE " + " AND ".join(where)
169
272
  query += " ORDER BY id ASC"
170
273
  if limit is not None:
171
274
  query += " LIMIT ?"
@@ -185,25 +288,51 @@ class ConversationStore:
185
288
  return 0
186
289
 
187
290
  # ── clears (legacy semantics preserved) ───────────────────────────────
188
- def clear_all(self, keep_last: int = 0) -> Dict[str, Any]:
291
+ def clear_all(
292
+ self,
293
+ keep_last: int = 0,
294
+ *,
295
+ user_email: Optional[str] = None,
296
+ allowed_workspaces: Optional[Any] = None,
297
+ include_legacy_global: bool = True,
298
+ ) -> Dict[str, Any]:
189
299
  keep_last = max(0, min(int(keep_last or 0), 20))
190
300
  with self._lock, self._connect() as conn:
191
301
  total = conn.execute("SELECT COUNT(*) FROM conversation_messages").fetchone()[0]
302
+ scope_sql, scope_params = self._scope_sql(
303
+ user_email=user_email,
304
+ allowed_workspaces=allowed_workspaces,
305
+ include_legacy_global=include_legacy_global,
306
+ )
307
+ scope_where = f" WHERE {scope_sql}" if scope_sql else ""
192
308
  if keep_last:
193
309
  conn.execute(
194
- """
195
- DELETE FROM conversation_messages WHERE id NOT IN (
196
- SELECT id FROM conversation_messages ORDER BY id DESC LIMIT ?
310
+ f"""
311
+ DELETE FROM conversation_messages
312
+ WHERE id IN (
313
+ SELECT id FROM conversation_messages{scope_where}
314
+ )
315
+ AND id NOT IN (
316
+ SELECT id FROM conversation_messages{scope_where}
317
+ ORDER BY id DESC LIMIT ?
197
318
  )
198
319
  """,
199
- (keep_last,),
320
+ (*scope_params, *scope_params, keep_last),
200
321
  )
201
322
  else:
202
- conn.execute("DELETE FROM conversation_messages")
323
+ conn.execute(f"DELETE FROM conversation_messages{scope_where}", scope_params)
203
324
  kept = conn.execute("SELECT COUNT(*) FROM conversation_messages").fetchone()[0]
204
325
  return {"status": "cleared", "removed": max(0, total - kept), "kept": kept}
205
326
 
206
- def clear_conversation(self, conversation_id: str, started_at: Optional[str] = None) -> Dict[str, Any]:
327
+ def clear_conversation(
328
+ self,
329
+ conversation_id: str,
330
+ started_at: Optional[str] = None,
331
+ *,
332
+ user_email: Optional[str] = None,
333
+ allowed_workspaces: Optional[Any] = None,
334
+ include_legacy_global: bool = True,
335
+ ) -> Dict[str, Any]:
207
336
  """Remove one conversation.
208
337
 
209
338
  ``legacy-previous-history`` targets unattributed messages; when
@@ -212,17 +341,23 @@ class ConversationStore:
212
341
  """
213
342
  with self._lock, self._connect() as conn:
214
343
  total = conn.execute("SELECT COUNT(*) FROM conversation_messages").fetchone()[0]
344
+ scope_sql, scope_params = self._scope_sql(
345
+ user_email=user_email,
346
+ allowed_workspaces=allowed_workspaces,
347
+ include_legacy_global=include_legacy_global,
348
+ )
349
+ scoped = f" AND {scope_sql}" if scope_sql else ""
215
350
  if conversation_id == "legacy-previous-history":
216
- conn.execute("DELETE FROM conversation_messages WHERE conversation_id IS NULL")
351
+ conn.execute(f"DELETE FROM conversation_messages WHERE conversation_id IS NULL{scoped}", scope_params)
217
352
  else:
218
353
  conn.execute(
219
- "DELETE FROM conversation_messages WHERE conversation_id = ?",
220
- (conversation_id,),
354
+ f"DELETE FROM conversation_messages WHERE conversation_id = ?{scoped}",
355
+ (conversation_id, *scope_params),
221
356
  )
222
357
  if started_at:
223
358
  conn.execute(
224
- "DELETE FROM conversation_messages WHERE conversation_id IS NULL AND timestamp >= ?",
225
- (str(started_at),),
359
+ f"DELETE FROM conversation_messages WHERE conversation_id IS NULL AND timestamp >= ?{scoped}",
360
+ (str(started_at), *scope_params),
226
361
  )
227
362
  kept = conn.execute("SELECT COUNT(*) FROM conversation_messages").fetchone()[0]
228
363
  return {
@@ -8,7 +8,7 @@ import os
8
8
  import re
9
9
  import struct
10
10
  from dataclasses import dataclass
11
- from typing import Iterable, List
11
+ from typing import Any, Dict, Iterable, List, Optional
12
12
 
13
13
 
14
14
  DEFAULT_EMBEDDING_DIM = int(os.getenv("LATTICEAI_VECTOR_DIM", "384"))
@@ -87,4 +87,40 @@ class LocalEmbeddingModel:
87
87
  return list(struct.unpack(f"<{count}f", payload[: count * 4]))
88
88
 
89
89
 
90
- __all__ = ["DEFAULT_EMBEDDING_DIM", "EMBEDDING_MODEL_ID", "LocalEmbeddingModel", "embedding_model_id"]
90
+ # --- Large candidate #2 slice: multimodal (vision) stubs ---
91
+ # Schema already defines IMAGE / IMAGE_TEXT / CONTAINS_IMAGE.
92
+ # These stubs allow ingestion + retrieval paths to carry image signals without
93
+ # requiring heavy deps at core. Real impl can swap in local vision (e.g. via
94
+ # ollama vision or onnx CLIP) behind the same interface.
95
+ @dataclass(frozen=True)
96
+ class VisionStub:
97
+ """Offline vision describe + embed stubs for multimodal Brain.
98
+
99
+ describe: returns a short textual caption derived from metadata/filename.
100
+ embed: produces a vector from image meta (size+format) + optional caption hash.
101
+ Later: replace with real embedding model that accepts image bytes/path.
102
+ """
103
+ dim: int = DEFAULT_EMBEDDING_DIM
104
+
105
+ def describe(self, path: str | None = None, meta: Optional[Dict[str, Any]] = None) -> str:
106
+ meta = meta or {}
107
+ w = meta.get("width") or meta.get("w") or "?"
108
+ h = meta.get("height") or meta.get("h") or "?"
109
+ fmt = meta.get("format") or meta.get("ext") or "img"
110
+ name = (path or "").split("/")[-1] or "image"
111
+ # deterministic caption stub (no external call)
112
+ return f"Image {name} ({fmt} {w}x{h})"
113
+
114
+ def embed_image(self, path: str | None = None, meta: Optional[Dict[str, Any]] = None, caption: str = "") -> List[float]:
115
+ meta = meta or {}
116
+ basis = f"{path or ''}|{meta.get('width',0)}x{meta.get('height',0)}|{meta.get('format','')}|{caption[:120]}"
117
+ # reuse text embedder for determinism (image content hash would be better with real vision)
118
+ model = LocalEmbeddingModel(dim=self.dim)
119
+ return model.embed(basis)
120
+
121
+
122
+ def get_vision_embedder(dim: int | None = None) -> VisionStub:
123
+ return VisionStub(dim=dim or DEFAULT_EMBEDDING_DIM)
124
+
125
+
126
+ __all__ = ["DEFAULT_EMBEDDING_DIM", "EMBEDDING_MODEL_ID", "LocalEmbeddingModel", "embedding_model_id", "VisionStub", "get_vision_embedder"]