ltcai 4.7.0 → 4.7.2
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 +49 -38
- package/docs/CHANGELOG.md +80 -0
- package/docs/V4_7_1_ADMIN_OPERATIONS_REPORT.md +49 -0
- package/docs/V4_7_2_INTUITIVE_BRAIN_UX_REPORT.md +62 -0
- package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +16 -14
- package/frontend/src/App.tsx +191 -15
- package/frontend/src/api/client.ts +9 -1
- package/frontend/src/components/ProductFlow.tsx +89 -57
- package/frontend/src/pages/System.tsx +1 -1
- package/frontend/src/styles.css +205 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +112 -3
- package/latticeai/api/chat.py +11 -3
- package/latticeai/app_factory.py +0 -2
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/package.json +2 -1
- 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 +5 -5
- package/static/app/assets/index-DdAB4yfa.js +16 -0
- package/static/app/assets/index-DdAB4yfa.js.map +1 -0
- package/static/app/assets/{index-DFmuiJ6t.css → index-KlQ04wVv.css} +1 -1
- package/static/app/index.html +2 -2
- package/scripts/launch-pts-grok.sh +0 -56
- package/static/app/assets/index-DwX3rNfA.js +0 -16
- package/static/app/assets/index-DwX3rNfA.js.map +0 -1
package/latticeai/api/admin.py
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
from collections import defaultdict
|
|
5
|
+
from datetime import datetime, timedelta
|
|
5
6
|
from typing import Callable, Dict, List, Optional
|
|
6
7
|
|
|
7
|
-
from fastapi import APIRouter, HTTPException, Request
|
|
8
|
+
from fastapi import APIRouter, HTTPException, Query, Request
|
|
8
9
|
from pydantic import BaseModel
|
|
9
10
|
|
|
10
11
|
from latticeai.core.workspace_os import DEFAULT_WORKSPACE_ID
|
|
@@ -86,6 +87,62 @@ def create_admin_router(
|
|
|
86
87
|
scope = _workspace_scope(request)
|
|
87
88
|
return [item for item in get_audit_log() if _matches_scope(item, scope)]
|
|
88
89
|
|
|
90
|
+
def _filter_audit_log(
|
|
91
|
+
events: List[Dict],
|
|
92
|
+
*,
|
|
93
|
+
q: Optional[str] = None,
|
|
94
|
+
actor: Optional[str] = None,
|
|
95
|
+
action: Optional[str] = None,
|
|
96
|
+
severity: Optional[str] = None,
|
|
97
|
+
limit: int = 50,
|
|
98
|
+
) -> List[Dict]:
|
|
99
|
+
needle = (q or "").strip().lower()
|
|
100
|
+
actor_filter = (actor or "").strip().lower()
|
|
101
|
+
action_filter = (action or "").strip().lower()
|
|
102
|
+
severity_filter = (severity or "").strip().lower()
|
|
103
|
+
|
|
104
|
+
def matches(event: Dict) -> bool:
|
|
105
|
+
public = _event_public_text(event)
|
|
106
|
+
if needle and needle not in public:
|
|
107
|
+
return False
|
|
108
|
+
if actor_filter and actor_filter not in str(event.get("user_email") or event.get("actor") or "").lower():
|
|
109
|
+
return False
|
|
110
|
+
event_action = str(event.get("event_type") or event.get("action") or "").lower()
|
|
111
|
+
if action_filter and action_filter not in event_action:
|
|
112
|
+
return False
|
|
113
|
+
event_severity = str(event.get("severity") or event.get("sev") or "").lower()
|
|
114
|
+
if severity_filter and event_severity != severity_filter:
|
|
115
|
+
return False
|
|
116
|
+
return True
|
|
117
|
+
|
|
118
|
+
capped_limit = max(1, min(int(limit or 50), 250))
|
|
119
|
+
return [event for event in events if matches(event)][-capped_limit:]
|
|
120
|
+
|
|
121
|
+
def _event_public_text(event: Dict) -> str:
|
|
122
|
+
parts = [
|
|
123
|
+
event.get("event_type"),
|
|
124
|
+
event.get("action"),
|
|
125
|
+
event.get("user_email"),
|
|
126
|
+
event.get("actor"),
|
|
127
|
+
event.get("target"),
|
|
128
|
+
event.get("target_email"),
|
|
129
|
+
event.get("workspace_id"),
|
|
130
|
+
event.get("severity"),
|
|
131
|
+
event.get("sev"),
|
|
132
|
+
]
|
|
133
|
+
return " ".join(str(part).lower() for part in parts if part is not None)
|
|
134
|
+
|
|
135
|
+
def _parse_timestamp(value: object) -> Optional[datetime]:
|
|
136
|
+
if not value:
|
|
137
|
+
return None
|
|
138
|
+
try:
|
|
139
|
+
parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
|
|
140
|
+
if parsed.tzinfo is not None:
|
|
141
|
+
return parsed.astimezone().replace(tzinfo=None)
|
|
142
|
+
return parsed
|
|
143
|
+
except ValueError:
|
|
144
|
+
return None
|
|
145
|
+
|
|
89
146
|
@router.get("/admin/summary")
|
|
90
147
|
async def admin_summary(request: Request):
|
|
91
148
|
_, users = require_admin(request)
|
|
@@ -127,9 +184,34 @@ def create_admin_router(
|
|
|
127
184
|
return build_sensitivity_report(_scoped_history(request))
|
|
128
185
|
|
|
129
186
|
@router.get("/admin/audit")
|
|
130
|
-
async def admin_audit(
|
|
187
|
+
async def admin_audit(
|
|
188
|
+
request: Request,
|
|
189
|
+
q: Optional[str] = Query(None),
|
|
190
|
+
actor: Optional[str] = Query(None),
|
|
191
|
+
action: Optional[str] = Query(None),
|
|
192
|
+
severity: Optional[str] = Query(None),
|
|
193
|
+
limit: int = Query(50, ge=1, le=250),
|
|
194
|
+
):
|
|
131
195
|
_, users = require_admin(request)
|
|
132
|
-
|
|
196
|
+
scoped_events = _scoped_audit_log(request)
|
|
197
|
+
filtered_events = _filter_audit_log(
|
|
198
|
+
scoped_events,
|
|
199
|
+
q=q,
|
|
200
|
+
actor=actor,
|
|
201
|
+
action=action,
|
|
202
|
+
severity=severity,
|
|
203
|
+
limit=limit,
|
|
204
|
+
)
|
|
205
|
+
report = build_admin_audit_report(users, filtered_events)
|
|
206
|
+
report["filters"] = {
|
|
207
|
+
"q": q or "",
|
|
208
|
+
"actor": actor or "",
|
|
209
|
+
"action": action or "",
|
|
210
|
+
"severity": severity or "",
|
|
211
|
+
"limit": limit,
|
|
212
|
+
"matched_events": len(filtered_events),
|
|
213
|
+
"scoped_events": len(scoped_events),
|
|
214
|
+
}
|
|
133
215
|
try:
|
|
134
216
|
report["graph"] = get_graph_stats() if enable_graph else {"disabled": True}
|
|
135
217
|
except Exception as e:
|
|
@@ -179,9 +261,36 @@ def create_admin_router(
|
|
|
179
261
|
{"id": "invite_gate", "label": "Invite gate",
|
|
180
262
|
"value": "Required for new accounts" if invite_gate_enabled else "Open registration",
|
|
181
263
|
"enforced": bool(invite_gate_enabled)},
|
|
264
|
+
{"id": "log_retention", "label": "Log retention",
|
|
265
|
+
"value": "90 day local audit window with manual export before pruning", "enforced": True},
|
|
182
266
|
]
|
|
183
267
|
}
|
|
184
268
|
|
|
269
|
+
@router.get("/admin/log-retention")
|
|
270
|
+
async def admin_log_retention(request: Request):
|
|
271
|
+
require_admin(request)
|
|
272
|
+
events = _scoped_audit_log(request)
|
|
273
|
+
retention_days = 90
|
|
274
|
+
cutoff = datetime.now() - timedelta(days=retention_days)
|
|
275
|
+
retained = 0
|
|
276
|
+
prune_candidates = 0
|
|
277
|
+
for event in events:
|
|
278
|
+
ts = _parse_timestamp(event.get("timestamp") or event.get("ts"))
|
|
279
|
+
if ts and ts < cutoff:
|
|
280
|
+
prune_candidates += 1
|
|
281
|
+
else:
|
|
282
|
+
retained += 1
|
|
283
|
+
return {
|
|
284
|
+
"mode": "local-first",
|
|
285
|
+
"retention_days": retention_days,
|
|
286
|
+
"total_events": len(events),
|
|
287
|
+
"retained_events": retained,
|
|
288
|
+
"prune_candidates": prune_candidates,
|
|
289
|
+
"export_before_prune": True,
|
|
290
|
+
"editable": False,
|
|
291
|
+
"reason": "Retention is reported in Community mode; destructive pruning requires an explicit export workflow.",
|
|
292
|
+
}
|
|
293
|
+
|
|
185
294
|
@router.get("/admin/product-hardening")
|
|
186
295
|
async def admin_product_hardening(request: Request):
|
|
187
296
|
require_admin(request)
|
package/latticeai/api/chat.py
CHANGED
|
@@ -543,7 +543,14 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
543
543
|
if recent_context:
|
|
544
544
|
stream_context = f"[RECENT CONVERSATION]\n{recent_context}\n\n{context}".strip()
|
|
545
545
|
return StreamingResponse(
|
|
546
|
-
_stream_chat(
|
|
546
|
+
_stream_chat(
|
|
547
|
+
req,
|
|
548
|
+
stream_context,
|
|
549
|
+
req.image_data,
|
|
550
|
+
trace_seed=trace_seed,
|
|
551
|
+
effective_email=effective_email,
|
|
552
|
+
history_meta=history_meta,
|
|
553
|
+
),
|
|
547
554
|
media_type="text/event-stream",
|
|
548
555
|
headers={"X-Model": router.current_model_id},
|
|
549
556
|
)
|
|
@@ -650,6 +657,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
650
657
|
*,
|
|
651
658
|
trace_seed: Optional[Dict] = None,
|
|
652
659
|
effective_email: Optional[str] = None,
|
|
660
|
+
history_meta: Optional[Dict] = None,
|
|
653
661
|
) -> AsyncIterator[str]:
|
|
654
662
|
full_response = ""
|
|
655
663
|
async for chunk in router.stream_generate(req.message, context, req.max_tokens, req.temperature, image_data):
|
|
@@ -664,8 +672,8 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
664
672
|
|
|
665
673
|
full_response += str(clean_chunk)
|
|
666
674
|
yield f"data: {json.dumps({'chunk': clean_chunk, 'model': router.current_model_id}, ensure_ascii=False)}\n\n"
|
|
667
|
-
history_user = get_history_user(req.user_email, req.user_nickname)
|
|
668
|
-
save_to_history("assistant", full_response, **history_meta, **history_user)
|
|
675
|
+
history_user = get_history_user(effective_email or req.user_email, req.user_nickname)
|
|
676
|
+
save_to_history("assistant", full_response, **(history_meta or {}), **history_user)
|
|
669
677
|
trace_record = CHAT_SERVICE.record_trace(
|
|
670
678
|
question=req.message,
|
|
671
679
|
response=full_response,
|
package/latticeai/app_factory.py
CHANGED
|
@@ -51,8 +51,6 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
51
51
|
except Exception as e:
|
|
52
52
|
print(f"⚠️ MLX Metal context unavailable: {e}")
|
|
53
53
|
mx = None
|
|
54
|
-
from typing import List
|
|
55
|
-
|
|
56
54
|
import uvicorn
|
|
57
55
|
from fastapi import FastAPI, HTTPException, Request
|
|
58
56
|
from fastapi.middleware.cors import CORSMiddleware
|
|
@@ -19,7 +19,7 @@ from pathlib import Path
|
|
|
19
19
|
from typing import Any, Callable, Dict, Iterable, List, Optional
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
WORKSPACE_OS_VERSION = "4.7.
|
|
22
|
+
WORKSPACE_OS_VERSION = "4.7.2"
|
|
23
23
|
|
|
24
24
|
# Workspace types separate single-user Personal workspaces from shared
|
|
25
25
|
# Organization workspaces. Both keep the same local-first JSON store; the type
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ltcai",
|
|
3
|
-
"version": "4.7.
|
|
3
|
+
"version": "4.7.2",
|
|
4
4
|
"description": "Lattice AI — local-first Living Brain workspace (conversation, durable memory, hybrid search, agents, advanced graph exploration, portable encrypted brain archives)",
|
|
5
5
|
"homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
|
|
6
6
|
"repository": {
|
|
@@ -89,6 +89,7 @@
|
|
|
89
89
|
"static/icons/",
|
|
90
90
|
"plugins/",
|
|
91
91
|
"scripts/",
|
|
92
|
+
"!scripts/launch-pts-grok.sh",
|
|
92
93
|
"!docs/images/tmp_frames/",
|
|
93
94
|
"!**/__pycache__/",
|
|
94
95
|
"!**/*.pyc",
|
package/src-tauri/Cargo.lock
CHANGED
package/src-tauri/Cargo.toml
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "4.7.
|
|
2
|
+
"version": "4.7.2",
|
|
3
3
|
"generated_at": "vite",
|
|
4
4
|
"entrypoints": {
|
|
5
5
|
"app": "/static/app/index.html"
|
|
6
6
|
},
|
|
7
7
|
"assets": {
|
|
8
8
|
"../node_modules/@tauri-apps/api/core.js": "/static/app/assets/core-CwxXejkd.js",
|
|
9
|
-
"index.html": "/static/app/assets/index-
|
|
10
|
-
"assets/index-
|
|
9
|
+
"index.html": "/static/app/assets/index-DdAB4yfa.js",
|
|
10
|
+
"assets/index-KlQ04wVv.css": "/static/app/assets/index-KlQ04wVv.css"
|
|
11
11
|
},
|
|
12
12
|
"vite": {
|
|
13
13
|
"../node_modules/@tauri-apps/api/core.js": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"isDynamicEntry": true
|
|
18
18
|
},
|
|
19
19
|
"index.html": {
|
|
20
|
-
"file": "assets/index-
|
|
20
|
+
"file": "assets/index-DdAB4yfa.js",
|
|
21
21
|
"name": "index",
|
|
22
22
|
"src": "index.html",
|
|
23
23
|
"isEntry": true,
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"../node_modules/@tauri-apps/api/core.js"
|
|
26
26
|
],
|
|
27
27
|
"css": [
|
|
28
|
-
"assets/index-
|
|
28
|
+
"assets/index-KlQ04wVv.css"
|
|
29
29
|
]
|
|
30
30
|
}
|
|
31
31
|
}
|