ltcai 8.9.0 → 9.1.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 +81 -58
- package/auto_setup.py +7 -904
- package/desktop/electron/README.md +9 -0
- package/desktop/electron/main.cjs +5 -3
- package/docs/CHANGELOG.md +221 -238
- package/docs/COMMUNITY_AND_PLUGINS.md +3 -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/ROADMAP_RECOMMENDATIONS.md +61 -36
- 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 +4 -14
- package/lattice_brain/context.py +66 -9
- package/lattice_brain/embeddings.py +38 -2
- package/lattice_brain/graph/_kg_common.py +27 -462
- package/lattice_brain/graph/_kg_constants.py +243 -0
- package/lattice_brain/graph/_kg_fsutil.py +293 -0
- package/lattice_brain/graph/discovery.py +0 -948
- package/lattice_brain/graph/discovery_index.py +1126 -0
- 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 +141 -610
- package/lattice_brain/graph/retrieval_docgen.py +243 -0
- package/lattice_brain/graph/retrieval_vector.py +460 -0
- package/lattice_brain/graph/store.py +6 -0
- package/lattice_brain/ingestion.py +69 -4
- package/lattice_brain/portability.py +29 -23
- package/lattice_brain/quality.py +98 -4
- package/lattice_brain/runtime/agent_runtime.py +169 -7
- 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 +46 -0
- 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 +355 -952
- 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 +250 -0
- package/latticeai/api/chat_history.py +99 -0
- package/latticeai/api/chat_intents.py +302 -0
- package/latticeai/api/chat_stream.py +115 -0
- package/latticeai/api/computer_use.py +211 -40
- 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/marketplace.py +11 -0
- 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 +70 -29
- 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 +97 -14
- package/latticeai/api/workflow_designer.py +37 -0
- package/latticeai/api/workspace.py +2 -1
- package/latticeai/app_factory.py +185 -405
- package/latticeai/core/agent.py +19 -9
- 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/invitations.py +1 -4
- package/latticeai/core/io_utils.py +45 -0
- package/latticeai/core/legacy_compatibility.py +24 -3
- package/latticeai/core/local_embeddings.py +2 -4
- package/latticeai/core/marketplace.py +33 -2
- package/latticeai/core/mcp_catalog.py +450 -0
- package/latticeai/core/mcp_registry.py +2 -441
- 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 +5 -14
- 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 +2 -21
- 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 +96 -40
- package/latticeai/models/model_providers.py +111 -0
- package/latticeai/models/router.py +189 -173
- package/latticeai/runtime/access_runtime.py +62 -4
- package/latticeai/runtime/audit_runtime.py +27 -16
- package/latticeai/runtime/automation_runtime.py +22 -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/history_runtime.py +163 -0
- package/latticeai/runtime/hooks_runtime.py +1 -1
- package/latticeai/runtime/model_wiring.py +33 -5
- package/latticeai/runtime/namespace_runtime.py +163 -0
- package/latticeai/runtime/network_config_runtime.py +56 -0
- 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/sso_config_runtime.py +128 -0
- package/latticeai/runtime/stages.py +27 -0
- package/latticeai/runtime/user_key_runtime.py +106 -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 +268 -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 +92 -24
- package/latticeai/services/product_readiness.py +19 -17
- package/latticeai/services/review_queue.py +76 -11
- package/latticeai/services/router_context.py +1 -0
- package/latticeai/services/run_executor.py +25 -9
- package/latticeai/services/search_service.py +203 -28
- package/latticeai/services/setup_detection.py +80 -0
- 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 +7 -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 -1304
- 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-Bzz0bUyW.js +1 -0
- package/static/app/assets/Brain-Dj2J20YA.js +321 -0
- package/static/app/assets/Capture-CqlEl1Ga.js +1 -0
- package/static/app/assets/Library-B03FP1Yx.js +1 -0
- package/static/app/assets/System-80lHW0Ux.js +1 -0
- package/static/app/assets/index-BiMofBTM.js +17 -0
- package/static/app/assets/index-Bmx9rzTc.css +2 -0
- package/static/app/assets/primitives-Q1A96_7v.js +1 -0
- package/static/app/assets/textarea-D13RtnTo.js +1 -0
- package/static/app/index.html +2 -2
- package/static/css/tokens.css +4 -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/sso_runtime.py +0 -52
- 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-fZokUnC0.js +0 -1
- package/static/app/assets/Brain-DtyuWubr.js +0 -321
- package/static/app/assets/Capture-D5KV3Cu7.js +0 -1
- package/static/app/assets/Library-C9kyFkSt.js +0 -1
- package/static/app/assets/System-VbChmX7r.js +0 -1
- package/static/app/assets/index-DCh5AoXt.css +0 -2
- package/static/app/assets/index-DPdcPoF0.js +0 -17
- package/static/app/assets/primitives-DFeanEV6.js +0 -1
- package/static/app/assets/textarea-CD8UNKIy.js +0 -1
- package/tools/knowledge.py +0 -95
|
@@ -22,11 +22,12 @@ underlying store, and missing stores surface as ``unavailable``.
|
|
|
22
22
|
from __future__ import annotations
|
|
23
23
|
|
|
24
24
|
import json
|
|
25
|
-
|
|
25
|
+
import logging
|
|
26
26
|
from pathlib import Path
|
|
27
27
|
from typing import Any, Dict, List, Optional
|
|
28
28
|
|
|
29
29
|
from latticeai.core.workspace_os_utils import _file_size
|
|
30
|
+
from latticeai.core.timeutil import now_iso as _now
|
|
30
31
|
|
|
31
32
|
# Personal workspace memory kinds (from WorkspaceOS.MEMORY_KINDS).
|
|
32
33
|
WORKSPACE_KINDS = (
|
|
@@ -40,10 +41,11 @@ WORKSPACE_KINDS = (
|
|
|
40
41
|
)
|
|
41
42
|
|
|
42
43
|
TIERS = ("workspace", "project", "agent", "conversation", "graph", "vector")
|
|
44
|
+
LOGGER = logging.getLogger(__name__)
|
|
43
45
|
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
class MemoryServiceError(RuntimeError):
|
|
48
|
+
"""Raised when a configured memory backend cannot be read reliably."""
|
|
47
49
|
|
|
48
50
|
|
|
49
51
|
class MemoryService:
|
|
@@ -70,20 +72,23 @@ class MemoryService:
|
|
|
70
72
|
def _workspace_memories(self, *, user_email: Optional[str], workspace_id: Optional[str]) -> List[Dict[str, Any]]:
|
|
71
73
|
try:
|
|
72
74
|
return list(self._store.list_memories(user_email=user_email, workspace_id=workspace_id).get("memories", []))
|
|
73
|
-
except Exception:
|
|
74
|
-
|
|
75
|
+
except Exception as exc:
|
|
76
|
+
LOGGER.exception("workspace memory read failed")
|
|
77
|
+
raise MemoryServiceError("workspace memory backend unavailable") from exc
|
|
75
78
|
|
|
76
79
|
def _all_memories(self) -> List[Dict[str, Any]]:
|
|
77
80
|
try:
|
|
78
81
|
return list(self._store.list_memories().get("memories", []))
|
|
79
|
-
except Exception:
|
|
80
|
-
|
|
82
|
+
except Exception as exc:
|
|
83
|
+
LOGGER.exception("global memory read failed")
|
|
84
|
+
raise MemoryServiceError("memory backend unavailable") from exc
|
|
81
85
|
|
|
82
86
|
def _snapshots(self, *, workspace_id: Optional[str]) -> List[Dict[str, Any]]:
|
|
83
87
|
try:
|
|
84
88
|
return list(self._store.list_memory_snapshots(workspace_id=workspace_id, limit=200).get("snapshots", []))
|
|
85
|
-
except Exception:
|
|
86
|
-
|
|
89
|
+
except Exception as exc:
|
|
90
|
+
LOGGER.exception("memory snapshot read failed")
|
|
91
|
+
raise MemoryServiceError("memory snapshot backend unavailable") from exc
|
|
87
92
|
|
|
88
93
|
def _conversations(self) -> List[Dict[str, Any]]:
|
|
89
94
|
if self._conversation_store is not None:
|
|
@@ -92,15 +97,17 @@ class MemoryService:
|
|
|
92
97
|
for item in self._conversation_store.history():
|
|
93
98
|
grouped.setdefault(item.get("conversation_id") or "legacy-previous-history", []).append(item)
|
|
94
99
|
return [{"id": conv_id, "messages": msgs} for conv_id, msgs in grouped.items()]
|
|
95
|
-
except Exception:
|
|
96
|
-
|
|
100
|
+
except Exception as exc:
|
|
101
|
+
LOGGER.exception("conversation store read failed")
|
|
102
|
+
raise MemoryServiceError("conversation backend unavailable") from exc
|
|
97
103
|
if not self._history_file.exists():
|
|
98
104
|
return []
|
|
99
105
|
try:
|
|
100
106
|
with open(self._history_file, "r", encoding="utf-8") as fh:
|
|
101
107
|
data = json.load(fh)
|
|
102
|
-
except
|
|
103
|
-
|
|
108
|
+
except (OSError, json.JSONDecodeError, TypeError, ValueError) as exc:
|
|
109
|
+
LOGGER.exception("legacy conversation history read failed")
|
|
110
|
+
raise MemoryServiceError("conversation history is unreadable") from exc
|
|
104
111
|
if isinstance(data, dict):
|
|
105
112
|
convs = data.get("conversations")
|
|
106
113
|
if isinstance(convs, list):
|
|
@@ -136,6 +143,7 @@ class MemoryService:
|
|
|
136
143
|
try:
|
|
137
144
|
return self._kg.stats()
|
|
138
145
|
except Exception:
|
|
146
|
+
LOGGER.exception("knowledge graph stats read failed")
|
|
139
147
|
return None
|
|
140
148
|
|
|
141
149
|
def _kg_index(self) -> Optional[Dict[str, Any]]:
|
|
@@ -144,6 +152,7 @@ class MemoryService:
|
|
|
144
152
|
try:
|
|
145
153
|
return self._kg.index_status()
|
|
146
154
|
except Exception:
|
|
155
|
+
LOGGER.exception("knowledge graph index status read failed")
|
|
147
156
|
return None
|
|
148
157
|
|
|
149
158
|
# ── Memory Manager: sources / usage / health ──────────────────────────
|
|
@@ -271,6 +280,7 @@ class MemoryService:
|
|
|
271
280
|
memory_count = len(memory_ids) + len(snaps) + len(convs)
|
|
272
281
|
return {
|
|
273
282
|
"sources": sources,
|
|
283
|
+
"recent_memories": self._manager_recent_memories([*ws_mem, *project_mem], limit=8),
|
|
274
284
|
"tiers": list(TIERS),
|
|
275
285
|
"usage": {"total_items": total_items, "total_bytes": total_bytes, "sources": len(sources)},
|
|
276
286
|
"brain_readiness": self._brain_readiness(
|
|
@@ -284,6 +294,23 @@ class MemoryService:
|
|
|
284
294
|
"generated_at": _now(),
|
|
285
295
|
}
|
|
286
296
|
|
|
297
|
+
@staticmethod
|
|
298
|
+
def _manager_recent_memories(memories: List[Dict[str, Any]], *, limit: int = 8) -> List[Dict[str, Any]]:
|
|
299
|
+
rows: List[Dict[str, Any]] = []
|
|
300
|
+
for item in memories[: max(1, limit)]:
|
|
301
|
+
metadata = item.get("metadata") if isinstance(item.get("metadata"), dict) else {}
|
|
302
|
+
rows.append({
|
|
303
|
+
"id": item.get("id") or "",
|
|
304
|
+
"kind": item.get("kind") or "memory",
|
|
305
|
+
"content": str(item.get("content") or "")[:320],
|
|
306
|
+
"tags": item.get("tags") if isinstance(item.get("tags"), list) else [],
|
|
307
|
+
"metadata": metadata,
|
|
308
|
+
"workspace_id": item.get("workspace_id") or "personal",
|
|
309
|
+
"created_at": item.get("created_at"),
|
|
310
|
+
"updated_at": item.get("updated_at"),
|
|
311
|
+
})
|
|
312
|
+
return rows
|
|
313
|
+
|
|
287
314
|
def brain_quality_summary(self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None) -> Dict[str, Any]:
|
|
288
315
|
"""Return the backend-owned Brain readiness signal for API consumers."""
|
|
289
316
|
return self.manager(user_email=user_email, workspace_id=workspace_id)["brain_readiness"]
|
|
@@ -338,6 +365,22 @@ class MemoryService:
|
|
|
338
365
|
has_recall=bool(recall_items),
|
|
339
366
|
graph_concepts=graph_concepts,
|
|
340
367
|
)
|
|
368
|
+
suggested_questions = self._brain_brief_suggested_questions(
|
|
369
|
+
focus=focus,
|
|
370
|
+
has_durable_evidence=bool(proofs.get("has_durable_evidence")),
|
|
371
|
+
has_recall=bool(recall_items),
|
|
372
|
+
graph_concepts=graph_concepts,
|
|
373
|
+
conversations=conversations,
|
|
374
|
+
)
|
|
375
|
+
proactive_actions = self._brain_brief_proactive_actions(
|
|
376
|
+
focus=focus,
|
|
377
|
+
state=state,
|
|
378
|
+
has_durable_evidence=bool(proofs.get("has_durable_evidence")),
|
|
379
|
+
has_recall=bool(recall_items),
|
|
380
|
+
graph_concepts=graph_concepts,
|
|
381
|
+
vector_items=vector_items,
|
|
382
|
+
healthy_sources=healthy_sources,
|
|
383
|
+
)
|
|
341
384
|
return {
|
|
342
385
|
"status": state,
|
|
343
386
|
"score": int(readiness.get("score") or 0),
|
|
@@ -345,6 +388,8 @@ class MemoryService:
|
|
|
345
388
|
"body_key": f"brain.brief.body.{state if state in {'quiet', 'forming', 'alive'} else 'quiet'}",
|
|
346
389
|
"focus": focus,
|
|
347
390
|
"next_actions": actions,
|
|
391
|
+
"suggested_questions": suggested_questions,
|
|
392
|
+
"proactive_actions": proactive_actions,
|
|
348
393
|
"evidence": [
|
|
349
394
|
{
|
|
350
395
|
"id": "durable",
|
|
@@ -501,6 +546,183 @@ class MemoryService:
|
|
|
501
546
|
})
|
|
502
547
|
return sorted(actions, key=lambda item: int(item.get("priority") or 0), reverse=True)[:4]
|
|
503
548
|
|
|
549
|
+
@staticmethod
|
|
550
|
+
def _brain_brief_proactive_actions(
|
|
551
|
+
*,
|
|
552
|
+
focus: Dict[str, Any],
|
|
553
|
+
state: str,
|
|
554
|
+
has_durable_evidence: bool,
|
|
555
|
+
has_recall: bool,
|
|
556
|
+
graph_concepts: int,
|
|
557
|
+
vector_items: int,
|
|
558
|
+
healthy_sources: int,
|
|
559
|
+
) -> List[Dict[str, Any]]:
|
|
560
|
+
"""Return concrete, one-click actions Brain can proactively suggest."""
|
|
561
|
+
focus_title = str(focus.get("title") or "").strip() or "Brain"
|
|
562
|
+
focus_detail = str(focus.get("detail") or "").strip()
|
|
563
|
+
actions: List[Dict[str, Any]] = []
|
|
564
|
+
if not has_durable_evidence:
|
|
565
|
+
actions.append({
|
|
566
|
+
"id": "proactive_add_source",
|
|
567
|
+
"intent": "route",
|
|
568
|
+
"label_key": "brain.proactive.addSource.label",
|
|
569
|
+
"detail_key": "brain.proactive.addSource.detail",
|
|
570
|
+
"route": "/capture",
|
|
571
|
+
"prompt": "Add a useful source to my Brain and explain what it learned.",
|
|
572
|
+
"priority": 100,
|
|
573
|
+
})
|
|
574
|
+
actions.append({
|
|
575
|
+
"id": "proactive_seed_memory",
|
|
576
|
+
"intent": "ask",
|
|
577
|
+
"label_key": "brain.proactive.seed.label",
|
|
578
|
+
"detail_key": "brain.proactive.seed.detail",
|
|
579
|
+
"prompt": "Help me seed my Brain with the most useful personal context to remember.",
|
|
580
|
+
"priority": 90,
|
|
581
|
+
})
|
|
582
|
+
return actions
|
|
583
|
+
|
|
584
|
+
if has_recall:
|
|
585
|
+
actions.append({
|
|
586
|
+
"id": "proactive_evidence_review",
|
|
587
|
+
"intent": "ask",
|
|
588
|
+
"label_key": "brain.proactive.evidence.label",
|
|
589
|
+
"detail_key": "brain.proactive.evidence.detail",
|
|
590
|
+
"prompt": (
|
|
591
|
+
f"Review the evidence Brain has for {focus_title}. "
|
|
592
|
+
"Separate confirmed facts, weak signals, contradictions, and next checks."
|
|
593
|
+
),
|
|
594
|
+
"priority": 100,
|
|
595
|
+
"context": {"focus": focus_title, "detail": focus_detail},
|
|
596
|
+
})
|
|
597
|
+
actions.append({
|
|
598
|
+
"id": "proactive_delegate",
|
|
599
|
+
"intent": "delegate",
|
|
600
|
+
"label_key": "brain.proactive.delegate.label",
|
|
601
|
+
"detail_key": "brain.proactive.delegate.detail",
|
|
602
|
+
"prompt": (
|
|
603
|
+
f"Turn {focus_title} into an execution plan, verify the known context, "
|
|
604
|
+
"and return concrete next steps with risks."
|
|
605
|
+
),
|
|
606
|
+
"priority": 95,
|
|
607
|
+
"context": {"focus": focus_title, "detail": focus_detail},
|
|
608
|
+
})
|
|
609
|
+
actions.append({
|
|
610
|
+
"id": "proactive_review_draft",
|
|
611
|
+
"intent": "review",
|
|
612
|
+
"label_key": "brain.proactive.review.label",
|
|
613
|
+
"detail_key": "brain.proactive.review.detail",
|
|
614
|
+
"prompt": (
|
|
615
|
+
f"Create a reviewable task from Brain's current focus: {focus_title}. "
|
|
616
|
+
f"{focus_detail[:240]}"
|
|
617
|
+
).strip(),
|
|
618
|
+
"priority": 90,
|
|
619
|
+
"context": {"focus": focus_title, "detail": focus_detail},
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
if graph_concepts > 0:
|
|
623
|
+
actions.append({
|
|
624
|
+
"id": "proactive_map_connections",
|
|
625
|
+
"intent": "route",
|
|
626
|
+
"label_key": "brain.proactive.map.label",
|
|
627
|
+
"detail_key": "brain.proactive.map.detail",
|
|
628
|
+
"route": "/knowledge-graph",
|
|
629
|
+
"prompt": f"Map the strongest Knowledge Graph connections around {focus_title}.",
|
|
630
|
+
"priority": 82,
|
|
631
|
+
"context": {"focus": focus_title, "graph_concepts": graph_concepts},
|
|
632
|
+
})
|
|
633
|
+
|
|
634
|
+
if state == "alive" and vector_items > 0 and healthy_sources > 0:
|
|
635
|
+
actions.append({
|
|
636
|
+
"id": "proactive_weekly_brief",
|
|
637
|
+
"intent": "review",
|
|
638
|
+
"label_key": "brain.proactive.weekly.label",
|
|
639
|
+
"detail_key": "brain.proactive.weekly.detail",
|
|
640
|
+
"prompt": (
|
|
641
|
+
"Prepare a weekly Brain review: what changed, what decisions are pending, "
|
|
642
|
+
"what should be delegated, and what evidence is stale."
|
|
643
|
+
),
|
|
644
|
+
"priority": 78,
|
|
645
|
+
"context": {"vector_items": vector_items, "healthy_sources": healthy_sources},
|
|
646
|
+
})
|
|
647
|
+
|
|
648
|
+
return sorted(actions, key=lambda item: int(item.get("priority") or 0), reverse=True)[:4]
|
|
649
|
+
|
|
650
|
+
@staticmethod
|
|
651
|
+
def _brain_brief_suggested_questions(
|
|
652
|
+
*,
|
|
653
|
+
focus: Dict[str, Any],
|
|
654
|
+
has_durable_evidence: bool,
|
|
655
|
+
has_recall: bool,
|
|
656
|
+
graph_concepts: int,
|
|
657
|
+
conversations: int,
|
|
658
|
+
) -> List[Dict[str, Any]]:
|
|
659
|
+
"""Return reusable, localized UI prompt descriptors for the Brain home."""
|
|
660
|
+
focus_title = str(focus.get("title") or "").strip()
|
|
661
|
+
focus_kind = str(focus.get("kind") or "empty")
|
|
662
|
+
questions: List[Dict[str, Any]] = []
|
|
663
|
+
|
|
664
|
+
if not has_durable_evidence or focus_kind == "empty":
|
|
665
|
+
questions.extend([
|
|
666
|
+
{
|
|
667
|
+
"id": "start_brain",
|
|
668
|
+
"label_key": "brain.suggestion.start.label",
|
|
669
|
+
"detail_key": "brain.suggestion.start.detail",
|
|
670
|
+
"prompt_key": "brain.suggestion.start.prompt",
|
|
671
|
+
"params": {},
|
|
672
|
+
"priority": 10,
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
"id": "add_context",
|
|
676
|
+
"label_key": "brain.suggestion.context.label",
|
|
677
|
+
"detail_key": "brain.suggestion.context.detail",
|
|
678
|
+
"prompt_key": "brain.suggestion.context.prompt",
|
|
679
|
+
"params": {},
|
|
680
|
+
"priority": 9,
|
|
681
|
+
},
|
|
682
|
+
])
|
|
683
|
+
return questions
|
|
684
|
+
|
|
685
|
+
questions.append({
|
|
686
|
+
"id": "focus_next",
|
|
687
|
+
"label_key": "brain.suggestion.focus.label",
|
|
688
|
+
"detail_key": "brain.suggestion.focus.detail",
|
|
689
|
+
"prompt_key": "brain.suggestion.focus.prompt",
|
|
690
|
+
"params": {"focus": focus_title or "Brain"},
|
|
691
|
+
"priority": 10,
|
|
692
|
+
})
|
|
693
|
+
|
|
694
|
+
if has_recall:
|
|
695
|
+
questions.append({
|
|
696
|
+
"id": "evidence_check",
|
|
697
|
+
"label_key": "brain.suggestion.evidence.label",
|
|
698
|
+
"detail_key": "brain.suggestion.evidence.detail",
|
|
699
|
+
"prompt_key": "brain.suggestion.evidence.prompt",
|
|
700
|
+
"params": {"focus": focus_title or "this topic"},
|
|
701
|
+
"priority": 9,
|
|
702
|
+
})
|
|
703
|
+
|
|
704
|
+
if graph_concepts > 0:
|
|
705
|
+
questions.append({
|
|
706
|
+
"id": "graph_connections",
|
|
707
|
+
"label_key": "brain.suggestion.graph.label",
|
|
708
|
+
"detail_key": "brain.suggestion.graph.detail",
|
|
709
|
+
"prompt_key": "brain.suggestion.graph.prompt",
|
|
710
|
+
"params": {"focus": focus_title or "Knowledge Graph"},
|
|
711
|
+
"priority": 8,
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
if conversations > 0:
|
|
715
|
+
questions.append({
|
|
716
|
+
"id": "conversation_followup",
|
|
717
|
+
"label_key": "brain.suggestion.history.label",
|
|
718
|
+
"detail_key": "brain.suggestion.history.detail",
|
|
719
|
+
"prompt_key": "brain.suggestion.history.prompt",
|
|
720
|
+
"params": {"focus": focus_title or "recent conversations"},
|
|
721
|
+
"priority": 7,
|
|
722
|
+
})
|
|
723
|
+
|
|
724
|
+
return sorted(questions, key=lambda item: int(item.get("priority") or 0), reverse=True)[:4]
|
|
725
|
+
|
|
504
726
|
def brain_proof(
|
|
505
727
|
self,
|
|
506
728
|
*,
|
|
@@ -634,9 +856,12 @@ class MemoryService:
|
|
|
634
856
|
|
|
635
857
|
results: List[Dict[str, Any]] = []
|
|
636
858
|
|
|
859
|
+
errors: List[Dict[str, str]] = []
|
|
637
860
|
try:
|
|
638
861
|
mem = self._store.search_memories(q, user_email=user_email, limit=limit, workspace_id=workspace_id).get("memories", [])
|
|
639
|
-
except Exception:
|
|
862
|
+
except Exception as exc:
|
|
863
|
+
LOGGER.exception("workspace memory search failed")
|
|
864
|
+
errors.append({"source": "workspace", "detail": str(exc)})
|
|
640
865
|
mem = []
|
|
641
866
|
for m in mem:
|
|
642
867
|
matched = _matched_terms(m.get("content"), " ".join(m.get("tags") or []), m.get("kind"))
|
|
@@ -654,8 +879,15 @@ class MemoryService:
|
|
|
654
879
|
if self._enable_graph and q:
|
|
655
880
|
try:
|
|
656
881
|
# KnowledgeGraph.search returns {"query": ..., "matches": [...]}.
|
|
657
|
-
|
|
658
|
-
|
|
882
|
+
search_kwargs = (
|
|
883
|
+
{"allowed_workspaces": {workspace_id}}
|
|
884
|
+
if workspace_id is not None
|
|
885
|
+
else {}
|
|
886
|
+
)
|
|
887
|
+
hits = self._kg.search(q, limit, **search_kwargs).get("matches", [])
|
|
888
|
+
except Exception as exc:
|
|
889
|
+
LOGGER.exception("knowledge graph memory search failed")
|
|
890
|
+
errors.append({"source": "graph", "detail": str(exc)})
|
|
659
891
|
hits = []
|
|
660
892
|
for hit in hits[:limit]:
|
|
661
893
|
matched = _matched_terms(hit.get("title"), hit.get("name"), hit.get("summary"), hit.get("content"))
|
|
@@ -685,6 +917,8 @@ class MemoryService:
|
|
|
685
917
|
"results": results[: max(1, min(limit, 100))],
|
|
686
918
|
"count": len(results),
|
|
687
919
|
"source": "live",
|
|
920
|
+
"status": "degraded" if errors else "ok",
|
|
921
|
+
"errors": errors,
|
|
688
922
|
"quality_gate": {
|
|
689
923
|
"candidates": candidates,
|
|
690
924
|
"passed": len(results),
|
|
@@ -736,6 +970,7 @@ class MemoryService:
|
|
|
736
970
|
if m.get("id")
|
|
737
971
|
}
|
|
738
972
|
removed: List[str] = []
|
|
973
|
+
failed: List[Dict[str, str]] = []
|
|
739
974
|
skipped: List[str] = []
|
|
740
975
|
target_ids: List[str] = []
|
|
741
976
|
seen: set = set()
|
|
@@ -756,11 +991,15 @@ class MemoryService:
|
|
|
756
991
|
try:
|
|
757
992
|
self._store.delete_memory(mid)
|
|
758
993
|
removed.append(mid)
|
|
759
|
-
except Exception:
|
|
760
|
-
|
|
994
|
+
except Exception as exc:
|
|
995
|
+
LOGGER.exception("memory deletion failed for %s", mid)
|
|
996
|
+
failed.append({"id": mid, "detail": str(exc)})
|
|
761
997
|
result: Dict[str, Any] = {"removed": removed, "count": len(removed)}
|
|
762
998
|
if skipped:
|
|
763
999
|
result["skipped"] = skipped
|
|
1000
|
+
if failed:
|
|
1001
|
+
result["failed"] = failed
|
|
1002
|
+
result["status"] = "partial" if removed else "error"
|
|
764
1003
|
return result
|
|
765
1004
|
|
|
766
1005
|
def compact(
|
|
@@ -772,6 +1011,7 @@ class MemoryService:
|
|
|
772
1011
|
"""Dedupe workspace memories with identical (kind, content)."""
|
|
773
1012
|
seen: set = set()
|
|
774
1013
|
removed: List[str] = []
|
|
1014
|
+
failed: List[Dict[str, str]] = []
|
|
775
1015
|
# Oldest first so the first occurrence (oldest) is kept.
|
|
776
1016
|
memories = list(reversed(self._workspace_memories(user_email=user_email, workspace_id=workspace_id)))
|
|
777
1017
|
for m in memories:
|
|
@@ -781,11 +1021,18 @@ class MemoryService:
|
|
|
781
1021
|
try:
|
|
782
1022
|
self._store.delete_memory(m["id"])
|
|
783
1023
|
removed.append(m["id"])
|
|
784
|
-
except Exception:
|
|
785
|
-
|
|
1024
|
+
except Exception as exc:
|
|
1025
|
+
LOGGER.exception("memory compaction deletion failed for %s", m["id"])
|
|
1026
|
+
failed.append({"id": m["id"], "detail": str(exc)})
|
|
786
1027
|
else:
|
|
787
1028
|
seen.add(key)
|
|
788
|
-
return {
|
|
1029
|
+
return {
|
|
1030
|
+
"compacted": len(removed),
|
|
1031
|
+
"removed": removed,
|
|
1032
|
+
"remaining": len(seen),
|
|
1033
|
+
"failed": failed,
|
|
1034
|
+
"status": "partial" if failed and removed else "error" if failed else "ok",
|
|
1035
|
+
}
|
|
789
1036
|
|
|
790
1037
|
def rebuild(self, target: str = "vector") -> Dict[str, Any]:
|
|
791
1038
|
if target in {"vector", "index", "vector_index"}:
|