ltcai 8.8.0 → 8.9.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 +34 -27
- package/auto_setup.py +73 -8
- package/docs/CHANGELOG.md +37 -0
- package/docs/CODE_REVIEW_2026-07-06.md +764 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +3 -3
- package/docs/DEVELOPMENT.md +10 -10
- package/docs/LEGACY_COMPATIBILITY.md +1 -1
- package/docs/ONBOARDING.md +2 -2
- package/docs/PRODUCT_DIRECTION_REVIEW.md +3 -2
- package/docs/TRUST_MODEL.md +5 -1
- package/docs/WHY_LATTICE.md +4 -3
- package/docs/architecture.md +4 -0
- package/docs/kg-schema.md +1 -1
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/conversations.py +156 -21
- package/lattice_brain/graph/_kg_common.py +12 -34
- package/lattice_brain/graph/json_utils.py +25 -0
- package/lattice_brain/graph/retrieval.py +66 -27
- package/lattice_brain/graph/runtime.py +16 -0
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/chat.py +31 -14
- package/latticeai/api/mcp.py +3 -2
- package/latticeai/api/models.py +4 -1
- package/latticeai/api/permissions.py +69 -30
- package/latticeai/api/setup.py +17 -2
- package/latticeai/api/tools.py +104 -62
- package/latticeai/app_factory.py +93 -10
- package/latticeai/core/agent.py +25 -7
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/sessions.py +11 -3
- package/latticeai/core/tool_registry.py +15 -4
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/runtime/bootstrap.py +1 -1
- package/latticeai/services/app_context.py +1 -0
- package/latticeai/services/architecture_readiness.py +2 -2
- package/latticeai/services/model_engines.py +79 -12
- package/latticeai/services/model_runtime.py +24 -4
- package/latticeai/services/process_audit.py +208 -0
- package/latticeai/services/product_readiness.py +11 -11
- package/latticeai/services/search_service.py +106 -30
- package/latticeai/services/tool_dispatch.py +66 -0
- package/latticeai/services/workspace_service.py +15 -0
- package/package.json +1 -1
- package/scripts/check_i18n_literals.mjs +20 -8
- package/scripts/i18n_literal_allowlist.json +34 -0
- package/scripts/lint_frontend.mjs +6 -2
- package/setup_wizard.py +185 -19
- 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 +10 -10
- package/static/app/assets/{Act-C7K9wsO9.js → Act-fZokUnC0.js} +1 -1
- package/static/app/assets/{Brain-I1OSzxJu.js → Brain-DtyuWubr.js} +1 -1
- package/static/app/assets/{Capture-B3V4_5Xp.js → Capture-D5KV3Cu7.js} +1 -1
- package/static/app/assets/{Library-Cgj-EF50.js → Library-C9kyFkSt.js} +1 -1
- package/static/app/assets/{System-D1Lkei3I.js → System-VbChmX7r.js} +1 -1
- package/static/app/assets/{index--P0ksosz.js → index-DPdcPoF0.js} +5 -5
- package/static/app/assets/{primitives-BLqaKk5g.js → primitives-DFeanEV6.js} +1 -1
- package/static/app/assets/{textarea-CVQkN2Tk.js → textarea-CD8UNKIy.js} +1 -1
- package/static/app/index.html +1 -1
- package/static/sw.js +1 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Community And Plugins
|
|
2
2
|
|
|
3
|
-
Current release: **8.
|
|
3
|
+
Current release: **8.9.0 — Scoped Memory & Tool Policy Hardening**.
|
|
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,
|
|
8
|
-
readiness) to a product ecosystem. The
|
|
7
|
+
reliability, 8.7.0 runtime-state hygiene, 8.8.0 Brain Core extraction
|
|
8
|
+
readiness, and 8.9.0 scoped Tool API hardening) 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.
|
package/docs/DEVELOPMENT.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Lattice AI Development
|
|
2
2
|
|
|
3
|
-
Current release: **8.
|
|
3
|
+
Current release: **8.9.0 — Scoped Memory & Tool Policy Hardening**.
|
|
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.
|
|
7
|
+
history is intentionally limited to 7.0.0-8.9.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
|
|
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.
|
|
117
|
+
For 8.9.0 release work, exact artifacts are:
|
|
118
118
|
|
|
119
|
-
- `dist/ltcai-8.
|
|
120
|
-
- `dist/ltcai-8.
|
|
121
|
-
- `ltcai-8.
|
|
122
|
-
- `dist/ltcai-8.
|
|
123
|
-
- `src-tauri/target/release/bundle/dmg/Lattice AI_8.
|
|
119
|
+
- `dist/ltcai-8.9.0-py3-none-any.whl`
|
|
120
|
+
- `dist/ltcai-8.9.0.tar.gz`
|
|
121
|
+
- `ltcai-8.9.0.tgz`
|
|
122
|
+
- `dist/ltcai-8.9.0.vsix`
|
|
123
|
+
- `src-tauri/target/release/bundle/dmg/Lattice AI_8.9.0_aarch64.dmg`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Legacy Compatibility Map
|
|
2
2
|
|
|
3
|
-
Current target: **8.
|
|
3
|
+
Current target: **8.9.0 — Scoped Memory & Tool Policy Hardening**.
|
|
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`.
|
package/docs/ONBOARDING.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Lattice AI Onboarding
|
|
2
2
|
|
|
3
|
-
Current release: **8.
|
|
3
|
+
Current release: **8.9.0 — Scoped Memory & Tool Policy Hardening**.
|
|
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.
|
|
35
|
+
8.9.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
|
|
27
|
-
backup/restore, and encrypted `.latticebrain`
|
|
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:
|
package/docs/TRUST_MODEL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Lattice AI Trust Model
|
|
2
2
|
|
|
3
|
-
Current release: **8.
|
|
3
|
+
Current release: **8.9.0 — Scoped Memory & Tool Policy Hardening**.
|
|
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:
|
package/docs/WHY_LATTICE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Why Lattice AI Exists
|
|
2
2
|
|
|
3
|
-
Current release: **8.
|
|
3
|
+
Current release: **8.9.0 — Scoped Memory & Tool Policy Hardening**.
|
|
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.
|
|
36
|
-
conversation composer, evidence-backed Brain Brief,
|
|
35
|
+
In 8.9.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
|
|
package/docs/architecture.md
CHANGED
|
@@ -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
|
@@ -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 = {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
196
|
-
|
|
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(
|
|
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 {
|
|
@@ -34,6 +34,8 @@ except Exception: # pragma: no cover - v2 schema is optional at import time
|
|
|
34
34
|
_exec_script = None # type: ignore[assignment]
|
|
35
35
|
|
|
36
36
|
from ..embeddings import LocalEmbeddingModel
|
|
37
|
+
from .json_utils import _json, _safe_loads
|
|
38
|
+
from .runtime import get_llm_router, set_llm_router
|
|
37
39
|
|
|
38
40
|
# Default read source for the graph queries: v2 reconstruction views.
|
|
39
41
|
# Override with LATTICEAI_KG_READ_V2=0 to fall back to the legacy tables.
|
|
@@ -48,14 +50,6 @@ _KG_DB_FORMAT_VERSION = 4
|
|
|
48
50
|
_KG_DB_FORMAT_KEY = "db_format_version"
|
|
49
51
|
_V2_WRITE_MASTER_KEY = "v2_write_mastered_at"
|
|
50
52
|
|
|
51
|
-
_llm_router_ref = None
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def set_llm_router(router_instance):
|
|
55
|
-
global _llm_router_ref
|
|
56
|
-
_llm_router_ref = router_instance
|
|
57
|
-
|
|
58
|
-
|
|
59
53
|
GRAPH_SCHEMA_VERSION = 1
|
|
60
54
|
|
|
61
55
|
LOCAL_TEXT_EXTENSIONS = {".txt", ".md"}
|
|
@@ -286,24 +280,6 @@ def _recency_score(
|
|
|
286
280
|
return math.exp(-decay * age_days)
|
|
287
281
|
|
|
288
282
|
|
|
289
|
-
def _json(data: Optional[Dict[str, Any]]) -> str:
|
|
290
|
-
return json.dumps(data or {}, ensure_ascii=False, sort_keys=True)
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
def _safe_loads(raw: Optional[str]) -> Dict[str, Any]:
|
|
294
|
-
"""Tolerantly parse a metadata_json column — returns {} on corrupt rows."""
|
|
295
|
-
if not raw:
|
|
296
|
-
return {}
|
|
297
|
-
try:
|
|
298
|
-
value = json.loads(raw)
|
|
299
|
-
return value if isinstance(value, dict) else {}
|
|
300
|
-
except (json.JSONDecodeError, TypeError) as e:
|
|
301
|
-
logging.warning(
|
|
302
|
-
"knowledge_graph: corrupt metadata_json (%s) — using empty dict", e
|
|
303
|
-
)
|
|
304
|
-
return {}
|
|
305
|
-
|
|
306
|
-
|
|
307
283
|
def _slug(text: str, max_len: int = 96) -> str:
|
|
308
284
|
value = re.sub(r"\s+", " ", str(text or "")).strip().lower()
|
|
309
285
|
value = re.sub(r"[^0-9a-zA-Z가-힣._:@/-]+", "-", value).strip("-")
|
|
@@ -586,9 +562,10 @@ ENABLE_LLM_EXTRACTION = os.getenv("LATTICEAI_LLM_EXTRACTION", "true").lower() in
|
|
|
586
562
|
|
|
587
563
|
|
|
588
564
|
def _llm_extract_concepts(text: str, limit: int = 12) -> Optional[List[str]]:
|
|
589
|
-
|
|
565
|
+
router = get_llm_router()
|
|
566
|
+
if not ENABLE_LLM_EXTRACTION or not router:
|
|
590
567
|
return None
|
|
591
|
-
if not
|
|
568
|
+
if not router.current_model_id:
|
|
592
569
|
return None
|
|
593
570
|
prompt = _LLM_EXTRACT_CONCEPT_PROMPT.format(text=text[:3000], limit=limit)
|
|
594
571
|
try:
|
|
@@ -599,12 +576,12 @@ def _llm_extract_concepts(text: str, limit: int = 12) -> Optional[List[str]]:
|
|
|
599
576
|
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
|
600
577
|
future = pool.submit(
|
|
601
578
|
asyncio.run,
|
|
602
|
-
|
|
579
|
+
router.generate(prompt, max_tokens=1024, temperature=0.1),
|
|
603
580
|
)
|
|
604
581
|
raw = future.result(timeout=30)
|
|
605
582
|
else:
|
|
606
583
|
raw = asyncio.run(
|
|
607
|
-
|
|
584
|
+
router.generate(prompt, max_tokens=1024, temperature=0.1)
|
|
608
585
|
)
|
|
609
586
|
raw = raw.strip()
|
|
610
587
|
if raw.startswith("```"):
|
|
@@ -627,9 +604,10 @@ def _llm_extract_concepts(text: str, limit: int = 12) -> Optional[List[str]]:
|
|
|
627
604
|
def _llm_extract_triples(
|
|
628
605
|
text: str, concepts: List[str], limit: int = 20
|
|
629
606
|
) -> Optional[List[Dict[str, str]]]:
|
|
630
|
-
|
|
607
|
+
router = get_llm_router()
|
|
608
|
+
if not ENABLE_LLM_EXTRACTION or not router:
|
|
631
609
|
return None
|
|
632
|
-
if not
|
|
610
|
+
if not router.current_model_id:
|
|
633
611
|
return None
|
|
634
612
|
prompt = _LLM_EXTRACT_TRIPLE_PROMPT.format(
|
|
635
613
|
text=text[:3000],
|
|
@@ -644,12 +622,12 @@ def _llm_extract_triples(
|
|
|
644
622
|
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
|
645
623
|
future = pool.submit(
|
|
646
624
|
asyncio.run,
|
|
647
|
-
|
|
625
|
+
router.generate(prompt, max_tokens=2048, temperature=0.1),
|
|
648
626
|
)
|
|
649
627
|
raw = future.result(timeout=30)
|
|
650
628
|
else:
|
|
651
629
|
raw = asyncio.run(
|
|
652
|
-
|
|
630
|
+
router.generate(prompt, max_tokens=2048, temperature=0.1)
|
|
653
631
|
)
|
|
654
632
|
raw = raw.strip()
|
|
655
633
|
if raw.startswith("```"):
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""JSON helpers shared by graph storage, projection, and retrieval modules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _json(data: Optional[Dict[str, Any]]) -> str:
|
|
11
|
+
return json.dumps(data or {}, ensure_ascii=False, sort_keys=True)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _safe_loads(raw: Optional[str]) -> Dict[str, Any]:
|
|
15
|
+
"""Tolerantly parse a metadata_json column — returns {} on corrupt rows."""
|
|
16
|
+
if not raw:
|
|
17
|
+
return {}
|
|
18
|
+
try:
|
|
19
|
+
value = json.loads(raw)
|
|
20
|
+
return value if isinstance(value, dict) else {}
|
|
21
|
+
except (json.JSONDecodeError, TypeError) as e:
|
|
22
|
+
logging.warning(
|
|
23
|
+
"knowledge_graph: corrupt metadata_json (%s) — using empty dict", e
|
|
24
|
+
)
|
|
25
|
+
return {}
|